2026-07-12 17:49:34 +07:00
//! `spawn_agents` tool — simple interface for the main agent to fan out work
//! to multiple subagents running in parallel.
//!
//! Unlike `workflow_run` (which requires a JSON-encoded `WorkflowScript`),
//! `spawn_agents` accepts a plain list of prompt strings and automatically
//! runs them as a `Parallel` workflow. The agent just says what each
//! subagent should do, not how to encode the script.
//!
//! Also provides a pipeline variant: `spawn_pipeline` runs agents
//! sequentially so each stage sees the previous stage's findings.
use serde_json ::{ json , Value };
use anyhow ::{ Result , anyhow };
use std ::collections ::HashMap ;
use super ::{ Tool , ToolCtx };
use crate ::app ::workflow ::script ::{ ScriptPrimitive , ScriptOptions , WorkflowScript };
/// Fan out a list of prompts to independent parallel subagents.
pub struct SpawnAgents ;
impl Tool for SpawnAgents {
fn name ( & self ) -> & 'static str { "spawn_agents" }
fn description ( & self ) -> & 'static str {
"Fan out independent subtasks to multiple subagents running in PARALLEL. \
Pass a list of prompt strings — each becomes one autonomous subagent with \
access to all tools. Use this whenever a task has independent parts that do \
not need each other's output (e.g. analysing multiple files simultaneously, \
writing multiple independent modules, parallel verification). \
Results from all agents are returned together. \
Use spawn_pipeline instead when each stage needs the previous stage's output."
}
fn parameters ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"agents" : {
"type" : "array" ,
"description" : "List of prompt strings, one per subagent. Each subagent runs independently and in parallel." ,
"items" : { "type" : "string" },
"minItems" : 2
},
"max_concurrency" : {
"type" : "integer" ,
2026-07-12 18:17:09 +07:00
"description" : "Maximum number of agents to run simultaneously (default: 10, max: 10)." ,
"default" : 10
2026-07-12 17:49:34 +07:00
}
},
"required" : [ "agents" ]
})
}
2026-07-13 08:12:02 +07:00
fn run ( & self , ctx : & ToolCtx , args : & Value ) -> Result < String > {
use std ::sync ::{ Arc , Mutex };
2026-07-12 17:49:34 +07:00
let agents : Vec < String > = args . get ( "agents" )
. and_then ( | v | v . as_array ())
. ok_or_else ( || anyhow! ( "missing required argument: agents" )) ?
. iter ()
2026-07-13 08:12:02 +07:00
. filter_map ( | v | v . as_str (). map ( std ::string ::ToString ::to_string ))
2026-07-12 17:49:34 +07:00
. collect ();
if agents . is_empty () {
return Err ( anyhow! ( "agents list must not be empty" ));
}
if agents . len () == 1 {
return Err ( anyhow! ( "use a single agent tool call for one task; spawn_agents is for 2+ parallel tasks" ));
}
let max_concurrency = args . get ( "max_concurrency" )
2026-07-13 08:12:02 +07:00
. and_then ( serde_json ::Value ::as_u64 )
. map_or ( 10 , | v | v . min ( 10 ) as usize );
2026-07-12 17:49:34 +07:00
let agent_count = agents . len ();
let primitives : Vec < ScriptPrimitive > = agents
. into_iter ()
. map ( ScriptPrimitive ::Agent )
. collect ();
let wf = WorkflowScript {
2026-07-13 08:12:02 +07:00
name : format ! ( "parallel-{agent_count}-agents" ),
description : format ! ( "Auto-spawned parallel workflow with {agent_count} agents" ),
2026-07-12 17:49:34 +07:00
script : ScriptPrimitive ::Parallel ( primitives ),
options : ScriptOptions {
max_concurrency ,
continue_on_error : true ,
timeout_ms : None ,
},
};
2026-07-13 08:12:02 +07:00
let live : Option < crate ::app ::workflow ::engine ::LiveStateFn > = ctx . turn_events . as_ref (). map ( | turn_events | {
2026-07-12 17:50:38 +07:00
let turn_events = turn_events . clone ();
2026-07-13 05:23:38 +07:00
let f : crate ::app ::workflow ::engine ::LiveStateFn = Arc ::new ( move | agent_id : String , agent_name : String , status | {
2026-07-12 17:50:38 +07:00
if let Ok ( mut q ) = turn_events . lock () {
q . push_back ( crate ::app ::state ::runtime ::TurnEvent ::WorkflowAgentUpdate {
agent_id ,
2026-07-13 05:23:38 +07:00
agent_name ,
2026-07-12 17:50:38 +07:00
status ,
});
}
});
f
});
2026-07-13 03:12:37 +07:00
// Create a per-invocation findings scope so subagents spawned
// by this tool call are isolated from any other concurrent
// spawn_agents or workflow_run invocations.
let findings : Arc < Mutex < Vec < String >>> = Arc ::new ( Mutex ::new ( Vec ::new ()));
2026-07-12 17:49:34 +07:00
let results = crate ::app ::workflow ::engine ::execute_primitive (
& wf . script ,
& HashMap ::new (),
max_concurrency ,
true ,
2026-07-12 17:50:38 +07:00
live . as_ref (),
2026-07-13 08:12:02 +07:00
& ctx . session_dir ,
& ctx . workspaces ,
2026-07-13 03:12:37 +07:00
& findings ,
2026-07-13 04:41:26 +07:00
None , // no per-agent timeout for spawn_agents
2026-07-12 17:49:34 +07:00
) ? ;
2026-07-13 08:12:02 +07:00
Ok ( format_results ( & results , "parallel" ))
2026-07-12 17:49:34 +07:00
}
}
/// Run agents sequentially in a pipeline — each stage sees previous findings.
pub struct SpawnPipeline ;
impl Tool for SpawnPipeline {
fn name ( & self ) -> & 'static str { "spawn_pipeline" }
fn description ( & self ) -> & 'static str {
"Run subagents SEQUENTIALLY in a pipeline — each stage sees findings \
shared by previous stages via note_finding. Use when stages build on each \
other (e.g. 'research -> plan -> implement -> test'). \
Use spawn_agents instead when tasks are truly independent and order does not matter."
}
fn parameters ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"stages" : {
"type" : "array" ,
"description" : "Ordered list of prompt strings. Each stage runs after the previous one completes. Stages can call note_finding() to pass data to later stages." ,
"items" : { "type" : "string" },
"minItems" : 2
}
},
"required" : [ "stages" ]
})
}
2026-07-13 08:12:02 +07:00
fn run ( & self , ctx : & ToolCtx , args : & Value ) -> Result < String > {
use std ::sync ::{ Arc , Mutex };
2026-07-12 17:49:34 +07:00
let stages : Vec < String > = args . get ( "stages" )
. and_then ( | v | v . as_array ())
. ok_or_else ( || anyhow! ( "missing required argument: stages" )) ?
. iter ()
2026-07-13 08:12:02 +07:00
. filter_map ( | v | v . as_str (). map ( std ::string ::ToString ::to_string ))
2026-07-12 17:49:34 +07:00
. collect ();
if stages . is_empty () {
return Err ( anyhow! ( "stages list must not be empty" ));
}
let primitives : Vec < ScriptPrimitive > = stages
. into_iter ()
. map ( ScriptPrimitive ::Agent )
. collect ();
let wf = WorkflowScript {
name : "pipeline" . to_string (),
description : "Auto-spawned pipeline workflow" . to_string (),
script : ScriptPrimitive ::Pipeline ( primitives ),
options : ScriptOptions {
max_concurrency : 1 ,
continue_on_error : false ,
timeout_ms : None ,
},
};
2026-07-13 08:12:02 +07:00
let live : Option < crate ::app ::workflow ::engine ::LiveStateFn > = ctx . turn_events . as_ref (). map ( | turn_events | {
2026-07-12 17:50:38 +07:00
let turn_events = turn_events . clone ();
2026-07-13 05:23:38 +07:00
let f : crate ::app ::workflow ::engine ::LiveStateFn = Arc ::new ( move | agent_id : String , agent_name : String , status | {
2026-07-12 17:50:38 +07:00
if let Ok ( mut q ) = turn_events . lock () {
q . push_back ( crate ::app ::state ::runtime ::TurnEvent ::WorkflowAgentUpdate {
agent_id ,
2026-07-13 05:23:38 +07:00
agent_name ,
2026-07-12 17:50:38 +07:00
status ,
});
}
});
f
});
2026-07-13 03:12:37 +07:00
// Per-invocation findings scope isolates this pipeline from any
// other concurrent spawn_agents / spawn_pipeline / workflow_run.
let findings : Arc < Mutex < Vec < String >>> = Arc ::new ( Mutex ::new ( Vec ::new ()));
2026-07-12 17:49:34 +07:00
let results = crate ::app ::workflow ::engine ::execute_primitive (
& wf . script ,
& HashMap ::new (),
1 ,
false ,
2026-07-12 17:50:38 +07:00
live . as_ref (),
2026-07-13 08:12:02 +07:00
& ctx . session_dir ,
& ctx . workspaces ,
2026-07-13 03:12:37 +07:00
& findings ,
2026-07-13 04:41:26 +07:00
None , // no per-agent timeout for spawn_pipeline
2026-07-12 17:49:34 +07:00
) ? ;
2026-07-13 08:12:02 +07:00
Ok ( format_results ( & results , "pipeline" ))
2026-07-12 17:49:34 +07:00
}
}
/// Format a list of agent results into a readable summary string.
2026-07-13 08:12:02 +07:00
fn format_results ( results : & [ String ], mode : & str ) -> String {
2026-07-12 17:49:34 +07:00
if results . is_empty () {
2026-07-13 08:12:02 +07:00
return format! ( " {mode} workflow completed with no output" );
2026-07-12 17:49:34 +07:00
}
let formatted : Vec < String > = results
. iter ()
. enumerate ()
. map ( | ( i , r ) | format! ( "=== Agent {} === \n {} " , i + 1 , r . trim ()))
. collect ();
2026-07-13 08:12:02 +07:00
formatted . join ( " \n\n " )
2026-07-12 17:49:34 +07:00
}