@@ -11,13 +11,15 @@ use crate::hooks::{HumanApprovalHook, SecurityGateHook};
1111use crate :: prompt:: build_system_prompt;
1212use crate :: streaming:: StreamingOpenAiProvider ;
1313use crate :: tools:: {
14- AddTaskTool , BrowserTool , CancelTaskTool , CompleteTaskTool , DockerRunCommandTool , EditFileTool ,
15- GlobTool , GrepTool , GroupRegistry , ListGroupsTool , ListSessionTasksTool , ListTasksTool , LsTool ,
16- MessageGroupTool , PauseTaskTool , ReadFileTool , RegisterGroupTool , ScheduleState ,
17- ScheduleTaskTool , TaskState , UpdateTaskTool , WebFetchTool , WebSearchTool , WriteFileTool ,
18- load_tasks, new_group_registry, new_task_state,
14+ AddTaskTool , AgentSpawner , BrowserTool , CancelTaskTool , CompleteTaskTool ,
15+ DockerRunCommandTool , EditFileTool , GlobTool , GrepTool , GroupRegistry , ListGroupsTool ,
16+ ListSessionTasksTool , ListTasksTool , LsTool , MessageGroupTool , PauseTaskTool , ReadFileTool ,
17+ RegisterGroupTool , ScheduleState , ScheduleTaskTool , SpawnGroupTool , TaskState , UpdateTaskTool ,
18+ WebFetchTool , WebSearchTool , WriteFileTool , load_tasks, new_group_registry, new_task_state,
19+ } ;
20+ use crate :: workers:: {
21+ ChannelOutputWorker , DelegationReplyWorker , GroupBuses , RelayWorker , SchedulerWorker ,
1922} ;
20- use crate :: workers:: { ChannelOutputWorker , DelegationReplyWorker , RelayWorker , SchedulerWorker } ;
2123use eventage:: {
2224 agent:: { ContextAssembler , DefaultContextAssembler } ,
2325 llm:: OpenAiProvider ,
@@ -26,7 +28,7 @@ use eventage::{
2628} ;
2729use std:: collections:: HashMap ;
2830use std:: sync:: { atomic:: AtomicBool , Arc } ;
29- use tokio:: sync:: Mutex ;
31+ use tokio:: sync:: { Mutex , RwLock } ;
3032use tracing:: info;
3133use uuid:: Uuid ;
3234
@@ -186,10 +188,13 @@ impl ClawAgentBuilder {
186188 . map ( |g| g. name . clone ( ) )
187189 . collect ( ) ;
188190
189- // Build per-group buses first so we can pass them to workers
190- let mut group_buses: HashMap < String , EventBus > = HashMap :: new ( ) ;
191+ // Shared live map: group name → per-group EventBus.
192+ // Arc<RwLock<…>> so dynamically spawned groups are immediately routable.
193+ let group_buses: GroupBuses = Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ;
191194 for g in & config. groups {
192- group_buses. insert ( g. name . clone ( ) , EventBus :: new ( ) ) ;
195+ group_buses
196+ . blocking_write ( )
197+ . insert ( g. name . clone ( ) , EventBus :: new ( ) ) ;
193198 }
194199
195200 // Build shared workers
@@ -203,12 +208,24 @@ impl ClawAgentBuilder {
203208 group_buses : group_buses. clone ( ) ,
204209 } ) ;
205210
211+ // Spawner used by SpawnGroupTool — holds everything needed to build a
212+ // new GroupAgent at runtime and insert it into the live routing table.
213+ let spawner: Arc < dyn AgentSpawner > = Arc :: new ( ClawGroupSpawner {
214+ config : Arc :: new ( config. clone ( ) ) ,
215+ shared_bus : shared_bus. clone ( ) ,
216+ group_buses : group_buses. clone ( ) ,
217+ group_registry : group_registry. clone ( ) ,
218+ schedule_state : schedule_state. clone ( ) ,
219+ session_id_prefix : self . session_id_prefix . clone ( ) ,
220+ tui_mode : self . tui_mode ,
221+ } ) ;
222+
206223 // Build each group agent
207224 let mut groups: HashMap < String , GroupAgent > = HashMap :: new ( ) ;
208225 let active_group_name = config. groups . first ( ) . map ( |g| g. name . clone ( ) ) . unwrap_or_default ( ) ;
209226
210227 for group_config in & config. groups {
211- let group_bus = group_buses[ & group_config. name ] . clone ( ) ;
228+ let group_bus = group_buses. blocking_read ( ) [ & group_config. name ] . clone ( ) ;
212229 let session_id = format ! ( "{}-{}" , self . session_id_prefix, group_config. name) ;
213230 let task_state = new_task_state ( ) ;
214231
@@ -223,6 +240,7 @@ impl ClawAgentBuilder {
223240 self . tui_mode ,
224241 session_id,
225242 task_state,
243+ spawner. clone ( ) ,
226244 ) ;
227245
228246 groups. insert ( group_config. name . clone ( ) , group_agent) ;
@@ -239,6 +257,103 @@ impl ClawAgentBuilder {
239257 }
240258}
241259
260+ // ── ClawGroupSpawner ──────────────────────────────────────────────────────────
261+
262+ /// Implements [`AgentSpawner`] for the claw runtime.
263+ ///
264+ /// Held by `SpawnGroupTool` (main group only). On `spawn()`, it builds a full
265+ /// `GroupAgent`, inserts the new bus into the shared routing table, and starts
266+ /// the agent task — all without restarting the process.
267+ struct ClawGroupSpawner {
268+ config : Arc < ClawConfig > ,
269+ shared_bus : EventBus ,
270+ group_buses : GroupBuses ,
271+ group_registry : GroupRegistry ,
272+ schedule_state : ScheduleState ,
273+ session_id_prefix : String ,
274+ tui_mode : bool ,
275+ }
276+
277+ #[ async_trait:: async_trait]
278+ impl AgentSpawner for ClawGroupSpawner {
279+ async fn spawn ( & self , name : & str , system_prompt : Option < & str > ) -> Result < ( ) , String > {
280+ // Reject duplicates.
281+ if self . group_buses . read ( ) . await . contains_key ( name) {
282+ return Err ( format ! ( "group '{name}' already exists" ) ) ;
283+ }
284+
285+ let group_bus = EventBus :: new ( ) ;
286+ let session_id = format ! ( "{}-{}" , self . session_id_prefix, name) ;
287+ let task_state = new_task_state ( ) ;
288+
289+ let group_config = GroupConfig {
290+ name : name. to_string ( ) ,
291+ is_main : false ,
292+ system_prompt_suffix : system_prompt. map ( |s| s. to_string ( ) ) ,
293+ human_approval_tools : vec ! [ ] ,
294+ require_approve_all : false ,
295+ work_dir : None ,
296+ allowed_senders : vec ! [ ] ,
297+ } ;
298+
299+ // Snapshot current group names for the new agent's MessageGroupTool hint.
300+ let known_groups: Vec < String > = self . group_registry . lock ( ) . await . clone ( ) ;
301+
302+ // No recursive spawning from spawned sub-agents — pass a no-op spawner.
303+ let no_spawn: Arc < dyn AgentSpawner > = Arc :: new ( NoopSpawner ) ;
304+
305+ let group_agent = build_group_agent (
306+ & group_config,
307+ & self . config ,
308+ group_bus. clone ( ) ,
309+ self . shared_bus . clone ( ) ,
310+ self . schedule_state . clone ( ) ,
311+ self . group_registry . clone ( ) ,
312+ & known_groups,
313+ self . tui_mode ,
314+ session_id,
315+ task_state,
316+ no_spawn,
317+ ) ;
318+
319+ // Register in routing table before spawning so RelayWorker can route
320+ // the very first message the main agent sends after spawn returns.
321+ self . group_buses . write ( ) . await . insert ( name. to_string ( ) , group_bus) ;
322+ self . group_registry . lock ( ) . await . push ( name. to_string ( ) ) ;
323+
324+ let group_name = name. to_string ( ) ;
325+ let agent = group_agent. agent ;
326+ let ws = group_agent. worker_set ;
327+ let bus = group_agent. bus ;
328+
329+ tokio:: spawn ( async move {
330+ let worker_bus = bus. clone ( ) ;
331+ let worker_name = group_name. clone ( ) ;
332+ tokio:: spawn ( async move {
333+ if let Err ( e) = ws. run_on ( worker_bus) . await {
334+ tracing:: warn!( group = %worker_name, "spawned group worker error: {e}" ) ;
335+ }
336+ } ) ;
337+ if let Err ( e) = agent. run ( ) . await {
338+ tracing:: warn!( group = %group_name, "spawned group agent exited: {e}" ) ;
339+ }
340+ } ) ;
341+
342+ info ! ( group = %name, "ClawGroupSpawner: sub-agent spawned" ) ;
343+ Ok ( ( ) )
344+ }
345+ }
346+
347+ /// Placeholder spawner for sub-agents that should not spawn further agents.
348+ struct NoopSpawner ;
349+
350+ #[ async_trait:: async_trait]
351+ impl AgentSpawner for NoopSpawner {
352+ async fn spawn ( & self , name : & str , _system_prompt : Option < & str > ) -> Result < ( ) , String > {
353+ Err ( format ! ( "sub-agent cannot spawn further agents (requested: '{name}')" ) )
354+ }
355+ }
356+
242357// ── build_group_agent ─────────────────────────────────────────────────────────
243358
244359#[ allow( clippy:: too_many_arguments) ]
@@ -253,6 +368,7 @@ fn build_group_agent(
253368 tui_mode : bool ,
254369 session_id : String ,
255370 task_state : TaskState ,
371+ spawner : Arc < dyn AgentSpawner > ,
256372) -> GroupAgent {
257373 let work_dir = config. group_work_dir ( & group_config. name ) ;
258374 let _ = std:: fs:: create_dir_all ( & work_dir) ;
@@ -398,7 +514,8 @@ fn build_group_agent(
398514 } )
399515 . tool ( ListGroupsTool {
400516 registry : group_registry. clone ( ) ,
401- } ) ;
517+ } )
518+ . tool ( SpawnGroupTool { spawner } ) ;
402519 }
403520
404521 // ── Hook ──────────────────────────────────────────────────────────────────
0 commit comments