1use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14
15use chrono::Utc;
16use serde::{Deserialize, Serialize};
17use tokio::sync::RwLock;
18
19use crate::acp::AcpManager;
20use crate::error::ServerError;
21use crate::events::{AgentEvent, AgentEventType, EventBus};
22use crate::models::agent::{AgentRole, AgentStatus, ModelTier};
23use crate::models::task::TaskStatus;
24use crate::store::{AgentStore, TaskStore};
25use crate::tools::{CompletionReport, ToolResult};
26use crate::workflow::specialist::{SpecialistDef, SpecialistLoader};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct SpecialistConfig {
34 pub id: String,
35 pub name: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub description: Option<String>,
38 pub role: AgentRole,
39 pub default_model_tier: ModelTier,
40 pub system_prompt: String,
41 pub role_reminder: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub default_provider: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub default_adapter: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub default_model: Option<String>,
48}
49
50impl SpecialistConfig {
51 pub fn crafter() -> Self {
53 Self {
54 id: "crafter".to_string(),
55 name: "Implementor".to_string(),
56 description: Some("Executes implementation tasks, writes code".to_string()),
57 role: AgentRole::Crafter,
58 default_model_tier: ModelTier::Fast,
59 system_prompt: CRAFTER_SYSTEM_PROMPT.to_string(),
60 role_reminder: CRAFTER_ROLE_REMINDER.to_string(),
61 default_provider: None,
62 default_adapter: None,
63 default_model: None,
64 }
65 }
66
67 pub fn gate() -> Self {
69 Self {
70 id: "gate".to_string(),
71 name: "Verifier".to_string(),
72 description: Some("Reviews work and verifies completeness".to_string()),
73 role: AgentRole::Gate,
74 default_model_tier: ModelTier::Smart,
75 system_prompt: GATE_SYSTEM_PROMPT.to_string(),
76 role_reminder: GATE_ROLE_REMINDER.to_string(),
77 default_provider: None,
78 default_adapter: None,
79 default_model: None,
80 }
81 }
82
83 pub fn developer() -> Self {
85 Self {
86 id: "developer".to_string(),
87 name: "Developer".to_string(),
88 description: Some("Plans then implements itself".to_string()),
89 role: AgentRole::Developer,
90 default_model_tier: ModelTier::Smart,
91 system_prompt: DEVELOPER_SYSTEM_PROMPT.to_string(),
92 role_reminder: DEVELOPER_ROLE_REMINDER.to_string(),
93 default_provider: None,
94 default_adapter: None,
95 default_model: None,
96 }
97 }
98
99 pub fn by_role(role: &AgentRole) -> Option<Self> {
101 match role {
102 AgentRole::Crafter => Some(Self::crafter()),
103 AgentRole::Gate => Some(Self::gate()),
104 AgentRole::Developer => Some(Self::developer()),
105 AgentRole::Routa => None, }
107 }
108
109 pub fn by_id(id: &str) -> Option<Self> {
111 match id.to_lowercase().as_str() {
112 "crafter" => Some(Self::crafter()),
113 "backend-dev" => Some(Self::crafter()),
114 "backend" => Some(Self::crafter()),
115 "frontend-dev" => Some(Self::crafter()),
116 "frontend" => Some(Self::crafter()),
117 "general-engineer" => Some(Self::crafter()),
118 "operations" => Some(Self::crafter()),
119 "ops" => Some(Self::crafter()),
120 "gate" => Some(Self::gate()),
121 "qa" => Some(Self::gate()),
122 "qa-specialist" => Some(Self::gate()),
123 "code-reviewer" => Some(Self::gate()),
124 "reviewer" => Some(Self::gate()),
125 "developer" => Some(Self::developer()),
126 "researcher" => Some(Self::developer()),
127 "ux-designer" => Some(Self::developer()),
128 _ => None,
129 }
130 }
131
132 pub fn from_specialist_def(def: SpecialistDef) -> Option<Self> {
133 let role_name = def.role.to_ascii_uppercase();
134 let role = AgentRole::from_str(&role_name)?;
135 let model_tier = match def.model_tier.to_ascii_uppercase().as_str() {
136 "FAST" => ModelTier::Fast,
137 "BALANCED" => ModelTier::Balanced,
138 _ => ModelTier::Smart,
139 };
140
141 Some(Self {
142 id: def.id,
143 name: def.name,
144 description: def.description,
145 role,
146 default_model_tier: model_tier,
147 system_prompt: def.system_prompt,
148 role_reminder: def.role_reminder.unwrap_or_default(),
149 default_provider: def.default_provider,
150 default_adapter: def.default_adapter,
151 default_model: def.default_model,
152 })
153 }
154
155 pub fn list_available() -> Vec<Self> {
156 let mut specialists = HashMap::new();
157
158 for specialist in [Self::developer(), Self::crafter(), Self::gate()] {
159 specialists.insert(specialist.id.clone(), specialist);
160 }
161
162 let mut loader = SpecialistLoader::new();
163 loader.load_default_dirs();
164
165 for specialist in loader
166 .all()
167 .values()
168 .cloned()
169 .filter_map(Self::from_specialist_def)
170 {
171 specialists.insert(specialist.id.clone(), specialist);
172 }
173
174 let mut values: Vec<_> = specialists.into_values().collect();
175 values.sort_by(|left, right| left.id.cmp(&right.id));
176 values
177 }
178
179 pub fn resolve(input: &str) -> Option<Self> {
180 if let Some(role) = AgentRole::from_str(input) {
181 return Self::by_role(&role);
182 }
183
184 let target = input.to_lowercase();
185
186 if let Some(alias) = Self::by_id(&target) {
187 return Some(alias);
188 }
189
190 Self::list_available()
191 .into_iter()
192 .find(|specialist| specialist.id == target)
193 }
194}
195
196const CRAFTER_SYSTEM_PROMPT: &str = r#"## Crafter (Implementor)
199
200Implement your assigned task — nothing more, nothing less. Produce minimal, clean changes.
201
202## Hard Rules
2031. **No scope creep** — only what the task asks
2042. **No refactors** — if needed, report to parent for a separate task
2053. **Coordinate** — check `list_agents`/`read_agent_conversation` to avoid conflicts
2064. **Notes only** — don't create markdown files for collaboration
2075. **Don't delegate** — message parent coordinator if blocked
208
209## Completion (REQUIRED)
210When done, you MUST call `report_to_parent` with:
211- summary: 1-3 sentences of what you did
212- success: true/false
213- filesModified: list of files you changed
214- taskId: the task ID you were assigned
215"#;
216
217const CRAFTER_ROLE_REMINDER: &str =
218 "Stay within task scope. No refactors, no scope creep. Call report_to_parent when complete.";
219
220const GATE_SYSTEM_PROMPT: &str = r#"## Gate (Verifier)
221
222You verify the implementation against the spec's **Acceptance Criteria**.
223You are evidence-driven: if you can't point to concrete evidence, it's not verified.
224
225## Hard Rules
2261) **Acceptance Criteria is the checklist.** Do not verify against vibes.
2272) **No evidence, no verification.** If you can't cite evidence, mark ⚠️ or ❌.
2283) **No partial approvals.** "APPROVED" only if every criterion is ✅ VERIFIED.
229
230## Completion (REQUIRED)
231Call `report_to_parent` with:
232- summary: verdict + confidence, tests run, top 1-3 issues
233- success: true only if ALL criteria are VERIFIED
234- taskId: the task ID you were verifying
235"#;
236
237const GATE_ROLE_REMINDER: &str =
238 "Verify against Acceptance Criteria ONLY. Be evidence-driven. Call report_to_parent with verdict.";
239
240const DEVELOPER_SYSTEM_PROMPT: &str = r#"## Developer
241
242You plan and implement. You write specs first, then implement the work yourself after approval.
243
244## Hard Rules
2451. **Spec first, always** — Create/update the spec BEFORE any implementation.
2462. **Wait for approval** — Present the plan and STOP. Wait for user approval.
2473. **No delegation** — Never use `delegate_task` or `create_agent`.
248"#;
249
250const DEVELOPER_ROLE_REMINDER: &str =
251 "You work ALONE — never use delegate_task or create_agent. Spec first, wait for approval.";
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct DelegateWithSpawnParams {
259 pub task_id: String,
261 pub caller_agent_id: String,
263 pub caller_session_id: String,
265 pub workspace_id: String,
267 pub specialist: String,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub provider: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub cwd: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub additional_instructions: Option<String>,
278 #[serde(default = "default_wait_mode")]
280 pub wait_mode: String,
281}
282
283fn default_wait_mode() -> String {
284 "immediate".to_string()
285}
286
287#[derive(Debug, Clone)]
289pub struct OrchestratorConfig {
290 pub default_crafter_provider: String,
292 pub default_gate_provider: String,
294 pub default_cwd: String,
296}
297
298impl Default for OrchestratorConfig {
299 fn default() -> Self {
300 Self {
301 default_crafter_provider: "opencode".to_string(),
302 default_gate_provider: "opencode".to_string(),
303 default_cwd: ".".to_string(),
304 }
305 }
306}
307
308#[derive(Debug, Clone)]
312#[allow(dead_code)]
313struct ChildAgentRecord {
314 agent_id: String,
315 session_id: String,
316 parent_agent_id: String,
317 parent_session_id: String,
318 task_id: String,
319 role: AgentRole,
320 provider: String,
321}
322
323#[derive(Debug)]
325struct DelegationGroup {
326 #[allow(dead_code)]
327 group_id: String,
328 parent_agent_id: String,
329 parent_session_id: String,
330 child_agent_ids: Vec<String>,
331 completed_agent_ids: HashSet<String>,
332}
333
334struct OrchestratorInner {
337 child_agents: HashMap<String, ChildAgentRecord>,
339 agent_session_map: HashMap<String, String>,
341 delegation_groups: HashMap<String, DelegationGroup>,
343 active_group_by_agent: HashMap<String, String>,
345}
346
347pub struct RoutaOrchestrator {
351 inner: Arc<RwLock<OrchestratorInner>>,
352 config: OrchestratorConfig,
353 acp_manager: Arc<AcpManager>,
354 agent_store: AgentStore,
355 task_store: TaskStore,
356 event_bus: EventBus,
357}
358
359impl RoutaOrchestrator {
360 pub fn new(
361 config: OrchestratorConfig,
362 acp_manager: Arc<AcpManager>,
363 agent_store: AgentStore,
364 task_store: TaskStore,
365 event_bus: EventBus,
366 ) -> Self {
367 Self {
368 inner: Arc::new(RwLock::new(OrchestratorInner {
369 child_agents: HashMap::new(),
370 agent_session_map: HashMap::new(),
371 delegation_groups: HashMap::new(),
372 active_group_by_agent: HashMap::new(),
373 })),
374 config,
375 acp_manager,
376 agent_store,
377 task_store,
378 event_bus,
379 }
380 }
381
382 pub async fn register_agent_session(&self, agent_id: &str, session_id: &str) {
384 let mut inner = self.inner.write().await;
385 inner
386 .agent_session_map
387 .insert(agent_id.to_string(), session_id.to_string());
388 tracing::info!(
389 "[Orchestrator] Registered agent session: {} → {}",
390 agent_id,
391 session_id
392 );
393 }
394
395 pub async fn get_session_for_agent(&self, agent_id: &str) -> Option<String> {
397 let inner = self.inner.read().await;
398 inner.agent_session_map.get(agent_id).cloned()
399 }
400
401 pub async fn delegate_task_with_spawn(
403 &self,
404 params: DelegateWithSpawnParams,
405 ) -> Result<ToolResult, ServerError> {
406 let specialist_config = self.resolve_specialist(¶ms.specialist);
408 let specialist_config = match specialist_config {
409 Some(s) => s,
410 None => {
411 return Ok(ToolResult::error(format!(
412 "Unknown specialist: {}. Use CRAFTER, GATE, or DEVELOPER.",
413 params.specialist
414 )));
415 }
416 };
417
418 let task = match self.task_store.get(¶ms.task_id).await? {
420 Some(t) => t,
421 None => {
422 return Ok(ToolResult::error(format!(
423 "Task not found: {}",
424 params.task_id
425 )));
426 }
427 };
428
429 let provider = params.provider.unwrap_or_else(|| {
431 if specialist_config.role == AgentRole::Crafter {
432 self.config.default_crafter_provider.clone()
433 } else {
434 self.config.default_gate_provider.clone()
435 }
436 });
437
438 let cwd = params
439 .cwd
440 .unwrap_or_else(|| self.config.default_cwd.clone());
441
442 let agent_id = uuid::Uuid::new_v4().to_string();
444 let agent_name = format!(
445 "{}-{}",
446 specialist_config.id,
447 task.title
448 .chars()
449 .take(30)
450 .collect::<String>()
451 .replace(' ', "-")
452 .to_lowercase()
453 );
454
455 let agent = crate::models::agent::Agent::new(
456 agent_id.clone(),
457 agent_name.clone(),
458 specialist_config.role.clone(),
459 params.workspace_id.clone(),
460 Some(params.caller_agent_id.clone()),
461 Some(specialist_config.default_model_tier.clone()),
462 None,
463 );
464 self.agent_store.save(&agent).await?;
465
466 let delegation_prompt = build_delegation_prompt(
468 &specialist_config,
469 &agent_id,
470 ¶ms.task_id,
471 &task.title,
472 &task.objective,
473 task.scope.as_deref(),
474 task.acceptance_criteria.as_ref(),
475 task.verification_commands.as_ref(),
476 task.test_cases.as_ref(),
477 ¶ms.caller_agent_id,
478 params.additional_instructions.as_deref(),
479 );
480
481 let mut task = task;
483 task.assigned_to = Some(agent_id.clone());
484 task.status = TaskStatus::InProgress;
485 task.updated_at = Utc::now();
486 self.task_store.save(&task).await?;
487 self.agent_store
488 .update_status(&agent_id, &AgentStatus::Active)
489 .await?;
490
491 let child_session_id = uuid::Uuid::new_v4().to_string();
493 let spawn_result = self
494 .acp_manager
495 .create_session(
496 child_session_id.clone(),
497 cwd.clone(),
498 params.workspace_id.clone(),
499 Some(provider.clone()),
500 Some(specialist_config.role.as_str().to_string()),
501 None,
502 Some(params.caller_session_id.clone()), None,
504 None,
505 )
506 .await;
507
508 let (_, _acp_session_id) = match spawn_result {
509 Ok(ids) => ids,
510 Err(e) => {
511 self.agent_store
513 .update_status(&agent_id, &AgentStatus::Error)
514 .await?;
515 task.status = TaskStatus::Blocked;
516 task.updated_at = Utc::now();
517 self.task_store.save(&task).await?;
518 return Ok(ToolResult::error(format!(
519 "Failed to spawn agent process: {}",
520 e
521 )));
522 }
523 };
524
525 self.acp_manager
529 .mark_first_prompt_sent(&child_session_id)
530 .await;
531 let child_prompt_manager = Arc::clone(&self.acp_manager);
532 let child_prompt_session_id = child_session_id.clone();
533 let child_prompt_agent_id = agent_id.clone();
534 tokio::spawn(async move {
535 if let Err(e) = child_prompt_manager
536 .prompt(&child_prompt_session_id, &delegation_prompt)
537 .await
538 {
539 tracing::error!(
540 "[Orchestrator] Failed to send initial prompt to agent {}: {}",
541 child_prompt_agent_id,
542 e
543 );
544 }
545 });
546
547 self.acp_manager
548 .push_to_history(
549 &child_session_id,
550 serde_json::json!({
551 "sessionId": child_session_id,
552 "update": {
553 "sessionUpdate": "agent_message",
554 "content": {
555 "type": "text",
556 "text": format!(
557 "Delegated task '{}' to child agent {}. Child session launched and awaiting transcript updates.",
558 task.title, agent_name
559 )
560 }
561 }
562 }),
563 )
564 .await;
565
566 {
568 let mut inner = self.inner.write().await;
569 let record = ChildAgentRecord {
570 agent_id: agent_id.clone(),
571 session_id: child_session_id.clone(),
572 parent_agent_id: params.caller_agent_id.clone(),
573 parent_session_id: params.caller_session_id.clone(),
574 task_id: params.task_id.clone(),
575 role: specialist_config.role.clone(),
576 provider: provider.clone(),
577 };
578 inner.child_agents.insert(agent_id.clone(), record);
579 inner
580 .agent_session_map
581 .insert(agent_id.clone(), child_session_id.clone());
582
583 if params.wait_mode == "after_all" {
585 let group_id = inner
586 .active_group_by_agent
587 .get(¶ms.caller_agent_id)
588 .cloned();
589
590 let group_id = match group_id {
591 Some(gid) => gid,
592 None => {
593 let new_group_id = format!("delegation-group-{}", uuid::Uuid::new_v4());
594 inner
595 .active_group_by_agent
596 .insert(params.caller_agent_id.clone(), new_group_id.clone());
597 inner.delegation_groups.insert(
598 new_group_id.clone(),
599 DelegationGroup {
600 group_id: new_group_id.clone(),
601 parent_agent_id: params.caller_agent_id.clone(),
602 parent_session_id: params.caller_session_id.clone(),
603 child_agent_ids: Vec::new(),
604 completed_agent_ids: HashSet::new(),
605 },
606 );
607 new_group_id
608 }
609 };
610
611 if let Some(group) = inner.delegation_groups.get_mut(&group_id) {
612 group.child_agent_ids.push(agent_id.clone());
613 }
614 }
615 }
616
617 self.event_bus
619 .emit(AgentEvent {
620 event_type: AgentEventType::TaskAssigned,
621 agent_id: agent_id.clone(),
622 workspace_id: params.workspace_id.clone(),
623 data: serde_json::json!({
624 "taskId": params.task_id,
625 "callerAgentId": params.caller_agent_id,
626 "taskTitle": task.title,
627 "provider": provider,
628 "specialist": specialist_config.id,
629 }),
630 timestamp: Utc::now(),
631 })
632 .await;
633
634 let wait_message = if params.wait_mode == "after_all" {
635 "You will be notified when ALL delegated agents in this group complete."
636 } else {
637 "You will be notified when this agent completes."
638 };
639
640 tracing::info!(
641 "[Orchestrator] Delegated task \"{}\" to {} agent {} (provider: {})",
642 task.title,
643 specialist_config.name,
644 agent_id,
645 provider
646 );
647
648 Ok(ToolResult::success(serde_json::json!({
649 "agentId": agent_id,
650 "taskId": params.task_id,
651 "agentName": agent_name,
652 "specialist": specialist_config.id,
653 "provider": provider,
654 "sessionId": child_session_id,
655 "waitMode": params.wait_mode,
656 "message": format!("Task \"{}\" delegated to {} agent. {}", task.title, specialist_config.name, wait_message),
657 })))
658 }
659
660 pub async fn handle_report_submitted(
662 &self,
663 child_agent_id: &str,
664 report: &CompletionReport,
665 ) -> Result<(), ServerError> {
666 let record = {
667 let inner = self.inner.read().await;
668 inner.child_agents.get(child_agent_id).cloned()
669 };
670
671 let record = match record {
672 Some(r) => r,
673 None => {
674 tracing::warn!(
675 "[Orchestrator] Report from unknown child agent {}, ignoring",
676 child_agent_id
677 );
678 return Ok(());
679 }
680 };
681
682 if let Some(task_id) = &report.task_id {
684 if let Some(mut task) = self.task_store.get(task_id).await? {
685 task.status = if report.success {
686 TaskStatus::Completed
687 } else {
688 TaskStatus::NeedsFix
689 };
690 task.completion_summary = Some(report.summary.clone());
691 task.updated_at = Utc::now();
692 self.task_store.save(&task).await?;
693 }
694 }
695
696 self.agent_store
698 .update_status(child_agent_id, &AgentStatus::Completed)
699 .await?;
700
701 self.handle_child_completion(child_agent_id, &record)
703 .await?;
704
705 Ok(())
706 }
707
708 async fn handle_child_completion(
710 &self,
711 child_agent_id: &str,
712 record: &ChildAgentRecord,
713 ) -> Result<(), ServerError> {
714 let mut inner = self.inner.write().await;
715
716 let mut group_complete = None;
718 for (group_id, group) in inner.delegation_groups.iter_mut() {
719 if group.child_agent_ids.contains(&child_agent_id.to_string()) {
720 group.completed_agent_ids.insert(child_agent_id.to_string());
721 tracing::info!(
722 "[Orchestrator] Agent {} completed in group {} ({}/{})",
723 child_agent_id,
724 group_id,
725 group.completed_agent_ids.len(),
726 group.child_agent_ids.len()
727 );
728
729 if group.completed_agent_ids.len() >= group.child_agent_ids.len() {
730 group_complete = Some((
731 group_id.clone(),
732 group.parent_agent_id.clone(),
733 group.parent_session_id.clone(),
734 ));
735 }
736 break;
737 }
738 }
739
740 if let Some((group_id, parent_agent_id, parent_session_id)) = group_complete {
741 tracing::info!(
742 "[Orchestrator] All agents in group {} completed, waking parent",
743 group_id
744 );
745 inner.delegation_groups.remove(&group_id);
746 inner.active_group_by_agent.remove(&parent_agent_id);
747
748 drop(inner); self.wake_parent_with_group_completion(&parent_session_id, &group_id)
751 .await?;
752 } else {
753 tracing::info!(
755 "[Orchestrator] Child agent {} completed, waking parent {}",
756 child_agent_id,
757 record.parent_agent_id
758 );
759 drop(inner);
760 self.wake_parent(&record.parent_session_id, child_agent_id, &record.task_id)
761 .await?;
762 }
763
764 Ok(())
765 }
766
767 async fn wake_parent(
769 &self,
770 parent_session_id: &str,
771 child_agent_id: &str,
772 task_id: &str,
773 ) -> Result<(), ServerError> {
774 let agent = self.agent_store.get(child_agent_id).await?;
775 let task = self.task_store.get(task_id).await?;
776
777 let wake_message = format!(
778 "## Agent Completion Report\n\n\
779 **Agent:** {} ({})\n\
780 **Task:** {}\n\
781 **Status:** {:?}\n\
782 {}\n\
783 Review the results and decide next steps.",
784 agent
785 .as_ref()
786 .map(|a| a.name.as_str())
787 .unwrap_or(child_agent_id),
788 child_agent_id,
789 task.as_ref().map(|t| t.title.as_str()).unwrap_or(task_id),
790 task.as_ref().map(|t| &t.status),
791 task.as_ref()
792 .and_then(|t| t.completion_summary.as_ref())
793 .map(|s| format!("**Summary:** {}\n", s))
794 .unwrap_or_default()
795 );
796
797 if let Err(e) = self
798 .acp_manager
799 .prompt(parent_session_id, &wake_message)
800 .await
801 {
802 tracing::error!(
803 "[Orchestrator] Failed to wake parent session {}: {}",
804 parent_session_id,
805 e
806 );
807 }
808
809 Ok(())
810 }
811
812 async fn wake_parent_with_group_completion(
814 &self,
815 parent_session_id: &str,
816 _group_id: &str,
817 ) -> Result<(), ServerError> {
818 let wake_message = "## Delegation Group Complete\n\n\
819 All delegated agents have completed their work.\n\
820 Review the results and decide next steps.\n\
821 You may want to delegate a GATE (verifier) agent to validate the work.";
822
823 if let Err(e) = self
824 .acp_manager
825 .prompt(parent_session_id, wake_message)
826 .await
827 {
828 tracing::error!(
829 "[Orchestrator] Failed to wake parent session {}: {}",
830 parent_session_id,
831 e
832 );
833 }
834
835 Ok(())
836 }
837
838 fn resolve_specialist(&self, input: &str) -> Option<SpecialistConfig> {
840 SpecialistConfig::resolve(input)
841 }
842
843 pub async fn cleanup(&self, session_id: &str) {
845 let mut inner = self.inner.write().await;
846 let agents_to_remove: Vec<String> = inner
847 .child_agents
848 .iter()
849 .filter(|(_, r)| r.parent_session_id == session_id || r.session_id == session_id)
850 .map(|(id, _)| id.clone())
851 .collect();
852
853 for agent_id in agents_to_remove {
854 if let Some(record) = inner.child_agents.remove(&agent_id) {
855 self.acp_manager.kill_session(&record.session_id).await;
856 }
857 inner.agent_session_map.remove(&agent_id);
858 }
859 }
860}
861
862#[allow(clippy::too_many_arguments)]
866fn build_delegation_prompt(
867 specialist: &SpecialistConfig,
868 agent_id: &str,
869 task_id: &str,
870 task_title: &str,
871 task_objective: &str,
872 task_scope: Option<&str>,
873 acceptance_criteria: Option<&Vec<String>>,
874 verification_commands: Option<&Vec<String>>,
875 test_cases: Option<&Vec<String>>,
876 parent_agent_id: &str,
877 additional_context: Option<&str>,
878) -> String {
879 let mut prompt = format!("{}\n\n---\n\n", specialist.system_prompt);
880 prompt.push_str(&format!("**Your Agent ID:** {}\n", agent_id));
881 prompt.push_str(&format!("**Your Parent Agent ID:** {}\n", parent_agent_id));
882 prompt.push_str(&format!("**Task ID:** {}\n\n", task_id));
883 prompt.push_str(&format!("# Task: {}\n\n", task_title));
884 prompt.push_str(&format!("## Objective\n{}\n", task_objective));
885
886 if let Some(scope) = task_scope {
887 prompt.push_str(&format!("\n## Scope\n{}\n", scope));
888 }
889
890 if let Some(criteria) = acceptance_criteria {
891 prompt.push_str("\n## Definition of Done\n");
892 for c in criteria {
893 prompt.push_str(&format!("- {}\n", c));
894 }
895 }
896
897 if let Some(commands) = verification_commands {
898 prompt.push_str("\n## Verification\n");
899 for c in commands {
900 prompt.push_str(&format!("- `{}`\n", c));
901 }
902 }
903
904 if let Some(cases) = test_cases {
905 prompt.push_str("\n## Test Cases\n");
906 for case in cases {
907 prompt.push_str(&format!("- {}\n", case));
908 }
909 }
910
911 prompt.push_str(&format!(
912 "\n---\n**Reminder:** {}\n",
913 specialist.role_reminder
914 ));
915
916 if let Some(ctx) = additional_context {
917 prompt.push_str(&format!("\n**Additional Context:** {}\n", ctx));
918 }
919
920 prompt.push_str("\n**SCOPE: Complete THIS task only.** When done, call `report_to_parent` with your results.");
921
922 prompt
923}