88
99use super :: * ;
1010use anyhow:: Result ;
11+ use futures:: future:: try_join_all;
1112use jcode_task_types:: TodoItem ;
1213use std:: collections:: HashSet ;
13- use futures:: future:: try_join_all;
1414
1515const MAX_RETRIES : u32 = 2 ;
1616
@@ -31,41 +31,76 @@ pub(super) struct PipelineResult {
3131pub ( super ) fn classify_todo ( todo : & TodoItem ) -> String {
3232 let c = todo. content . to_ascii_lowercase ( ) ;
3333 let g = todo. group . as_deref ( ) . unwrap_or ( "" ) . to_ascii_lowercase ( ) ;
34- if g. contains ( "plan" ) ||g. contains ( "foundation" ) { return "planner" . into ( ) ; }
35- if g. contains ( "test" ) ||g. contains ( "verify" ) ||g. contains ( "qa" ) { return "basher" . into ( ) ; }
36- if g. contains ( "review" ) { return "code-reviewer" . into ( ) ; }
37- if g. contains ( "search" ) ||g. contains ( "find" ) { return "file-picker" . into ( ) ; }
38- if c. contains ( "plan" ) ||c. contains ( "analyz" ) ||c. contains ( "design" ) { return "planner" . into ( ) ; }
39- if c. contains ( "test" ) ||c. contains ( "verif" ) ||c. contains ( "check" ) { return "basher" . into ( ) ; }
40- if c. contains ( "review" ) ||c. contains ( "audit" ) { return "code-reviewer" . into ( ) ; }
41- if c. contains ( "search" ) ||c. contains ( "find" ) ||c. starts_with ( "read" ) { return "file-picker" . into ( ) ; }
34+ if g. contains ( "plan" ) || g. contains ( "foundation" ) {
35+ return "planner" . into ( ) ;
36+ }
37+ if g. contains ( "test" ) || g. contains ( "verify" ) || g. contains ( "qa" ) {
38+ return "basher" . into ( ) ;
39+ }
40+ if g. contains ( "review" ) {
41+ return "code-reviewer" . into ( ) ;
42+ }
43+ if g. contains ( "search" ) || g. contains ( "find" ) {
44+ return "file-picker" . into ( ) ;
45+ }
46+ if c. contains ( "plan" ) || c. contains ( "analyz" ) || c. contains ( "design" ) {
47+ return "planner" . into ( ) ;
48+ }
49+ if c. contains ( "test" ) || c. contains ( "verif" ) || c. contains ( "check" ) {
50+ return "basher" . into ( ) ;
51+ }
52+ if c. contains ( "review" ) || c. contains ( "audit" ) {
53+ return "code-reviewer" . into ( ) ;
54+ }
55+ if c. contains ( "search" ) || c. contains ( "find" ) || c. starts_with ( "read" ) {
56+ return "file-picker" . into ( ) ;
57+ }
4258 "editor" . into ( )
4359}
4460
4561/// Build allowed-tool set matching each agent type.
4662pub ( crate ) fn build_allowed_tools ( tp : & str ) -> HashSet < String > {
4763 let tools: Vec < & str > = match tp {
48- "planner" => vec ! [ "read" , "glob" , "grep" , "codesearch" , "session_search" , "ls" ] ,
49- "file-picker" => vec ! [ "ls" , "glob" , "read" ] ,
50- "editor" => vec ! [ "read" , "write" , "edit" , "hashline_edit" , "propose_edit" , "glob" , "grep" , "codesearch" , "ls" , "bash" ] ,
51- "code-reviewer" => vec ! [ "read" , "glob" , "grep" , "codesearch" , "ls" ] ,
52- "basher" => vec ! [ "bash" , "read" , "glob" , "ls" ] ,
53- _ => vec ! [ "read" , "bash" ] ,
64+ "planner" => vec ! [ "read" , "glob" , "grep" , "codesearch" , "session_search" , "ls" ] ,
65+ "file-picker" => vec ! [ "ls" , "glob" , "read" ] ,
66+ "editor" => vec ! [
67+ "read" ,
68+ "write" ,
69+ "edit" ,
70+ "hashline_edit" ,
71+ "propose_edit" ,
72+ "glob" ,
73+ "grep" ,
74+ "codesearch" ,
75+ "ls" ,
76+ "bash" ,
77+ ] ,
78+ "code-reviewer" => vec ! [ "read" , "glob" , "grep" , "codesearch" , "ls" ] ,
79+ "basher" => vec ! [ "bash" , "read" , "glob" , "ls" ] ,
80+ _ => vec ! [ "read" , "bash" ] ,
5481 } ;
5582 tools. into_iter ( ) . map ( String :: from) . collect ( )
5683}
5784
5885impl Agent {
59- pub fn set_todo_orchestrator_enabled ( & mut self , v : bool ) { self . todo_orchestrator_enabled = v; }
60- pub fn todo_orchestrator_enabled ( & self ) -> bool { self . todo_orchestrator_enabled }
86+ pub fn set_todo_orchestrator_enabled ( & mut self , v : bool ) {
87+ self . todo_orchestrator_enabled = v;
88+ }
89+ pub fn todo_orchestrator_enabled ( & self ) -> bool {
90+ self . todo_orchestrator_enabled
91+ }
6192
6293 /// Run the full Codebuff pipeline for all incomplete todos.
6394 pub async fn poll_todo_pipeline ( & mut self ) -> Result < usize > {
6495 let sid = self . session . id . clone ( ) ;
6596 let todos = crate :: todo:: load_todos ( & sid) . unwrap_or_default ( ) ;
66- let incomplete: Vec < TodoItem > = todos. into_iter ( )
67- . filter ( |t| !matches ! ( t. status. as_str( ) , "completed" |"cancelled" ) ) . collect ( ) ;
68- if incomplete. is_empty ( ) { return Ok ( 0 ) ; }
97+ let incomplete: Vec < TodoItem > = todos
98+ . into_iter ( )
99+ . filter ( |t| !matches ! ( t. status. as_str( ) , "completed" | "cancelled" ) )
100+ . collect ( ) ;
101+ if incomplete. is_empty ( ) {
102+ return Ok ( 0 ) ;
103+ }
69104
70105 let provider = Arc :: clone ( & self . provider ) ;
71106 let registry = self . registry . clone ( ) ;
@@ -76,15 +111,22 @@ impl Agent {
76111 let result = orchestrate_one_todo ( & provider, & registry, & parent_sid, todo) . await ;
77112 match result {
78113 Ok ( r) => {
79- if r. all_tests_pass { processed += 1 ; }
114+ if r. all_tests_pass {
115+ processed += 1 ;
116+ }
80117 crate :: logging:: info ( & format ! (
81- "[orchestrator] '{}': {} subtasks, pass={}" , todo. content, r. subtask_count, r. all_tests_pass,
118+ "[orchestrator] '{}': {} subtasks, pass={}" ,
119+ todo. content, r. subtask_count, r. all_tests_pass,
82120 ) ) ;
83121 }
84- Err ( e) => crate :: logging:: warn ( & format ! ( "[orchestrator] '{}' failed: {e}" , todo. content) ) ,
122+ Err ( e) => {
123+ crate :: logging:: warn ( & format ! ( "[orchestrator] '{}' failed: {e}" , todo. content) )
124+ }
85125 }
86126 }
87- if processed > 0 { crate :: logging:: info ( & format ! ( "[orchestrator] processed {processed} todos" ) ) ; }
127+ if processed > 0 {
128+ crate :: logging:: info ( & format ! ( "[orchestrator] processed {processed} todos" ) ) ;
129+ }
88130 Ok ( processed)
89131 }
90132}
@@ -103,7 +145,8 @@ async fn orchestrate_one_todo(
103145 let plan_prompt = format ! (
104146 "Break this task into 2-4 subtasks. Return ONLY a JSON array of \
105147 objects with keys: description, prompt, subagent_type. \
106- No extra text.\n \n Task:\n {}", todo. content,
148+ No extra text.\n \n Task:\n {}",
149+ todo. content,
107150 ) ;
108151 let plan_text = spawn_child ( provider, registry, parent_sid, "planner" , & plan_prompt) . await ?;
109152 let mut subtasks = parse_swarm_tasks ( & plan_text) ;
@@ -119,18 +162,24 @@ async fn orchestrate_one_todo(
119162 let mut all_pass = false ;
120163 while attempts < MAX_RETRIES && !all_pass {
121164 // 2. Run subtasks in PARALLEL (try_join_all)
122- let futures: Vec < _ > = subtasks. iter ( ) . map ( |st| {
123- let p = Arc :: clone ( provider) ;
124- let r = registry. clone ( ) ;
125- let sid = parent_sid. to_string ( ) ;
126- let prompt = st. prompt . clone ( ) ;
127- let atype = st. subagent_type . clone ( ) ;
128- async move { spawn_child ( & p, & r, & sid, & atype, & prompt) . await }
129- } ) . collect ( ) ;
165+ let futures: Vec < _ > = subtasks
166+ . iter ( )
167+ . map ( |st| {
168+ let p = Arc :: clone ( provider) ;
169+ let r = registry. clone ( ) ;
170+ let sid = parent_sid. to_string ( ) ;
171+ let prompt = st. prompt . clone ( ) ;
172+ let atype = st. subagent_type . clone ( ) ;
173+ async move { spawn_child ( & p, & r, & sid, & atype, & prompt) . await }
174+ } )
175+ . collect ( ) ;
130176 let outputs = try_join_all ( futures) . await ?;
131177
132178 // 3. Basher child → run tests
133- let test_prompt = format ! ( "Run relevant tests for this task AND REPORT pass/fail:\n \n {}" , todo. content) ;
179+ let test_prompt = format ! (
180+ "Run relevant tests for this task AND REPORT pass/fail:\n \n {}" ,
181+ todo. content
182+ ) ;
134183 let test_out = spawn_child ( provider, registry, parent_sid, "basher" , & test_prompt) . await ?;
135184 all_pass = !test_out. to_ascii_lowercase ( ) . contains ( "fail" ) ;
136185 attempts += 1 ;
@@ -141,21 +190,35 @@ async fn orchestrate_one_todo(
141190 "Integrate the completed subtask results and produce a final summary.\n \n Task:\n {}" ,
142191 todo. content,
143192 ) ;
144- let _final_out = spawn_child ( provider, registry, parent_sid, "editor" , & integration_prompt) . await ?;
193+ let _final_out = spawn_child (
194+ provider,
195+ registry,
196+ parent_sid,
197+ "editor" ,
198+ & integration_prompt,
199+ )
200+ . await ?;
145201
146202 // 5. Persist and broadcast: load ALL todos, update the one just processed.
147203 // save_todos replaces the full list (whole-list replace pattern).
148204 let mut all_todos = crate :: todo:: load_todos ( parent_sid) . unwrap_or_default ( ) ;
149205 for t in & mut all_todos {
150206 if t. content == todo. content && t. id == todo. id {
151- t. status = if all_pass { "completed" . into ( ) } else { "blocked" . into ( ) } ;
207+ t. status = if all_pass {
208+ "completed" . into ( )
209+ } else {
210+ "blocked" . into ( )
211+ } ;
152212 break ;
153213 }
154214 }
155215 crate :: todo:: save_todos ( parent_sid, & all_todos) ?;
156216 // save_todos internally broadcasts BusEvent::TodoUpdated.
157217
158- Ok ( PipelineResult { all_tests_pass : all_pass, subtask_count : subtasks. len ( ) } )
218+ Ok ( PipelineResult {
219+ all_tests_pass : all_pass,
220+ subtask_count : subtasks. len ( ) ,
221+ } )
159222}
160223
161224/// Spawn a single child agent with given type and prompt.
@@ -203,28 +266,70 @@ fn parse_swarm_tasks(text: &str) -> Vec<SwarmTaskSpec> {
203266fn parse_one_task ( v : serde_json:: Value ) -> Option < SwarmTaskSpec > {
204267 let desc = v. get ( "description" ) ?. as_str ( ) ?. to_string ( ) ;
205268 let prompt = v. get ( "prompt" ) ?. as_str ( ) ?. to_string ( ) ;
206- let subagent_type = v. get ( "subagent_type" ) . and_then ( |s| s. as_str ( ) )
207- . unwrap_or ( "editor" ) . to_string ( ) ;
208- Some ( SwarmTaskSpec { description : desc, prompt, subagent_type } )
269+ let subagent_type = v
270+ . get ( "subagent_type" )
271+ . and_then ( |s| s. as_str ( ) )
272+ . unwrap_or ( "editor" )
273+ . to_string ( ) ;
274+ Some ( SwarmTaskSpec {
275+ description : desc,
276+ prompt,
277+ subagent_type,
278+ } )
209279}
210280
211281#[ cfg( test) ]
212282mod tests {
213283 use super :: * ;
214- fn td ( c : & str , g : Option < & str > ) -> TodoItem { TodoItem { content : c. into ( ) , group : g. map ( String :: from) , ..Default :: default ( ) } }
215- fn check ( c : & str , g : Option < & str > , e : & str ) { assert_eq ! ( classify_todo( & td( c, g) ) , e, "mismatch for {c:?} group={g:?}" ) ; }
216- #[ test] fn t_pl ( ) { check ( "Design auth" , None , "planner" ) ; }
217- #[ test] fn t_ed ( ) { check ( "Implement btn" , None , "editor" ) ; }
218- #[ test] fn t_ba ( ) { check ( "Fix test" , Some ( "qa" ) , "basher" ) ; }
219- #[ test] fn t_rv ( ) { check ( "Review PR" , None , "code-reviewer" ) ; }
220- #[ test] fn t_fp ( ) { check ( "Find files" , Some ( "search" ) , "file-picker" ) ; }
221- #[ test] fn t_tools ( ) { let t = build_allowed_tools ( "planner" ) ; assert ! ( t. contains( "read" ) ) ; assert ! ( !t. contains( "write" ) ) ; }
284+ fn td ( c : & str , g : Option < & str > ) -> TodoItem {
285+ TodoItem {
286+ content : c. into ( ) ,
287+ group : g. map ( String :: from) ,
288+ ..Default :: default ( )
289+ }
290+ }
291+ fn check ( c : & str , g : Option < & str > , e : & str ) {
292+ assert_eq ! (
293+ classify_todo( & td( c, g) ) ,
294+ e,
295+ "mismatch for {c:?} group={g:?}"
296+ ) ;
297+ }
298+ #[ test]
299+ fn t_pl ( ) {
300+ check ( "Design auth" , None , "planner" ) ;
301+ }
302+ #[ test]
303+ fn t_ed ( ) {
304+ check ( "Implement btn" , None , "editor" ) ;
305+ }
306+ #[ test]
307+ fn t_ba ( ) {
308+ check ( "Fix test" , Some ( "qa" ) , "basher" ) ;
309+ }
310+ #[ test]
311+ fn t_rv ( ) {
312+ check ( "Review PR" , None , "code-reviewer" ) ;
313+ }
314+ #[ test]
315+ fn t_fp ( ) {
316+ check ( "Find files" , Some ( "search" ) , "file-picker" ) ;
317+ }
318+ #[ test]
319+ fn t_tools ( ) {
320+ let t = build_allowed_tools ( "planner" ) ;
321+ assert ! ( t. contains( "read" ) ) ;
322+ assert ! ( !t. contains( "write" ) ) ;
323+ }
222324
223- fn parse ( s : & str ) -> Vec < SwarmTaskSpec > { parse_swarm_tasks ( s) }
325+ fn parse ( s : & str ) -> Vec < SwarmTaskSpec > {
326+ parse_swarm_tasks ( s)
327+ }
224328
225329 #[ test]
226330 fn parse_json_array ( ) {
227- let json = r#"[{"description":"Fix auth","prompt":"Update login.ts","subagent_type":"editor"}]"# ;
331+ let json =
332+ r#"[{"description":"Fix auth","prompt":"Update login.ts","subagent_type":"editor"}]"# ;
228333 assert_eq ! ( parse( json) . len( ) , 1 ) ;
229334 }
230335
0 commit comments