use crate::agent_loop::{agent_loop, AgentLoopConfig};
use crate::context::ExecutionLimits;
use crate::provider::{ModelConfig, StreamProvider};
use crate::types::*;
use std::sync::Arc;
use tokio::sync::mpsc;
const DEFAULT_MAX_TURNS: usize = 10;
pub struct SubAgentTool {
tool_name: String,
tool_description: String,
system_prompt: String,
model_config: ModelConfig,
provider_override: Option<Arc<dyn StreamProvider>>,
tools: Vec<Arc<dyn AgentTool>>,
thinking_level: ThinkingLevel,
max_tokens: Option<u32>,
cache_config: CacheConfig,
tool_execution: ToolExecutionStrategy,
retry_config: crate::provider::retry::RetryConfig,
max_turns: usize,
parent_loop_id: Option<String>,
}
impl SubAgentTool {
pub fn new(name: impl Into<String>, model_config: ModelConfig) -> Self {
let name = name.into();
Self {
tool_description: format!("Delegate a task to the '{}' sub-agent", name),
tool_name: name,
system_prompt: String::new(),
model_config,
provider_override: None,
tools: Vec::new(),
thinking_level: ThinkingLevel::Off,
max_tokens: None,
cache_config: CacheConfig::default(),
tool_execution: ToolExecutionStrategy::default(),
retry_config: crate::provider::retry::RetryConfig::default(),
max_turns: DEFAULT_MAX_TURNS,
parent_loop_id: None,
}
}
pub fn with_parent_loop_id(mut self, id: impl Into<String>) -> Self {
self.parent_loop_id = Some(id.into());
self
}
pub fn with_provider_override(mut self, provider: Arc<dyn StreamProvider>) -> Self {
self.provider_override = Some(provider);
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.tool_description = desc.into();
self
}
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = prompt.into();
self
}
pub fn with_tools(mut self, tools: Vec<Arc<dyn AgentTool>>) -> Self {
self.tools = tools;
self
}
pub fn with_thinking(mut self, level: ThinkingLevel) -> Self {
self.thinking_level = level;
self
}
pub fn with_max_tokens(mut self, max: u32) -> Self {
self.max_tokens = Some(max);
self
}
pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
self.cache_config = config;
self
}
pub fn with_tool_execution(mut self, strategy: ToolExecutionStrategy) -> Self {
self.tool_execution = strategy;
self
}
pub fn with_retry_config(mut self, config: crate::provider::retry::RetryConfig) -> Self {
self.retry_config = config;
self
}
pub fn with_max_turns(mut self, max: usize) -> Self {
self.max_turns = max;
self
}
}
#[async_trait::async_trait]
impl AgentTool for SubAgentTool {
fn name(&self) -> &str {
&self.tool_name
}
fn label(&self) -> &str {
&self.tool_name
}
fn description(&self) -> &str {
&self.tool_description
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The task to delegate to this sub-agent"
}
},
"required": ["task"]
})
}
async fn execute(
&self,
params: serde_json::Value, ctx: ToolContext, ) -> Result<ToolResult, ToolError> {
let cancel = ctx.cancel; let on_update = ctx.on_update; let on_progress = ctx.on_progress;
let task = params
.get("task") .and_then(|v| v.as_str()) .ok_or_else(|| ToolError::InvalidArgs("Missing required 'task' parameter".into()))?
.to_string();
let tools: Vec<Arc<dyn AgentTool>> = self.tools.iter().map(Arc::clone).collect();
let child_agent_id = uuid::Uuid::new_v4().to_string();
let child_session_id = uuid::Uuid::new_v4().to_string();
let child_loop_id = format!("{}.sub.1", child_session_id);
let mut context = AgentContext {
system_prompt: self.system_prompt.clone(),
messages: Vec::new(),
tools,
agent_id: Some(child_agent_id),
session_id: Some(child_session_id),
loop_id: Some(child_loop_id),
parent_loop_id: self.parent_loop_id.clone(), continuation_kind: None,
session: None,
user_context: Vec::new(),
inrun_context: Vec::new(),
active_node_id: None,
next_node_id: 0,
};
let config = AgentLoopConfig {
model_config: self.model_config.clone(),
provider_override: self.provider_override.clone(),
thinking_level: self.thinking_level,
max_tokens: self.max_tokens,
temperature: None,
convert_to_llm: None,
transform_context: None,
get_steering_messages: None,
get_follow_up_messages: None,
context_config: None,
execution_limits: Some(ExecutionLimits {
max_turns: self.max_turns,
max_total_tokens: 1_000_000,
max_duration: std::time::Duration::from_secs(300),
max_cost: None,
}),
cache_config: self.cache_config.clone(),
tool_execution: self.tool_execution.clone(),
tool_timeout: None,
response_format: crate::provider::ResponseFormat::Text,
retry_config: self.retry_config.clone(),
before_turn: None,
after_turn: None,
before_loop: None,
after_loop: None,
before_tool_execution: None,
after_tool_execution: None,
before_tool_execution_update: None,
after_tool_execution_update: None,
before_compaction_start: None,
after_compaction_end: None,
on_error: None,
input_filters: vec![],
first_turn_trigger: TurnTrigger::SubAgent,
config_id: None,
context_translation: None,
prun_pending: None,
revert_pending: None,
current_tool: None,
revert_render_policy: crate::types::RevertRenderPolicy::default(),
};
let (tx, mut rx) = mpsc::unbounded_channel();
let forward_handle = if on_update.is_some() || on_progress.is_some() {
let tool_name = self.tool_name.clone();
Some(tokio::spawn(async move {
while let Some(event) = rx.recv().await {
if let AgentEvent::ProgressMessage { text, .. } = &event {
if let Some(ref cb) = on_progress {
cb(text.clone());
}
}
if let Some(ref on_update) = on_update {
let update_text = match &event {
AgentEvent::MessageUpdate {
delta: StreamDelta::Text { delta },
..
} => Some(delta.clone()),
AgentEvent::ToolExecutionStart { tool_name, .. } => {
Some(format!("[sub-agent calling tool: {}]", tool_name))
}
_ => None,
};
if let Some(text) = update_text {
on_update(ToolResult {
content: vec![Content::Text { text }],
details: serde_json::json!({ "sub_agent": tool_name }),
child_loop_id: None,
});
}
}
}
}))
} else {
None
};
let prompt = AgentMessage::Llm(LlmMessage::new(Message::user(task)));
let new_messages = agent_loop(vec![prompt], &mut context, &config, tx, cancel).await;
let returned_child_loop_id = context.loop_id.clone();
if let Some(handle) = forward_handle {
let _ = handle.await; }
let result_text = extract_final_text(&new_messages);
let details = serde_json::json!({
"sub_agent": self.tool_name,
"turns": new_messages.len(),
});
Ok(ToolResult {
content: vec![Content::Text { text: result_text }],
details,
child_loop_id: returned_child_loop_id,
})
}
}
fn extract_final_text(messages: &[AgentMessage]) -> String {
for msg in messages.iter().rev() {
if let AgentMessage::Llm(LlmMessage {
message: Message::Assistant { content, .. },
..
}) = msg
{
let texts: Vec<&str> = content
.iter()
.filter_map(|c| match c {
Content::Text { text } => Some(text.as_str()),
_ => None,
})
.collect();
if !texts.is_empty() {
return texts.join("\n");
}
}
}
"(sub-agent produced no text output)".to_string()
}