use std::collections::BTreeMap;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use crate::agent::capability::Capability;
use crate::agent::driver::{LlmDriver, ToolDefinition};
use crate::agent::manifest::{AgentManifest, ResourceQuota};
use crate::agent::pool::{AgentPool, SpawnConfig};
use super::tool::{Tool, ToolResult};
#[derive(Debug, Clone)]
pub struct SubagentSpec {
pub name: String,
pub description: String,
pub system_prompt: String,
pub max_iterations: u32,
}
impl SubagentSpec {
pub fn general_purpose() -> Self {
Self {
name: "general-purpose".into(),
description: "General-purpose agent for researching questions, searching for code, \
and executing multi-step tasks. Use when the target is not known \
and you are not confident you will find the right match in the \
first few tries."
.into(),
system_prompt: "You are a general-purpose research subagent. Your job is to answer \
the user's question by gathering evidence from the codebase. Return \
a concise summary of findings, not running commentary."
.into(),
max_iterations: 10,
}
}
pub fn explore() -> Self {
Self {
name: "explore".into(),
description: "Fast agent specialized for exploring codebases. Use to find files \
by pattern, search code for keywords, or answer questions about \
structure. Returns a digest of findings."
.into(),
system_prompt: "You are a codebase exploration subagent. Prefer pmat_query over \
raw grep. Never edit files. Return a short digest of what you found \
with file:line citations."
.into(),
max_iterations: 8,
}
}
pub fn plan() -> Self {
Self {
name: "plan".into(),
description: "Software architect subagent for designing implementation plans. \
Use when you need a step-by-step plan, critical-file list, or \
architectural trade-offs before implementing."
.into(),
system_prompt: "You are a planning subagent. Read the relevant code, then return a \
numbered plan with (a) files to change, (b) order of changes, and \
(c) the key trade-off. Do not write code."
.into(),
max_iterations: 6,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SubagentRegistry {
by_name: BTreeMap<String, SubagentSpec>,
}
impl SubagentRegistry {
pub fn new() -> Self {
Self { by_name: BTreeMap::new() }
}
pub fn register(&mut self, spec: SubagentSpec) {
self.by_name.insert(spec.name.clone(), spec);
}
pub fn resolve(&self, name: &str) -> Option<&SubagentSpec> {
self.by_name.get(name)
}
pub fn len(&self) -> usize {
self.by_name.len()
}
pub fn is_empty(&self) -> bool {
self.by_name.is_empty()
}
pub fn names(&self) -> Vec<String> {
self.by_name.keys().cloned().collect()
}
}
pub fn default_registry() -> SubagentRegistry {
let mut r = SubagentRegistry::new();
r.register(SubagentSpec::general_purpose());
r.register(SubagentSpec::explore());
r.register(SubagentSpec::plan());
r
}
pub struct TaskTool {
registry: Arc<SubagentRegistry>,
pool: Arc<Mutex<AgentPool>>,
parent_manifest: AgentManifest,
current_depth: u32,
max_depth: u32,
}
impl TaskTool {
pub fn new(
registry: Arc<SubagentRegistry>,
pool: Arc<Mutex<AgentPool>>,
parent_manifest: AgentManifest,
current_depth: u32,
max_depth: u32,
) -> Self {
Self { registry, pool, parent_manifest, current_depth, max_depth }
}
pub fn from_driver(
driver: Arc<dyn LlmDriver>,
parent_manifest: AgentManifest,
max_depth: u32,
) -> Self {
Self::from_driver_with_registry(driver, parent_manifest, max_depth, default_registry())
}
pub fn from_driver_with_registry(
driver: Arc<dyn LlmDriver>,
parent_manifest: AgentManifest,
max_depth: u32,
registry: SubagentRegistry,
) -> Self {
let pool = Arc::new(Mutex::new(AgentPool::new(driver, 4)));
Self::new(Arc::new(registry), pool, parent_manifest, 0, max_depth)
}
fn build_child_manifest(&self, spec: &SubagentSpec) -> AgentManifest {
let mut child = self.parent_manifest.clone();
child.name = format!("{}/{}", self.parent_manifest.name, spec.name);
child.model.system_prompt = spec.system_prompt.clone();
child.resources = ResourceQuota {
max_iterations: child.resources.max_iterations.min(spec.max_iterations),
..child.resources.clone()
};
child
}
}
#[async_trait]
impl Tool for TaskTool {
fn name(&self) -> &'static str {
"task"
}
fn definition(&self) -> ToolDefinition {
let names = self.registry.names();
let listed = if names.is_empty() { "(none registered)".into() } else { names.join(", ") };
ToolDefinition {
name: "task".into(),
description: format!(
"Launch a typed sub-agent to handle a delegated task. \
Registered subagent types: {listed}. \
The child runs its own perceive-reason-act loop and its \
final response is returned as the tool result."
),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"subagent_type": {
"type": "string",
"description": "Registered subagent type (e.g. general-purpose, explore, plan)"
},
"description": {
"type": "string",
"description": "Short (3-7 word) label for the task — shown in telemetry"
},
"prompt": {
"type": "string",
"description": "The full task delegated to the child subagent"
}
},
"required": ["subagent_type", "prompt"]
}),
}
}
async fn execute(&self, input: serde_json::Value) -> ToolResult {
if self.current_depth >= self.max_depth {
return ToolResult::error(format!(
"task depth limit reached ({}/{})",
self.current_depth, self.max_depth,
));
}
let subagent_type = match input.get("subagent_type").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return ToolResult::error("missing required field: subagent_type");
}
};
let prompt = match input.get("prompt").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => {
return ToolResult::error("missing required field: prompt");
}
};
let spec = match self.registry.resolve(subagent_type) {
Some(s) => s.clone(),
None => {
let names = self.registry.names().join(", ");
return ToolResult::error(format!(
"unknown subagent_type '{subagent_type}' (registered: {names})"
));
}
};
let child_manifest = self.build_child_manifest(&spec);
let config = SpawnConfig { manifest: child_manifest, query: prompt };
let mut pool = self.pool.lock().await;
let id = match pool.spawn(config) {
Ok(id) => id,
Err(e) => return ToolResult::error(format!("task spawn failed: {e}")),
};
match pool.join_next().await {
Some((completed_id, Ok(result))) if completed_id == id => {
ToolResult::success(result.text)
}
Some((_, Ok(result))) => ToolResult::success(result.text),
Some((_, Err(e))) => ToolResult::error(format!("subagent error: {e}")),
None => ToolResult::error("subagent produced no result"),
}
}
fn required_capability(&self) -> Capability {
Capability::Spawn { max_depth: self.max_depth }
}
}
pub fn register_task_tool(
registry: &mut super::tool::ToolRegistry,
manifest: &AgentManifest,
driver: Arc<dyn LlmDriver>,
max_depth: u32,
) {
let mut subagents = default_registry();
if let Ok(cwd) = std::env::current_dir() {
let _ = super::custom_agents::register_discovered_into(&mut subagents, &cwd);
}
let tool = TaskTool::from_driver_with_registry(
Arc::clone(&driver),
manifest.clone(),
max_depth,
subagents,
);
registry.register(Box::new(tool));
}
#[cfg(test)]
mod tests;