use async_trait::async_trait;
use serde_json::{Value, json};
use theway_core::multiagent::jobs::SubagentJobRegistry;
use theway_core::{
AgentTool, AgentToolError, AgentToolResult, AgentToolUpdate, StreamFn, ToolExecutionMode,
};
use theway_llm_provider::{Model, Tool, UserContentBlock};
use tokio_util::sync::CancellationToken;
use theway_core::multiagent::runner::{AgentRunOptions, filter_tool_set, run_agent};
use theway_core::multiagent::types::AgentRunResolver;
use theway_core::multiagent::types::ToolSetResolver;
pub type SubagentToolsFn = ToolSetResolver;
pub struct SubagentTool {
model: Option<Model>,
stream_fn: Option<StreamFn>,
subagent_tools: SubagentToolsFn,
launch_resolver: AgentRunResolver,
spec_names: Vec<String>,
definition: Tool,
registry: SubagentJobRegistry,
session_id: Option<String>,
}
impl SubagentTool {
pub fn new(
model: impl Into<Option<Model>>,
stream_fn: Option<StreamFn>,
subagent_tools: SubagentToolsFn,
launch_resolver: AgentRunResolver,
spec_names: Vec<String>,
registry: SubagentJobRegistry,
) -> Self {
Self {
definition: build_definition(&spec_names),
model: model.into(),
stream_fn,
subagent_tools,
launch_resolver,
spec_names,
registry,
session_id: None,
}
}
#[allow(dead_code)]
pub fn with_session_id(mut self, session_id: Option<String>) -> Self {
self.session_id = session_id;
self
}
}
#[async_trait]
impl AgentTool for SubagentTool {
fn definition(&self) -> &Tool {
&self.definition
}
fn label(&self) -> &str {
"subagent"
}
fn execution_mode(&self) -> Option<ToolExecutionMode> {
Some(ToolExecutionMode::Parallel)
}
async fn execute(
&self,
_id: &str,
params: Value,
parent_cancel: CancellationToken,
_on_update: Option<AgentToolUpdate>,
) -> Result<AgentToolResult, AgentToolError> {
let subagent_type = params
.get("subagent_type")
.and_then(|v| v.as_str())
.unwrap_or("general");
if !self.spec_names.iter().any(|n| n == subagent_type) {
return Err(AgentToolError::Message(format!(
"unknown subagent_type: {subagent_type} (allowed: {})",
self.spec_names.join(", ")
)));
}
let prompt = params
.get("prompt")
.and_then(|v| v.as_str())
.ok_or_else(|| AgentToolError::Message("missing required arg: prompt".into()))?
.to_string();
let description = params
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let max_iterations = params
.get("max_iterations")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let tools_allow: Option<Vec<String>> =
params.get("tools").and_then(|v| v.as_array()).map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
});
let mut launch = (self.launch_resolver)(subagent_type).ok_or_else(|| {
AgentToolError::Message(format!("unknown subagent_type: {subagent_type}"))
})?;
if let Some(n) = max_iterations {
launch.max_iterations = n;
}
let tools = (self.subagent_tools)(subagent_type);
let tools = match tools_allow.as_deref() {
None => tools,
Some(allow) => filter_tool_set(tools, allow).map_err(AgentToolError::Message)?,
};
let model = self.model.clone().ok_or_else(|| {
AgentToolError::Message(
"no model set for this session; select a model in the TUI before delegating"
.to_string(),
)
})?;
let result = run_agent(AgentRunOptions {
launch,
tools,
prompt,
model,
stream_fn: self.stream_fn.clone(),
timeout: None,
thinking: None,
registry: self.registry.clone(),
source: "subagent".into(),
run_id: None,
node_id: None,
session_id: self.session_id.clone(),
observation_parent: None,
cancel: parent_cancel.clone(),
system_prompt_extra: Some(format!("Description of your task: {description}")),
on_turn_end: None,
})
.await;
if parent_cancel.is_cancelled() {
return Err(AgentToolError::Message("cancelled".into()));
}
if let Some(err) = result.error {
return Err(AgentToolError::Message(format!("subagent failed: {err}")));
}
let body = if result.text.is_empty() {
"(subagent produced no text output)".to_string()
} else {
result.text
};
Ok(AgentToolResult {
content: vec![UserContentBlock::text(body.clone())],
details: json!({
"subagent_type": subagent_type,
"description": description,
"chars": body.len(),
}),
terminate: None,
})
}
}
fn build_definition(spec_names: &[String]) -> Tool {
Tool {
name: "subagent".into(),
description:
"Delegate a self-contained task to a fresh sub-agent. The subagent gets its own context window and the uniform subagent tool set (engine tools minus subagent/dag_* plus local tools); this tool returns a single text result from the subagent. Use this when you need to inspect a large surface area (search, file reads) or run a contained change without polluting the main conversation.\n\
Budget: the subagent defaults to 300 LLM-turn attempts — the code-harness budget (compile → fix loops need it). For short, fast tasks (a quick read, a single check) lower max_iterations to a reasonable range like 4-32.\n\
Tools: by default the subagent gets every orchestrator tool except dag_* and subagent; pass tools: [\"read\", \"bash\"] to restrict it to specific tools (unknown names fail the call).".into(),
parameters: json!({
"type": "object",
"properties": {
"subagent_type": {
"type": "string",
"enum": spec_names,
"description": "Which subagent spec to spawn. All specs share ONE uniform tool set (engine tools minus subagent/dag_* plus local tools); the spec's system prompt defines the role (e.g. explorer: research, planner: planning, executor-coder: implementation, checker: verification, general: research).",
"default": "general",
},
"description": {
"type": "string",
"description": "Short label for the task (visible in UI logs).",
},
"prompt": {
"type": "string",
"description": "Full prompt the subagent will receive as its user message.",
},
"max_iterations": {
"type": "number",
"description": "Iteration-budget override (LLM-turn attempts); when set it wins over the spec default.",
},
"tools": {
"type": "array",
"items": { "type": "string" },
"description": "Tool allowlist (tool names): restricts the subagent to exactly these tools; unknown names fail the call. Omit for the full subagent tool set.",
},
},
"required": ["prompt"],
"additionalProperties": false,
}),
}
}
#[cfg(test)]
tests_bridge_macro::tests_bridge!("tools/subagent");
#[cfg(test)]
mod subagent_extra {
tests_bridge_macro::tests_bridge!("tools/subagent/extra");
}