22//!
33//! Provides helpers to spawn child agents using the same pattern as `SubagentTool`
44//! in `jcode-app-core/src/tool/task.rs`.
5+ //!
6+ //! The actual spawning implementation is registered via [`set_spawn_impl`] by
7+ //! `jcode-app-core` at startup. Until then, [`spawn_agent`] returns a placeholder.
58
69use super :: { SpawnResult , SpawnSpec } ;
10+ use std:: sync:: { LazyLock , Mutex } ;
711
812/// Maximum concurrent sub-agents per spawn call.
913const MAX_CONCURRENT : usize = 4 ;
1014
11- /// Spawn a single sub-agent synchronously and return its output.
12- ///
13- /// This is a placeholder that will be wired to the actual Agent spawning
14- /// mechanism via the `WorkflowExecutor` in `jcode-app-core`.
15+ /// A function that can spawn a sub-agent given a `SpawnSpec`.
16+ /// Returns the spawned agent's output as a `SpawnResult`.
17+ pub type SpawnFn = dyn Fn ( & SpawnSpec ) -> SpawnResult + Send + Sync ;
18+
19+ static SPAWN_IMPL : LazyLock < Mutex < Option < Box < SpawnFn > > > > = LazyLock :: new ( || Mutex :: new ( None ) ) ;
20+
21+ /// Register the real spawn implementation. Called by `jcode-app-core` at startup.
22+ /// Panics if already registered (idempotent — second call is a no-op).
23+ pub fn set_spawn_impl ( impl_fn : Box < SpawnFn > ) {
24+ let mut guard = SPAWN_IMPL . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
25+ if guard. is_some ( ) {
26+ return ; // already set
27+ }
28+ * guard = Some ( impl_fn) ;
29+ }
30+
31+ /// Spawn a single sub-agent and return its output.
32+ /// Delegates to the registered implementation, or returns a placeholder if none set.
1533pub async fn spawn_agent ( spec : & SpawnSpec ) -> SpawnResult {
16- // Stub implementation — real wiring happens in app-core
17- SpawnResult {
18- description : spec. description . clone ( ) ,
19- output : format ! (
20- "[Workflow sub-agent '{}']: {}" ,
21- spec. description, spec. prompt
22- ) ,
23- success : true ,
34+ let guard = SPAWN_IMPL . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
35+ if let Some ( ref spawn_fn) = * guard {
36+ ( spawn_fn) ( spec)
37+ } else {
38+ // Stub fallback
39+ SpawnResult {
40+ description : spec. description . clone ( ) ,
41+ output : format ! (
42+ "[Workflow sub-agent '{}']: {}" ,
43+ spec. description, spec. prompt
44+ ) ,
45+ success : true ,
46+ }
2447 }
2548}
2649
2750/// Spawn multiple sub-agents in parallel and collect results.
28- /// Concurrency is capped at MAX_CONCURRENT.
2951pub async fn spawn_parallel ( specs : & [ SpawnSpec ] ) -> Vec < SpawnResult > {
52+ // Snapshot the spec list so we don't hold the lock across awaits.
53+ let specs = specs. to_vec ( ) ;
3054 let mut results = Vec :: new ( ) ;
31-
3255 for chunk in specs. chunks ( MAX_CONCURRENT ) {
56+ let chunk = chunk. to_vec ( ) ;
3357 let mut handles = Vec :: new ( ) ;
3458 for spec in chunk {
35- let spec = spec. clone ( ) ;
3659 handles. push ( tokio:: spawn ( async move { spawn_agent ( & spec) . await } ) ) ;
3760 }
3861 for handle in handles {
3962 match handle. await {
4063 Ok ( result) => results. push ( result) ,
4164 Err ( e) => {
42- // Log JoinError instead of silently dropping
4365 results. push ( SpawnResult {
4466 description : "unknown" . to_string ( ) ,
4567 output : format ! ( "Sub-agent panicked: {}" , e) ,
@@ -49,7 +71,6 @@ pub async fn spawn_parallel(specs: &[SpawnSpec]) -> Vec<SpawnResult> {
4971 }
5072 }
5173 }
52-
5374 results
5475}
5576
@@ -58,25 +79,13 @@ pub fn aggregate_results(results: &[SpawnResult]) -> String {
5879 if results. is_empty ( ) {
5980 return "No results from sub-agents." . to_string ( ) ;
6081 }
61-
62- let mut output = String :: new ( ) ;
63- output. push_str ( "# Parallel Execution Results\n \n " ) ;
64-
65- for ( i, result) in results. iter ( ) . enumerate ( ) {
66- let status = if result. success { "✅" } else { "❌" } ;
67- output. push_str ( & format ! (
68- "## {} Task {}: {}\n \n {}\n \n " ,
69- status, i, result. description, result. output
70- ) ) ;
82+ let mut output = String :: from ( "# Parallel Execution Results\n \n " ) ;
83+ for ( i, r) in results. iter ( ) . enumerate ( ) {
84+ let s = if r. success { "✅" } else { "❌" } ;
85+ output. push_str ( & format ! ( "## {} Task {}: {}\n \n {}\n \n " , s, i, r. description, r. output) ) ;
7186 }
72-
73- let success_count = results. iter ( ) . filter ( |r| r. success ) . count ( ) ;
74- output. push_str ( & format ! (
75- "---\n **Summary**: {}/{} tasks completed successfully." ,
76- success_count,
77- results. len( )
78- ) ) ;
79-
87+ let ok = results. iter ( ) . filter ( |r| r. success ) . count ( ) ;
88+ output. push_str ( & format ! ( "---\n **Summary**: {}/{} tasks completed." , ok, results. len( ) ) ) ;
8089 output
8190}
8291
@@ -85,37 +94,18 @@ mod tests {
8594 use super :: * ;
8695
8796 #[ test]
88- fn aggregate_empty_results ( ) {
89- assert ! ( aggregate_results( & [ ] ) . contains( "No results" ) ) ;
90- }
91-
97+ fn aggregate_empty ( ) { assert ! ( aggregate_results( & [ ] ) . contains( "No results" ) ) ; }
9298 #[ test]
93- fn aggregate_single_result ( ) {
94- let results = vec ! [ SpawnResult {
95- description: "test task" . to_string( ) ,
96- output: "done" . to_string( ) ,
97- success: true ,
98- } ] ;
99- let summary = aggregate_results ( & results) ;
100- assert ! ( summary. contains( "1/1" ) ) ;
101- assert ! ( summary. contains( "test task" ) ) ;
99+ fn aggregate_single ( ) {
100+ let r = vec ! [ SpawnResult { description: "t" . into( ) , output: "done" . into( ) , success: true } ] ;
101+ assert ! ( aggregate_results( & r) . contains( "1/1" ) ) ;
102102 }
103-
104103 #[ test]
105- fn aggregate_mixed_results ( ) {
106- let results = vec ! [
107- SpawnResult {
108- description: "task 1" . to_string( ) ,
109- output: "ok" . to_string( ) ,
110- success: true ,
111- } ,
112- SpawnResult {
113- description: "task 2" . to_string( ) ,
114- output: "failed" . to_string( ) ,
115- success: false ,
116- } ,
104+ fn aggregate_mixed ( ) {
105+ let r = vec ! [
106+ SpawnResult { description: "a" . into( ) , output: "ok" . into( ) , success: true } ,
107+ SpawnResult { description: "b" . into( ) , output: "fail" . into( ) , success: false } ,
117108 ] ;
118- let summary = aggregate_results ( & results) ;
119- assert ! ( summary. contains( "1/2" ) ) ;
109+ assert ! ( aggregate_results( & r) . contains( "1/2" ) ) ;
120110 }
121111}
0 commit comments