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
+47
View File
@@ -465,6 +465,16 @@ impl BlocklistAIActionModel {
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
let pending_count = self
.pending_actions
.get(&conversation_id)
.map(|q| q.len())
.unwrap_or(0);
log::info!(
"[tool-debug] try_to_execute_available_actions: conversation={:?}, pending_count={}",
conversation_id,
pending_count
);
loop {
let Some(front_action) = self
.pending_actions
@@ -472,9 +482,16 @@ impl BlocklistAIActionModel {
.and_then(|queue| queue.front())
.cloned()
else {
log::info!("[tool-debug] try_to_execute_available_actions: no more pending actions");
return;
};
log::info!(
"[tool-debug] try_to_execute_available_actions: trying action id={:?}, type={:?}",
front_action.id,
std::mem::discriminant(&front_action.action)
);
if let Some(current_phase) = self.action_execution_phase(conversation_id) {
if !self.can_start_action_in_current_phase(
&front_action,
@@ -482,6 +499,10 @@ impl BlocklistAIActionModel {
current_phase,
ctx,
) {
log::info!(
"[tool-debug] try_to_execute_available_actions: cannot start in current phase {:?}",
current_phase
);
return;
}
}
@@ -489,15 +510,22 @@ impl BlocklistAIActionModel {
let Some(result) =
self.start_pending_action_by_id(&front_action.id, conversation_id, false, ctx)
else {
log::info!("[tool-debug] try_to_execute_available_actions: start_pending_action_by_id returned None (blocked)");
return;
};
log::info!(
"[tool-debug] try_to_execute_available_actions: action started, result={:?}",
std::mem::discriminant(&result)
);
if matches!(
result,
StartedAction::Async {
phase: RunningActionPhase::Serial
}
) {
log::info!("[tool-debug] try_to_execute_available_actions: serial async action, stopping loop");
return;
}
}
@@ -853,6 +881,19 @@ impl BlocklistAIActionModel {
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
log::info!(
"[tool-debug] queue_actions: queuing {} actions for conversation {:?}",
actions.len(),
conversation_id
);
for (i, action) in actions.iter().enumerate() {
log::info!(
"[tool-debug] queue_actions: [{}] id={:?}, type={:?}",
i,
action.id,
std::mem::discriminant(&action.action)
);
}
self.action_order.insert(
conversation_id,
actions
@@ -1135,6 +1176,12 @@ impl BlocklistAIActionModel {
cancellation_reason: Option<CancellationReason>,
ctx: &mut ModelContext<Self>,
) {
log::info!(
"[tool-debug] handle_action_result: action_id={:?}, result_type={:?}, cancellation={:?}",
action_result.id,
std::mem::discriminant(&action_result.result),
cancellation_reason
);
let should_remove_entry =
self.running_actions
.get_mut(&conversation_id)