Implement local sub-agent execution for OSS/Bedrock mode

- Fix start_agent tool mapping: route to Tool::StartAgent instead of
  dead-end Tool::Subagent so tool calls become executable actions
- Block start_agent until child finishes: parent waits for child
  conversation to complete and receives full output as tool result
- Fix tool result delivery: add StartAgent/StartAgentV2 cases to
  extract_tool_result_content so the model actually sees agent output
- Support parallel agent spawning: change StartAgent action phase from
  Serial to Parallel, and track multiple pending agents via Vec
- Mark child conversations as Success on EndTurn: emit
  ConversationStatus::Success when a child stream ends with no actions
- Skip orchestration SSE in local mode: prevent app freeze from trying
  to connect to non-existent server
- Remove send_message_to_agent and suggest_next_prompt from tool list:
  these require server infrastructure that doesn't exist in OSS mode
- Add "Waiting for sub-agents..." status message while agents process
- Fix child agent pane close: actually dismiss instead of re-hiding,
  track dismissed IDs to prevent re-creation on restart
- Fix cache_miss_tokens calculation and show per-block cache stats
- Add [tool-debug] logging throughout tool invocation pipeline

Bump version to 1.6.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Ward
2026-06-03 13:29:09 -05:00
co-authored by Claude Opus 4.6
parent 17ecf67970
commit a309006458
18 changed files with 458 additions and 114 deletions
+30 -25
View File
@@ -1109,7 +1109,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
},
ToolDefinition {
name: "start_agent".to_string(),
description: "Start a sub-agent to handle a specific task autonomously. Use for delegating independent work that can run in parallel. The agent gets its own conversation context and tool access.".to_string(),
description: "Start a sub-agent to handle a specific task autonomously. Use for delegating independent work that can run in parallel. The agent gets its own conversation context and tool access. IMPORTANT: Only use this for the initial investigation or when genuinely new research is needed. Do NOT re-spawn agents for follow-up questions if you already have their output in context — just answer from the information you already have.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
@@ -1119,18 +1119,9 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
"required": ["name", "prompt"]
}),
},
ToolDefinition {
name: "send_message_to_agent".to_string(),
description: "Send a message to a running sub-agent. Use to provide additional context, ask for updates, or redirect the agent's work.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "ID of the target sub-agent" },
"message": { "type": "string", "description": "Message to send to the agent" }
},
"required": ["agent_id", "message"]
}),
},
// send_message_to_agent removed — child agents in local/Bedrock mode
// run autonomously and cannot receive messages. Exposing this tool
// causes the model to poll in a loop.
ToolDefinition {
name: "ask_user_question".to_string(),
description: "Ask the user a question when you need clarification or a decision. Present clear options when possible. Use sparingly — prefer making reasonable assumptions.".to_string(),
@@ -1143,18 +1134,10 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
"required": ["question"]
}),
},
ToolDefinition {
name: "suggest_next_prompt".to_string(),
description: "After completing a task, suggest a relevant follow-up action. Only call once at the end of your response. Keep labels concise and action-oriented.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"prompt": { "type": "string", "description": "The full prompt text sent to the agent if the user clicks" },
"label": { "type": "string", "description": "Short display label (under 40 chars)" }
},
"required": ["prompt", "label"]
}),
},
// suggest_next_prompt removed — its executor hangs waiting for UI
// interaction that doesn't exist in the Bedrock path. The stream-level
// skip (response_translator) prevents deadlocks, but removing the tool
// definition avoids wasting output tokens on calls that will be discarded.
ToolDefinition {
name: "read_skill".to_string(),
description: "Read a skill definition to understand available capabilities and how to use them.".to_string(),
@@ -1398,6 +1381,28 @@ fn extract_tool_result_content(result: &api::request::input::ToolCallResult) ->
}
None => ("Write to shell command completed.".to_string(), false),
},
api::request::input::tool_call_result::Result::StartAgent(start_agent_result) => {
match &start_agent_result.result {
Some(api::start_agent_result::Result::Success(success)) => {
(success.agent_id.clone(), false)
}
Some(api::start_agent_result::Result::Error(error)) => {
(format!("Agent error: {}", error.error), true)
}
None => ("Agent completed.".to_string(), false),
}
}
api::request::input::tool_call_result::Result::StartAgentV2(start_agent_result) => {
match &start_agent_result.result {
Some(api::start_agent_v2_result::Result::Success(success)) => {
(success.agent_id.clone(), false)
}
Some(api::start_agent_v2_result::Result::Error(error)) => {
(format!("Agent error: {}", error.error), true)
}
None => ("Agent completed.".to_string(), false),
}
}
_ => ("Tool completed successfully.".to_string(), false),
}
} else {
+28 -6
View File
@@ -104,6 +104,7 @@ pub fn bedrock_stream_to_response_events(
let mut current_tool_name = String::new();
let mut current_tool_input_json = String::new();
let mut _has_tool_calls = false;
let mut has_start_agent_calls = false;
let mut input_tokens: i32 = 0;
let mut output_tokens: i32 = 0;
let mut cache_read_input_tokens: i32 = 0;
@@ -275,6 +276,9 @@ pub fn bedrock_stream_to_response_events(
current_tool_name, current_tool_use_id, current_tool_input_json
));
}
if current_tool_name == "start_agent" {
has_start_agent_calls = true;
}
let tool_msg = build_tool_call_message(
&task_id,
&current_tool_use_id,
@@ -372,12 +376,29 @@ pub fn bedrock_stream_to_response_events(
if !text_flushed {
let add_msg = build_add_agent_output_message(&task_id, &msg_id, &buffered_text);
yield Ok(add_msg);
text_flushed = true;
current_text_message_id = Some(msg_id);
} else {
let append = build_append_text(&task_id, &msg_id, &buffered_text);
yield Ok(append);
}
}
// Show a status message while sub-agents are working so the user
// knows we're waiting on them.
if has_start_agent_calls {
let status_text = "\n\n*Waiting for sub-agents to finish, then I'll review and continue...*";
if text_flushed {
let msg_id = current_text_message_id.as_ref().unwrap();
let append = build_append_text(&task_id, msg_id, status_text);
yield Ok(append);
} else {
let msg_id = Uuid::new_v4().to_string();
let add_msg = build_add_agent_output_message(&task_id, &msg_id, status_text);
yield Ok(add_msg);
}
}
let cost = estimate_cost_cents(
input_tokens as u32,
output_tokens as u32,
@@ -985,13 +1006,14 @@ fn build_tool_call_message(
))
}
"start_agent" => {
let _name = input.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
let name = input.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
let prompt = input.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
Some(api::message::tool_call::Tool::Subagent(
api::message::tool_call::Subagent {
task_id: String::new(),
payload: prompt,
metadata: None,
Some(api::message::tool_call::Tool::StartAgent(
api::StartAgent {
name,
prompt,
execution_mode: None,
lifecycle_subscription: None,
},
))
}