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: {}",
520                    e
521                )));
522            }
523        };
524
525        // Kick off the child prompt in the background. Waiting for the entire
526        // child turn here blocks the parent MCP tool call long enough for
527        // OpenCode to abort delegation before the child can report progress.
528        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        // 8. Track the child agent
567        {
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            // 9. Handle wait mode
584            if params.wait_mode == "after_all" {
585                let group_id = inner
586                    .active_group_by_agent
587                    .get(&params.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        // 10. Emit event
618        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    /// Handle a report submitted by a child agent.
661    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        // Update task status
683        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        // Mark agent completed
697        self.agent_store
698            .update_status(child_agent_id, &AgentStatus::Completed)
699            .await?;
700
701        // Handle completion (check groups or wake parent)
702        self.handle_child_completion(child_agent_id, &record)
703            .await?;
704
705        Ok(())
706    }
707
708    /// Handle child agent completion: check groups or immediately wake parent.
709    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        // Check if this child is part of an after_all group
717        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            // Wake parent with group completion message
749            drop(inner); // Release lock before async call
750            self.wake_parent_with_group_completion(&parent_session_id, &group_id)
751                .await?;
752        } else {
753            // Immediate mode: wake parent right away
754            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    /// Wake a parent agent by sending a completion prompt to its session.
768    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    /// Wake parent with group completion message.
813    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    /// Resolve specialist config from a string (role name or specialist ID).
839    fn resolve_specialist(&self, input: &str) -> Option<SpecialistConfig> {
840        SpecialistConfig::resolve(input)
841    }
842
843    /// Clean up resources for a session.
844    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// ─── Helper Functions ─────────────────────────────────────────────────────
863
864/// Build the initial prompt for a delegated agent.
865#[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}