Normalize shell and patch tool cards

This commit is contained in:
iris committed 2026-09-09 20:30:52 -04:00
1 parent 4dc3e3d784
commit b507656abd
11 files changed
+327 -16

No files matched your search

+87 -3
View File
@@ -15,7 +15,7 @@ use std::sync::{Arc, Mutex};
use serde_json::{Value, json};
use super::super::driver::{Event, QuestionOption, SessionStatus, context_tokens};
use super::super::driver::{Event, QuestionOption, SessionStatus, context_tokens, patch_start};
use super::super::subagent::Subagents;
/// Whether this line is the CLI opening a fresh model call.
@@ -125,6 +125,10 @@ pub(super) struct Translator {
/// translator existed, which is every one of them after a backend
/// restart adopts a running session.
open_tasks: HashSet<String>,
/// File-edit calls whose successful boilerplate result should not be drawn below their diff.
/// Each leaves here with its `tool_result`; a failure keeps its text because that is the part a
/// reader needs to act on.
patches: HashSet<String>,
/// Whether a turn is open, judged from this translator's own output: the
/// events that [`super::proves_a_turn`] accepts open one, and the status
/// that ends a turn closes it.
@@ -151,6 +155,7 @@ impl Translator {
rate_limited: false,
tasks: HashMap::new(),
open_tasks: HashSet::new(),
patches: HashSet::new(),
in_turn: false,
}
}
@@ -763,7 +768,12 @@ impl Translator {
if tool == "Task" || tool == "Agent" {
self.start_subagent_from_task(&id, &input);
}
Event::ToolStart { id, tool, input }
if tool == "Edit" {
self.patches.insert(id.clone());
patch_start(id, replacement_diff(&input))
} else {
Event::ToolStart { id, tool, input }
}
})
.collect()
}
@@ -936,7 +946,7 @@ impl Translator {
/// into the session dir and referenced by an Image event. Replayed and
/// synthetic user text is skipped -- the manager already recorded the
/// user's side.
fn translate_user(&self, message: &Value) -> Vec<Event> {
fn translate_user(&mut self, message: &Value) -> Vec<Event> {
// Only tool results are here. The CLI never echoes a person's own
// message back on stdout -- measured, because the obvious way to learn
// that a queued message had been taken was to watch for it coming back
@@ -979,6 +989,10 @@ impl Translator {
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let patch = self.patches.remove(&about);
if patch && block.get("is_error").and_then(Value::as_bool) != Some(true) {
texts.clear();
}
for image in images {
events.push(Event::Image {
image,
@@ -999,6 +1013,38 @@ impl Translator {
}
}
fn replacement_diff(input: &Value) -> String {
let path = input
.get("file_path")
.and_then(Value::as_str)
.unwrap_or("file");
let old = input
.get("old_string")
.and_then(Value::as_str)
.unwrap_or_default();
let new = input
.get("new_string")
.and_then(Value::as_str)
.unwrap_or_default();
format!(
"--- {path}\n+++ {path}\n@@\n{}{}",
prefixed_lines('-', old),
prefixed_lines('+', new)
)
}
fn prefixed_lines(prefix: char, text: &str) -> String {
text.split_inclusive('\n')
.map(|line| {
if line.ends_with('\n') {
format!("{prefix}{line}")
} else {
format!("{prefix}{line}\n")
}
})
.collect()
}
/// The title to start a subagent under when its own first line arrives
/// before (or without) its Task call ever being seen: the tool name of that
/// first line, which is the only thing known about it yet. `"subagent"` for
@@ -1351,6 +1397,44 @@ mod tests {
);
}
#[test]
fn an_edit_becomes_a_patch_and_drops_only_its_success_boilerplate() {
let dir = tempfile::tempdir().expect("tempdir");
let mut translator = Translator::new(dir.path().to_path_buf(), test_subagents(&dir));
let events = translate_lines(
&mut translator,
&[
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"edit-1","name":"Edit","input":{"file_path":"src/main.rs","old_string":"old","new_string":"new"}}]},"parent_tool_use_id":null}"#,
r#"{"type":"user","message":{"role":"user","content":[{"tool_use_id":"edit-1","type":"tool_result","content":"The file src/main.rs has been updated successfully.","is_error":false}]},"parent_tool_use_id":null}"#,
],
);
assert_eq!(
events,
vec![
patch_start(
"edit-1".to_string(),
"--- src/main.rs\n+++ src/main.rs\n@@\n-old\n+new\n".to_string()
),
Event::ToolEnd {
id: "edit-1".to_string(),
output: String::new()
}
]
);
let failed = translate_lines(
&mut translator,
&[
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"edit-2","name":"Edit","input":{"file_path":"src/main.rs","old_string":"missing","new_string":"new"}}]},"parent_tool_use_id":null}"#,
r#"{"type":"user","message":{"role":"user","content":[{"tool_use_id":"edit-2","type":"tool_result","content":"old_string was not found","is_error":true}]},"parent_tool_use_id":null}"#,
],
);
assert!(matches!(
&failed[1],
Event::ToolEnd { output, .. } if output == "old_string was not found"
));
}
#[test]
fn subagent_events_are_not_duplicated_into_the_transcript() {
let dir = tempfile::tempdir().expect("tempdir");