Keep Claude commands out of subagents
This commit is contained in:
1 parent
8262ceb786
commit
1c60e78b55
2 files changed
+81
-49
No files matched your search
@@ -214,13 +214,15 @@ impl Translator {
|
||||
self.interrupting = true;
|
||||
}
|
||||
pub(super) fn translate(&mut self, message: &Value) -> Vec<Event> {
|
||||
// Events from subagents (Task tool internals) carry a
|
||||
// parent_tool_use_id; the transcript shows the Task tool's own
|
||||
// start/end instead of every nested step. Routed into that
|
||||
// subagent's own transcript rather than dropped -- see
|
||||
// `SUBAGENTS.md`.
|
||||
// A parent id says that a line is nested, not what it is nested in.
|
||||
// Only a Task/Agent call or an agent lifecycle edge may create the
|
||||
// subagent; the id alone is not proof of either.
|
||||
if let Some(parent_id) = message.get("parent_tool_use_id").and_then(Value::as_str) {
|
||||
return self.translate_child(parent_id, message);
|
||||
return if self.subagents.get(parent_id).is_some() {
|
||||
self.translate_child(parent_id, message)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}
|
||||
self.dispatch(message)
|
||||
}
|
||||
@@ -239,14 +241,7 @@ impl Translator {
|
||||
self.subagents.reopen(id);
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
// Nobody has heard of this id yet: the Task call itself
|
||||
// either has not been seen or never will be. Started here
|
||||
// with the best title available -- the tool name of this
|
||||
// first line -- since SUBAGENTS.md's real title only
|
||||
// arrives with the Task call.
|
||||
self.subagents.start(id, &fallback_title(message), None);
|
||||
}
|
||||
None => return Vec::new(),
|
||||
}
|
||||
let child = self
|
||||
.children
|
||||
@@ -625,10 +620,16 @@ impl Translator {
|
||||
}
|
||||
Some("task_started") => {
|
||||
// What makes the session `Waiting` when its turn ends. The
|
||||
// subagent itself is created by the Task `tool_use` in the
|
||||
// parent's own message, which arrives first and carries the
|
||||
// title this side shows.
|
||||
// Task/Agent tool call ordinarily created the subagent first.
|
||||
// The lifecycle edge is the recovery path when an adopted
|
||||
// current CLI's earlier tool call was not observed. Commands
|
||||
// use `local_bash`, so they must never enter the registry.
|
||||
if let Some(about) = about {
|
||||
if message.get("task_type").and_then(Value::as_str) == Some("local_agent")
|
||||
&& self.subagents.get(&about).is_none()
|
||||
{
|
||||
self.start_subagent_from_task(&about, message);
|
||||
}
|
||||
self.awaiting_task_summaries.remove(&about);
|
||||
self.open_tasks.insert(about);
|
||||
}
|
||||
@@ -1180,23 +1181,6 @@ fn prefixed_lines(prefix: char, text: &str) -> String {
|
||||
.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
|
||||
/// a line this cannot even find a tool name in, such as one that opens with
|
||||
/// something other than a tool call.
|
||||
fn fallback_title(message: &Value) -> String {
|
||||
message["message"]["content"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|block| block.get("type").and_then(Value::as_str) == Some("tool_use"))
|
||||
.and_then(|block| block.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("subagent")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Whether this line is a subagent's *own* turn ending -- the only thing
|
||||
/// that does, per `SUBAGENTS.md`: not the parent's `tool_result`, which for
|
||||
/// a background Task arrives at launch rather than at completion.
|
||||
@@ -1578,16 +1562,22 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_events_are_not_duplicated_into_the_transcript() {
|
||||
fn a_background_commands_nested_line_does_not_create_a_subagent() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), test_subagents(&dir));
|
||||
let subagents = test_subagents(&dir);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
let events = translate_lines(
|
||||
&mut translator,
|
||||
&[
|
||||
r#"{"type":"system","subtype":"task_started","task_id":"command1","tool_use_id":"toolu_parent","description":"cargo test","is_backgrounded":true,"task_type":"local_bash"}"#,
|
||||
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_02","name":"Bash","input":{}}]},"parent_tool_use_id":"toolu_parent"}"#,
|
||||
],
|
||||
);
|
||||
assert!(events.is_empty());
|
||||
assert!(
|
||||
subagents.get("toolu_parent").is_none(),
|
||||
"a nested command is not a subagent"
|
||||
);
|
||||
}
|
||||
|
||||
/// A child line does not just vanish from the parent -- it lands in its
|
||||
@@ -1599,11 +1589,18 @@ mod tests {
|
||||
let subagents = test_subagents(&dir);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_parent","name":"Agent","input":{"description":"helper"}}]},"parent_tool_use_id":null}"#,
|
||||
],
|
||||
);
|
||||
let events = translate_lines(
|
||||
&mut translator,
|
||||
&[
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_c1","name":"Bash","input":{"command":"echo hi"}}]},"parent_tool_use_id":"toolu_parent"}"#,
|
||||
],
|
||||
);
|
||||
assert!(events.is_empty());
|
||||
let subagent = subagents.get("toolu_parent").expect("subagent started");
|
||||
let lines = crate::session::transcript::read_after(&subagent.transcript_path(), 0)
|
||||
.expect("read subagent transcript");
|
||||
@@ -1645,6 +1642,35 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_lifecycle_edge_recovers_an_unseen_subagent() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let subagents = test_subagents(&dir);
|
||||
let mut translator = Translator::new(dir.path().to_path_buf(), Arc::clone(&subagents));
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[
|
||||
r#"{"type":"system","subtype":"task_started","task_id":"agent1","tool_use_id":"toolu_agent","description":"Investigate","subagent_type":"general-purpose","prompt":"Find it","is_backgrounded":true,"task_type":"local_agent"}"#,
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_read","name":"Read","input":{}}]},"parent_tool_use_id":"toolu_agent"}"#,
|
||||
],
|
||||
);
|
||||
|
||||
let rows = subagents.list(true);
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].title, "Investigate (general-purpose)");
|
||||
let subagent = subagents.get("toolu_agent").expect("subagent recovered");
|
||||
let lines = crate::session::transcript::read_after(&subagent.transcript_path(), 0)
|
||||
.expect("read subagent transcript");
|
||||
assert!(lines.iter().any(
|
||||
|entry| matches!(&entry.event, Event::UserMessage { text, .. } if text == "Find it")
|
||||
));
|
||||
assert!(
|
||||
lines.iter().any(
|
||||
|entry| matches!(&entry.event, Event::ToolStart { tool, .. } if tool == "Read")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// The parent's `tool_result` for the Task id is what ends the
|
||||
/// subagent -- SUBAGENTS.md's lifecycle #3 -- and nothing else does.
|
||||
#[test]
|
||||
@@ -2288,6 +2314,7 @@ mod tests {
|
||||
translate_lines(
|
||||
&mut translator,
|
||||
&[
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_task_a","name":"Agent","input":{"description":"helper a"}},{"type":"tool_use","id":"toolu_task_b","name":"Agent","input":{"description":"helper b"}}]},"parent_tool_use_id":null}"#,
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_a","name":"Bash","input":{}}]},"parent_tool_use_id":"toolu_task_a"}"#,
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_b","name":"Read","input":{}}]},"parent_tool_use_id":"toolu_task_b"}"#,
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user