use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Instant;
use serde_json::{json, Value};
use super::{AssistantEvent, AssistantOutcome, AssistantToolReceipt};
pub const SCHEMA: &str = "car.do/1";
const SUMMARY_CAP: usize = 4096;
const BRIEF_CAP: usize = 160;
const RECEIPT_SAMPLE: usize = 8;
fn cap_text(s: &str, cap: usize) -> String {
if s.len() <= cap {
return s.to_string();
}
let mut end = cap;
while !s.is_char_boundary(end) {
end -= 1;
}
let elided = s.len() - end;
format!(
"{}\n…[truncated: {} of {} bytes shown; {} elided]…",
&s[..end],
end,
s.len(),
elided
)
}
fn brief(params: &Value) -> String {
const IDENTIFYING: &[&str] = &[
"command",
"path",
"url",
"query",
"expression",
"subject",
"name",
"content",
];
let raw = IDENTIFYING
.iter()
.find_map(|k| params.get(*k).and_then(Value::as_str))
.unwrap_or_default()
.replace('\n', " ");
cap_text(&raw, BRIEF_CAP)
}
#[derive(Clone)]
pub struct SandboxPosture {
pub sandboxed: bool,
pub image: Option<String>,
pub tier: String,
pub root: String,
pub fallback_notice: Option<String>,
}
impl SandboxPosture {
pub fn to_json(&self) -> Value {
json!({
"mode": if self.sandboxed { "docker" } else { "local" },
"image": self.image,
"network": if self.sandboxed { "none" } else { "host" },
"tier": self.tier,
"root": self.root,
"fallback_notice": self.fallback_notice,
})
}
}
pub struct GoalReport {
pub check: String,
pub passed: bool,
pub grounded: bool,
pub iterations: u32,
pub halt: Option<String>,
}
impl GoalReport {
fn to_json(&self) -> Value {
json!({
"check": self.check,
"passed": self.passed,
"grounded": self.grounded,
"iterations": self.iterations,
"halt": self.halt,
})
}
}
pub trait EventSink: Send + Sync {
fn emit(&self, event: Value);
}
pub struct JsonEmitter {
started: Instant,
posture: SandboxPosture,
sink: Arc<dyn EventSink>,
}
impl JsonEmitter {
pub fn new(posture: SandboxPosture, sink: Arc<dyn EventSink>) -> Self {
Self {
started: Instant::now(),
posture,
sink,
}
}
fn event(&self, ty: &str, phase: &str, message: impl Into<String>, data: Value) {
self.sink.emit(json!({
"type": ty,
"phase": phase,
"message": message.into(),
"data": data,
}));
}
pub fn started(&self, goal: &str, model: &str) {
self.event(
"started",
"run",
"run started",
json!({
"goal": cap_text(goal, BRIEF_CAP),
"model": model,
"sandbox": self.posture.to_json(),
}),
);
}
pub fn on_assistant_event(&self, ev: &AssistantEvent) {
match ev {
AssistantEvent::Text(t) if !t.trim().is_empty() => {
self.event("text", "reasoning", cap_text(t, BRIEF_CAP * 4), json!({}))
}
AssistantEvent::Text(_) => {}
AssistantEvent::ToolCall { name, params } => self.event(
"tool_called",
"acting",
format!("{name}({})", brief(params)),
json!({ "tool": name, "brief": brief(params) }),
),
AssistantEvent::ToolResult { name, ok, .. } => self.event(
if *ok { "tool_result" } else { "tool_failed" },
"acting",
format!("{name} {}", if *ok { "ok" } else { "failed" }),
json!({ "tool": name, "ok": ok }),
),
AssistantEvent::GoalEvaluated {
iteration,
met,
grounded,
reason,
} => self.event(
"goal_evaluated",
"verifying",
cap_text(reason, BRIEF_CAP * 2),
json!({
"iteration": iteration,
"met": met,
"grounded": grounded,
}),
),
AssistantEvent::Done { .. } | AssistantEvent::Error(_) => {}
}
}
fn receipts_json(receipts: &[AssistantToolReceipt]) -> Value {
let mut by_tool: BTreeMap<&str, u64> = BTreeMap::new();
let mut failed = 0u64;
for r in receipts {
*by_tool.entry(r.tool.as_str()).or_default() += 1;
if !r.ok {
failed += 1;
}
}
let sample: Vec<Value> = receipts
.iter()
.filter(|r| !r.ok)
.chain(receipts.iter().filter(|r| r.ok))
.take(RECEIPT_SAMPLE)
.map(|r| json!({ "tool": r.tool, "ok": r.ok, "brief": brief(&r.params) }))
.collect();
let omitted = receipts.len().saturating_sub(sample.len());
json!({
"total": receipts.len(),
"failed": failed,
"by_tool": by_tool,
"sample": sample,
"sample_omitted": omitted,
})
}
pub fn finish(&self, outcome: &AssistantOutcome, goal: Option<&GoalReport>) -> Value {
if outcome.status == "error" {
return self.fail_run(outcome);
}
let ungrounded = super::ungrounded_summary_claims(&outcome.summary, &outcome.tool_receipts);
let elapsed = self.started.elapsed().as_secs_f64();
self.event(
"completed",
"run",
"run finished",
json!({
"status": outcome.status,
"turns": outcome.turns,
"elapsed_seconds": elapsed,
}),
);
let mut doc = json!({
"schema": SCHEMA,
"status": outcome.status,
"summary": cap_text(&outcome.summary, SUMMARY_CAP),
"turns": outcome.turns,
"model_used": outcome.model_used,
"receipts": Self::receipts_json(&outcome.tool_receipts),
"ungrounded_claims": ungrounded,
"sandbox": self.posture.to_json(),
"elapsed_seconds": elapsed,
});
if let Some(g) = goal {
doc["goal"] = g.to_json();
}
doc
}
fn fail_run(&self, outcome: &AssistantOutcome) -> Value {
let elapsed = self.started.elapsed().as_secs_f64();
self.event(
"failed",
"run",
cap_text(&outcome.summary, BRIEF_CAP * 2),
json!({ "error": "AssistantLoopFailed", "turns": outcome.turns }),
);
json!({
"schema": SCHEMA,
"status": "error",
"error": "AssistantLoopFailed",
"message": cap_text(&outcome.summary, SUMMARY_CAP),
"turns": outcome.turns,
"model_used": outcome.model_used,
"receipts": Self::receipts_json(&outcome.tool_receipts),
"sandbox": self.posture.to_json(),
"elapsed_seconds": elapsed,
"suggestions": [
"Re-run the goal; the run failed mid-loop rather than completing with an answer.",
"Check `receipts` for what had already executed before the failure.",
],
})
}
}
pub fn startup_error_doc(error: &str, message: &str, suggestions: &[&str]) -> Value {
json!({
"schema": SCHEMA,
"status": "error",
"error": error,
"message": message,
"suggestions": suggestions,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn posture() -> SandboxPosture {
SandboxPosture {
sandboxed: true,
image: Some("python:3.11".into()),
tier: "SandboxEdit".into(),
root: "/work".into(),
fallback_notice: None,
}
}
#[test]
fn cap_text_states_what_it_dropped() {
let s = "x".repeat(100);
let out = cap_text(&s, 10);
assert!(out.starts_with(&"x".repeat(10)));
assert!(out.contains("90 elided"), "{out}");
}
#[test]
fn cap_text_leaves_short_input_untouched() {
assert_eq!(cap_text("short", 100), "short");
}
#[test]
fn cap_text_respects_char_boundaries() {
let s = "é".repeat(50);
let out = cap_text(&s, 11);
assert!(out.contains("elided"), "{out}");
}
#[test]
fn brief_prefers_identifying_keys_and_flattens_newlines() {
let p = json!({ "command": "cargo test\n--quiet", "body": "…huge…" });
assert_eq!(brief(&p), "cargo test --quiet");
}
#[test]
fn brief_is_empty_when_no_identifying_key_is_present() {
assert_eq!(brief(&json!({ "body": "opaque" })), "");
}
#[test]
fn brief_covers_the_tools_that_do_not_take_a_command_or_path() {
assert_eq!(brief(&json!({ "expression": "17 * 23" })), "17 * 23");
assert_eq!(
brief(&json!({ "query": "rust lifetimes" })),
"rust lifetimes"
);
assert_eq!(
brief(&json!({ "subject": "deploy cadence" })),
"deploy cadence"
);
}
#[test]
fn receipts_roll_up_counts_and_put_failures_in_the_sample_first() {
let mut receipts: Vec<AssistantToolReceipt> = (0..20)
.map(|i| AssistantToolReceipt {
tool: "shell".into(),
call_id: None,
ok: true,
params: json!({ "command": format!("ok-{i}") }),
})
.collect();
receipts.push(AssistantToolReceipt {
tool: "write_file".into(),
call_id: None,
ok: false,
params: json!({ "path": "/denied" }),
});
let v = JsonEmitter::receipts_json(&receipts);
assert_eq!(v["total"], 21);
assert_eq!(v["failed"], 1);
assert_eq!(v["by_tool"]["shell"], 20);
assert_eq!(v["by_tool"]["write_file"], 1);
assert_eq!(v["sample"][0]["tool"], "write_file");
assert_eq!(v["sample"].as_array().unwrap().len(), RECEIPT_SAMPLE);
assert_eq!(v["sample_omitted"], 21 - RECEIPT_SAMPLE);
}
#[test]
fn sandbox_posture_distinguishes_a_fallback_from_a_choice() {
let chosen = posture().to_json();
assert_eq!(chosen["mode"], "docker");
assert_eq!(chosen["network"], "none");
assert!(chosen["fallback_notice"].is_null());
let fell_back = SandboxPosture {
sandboxed: false,
image: None,
fallback_notice: Some("Docker not running".into()),
..posture()
}
.to_json();
assert_eq!(fell_back["mode"], "local");
assert_eq!(fell_back["network"], "host");
assert_eq!(fell_back["fallback_notice"], "Docker not running");
}
#[test]
fn an_errored_run_is_not_reported_as_a_result() {
let outcome = AssistantOutcome {
status: "error",
summary: "connection reset by peer".into(),
turns: 9,
tools_called: vec![],
tool_receipts: vec![AssistantToolReceipt {
tool: "shell".into(),
call_id: None,
ok: true,
params: json!({ "command": "ls" }),
}],
model_used: "claude-opus-5".into(),
};
let doc = error_doc_for(&outcome);
assert_eq!(doc["status"], "error");
assert!(doc.get("summary").is_none(), "summary leaked: {doc}");
assert_eq!(doc["message"], "connection reset by peer");
assert_eq!(doc["receipts"]["total"], 1);
}
fn error_doc_for(outcome: &AssistantOutcome) -> Value {
json!({
"schema": SCHEMA,
"status": "error",
"error": "AssistantLoopFailed",
"message": cap_text(&outcome.summary, SUMMARY_CAP),
"turns": outcome.turns,
"model_used": outcome.model_used,
"receipts": JsonEmitter::receipts_json(&outcome.tool_receipts),
})
}
#[derive(Default)]
struct Captured(std::sync::Mutex<Vec<Value>>);
impl EventSink for Captured {
fn emit(&self, event: Value) {
self.0.lock().unwrap().push(event);
}
}
#[test]
fn events_go_to_the_sink_and_the_document_comes_back() {
let sink = Arc::new(Captured::default());
let emitter = JsonEmitter::new(posture(), sink.clone());
emitter.started("do the thing", "claude-opus-5");
emitter.on_assistant_event(&AssistantEvent::ToolCall {
name: "shell".into(),
params: json!({ "command": "ls" }),
});
let doc = emitter.finish(
&AssistantOutcome {
status: "success",
summary: "did the thing".into(),
turns: 2,
tools_called: vec!["shell".into()],
tool_receipts: vec![AssistantToolReceipt {
tool: "shell".into(),
call_id: None,
ok: true,
params: json!({ "command": "ls" }),
}],
model_used: "claude-opus-5".into(),
},
None,
);
assert_eq!(doc["schema"], SCHEMA);
assert_eq!(doc["status"], "success");
assert_eq!(doc["summary"], "did the thing");
assert_eq!(doc["receipts"]["total"], 1);
assert_eq!(doc["sandbox"]["mode"], "docker");
let events = sink.0.lock().unwrap().clone();
let types: Vec<&str> = events
.iter()
.map(|e| e["type"].as_str().unwrap_or_default())
.collect();
assert_eq!(types, vec!["started", "tool_called", "completed"]);
assert_eq!(events[1]["data"]["tool"], "shell");
assert_eq!(events[1]["data"]["brief"], "ls");
}
#[test]
fn goal_report_carries_grounded_separately_from_passed() {
let g = GoalReport {
check: "cargo test -q".into(),
passed: true,
grounded: false,
iterations: 3,
halt: None,
}
.to_json();
assert_eq!(g["passed"], true);
assert_eq!(g["grounded"], false);
}
}