Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tycode-core/src/chat/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tycode-core/src/spawn/spawn_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
118 changes: 115 additions & 3 deletions tycode-core/tests/modules/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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;

Expand All @@ -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
);
});
}