From dd7b799160780b0e0212c89f1fe762b5daf17c1d Mon Sep 17 00:00:00 2001 From: Mintea97 Date: Fri, 30 Jan 2026 05:06:39 +0000 Subject: [PATCH] Fix bug with agent self spawning Claude is trying to spawn coder while being a coder when running in fork context mode. Claude is thinking its still the parent agent cause it sees a lot of the parent's context. When it tries to spawn a coder while being a coder it would get an error but we also incorrectly stopped the conversation so it couldnt retry and continue. This fixes both bugs --- tycode-core/src/chat/tools.rs | 14 ++++ tycode-core/src/spawn/spawn_agent.rs | 2 +- tycode-core/tests/modules/spawn.rs | 118 ++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 4 deletions(-) diff --git a/tycode-core/src/chat/tools.rs b/tycode-core/src/chat/tools.rs index 1d6a969..f8b637b 100644 --- a/tycode-core/src/chat/tools.rs +++ b/tycode-core/src/chat/tools.rs @@ -490,6 +490,20 @@ async fn execute_push_agent( if let Some(parent) = state.agent_stack.last() { new_agent.conversation = parent.conversation.clone(); } + + // Orientation message helps spawned agent understand its context + let orientation = format!( + "--- AGENT TRANSITION ---\n\ + You are now a {} sub-agent spawned to handle a specific task. \ + The conversation above is from the parent agent - use it for context only. \ + Focus on completing your assigned task below. \ + When done, use complete_task to return control to the parent.", + agent_type + ); + new_agent.conversation.push(Message { + role: MessageRole::User, + content: Content::text_only(orientation), + }); } new_agent.conversation.push(Message { diff --git a/tycode-core/src/spawn/spawn_agent.rs b/tycode-core/src/spawn/spawn_agent.rs index eb605a4..f09ecd5 100644 --- a/tycode-core/src/spawn/spawn_agent.rs +++ b/tycode-core/src/spawn/spawn_agent.rs @@ -77,7 +77,7 @@ impl ToolCallHandle for SpawnAgentHandle { self.agent_type ), is_error: true, - continuation: ContinuationPreference::Stop, + continuation: ContinuationPreference::Continue, ui_result: ToolExecutionResult::Error { short_message: format!("Cannot spawn self ({})", self.agent_type), detailed_message: format!( diff --git a/tycode-core/tests/modules/spawn.rs b/tycode-core/tests/modules/spawn.rs index 8b2b5cb..ccd668b 100644 --- a/tycode-core/tests/modules/spawn.rs +++ b/tycode-core/tests/modules/spawn.rs @@ -88,15 +88,100 @@ fn test_spawn_agent_description_includes_allowed_agents() { }); } +#[test] +fn test_spawned_agent_receives_orientation_message() { + fixture::run_with_agent("coordinator", |mut fixture| async move { + // Coordinator spawns a coder - should include orientation message + // Use BehaviorQueue: coordinator spawns coder, coder completes immediately + fixture.set_mock_behavior(MockBehavior::BehaviorQueue { + behaviors: vec![ + // Coordinator spawns coder + MockBehavior::ToolUse { + tool_name: "spawn_agent".to_string(), + tool_arguments: r#"{"agent_type": "coder", "task": "Write a test file"}"# + .to_string(), + }, + // Spawned coder completes immediately + MockBehavior::ToolUse { + tool_name: "complete_task".to_string(), + tool_arguments: r#"{"result": "done", "success": true}"#.to_string(), + }, + // Back to coordinator + MockBehavior::Success, + ], + }); + fixture.step("I need help writing tests").await; + + let all_requests = fixture.get_all_ai_requests(); + // First request is coordinator, second is the spawned coder + assert!( + all_requests.len() >= 2, + "Expected at least 2 AI requests (coordinator + spawned coder), got {}", + all_requests.len() + ); + + // The spawned coder's request should contain the orientation message + let coder_request = &all_requests[1]; + let messages_text: String = coder_request + .messages + .iter() + .map(|m| m.content.text()) + .collect::>() + .join("\n"); + + assert!( + messages_text.contains("AGENT TRANSITION"), + "Spawned agent should receive orientation message with AGENT TRANSITION marker. Messages: {}", + messages_text + ); + + assert!( + messages_text.contains("sub-agent"), + "Orientation message should mention sub-agent role. Messages: {}", + messages_text + ); + + assert!( + messages_text.contains("complete_task"), + "Orientation message should mention complete_task. Messages: {}", + messages_text + ); + + // Verify the task comes after the orientation + let orientation_pos = messages_text.find("AGENT TRANSITION"); + let task_pos = messages_text.find("Write a test file"); + assert!( + orientation_pos < task_pos, + "Orientation message should appear before the task. Orientation at {:?}, task at {:?}", + orientation_pos, + task_pos + ); + }); +} + #[test] fn test_coder_cannot_spawn_itself() { + use tycode_core::ai::ContentBlock; use tycode_core::chat::ChatEvent; fixture::run_with_agent("coder", |mut fixture| async move { // Coder tries to spawn another coder - should fail - fixture.set_mock_behavior(MockBehavior::ToolUseThenSuccess { - tool_name: "spawn_agent".to_string(), - tool_arguments: r#"{"agent_type": "coder", "task": "do something"}"#.to_string(), + // Use BehaviorQueue: coder tries to spawn (fails), then completes after error feedback + fixture.set_mock_behavior(MockBehavior::BehaviorQueue { + behaviors: vec![ + // Coder tries to spawn coder (will fail) + MockBehavior::ToolUse { + tool_name: "spawn_agent".to_string(), + tool_arguments: r#"{"agent_type": "coder", "task": "do something"}"# + .to_string(), + }, + // After error feedback, coder completes + MockBehavior::ToolUse { + tool_name: "complete_task".to_string(), + tool_arguments: r#"{"result": "failed to spawn", "success": false}"# + .to_string(), + }, + ], }); let events = fixture.step("I need help with a task").await; @@ -120,5 +205,32 @@ fn test_coder_cannot_spawn_itself() { "Coder should not be able to spawn itself. Events: {:?}", events ); + + // Verify the actor continued and the error was fed back to the model. + // If an anyhow error crashed the actor, there won't be a second AI request. + let all_requests = fixture.get_all_ai_requests(); + assert!( + all_requests.len() >= 2, + "Expected at least 2 AI requests (tool use + tool result), got {}", + all_requests.len() + ); + + // The second request should contain the tool error result fed back to the model + let second_request = &all_requests[1]; + let has_error_tool_result = second_request.messages.iter().any(|msg| { + msg.content.blocks().iter().any(|block| { + if let ContentBlock::ToolResult(result) = block { + result.is_error + } else { + false + } + }) + }); + + assert!( + has_error_tool_result, + "Second AI request should contain tool_result with is_error=true. Messages: {:?}", + second_request.messages + ); }); }