Skip to main content

routa_core/orchestration/
mod.rs

1//! RoutaOrchestrator - Task orchestration and child agent spawning.
2//!
3//! Port of the TypeScript RoutaOrchestrator from src/core/orchestration/orchestrator.ts
4//!
5//! The orchestrator bridges MCP tool calls with actual ACP process spawning:
6//!   1. Creates a child agent record
7//!   2. Spawns a real ACP process for the child agent
8//!   3. Sends the task as the initial prompt
9//!   4. Subscribes for completion events
10//!   5. When the child reports back, wakes the parent agent
11
12use 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// ─── Specialist Configuration ─────────────────────────────────────────────
29
30/// Specialist configuration for agent roles.
31#[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    /// Get the CRAFTER specialist config.
52    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    /// Get the GATE specialist config.
68    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    /// Get the DEVELOPER specialist config.
84    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    /// Get specialist by role.
100    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, // Coordinator doesn't delegate to itself
106        }
107    }
108
109    /// Get specialist by ID.
110    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
196// ─── System Prompts (Hardcoded Fallbacks) ─────────────────────────────────
197
198const 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// ─── Delegation Parameters ────────────────────────────────────────────────
254
255/// Parameters for delegating a task with agent spawning.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct DelegateWithSpawnParams {
259    /// Task ID to delegate
260    pub task_id: String,
261    /// Calling agent's ID
262    pub caller_agent_id: String,
263    /// Calling agent's session ID (for wake-up)
264    pub caller_session_id: String,
265    /// Workspace ID
266    pub workspace_id: String,
267    /// Specialist role: "CRAFTER", "GATE", "DEVELOPER"
268    pub specialist: String,
269    /// ACP provider to use for the child
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub provider: Option<String>,
272    /// Working directory for the child agent
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub cwd: Option<String>,
275    /// Additional instructions beyond the task content
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub additional_instructions: Option<String>,
278    /// Wait mode: "immediate" or "after_all"
279    #[serde(default = "default_wait_mode")]
280    pub wait_mode: String,
281}
282
283fn default_wait_mode() -> String {
284    "immediate".to_string()
285}
286
287/// Orchestrator configuration.
288#[derive(Debug, Clone)]
289pub struct OrchestratorConfig {
290    /// Default ACP provider for CRAFTER agents
291    pub default_crafter_provider: String,
292    /// Default ACP provider for GATE agents
293    pub default_gate_provider: String,
294    /// Default working directory
295    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// ─── Child Agent Record ───────────────────────────────────────────────────
309
310/// Tracks a spawned child agent and its relationship to a parent.
311#[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/// Delegation group for wait_mode="after_all"
324#[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
334// ─── Orchestrator Inner State ─────────────────────────────────────────────
335
336struct OrchestratorInner {
337    /// Map: agentId → ChildAgentRecord
338    child_agents: HashMap<String, ChildAgentRecord>,
339    /// Map: agentId → sessionId
340    agent_session_map: HashMap<String, String>,
341    /// Map: groupId → DelegationGroup
342    delegation_groups: HashMap<String, DelegationGroup>,
343    /// Map: callerAgentId → current groupId (for after_all mode)
344    active_group_by_agent: HashMap<String, String>,
345}
346
347// ─── Routa Orchestrator ───────────────────────────────────────────────────
348
349/// The core orchestration engine that bridges MCP tool calls with ACP process spawning.
350pub 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    /// Register the mapping between an agent ID and its ACP session ID.
383    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    /// Get the session ID for an agent.
396    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    /// Delegate a task to a new agent by spawning a real ACP process.
402    pub async fn delegate_task_with_spawn(
403        &self,
404        params: DelegateWithSpawnParams,
405    ) -> Result<ToolResult, ServerError> {
406        // 1. Resolve specialist config
407        let specialist_config = self.resolve_specialist(&params.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        // 2. Get the task
419        let task = match self.task_store.get(&params.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        // 3. Determine provider
430        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        // 4. Create agent record
443        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        // 5. Build the delegation prompt
467        let delegation_prompt = build_delegation_prompt(
468            &specialist_config,
469            &agent_id,
470            &params.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            &params.caller_agent_id,
478            params.additional_instructions.as_deref(),
479        );
480
481        // 6. Assign task to agent and update status
482        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        // 7. Spawn the ACP process
492        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()), // parent_session_id
503                None,
504                None,
505            )
506            .await;
507
508        let (_, _acp_session_id) = match spawn_result {
509            Ok(ids) => ids,
510            Err(e) => {
511                // Clean up on spawn failure
512                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: {e}"
520                )));
521            }
522        };
523
524        // Kick off the child prompt in the background. Waiting for the entire
525        // child turn here blocks the parent MCP tool call long enough for
526        // OpenCode to abort delegation before the child can report progress.
527        self.acp_manager
528            .mark_first_prompt_sent(&child_session_id)
529            .await;
530        let child_prompt_manager = Arc::clone(&self.acp_manager);
531        let child_prompt_session_id = child_session_id.clone();
532        let child_prompt_agent_id = agent_id.clone();
533        tokio::spawn(async move {
534            if let Err(e) = child_prompt_manager
535                .prompt(&child_prompt_session_id, &delegation_prompt)
536                .await
537            {
538                tracing::error!(
539                    "[Orchestrator] Failed to send initial prompt to agent {}: {}",
540                    child_prompt_agent_id,
541                    e
542                );
543            }
544        });
545
546        self.acp_manager
547            .push_to_history(
548                &child_session_id,
549                serde_json::json!({
550                    "sessionId": child_session_id,
551                    "update": {
552                        "sessionUpdate": "agent_message",
553                        "content": {
554                            "type": "text",
555                            "text": format!(
556                                "Delegated task '{}' to child agent {}. Child session launched and awaiting transcript updates.",
557                                task.title, agent_name
558                            )
559                        }
560                    }
561                }),
562            )
563            .await;
564
565        // 8. Track the child agent
566        {
567            let mut inner = self.inner.write().await;
568            let record = ChildAgentRecord {
569                agent_id: agent_id.clone(),
570                session_id: child_session_id.clone(),
571                parent_agent_id: params.caller_agent_id.clone(),
572                parent_session_id: params.caller_session_id.clone(),
573                task_id: params.task_id.clone(),
574                role: specialist_config.role.clone(),
575                provider: provider.clone(),
576            };
577            inner.child_agents.insert(agent_id.clone(), record);
578            inner
579                .agent_session_map
580                .insert(agent_id.clone(), child_session_id.clone());
581
582            // 9. Handle wait mode
583            if params.wait_mode == "after_all" {
584                let group_id = inner
585                    .active_group_by_agent
586                    .get(&params.caller_agent_id)
587                    .cloned();
588
589                let group_id = match group_id {
590                    Some(gid) => gid,
591                    None => {
592                        let new_group_id = format!("delegation-group-{}", uuid::Uuid::new_v4());
593                        inner
594                            .active_group_by_agent
595                            .insert(params.caller_agent_id.clone(), new_group_id.clone());
596                        inner.delegation_groups.insert(
597                            new_group_id.clone(),
598                            DelegationGroup {
599                                group_id: new_group_id.clone(),
600                                parent_agent_id: params.caller_agent_id.clone(),
601                                parent_session_id: params.caller_session_id.clone(),
602                                child_agent_ids: Vec::new(),
603                                completed_agent_ids: HashSet::new(),
604                            },
605                        );
606                        new_group_id
607                    }
608                };
609
610                if let Some(group) = inner.delegation_groups.get_mut(&group_id) {
611                    group.child_agent_ids.push(agent_id.clone());
612                }
613            }
614        }
615
616        // 10. Emit event
617        self.event_bus
618            .emit(AgentEvent {
619                event_type: AgentEventType::TaskAssigned,
620                agent_id: agent_id.clone(),
621                workspace_id: params.workspace_id.clone(),
622                data: serde_json::json!({
623                    "taskId": params.task_id,
624                    "callerAgentId": params.caller_agent_id,
625                    "taskTitle": task.title,
626                    "provider": provider,
627                    "specialist": specialist_config.id,
628                }),
629                timestamp: Utc::now(),
630            })
631            .await;
632
633        let wait_message = if params.wait_mode == "after_all" {
634            "You will be notified when ALL delegated agents in this group complete."
635        } else {
636            "You will be notified when this agent completes."
637        };
638
639        tracing::info!(
640            "[Orchestrator] Delegated task \"{}\" to {} agent {} (provider: {})",
641            task.title,
642            specialist_config.name,
643            agent_id,
644            provider
645        );
646
647        Ok(ToolResult::success(serde_json::json!({
648            "agentId": agent_id,
649            "taskId": params.task_id,
650            "agentName": agent_name,
651            "specialist": specialist_config.id,
652            "provider": provider,
653            "sessionId": child_session_id,
654            "waitMode": params.wait_mode,
655            "message": format!("Task \"{}\" delegated to {} agent. {}", task.title, specialist_config.name, wait_message),
656        })))
657    }
658
659    /// Handle a report submitted by a child agent.
660    pub async fn handle_report_submitted(
661        &self,
662        child_agent_id: &str,
663        report: &CompletionReport,
664    ) -> Result<(), ServerError> {
665        let record = {
666            let inner = self.inner.read().await;
667            inner.child_agents.get(child_agent_id).cloned()
668        };
669
670        let record = match record {
671            Some(r) => r,
672            None => {
673                tracing::warn!(
674                    "[Orchestrator] Report from unknown child agent {}, ignoring",
675                    child_agent_id
676                );
677                return Ok(());
678            }
679        };
680
681        // Update task status
682        if let Some(task_id) = &report.task_id {
683            if let Some(mut task) = self.task_store.get(task_id).await? {
684                task.status = if report.success {
685                    TaskStatus::Completed
686                } else {
687                    TaskStatus::NeedsFix
688                };
689                task.completion_summary = Some(report.summary.clone());
690                task.updated_at = Utc::now();
691                self.task_store.save(&task).await?;
692            }
693        }
694
695        // Mark agent completed
696        self.agent_store
697            .update_status(child_agent_id, &AgentStatus::Completed)
698            .await?;
699
700        // Handle completion (check groups or wake parent)
701        self.handle_child_completion(child_agent_id, &record)
702            .await?;
703
704        Ok(())
705    }
706
707    /// Handle child agent completion: check groups or immediately wake parent.
708    async fn handle_child_completion(
709        &self,
710        child_agent_id: &str,
711        record: &ChildAgentRecord,
712    ) -> Result<(), ServerError> {
713        let mut inner = self.inner.write().await;
714
715        // Check if this child is part of an after_all group
716        let mut group_complete = None;
717        for (group_id, group) in inner.delegation_groups.iter_mut() {
718            if group.child_agent_ids.contains(&child_agent_id.to_string()) {
719                group.completed_agent_ids.insert(child_agent_id.to_string());
720                tracing::info!(
721                    "[Orchestrator] Agent {} completed in group {} ({}/{})",
722                    child_agent_id,
723                    group_id,
724                    group.completed_agent_ids.len(),
725                    group.child_agent_ids.len()
726                );
727
728                if group.completed_agent_ids.len() >= group.child_agent_ids.len() {
729                    group_complete = Some((
730                        group_id.clone(),
731                        group.parent_agent_id.clone(),
732                        group.parent_session_id.clone(),
733                    ));
734                }
735                break;
736            }
737        }
738
739        if let Some((group_id, parent_agent_id, parent_session_id)) = group_complete {
740            tracing::info!(
741                "[Orchestrator] All agents in group {} completed, waking parent",
742                group_id
743            );
744            inner.delegation_groups.remove(&group_id);
745            inner.active_group_by_agent.remove(&parent_agent_id);
746
747            // Wake parent with group completion message
748            drop(inner); // Release lock before async call
749            self.wake_parent_with_group_completion(&parent_session_id, &group_id)
750                .await?;
751        } else {
752            // Immediate mode: wake parent right away
753            tracing::info!(
754                "[Orchestrator] Child agent {} completed, waking parent {}",
755                child_agent_id,
756                record.parent_agent_id
757            );
758            drop(inner);
759            self.wake_parent(&record.parent_session_id, child_agent_id, &record.task_id)
760                .await?;
761        }
762
763        Ok(())
764    }
765
766    /// Wake a parent agent by sending a completion prompt to its session.
767    async fn wake_parent(
768        &self,
769        parent_session_id: &str,
770        child_agent_id: &str,
771        task_id: &str,
772    ) -> Result<(), ServerError> {
773        let agent = self.agent_store.get(child_agent_id).await?;
774        let task = self.task_store.get(task_id).await?;
775
776        let wake_message = format!(
777            "## Agent Completion Report\n\n\
778             **Agent:** {} ({})\n\
779             **Task:** {}\n\
780             **Status:** {:?}\n\
781             {}\n\
782             Review the results and decide next steps.",
783            agent
784                .as_ref()
785                .map(|a| a.name.as_str())
786                .unwrap_or(child_agent_id),
787            child_agent_id,
788            task.as_ref().map(|t| t.title.as_str()).unwrap_or(task_id),
789            task.as_ref().map(|t| &t.status),
790            task.as_ref()
791                .and_then(|t| t.completion_summary.as_ref())
792                .map(|s| format!("**Summary:** {s}\n"))
793                .unwrap_or_default()
794        );
795
796        if let Err(e) = self
797            .acp_manager
798            .prompt(parent_session_id, &wake_message)
799            .await
800        {
801            tracing::error!(
802                "[Orchestrator] Failed to wake parent session {}: {}",
803                parent_session_id,
804                e
805            );
806        }
807
808        Ok(())
809    }
810
811    /// Wake parent with group completion message.
812    async fn wake_parent_with_group_completion(
813        &self,
814        parent_session_id: &str,
815        _group_id: &str,
816    ) -> Result<(), ServerError> {
817        let wake_message = "## Delegation Group Complete\n\n\
818            All delegated agents have completed their work.\n\
819            Review the results and decide next steps.\n\
820            You may want to delegate a GATE (verifier) agent to validate the work.";
821
822        if let Err(e) = self
823            .acp_manager
824            .prompt(parent_session_id, wake_message)
825            .await
826        {
827            tracing::error!(
828                "[Orchestrator] Failed to wake parent session {}: {}",
829                parent_session_id,
830                e
831            );
832        }
833
834        Ok(())
835    }
836
837    /// Resolve specialist config from a string (role name or specialist ID).
838    fn resolve_specialist(&self, input: &str) -> Option<SpecialistConfig> {
839        SpecialistConfig::resolve(input)
840    }
841
842    /// Clean up resources for a session.
843    pub async fn cleanup(&self, session_id: &str) {
844        let mut inner = self.inner.write().await;
845        let agents_to_remove: Vec<String> = inner
846            .child_agents
847            .iter()
848            .filter(|(_, r)| r.parent_session_id == session_id || r.session_id == session_id)
849            .map(|(id, _)| id.clone())
850            .collect();
851
852        for agent_id in agents_to_remove {
853            if let Some(record) = inner.child_agents.remove(&agent_id) {
854                self.acp_manager.kill_session(&record.session_id).await;
855            }
856            inner.agent_session_map.remove(&agent_id);
857        }
858    }
859}
860
861// ─── Helper Functions ─────────────────────────────────────────────────────
862
863/// Build the initial prompt for a delegated agent.
864#[allow(clippy::too_many_arguments)]
865fn build_delegation_prompt(
866    specialist: &SpecialistConfig,
867    agent_id: &str,
868    task_id: &str,
869    task_title: &str,
870    task_objective: &str,
871    task_scope: Option<&str>,
872    acceptance_criteria: Option<&Vec<String>>,
873    verification_commands: Option<&Vec<String>>,
874    test_cases: Option<&Vec<String>>,
875    parent_agent_id: &str,
876    additional_context: Option<&str>,
877) -> String {
878    let mut prompt = format!("{}\n\n---\n\n", specialist.system_prompt);
879    prompt.push_str(&format!("**Your Agent ID:** {agent_id}\n"));
880    prompt.push_str(&format!("**Your Parent Agent ID:** {parent_agent_id}\n"));
881    prompt.push_str(&format!("**Task ID:** {task_id}\n\n"));
882    prompt.push_str(&format!("# Task: {task_title}\n\n"));
883    prompt.push_str(&format!("## Objective\n{task_objective}\n"));
884
885    if let Some(scope) = task_scope {
886        prompt.push_str(&format!("\n## Scope\n{scope}\n"));
887    }
888
889    if let Some(criteria) = acceptance_criteria {
890        prompt.push_str("\n## Definition of Done\n");
891        for c in criteria {
892            prompt.push_str(&format!("- {c}\n"));
893        }
894    }
895
896    if let Some(commands) = verification_commands {
897        prompt.push_str("\n## Verification\n");
898        for c in commands {
899            prompt.push_str(&format!("- `{c}`\n"));
900        }
901    }
902
903    if let Some(cases) = test_cases {
904        prompt.push_str("\n## Test Cases\n");
905        for case in cases {
906            prompt.push_str(&format!("- {case}\n"));
907        }
908    }
909
910    prompt.push_str(&format!(
911        "\n---\n**Reminder:** {}\n",
912        specialist.role_reminder
913    ));
914
915    if let Some(ctx) = additional_context {
916        prompt.push_str(&format!("\n**Additional Context:** {ctx}\n"));
917    }
918
919    prompt.push_str("\n**SCOPE: Complete THIS task only.** When done, call `report_to_parent` with your results.");
920
921    prompt
922}