gestura_core_agents/
lib.rs

1//! Agent lifecycle management and delegation primitives for Gestura.
2//!
3//! `gestura-core-agents` provides the typed orchestration model for spawning
4//! specialist agents, delegating structured work, and tracking task execution
5//! results across local and remote execution modes.
6//!
7//! ## Design role
8//!
9//! This crate owns the agent/delegation vocabulary used by supervisor-style
10//! orchestration without pulling in GUI-specific concerns. It is intentionally a
11//! domain crate for *types and lifecycle primitives*, while presentation-layer
12//! integration and higher-level runtime coordination remain elsewhere.
13//!
14//! ## Main concepts
15//!
16//! - `AgentRole`: specialist roles such as supervisor, implementer, reviewer,
17//!   tester, and remote worker
18//! - `AgentSpawnRequest`: spawn-time contract including workspace, execution
19//!   mode, and advertised capabilities
20//! - `DelegatedTask`: structured delegated work with approvals, dependencies,
21//!   reviewer/test gates, memory tags, and remote-target metadata
22//! - `TaskResult`: normalized result payload including artifacts and tool-call
23//!   provenance
24//! - `AgentManager` and `AgentSpawner`: lifecycle primitives for creating and
25//!   managing active agents
26//!
27//! ## Execution model
28//!
29//! Delegated work supports several execution environments:
30//!
31//! - shared workspace
32//! - isolated workspace
33//! - git worktree-backed execution
34//! - remote execution targets
35//!
36//! This lets the rest of the system reason about isolation and provenance using
37//! typed metadata instead of ad hoc strings.
38//!
39//! ## Stable import paths
40//!
41//! Most application code should import these types through `gestura_core::agents::*`.
42//! GUI-specific orchestration wrappers remain in the GUI crate.
43
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::path::PathBuf;
47use std::sync::Arc;
48use std::time::Duration;
49use tokio::sync::{Mutex, mpsc};
50use tokio::task::JoinHandle;
51
52/// Specialist role assigned to a managed agent.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[serde(rename_all = "snake_case")]
55pub enum AgentRole {
56    /// Supervisor/lead coordinator for a team run.
57    Supervisor,
58    /// Research-focused subagent.
59    Researcher,
60    /// Default implementation-oriented subagent.
61    #[default]
62    Implementer,
63    /// Review and critique specialist.
64    Reviewer,
65    /// Test authoring and validation specialist.
66    Tester,
67    /// Security-focused reviewer.
68    SecurityReviewer,
69    /// Remote execution worker.
70    RemoteWorker,
71    /// Custom role label.
72    Custom(String),
73}
74
75impl AgentRole {
76    /// Human-readable label for the role.
77    pub fn label(&self) -> String {
78        match self {
79            Self::Supervisor => "Supervisor".to_string(),
80            Self::Researcher => "Researcher".to_string(),
81            Self::Implementer => "Implementer".to_string(),
82            Self::Reviewer => "Reviewer".to_string(),
83            Self::Tester => "Tester".to_string(),
84            Self::SecurityReviewer => "Security Reviewer".to_string(),
85            Self::RemoteWorker => "Remote Worker".to_string(),
86            Self::Custom(value) => value.clone(),
87        }
88    }
89
90    /// Default capability tags advertised for this role.
91    pub fn default_capabilities(&self) -> Vec<String> {
92        match self {
93            Self::Supervisor => vec!["planning", "delegation", "synthesis"],
94            Self::Researcher => vec!["research", "analysis", "summarization"],
95            Self::Implementer => vec!["implementation", "editing", "refactoring"],
96            Self::Reviewer => vec!["review", "critique", "quality"],
97            Self::Tester => vec!["testing", "validation", "regression"],
98            Self::SecurityReviewer => vec!["security", "threat-modeling", "review"],
99            Self::RemoteWorker => vec!["remote_execution", "handoff", "artifacts"],
100            Self::Custom(_) => vec!["custom"],
101        }
102        .into_iter()
103        .map(str::to_string)
104        .collect()
105    }
106
107    /// Prompt preamble that reinforces specialist behavior.
108    pub fn prompt_preamble(&self) -> &'static str {
109        match self {
110            Self::Supervisor => {
111                "You are the team supervisor. Plan carefully, coordinate subtasks, and synthesize outcomes for the user."
112            }
113            Self::Researcher => {
114                "You are the research specialist. Focus on collecting evidence, clarifying unknowns, and producing concise findings."
115            }
116            Self::Implementer => {
117                "You are the implementation specialist. Make precise code changes, respect existing patterns, and explain what changed."
118            }
119            Self::Reviewer => {
120                "You are the reviewer specialist. Critique plans and changes, identify risks, and recommend concrete fixes."
121            }
122            Self::Tester => {
123                "You are the testing specialist. Design coverage, verify behavior, and surface regressions clearly."
124            }
125            Self::SecurityReviewer => {
126                "You are the security reviewer. Prioritize threat modeling, permissions, trust boundaries, and misuse risks."
127            }
128            Self::RemoteWorker => {
129                "You are a remote worker. Operate with explicit contracts, produce durable artifacts, and report provenance."
130            }
131            Self::Custom(_) => {
132                "You are a specialist subagent. Operate within the delegated role, constraints, and deliverables."
133            }
134        }
135    }
136}
137
138/// Execution mode used by a subagent.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
140#[serde(rename_all = "snake_case")]
141pub enum AgentExecutionMode {
142    /// Shared workspace with read/write access controlled elsewhere.
143    #[default]
144    SharedWorkspace,
145    /// Dedicated isolated workspace path.
146    IsolatedWorkspace,
147    /// Git worktree-backed isolated execution.
148    GitWorktree,
149    /// Remote execution target.
150    Remote,
151}
152
153/// Structured brief attached to delegated work.
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
155pub struct DelegationBrief {
156    /// High-level objective for the delegated work.
157    pub objective: String,
158    /// Acceptance criteria for completion.
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub acceptance_criteria: Vec<String>,
161    /// Constraints the subagent must respect.
162    #[serde(default, skip_serializing_if = "Vec::is_empty")]
163    pub constraints: Vec<String>,
164    /// Expected deliverables/artifacts.
165    #[serde(default, skip_serializing_if = "Vec::is_empty")]
166    pub deliverables: Vec<String>,
167    /// Condensed context summary for the child agent.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub context_summary: Option<String>,
170}
171
172impl DelegationBrief {
173    /// Render the brief into prompt text.
174    pub fn as_prompt_section(&self) -> String {
175        let acceptance = if self.acceptance_criteria.is_empty() {
176            "- No explicit acceptance criteria provided".to_string()
177        } else {
178            self.acceptance_criteria
179                .iter()
180                .map(|item| format!("- {item}"))
181                .collect::<Vec<_>>()
182                .join("\n")
183        };
184        let constraints = if self.constraints.is_empty() {
185            "- No additional constraints provided".to_string()
186        } else {
187            self.constraints
188                .iter()
189                .map(|item| format!("- {item}"))
190                .collect::<Vec<_>>()
191                .join("\n")
192        };
193        let deliverables = if self.deliverables.is_empty() {
194            "- Report results in plain text".to_string()
195        } else {
196            self.deliverables
197                .iter()
198                .map(|item| format!("- {item}"))
199                .collect::<Vec<_>>()
200                .join("\n")
201        };
202
203        format!(
204            "Objective:\n{}\n\nAcceptance Criteria:\n{}\n\nConstraints:\n{}\n\nDeliverables:\n{}{}",
205            self.objective,
206            acceptance,
207            constraints,
208            deliverables,
209            self.context_summary
210                .as_ref()
211                .map(|summary| format!("\n\nContext Summary:\n{summary}"))
212                .unwrap_or_default()
213        )
214    }
215}
216
217/// Remote target details for delegated work that may execute via A2A.
218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct RemoteAgentTarget {
220    /// Remote agent base URL.
221    pub url: String,
222    /// Optional remote agent display name.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub name: Option<String>,
225    /// Optional bearer token for authenticated remote calls.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub auth_token: Option<String>,
228    /// Capability tags requested from the remote agent.
229    #[serde(default, skip_serializing_if = "Vec::is_empty")]
230    pub capabilities: Vec<String>,
231}
232
233/// Spawn configuration for a managed agent.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct AgentSpawnRequest {
236    /// Unique agent identifier.
237    pub id: String,
238    /// Human-readable agent name.
239    pub name: String,
240    /// Specialist role.
241    #[serde(default)]
242    pub role: AgentRole,
243    /// Workspace path assigned to the agent.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub workspace_dir: Option<PathBuf>,
246    /// Execution mode for the agent.
247    #[serde(default)]
248    pub execution_mode: AgentExecutionMode,
249    /// Capability tags this agent should advertise.
250    #[serde(default, skip_serializing_if = "Vec::is_empty")]
251    pub capabilities: Vec<String>,
252}
253
254impl AgentSpawnRequest {
255    /// Create a new spawn request using role defaults.
256    pub fn new(id: impl Into<String>, name: impl Into<String>, role: AgentRole) -> Self {
257        Self {
258            id: id.into(),
259            name: name.into(),
260            capabilities: role.default_capabilities(),
261            role,
262            workspace_dir: None,
263            execution_mode: AgentExecutionMode::SharedWorkspace,
264        }
265    }
266}
267
268/// Artifact produced by delegated task execution.
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct TaskArtifactRecord {
271    /// Artifact display name.
272    pub name: String,
273    /// Artifact kind/category.
274    pub kind: String,
275    /// Optional URI/path.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub uri: Option<String>,
278    /// Optional summary.
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub summary: Option<String>,
281}
282
283/// IPC envelope for events exchanged with agents
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct AgentEnvelope {
286    /// Agent identifier
287    pub agent_id: String,
288    /// Subject/topic of the event
289    pub subject: String,
290    /// JSON payload
291    pub payload: serde_json::Value,
292}
293
294/// Commands that can be sent to an agent task
295#[derive(Debug, Clone)]
296pub enum AgentCommand {
297    /// Instruct the agent to shutdown
298    Shutdown,
299    /// Deliver a generic event from MQ or system
300    Event(String),
301}
302
303/// Status value for an agent
304#[derive(Debug, Clone, PartialEq, Eq)]
305pub enum AgentStatus {
306    /// Agent is running
307    Running,
308    /// Agent has stopped
309    Stopped,
310}
311
312impl AgentStatus {
313    /// Get string representation
314    pub fn as_str(&self) -> &'static str {
315        match self {
316            AgentStatus::Running => "running",
317            AgentStatus::Stopped => "stopped",
318        }
319    }
320}
321
322/// Public agent info for status queries
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct AgentInfo {
325    /// Unique agent identifier
326    pub id: String,
327    /// Human-readable agent name
328    pub name: String,
329    /// Current status string
330    pub status: String,
331    /// Last activity timestamp
332    pub last_activity: chrono::DateTime<chrono::Utc>,
333    /// Specialist role assigned to the agent.
334    #[serde(default)]
335    pub role: AgentRole,
336    /// Capability tags advertised by the agent.
337    #[serde(default, skip_serializing_if = "Vec::is_empty")]
338    pub capabilities: Vec<String>,
339    /// Optional workspace path bound to the agent.
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub workspace_dir: Option<PathBuf>,
342    /// Execution mode for the agent.
343    #[serde(default)]
344    pub execution_mode: AgentExecutionMode,
345}
346
347/// Task that can be delegated to a subagent
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct DelegatedTask {
350    /// Unique task identifier
351    pub id: String,
352    /// Agent ID to delegate to
353    pub agent_id: String,
354    /// Task description/prompt
355    pub prompt: String,
356    /// Optional context from parent
357    pub context: Option<serde_json::Value>,
358    /// Required tools for the task
359    pub required_tools: Vec<String>,
360    /// Priority (lower = higher priority)
361    pub priority: u8,
362    /// Session ID for UI integration
363    #[serde(default)]
364    pub session_id: Option<String>,
365    /// Shared directive identifier for cross-agent coordination.
366    #[serde(default)]
367    pub directive_id: Option<String>,
368    /// Optional task identifier in the session task list used for lifecycle tracking.
369    #[serde(default)]
370    pub tracking_task_id: Option<String>,
371    /// Supervisor run identifier.
372    #[serde(default)]
373    pub run_id: Option<String>,
374    /// Parent delegated task identifier for hierarchical delegation.
375    #[serde(default)]
376    pub parent_task_id: Option<String>,
377    /// Dependencies that must complete before execution can start.
378    #[serde(default)]
379    pub depends_on: Vec<String>,
380    /// Specialist role requested for the assignee.
381    #[serde(default)]
382    pub role: Option<AgentRole>,
383    /// Structured brief generated by the supervisor.
384    #[serde(default)]
385    pub delegation_brief: Option<DelegationBrief>,
386    /// Whether the task should stop after planning.
387    #[serde(default)]
388    pub planning_only: bool,
389    /// Whether execution requires explicit approval before running.
390    #[serde(default)]
391    pub approval_required: bool,
392    /// Whether review must occur before the task is considered complete.
393    #[serde(default)]
394    pub reviewer_required: bool,
395    /// Whether test validation must occur before the task is considered complete.
396    #[serde(default)]
397    pub test_required: bool,
398    /// Workspace root for sandboxing and durable memory persistence.
399    #[serde(default)]
400    pub workspace_dir: Option<PathBuf>,
401    /// Assigned execution mode.
402    #[serde(default)]
403    pub execution_mode: AgentExecutionMode,
404    /// Optional environment identifier managed by the supervisor.
405    #[serde(default)]
406    pub environment_id: Option<String>,
407    /// Optional remote execution target.
408    #[serde(default)]
409    pub remote_target: Option<RemoteAgentTarget>,
410    /// Tags used for targeted memory retrieval and promotion.
411    #[serde(default)]
412    pub memory_tags: Vec<String>,
413    /// Human-readable task name for UI display
414    #[serde(default)]
415    pub name: Option<String>,
416}
417
418/// Result from a delegated task
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct TaskResult {
421    /// Task identifier
422    pub task_id: String,
423    /// Agent that executed the task
424    pub agent_id: String,
425    /// Whether task completed successfully
426    pub success: bool,
427    /// Run identifier for grouped orchestration.
428    #[serde(default)]
429    pub run_id: Option<String>,
430    /// Session task tracking identifier.
431    #[serde(default)]
432    pub tracking_task_id: Option<String>,
433    /// Output or error message
434    pub output: String,
435    /// Optional concise summary.
436    #[serde(default)]
437    pub summary: Option<String>,
438    /// Tool calls made during execution
439    pub tool_calls: Vec<OrchestratorToolCall>,
440    /// Produced artifacts.
441    #[serde(default, skip_serializing_if = "Vec::is_empty")]
442    pub artifacts: Vec<TaskArtifactRecord>,
443    /// Optional hint for the final local task state.
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    pub terminal_state_hint: Option<TaskTerminalStateHint>,
446    /// Execution duration in milliseconds
447    pub duration_ms: u64,
448}
449
450/// Optional terminal-state mapping returned by delegated execution.
451#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
452#[serde(rename_all = "snake_case")]
453pub enum TaskTerminalStateHint {
454    Completed,
455    Failed,
456    Cancelled,
457    Blocked,
458}
459
460/// Record of a tool call during orchestrated task execution
461///
462/// This is separate from `ToolCallRecord` in the pipeline module, which tracks
463/// raw tool calls. This struct is for orchestrator-level task tracking with
464/// structured input/output JSON values.
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct OrchestratorToolCall {
467    /// Tool name
468    pub tool_name: String,
469    /// Input parameters as JSON
470    pub input: serde_json::Value,
471    /// Output value as JSON
472    pub output: serde_json::Value,
473    /// Whether the call succeeded
474    pub success: bool,
475    /// Call duration in milliseconds
476    pub duration_ms: u64,
477}
478
479/// Trait for spawning and managing isolated agents
480#[async_trait::async_trait]
481pub trait AgentSpawner: Send + Sync {
482    /// Spawn an agent and return its id
483    async fn spawn_agent(&self, id: String, name: String);
484    /// Spawn an agent with an explicit request payload.
485    async fn spawn_agent_with_request(&self, request: AgentSpawnRequest) {
486        self.spawn_agent(request.id, request.name).await;
487    }
488    /// Send an event envelope to a running agent
489    async fn send_event(&self, id: &str, payload: String);
490    /// Attempt to restore state for an agent
491    async fn load_state(&self, id: &str) -> Option<String>;
492    /// Shutdown all agents with a grace period
493    async fn shutdown_all(&self, grace_secs: u64);
494}
495
496/// Record kept for each agent in memory
497struct AgentRecord {
498    name: String,
499    tx: mpsc::Sender<AgentCommand>,
500    _handle: JoinHandle<()>,
501    role: AgentRole,
502    capabilities: Vec<String>,
503    workspace_dir: Option<PathBuf>,
504    execution_mode: AgentExecutionMode,
505    #[allow(dead_code)]
506    created_at: chrono::DateTime<chrono::Utc>,
507    last_activity: chrono::DateTime<chrono::Utc>,
508}
509
510#[derive(Default)]
511struct Inner {
512    agents: HashMap<String, AgentRecord>,
513}
514
515/// Core agent manager implementation
516///
517/// Manages agent lifecycles without GUI dependencies.
518/// GUI/Tauri-specific features are in the GUI crate's wrapper.
519#[derive(Clone)]
520pub struct AgentManager {
521    inner: Arc<Mutex<Inner>>,
522    #[allow(dead_code)]
523    db_path: PathBuf,
524}
525
526impl AgentManager {
527    /// Create a new AgentManager with the given database path
528    pub fn new(db_path: PathBuf) -> Self {
529        Self {
530            inner: Arc::new(Mutex::new(Inner::default())),
531            db_path,
532        }
533    }
534
535    /// Spawn a lightweight agent task that listens for commands
536    ///
537    /// This is a basic implementation. GUI can override spawn behavior
538    /// by wrapping this manager.
539    pub async fn spawn_agent(&self, id: String, name: String) {
540        self.spawn_agent_with_request(AgentSpawnRequest::new(id, name, AgentRole::Implementer))
541            .await;
542    }
543
544    /// Spawn a lightweight agent using an explicit configuration request.
545    pub async fn spawn_agent_with_request(&self, request: AgentSpawnRequest) {
546        let (tx, mut rx) = mpsc::channel::<AgentCommand>(32);
547
548        // Basic agent task body
549        let handle = tokio::spawn(async move {
550            while let Some(cmd) = rx.recv().await {
551                match cmd {
552                    AgentCommand::Shutdown => break,
553                    AgentCommand::Event(_payload) => {
554                        // Basic event handling - GUI can override with richer behavior
555                        tracing::debug!(payload = %_payload, "Agent received event");
556                    }
557                }
558            }
559        });
560
561        let now = chrono::Utc::now();
562        let rec = AgentRecord {
563            name: request.name,
564            tx,
565            _handle: handle,
566            role: request.role,
567            capabilities: request.capabilities,
568            workspace_dir: request.workspace_dir,
569            execution_mode: request.execution_mode,
570            created_at: now,
571            last_activity: now,
572        };
573        let mut inner = self.inner.lock().await;
574        inner.agents.insert(request.id, rec);
575    }
576
577    /// Get status information for a specific agent
578    pub async fn get_agent_status(&self, id: &str) -> Option<AgentInfo> {
579        let inner = self.inner.lock().await;
580        inner.agents.get(id).map(|rec| AgentInfo {
581            id: id.to_string(),
582            name: rec.name.clone(),
583            status: "running".to_string(),
584            last_activity: rec.last_activity,
585            role: rec.role.clone(),
586            capabilities: rec.capabilities.clone(),
587            workspace_dir: rec.workspace_dir.clone(),
588            execution_mode: rec.execution_mode.clone(),
589        })
590    }
591
592    /// List all active agents
593    pub async fn list_agents(&self) -> Vec<AgentInfo> {
594        let inner = self.inner.lock().await;
595        inner
596            .agents
597            .iter()
598            .map(|(id, rec)| AgentInfo {
599                id: id.clone(),
600                name: rec.name.clone(),
601                status: "running".to_string(),
602                last_activity: rec.last_activity,
603                role: rec.role.clone(),
604                capabilities: rec.capabilities.clone(),
605                workspace_dir: rec.workspace_dir.clone(),
606                execution_mode: rec.execution_mode.clone(),
607            })
608            .collect()
609    }
610
611    /// Update last activity timestamp for an agent
612    pub async fn update_activity(&self, id: &str) {
613        let mut inner = self.inner.lock().await;
614        if let Some(rec) = inner.agents.get_mut(id) {
615            rec.last_activity = chrono::Utc::now();
616        }
617    }
618
619    /// Publish an event to a specific agent if present
620    pub async fn send_event(&self, id: &str, payload: String) {
621        let tx_opt = {
622            let inner = self.inner.lock().await;
623            inner.agents.get(id).map(|r| r.tx.clone())
624        };
625        if let Some(tx) = tx_opt {
626            let _ = tx.send(AgentCommand::Event(payload)).await;
627        }
628    }
629
630    /// Gracefully shutdown all agents, waiting up to `grace_secs` for completion
631    pub async fn shutdown_all(&self, grace_secs: u64) {
632        let mut to_shutdown: Vec<mpsc::Sender<AgentCommand>> = Vec::new();
633        {
634            let inner = self.inner.lock().await;
635            for (_id, rec) in inner.agents.iter() {
636                to_shutdown.push(rec.tx.clone());
637            }
638        }
639        for tx in to_shutdown {
640            let _ = tx.send(AgentCommand::Shutdown).await;
641        }
642        tokio::time::sleep(Duration::from_secs(grace_secs)).await;
643    }
644
645    /// Compute a default DB path under the user's data dir
646    pub fn default_db_path() -> PathBuf {
647        let mut dir = dirs::data_dir().unwrap_or_default();
648        dir.push("Gestura");
649        std::fs::create_dir_all(&dir).ok();
650        dir.push("gestura.db");
651        dir
652    }
653}
654
655#[async_trait::async_trait]
656impl AgentSpawner for AgentManager {
657    async fn spawn_agent(&self, id: String, name: String) {
658        AgentManager::spawn_agent(self, id, name).await;
659    }
660
661    async fn spawn_agent_with_request(&self, request: AgentSpawnRequest) {
662        AgentManager::spawn_agent_with_request(self, request).await;
663    }
664
665    async fn send_event(&self, id: &str, payload: String) {
666        AgentManager::send_event(self, id, payload).await;
667    }
668
669    async fn load_state(&self, _id: &str) -> Option<String> {
670        // Core manager doesn't have KV store - GUI wrapper provides this
671        None
672    }
673
674    async fn shutdown_all(&self, grace_secs: u64) {
675        AgentManager::shutdown_all(self, grace_secs).await;
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[tokio::test]
684    async fn test_agent_manager_new() {
685        let manager = AgentManager::new(PathBuf::from("/tmp/test.db"));
686        assert!(manager.list_agents().await.is_empty());
687    }
688
689    #[tokio::test]
690    async fn test_spawn_and_list_agents() {
691        let manager = AgentManager::new(PathBuf::from("/tmp/test.db"));
692        manager
693            .spawn_agent("agent-1".into(), "Test Agent".into())
694            .await;
695
696        let agents = manager.list_agents().await;
697        assert_eq!(agents.len(), 1);
698        assert_eq!(agents[0].id, "agent-1");
699        assert_eq!(agents[0].name, "Test Agent");
700        assert_eq!(agents[0].role, AgentRole::Implementer);
701    }
702
703    #[tokio::test]
704    async fn test_spawn_with_role_and_workspace() {
705        let manager = AgentManager::new(PathBuf::from("/tmp/test.db"));
706        manager
707            .spawn_agent_with_request(AgentSpawnRequest {
708                id: "reviewer-1".into(),
709                name: "Reviewer".into(),
710                role: AgentRole::Reviewer,
711                workspace_dir: Some(PathBuf::from("/tmp/worktree/reviewer-1")),
712                execution_mode: AgentExecutionMode::GitWorktree,
713                capabilities: vec!["review".into(), "quality".into()],
714            })
715            .await;
716
717        let status = manager.get_agent_status("reviewer-1").await.unwrap();
718        assert_eq!(status.role, AgentRole::Reviewer);
719        assert_eq!(status.execution_mode, AgentExecutionMode::GitWorktree);
720        assert_eq!(
721            status.workspace_dir,
722            Some(PathBuf::from("/tmp/worktree/reviewer-1"))
723        );
724    }
725
726    #[tokio::test]
727    async fn test_get_agent_status() {
728        let manager = AgentManager::new(PathBuf::from("/tmp/test.db"));
729        manager
730            .spawn_agent("agent-1".into(), "Test Agent".into())
731            .await;
732
733        let status = manager.get_agent_status("agent-1").await;
734        assert!(status.is_some());
735        assert_eq!(status.unwrap().status, "running");
736
737        let missing = manager.get_agent_status("nonexistent").await;
738        assert!(missing.is_none());
739    }
740
741    #[tokio::test]
742    async fn test_send_event() {
743        let manager = AgentManager::new(PathBuf::from("/tmp/test.db"));
744        manager
745            .spawn_agent("agent-1".into(), "Test Agent".into())
746            .await;
747
748        // Should not panic
749        manager.send_event("agent-1", "test-event".into()).await;
750        manager.send_event("nonexistent", "test-event".into()).await;
751    }
752
753    #[test]
754    fn test_delegated_task_serialization() {
755        let task = DelegatedTask {
756            id: "task-1".into(),
757            agent_id: "agent-1".into(),
758            prompt: "Do something".into(),
759            context: Some(serde_json::json!({"key": "value"})),
760            required_tools: vec!["shell".into()],
761            priority: 1,
762            session_id: Some("session-123".into()),
763            directive_id: Some("directive-1".into()),
764            tracking_task_id: Some("task-track-1".into()),
765            run_id: Some("run-1".into()),
766            parent_task_id: Some("task-parent".into()),
767            depends_on: vec!["task-prep".into()],
768            role: Some(AgentRole::Reviewer),
769            delegation_brief: Some(DelegationBrief {
770                objective: "Review the patch".into(),
771                acceptance_criteria: vec!["List risks".into()],
772                constraints: vec!["Do not modify files".into()],
773                deliverables: vec!["Risk summary".into()],
774                context_summary: Some("User requested a review".into()),
775            }),
776            planning_only: true,
777            approval_required: true,
778            reviewer_required: true,
779            test_required: false,
780            workspace_dir: Some(PathBuf::from("/tmp/workspace")),
781            execution_mode: AgentExecutionMode::GitWorktree,
782            environment_id: Some("env-1".into()),
783            remote_target: Some(RemoteAgentTarget {
784                url: "https://remote.example".into(),
785                name: Some("Remote Reviewer".into()),
786                auth_token: None,
787                capabilities: vec!["review".into()],
788            }),
789            memory_tags: vec!["memory".into(), "delegation".into()],
790            name: Some("Test Task".into()),
791        };
792
793        let json = serde_json::to_string(&task).unwrap();
794        let parsed: DelegatedTask = serde_json::from_str(&json).unwrap();
795        assert_eq!(parsed.id, "task-1");
796        assert_eq!(parsed.session_id, Some("session-123".into()));
797        assert_eq!(parsed.directive_id, Some("directive-1".into()));
798        assert_eq!(parsed.tracking_task_id, Some("task-track-1".into()));
799        assert_eq!(parsed.run_id, Some("run-1".into()));
800        assert_eq!(parsed.role, Some(AgentRole::Reviewer));
801        assert_eq!(parsed.memory_tags, vec!["memory", "delegation"]);
802    }
803
804    #[test]
805    fn test_task_result_serialization() {
806        let result = TaskResult {
807            task_id: "task-1".into(),
808            agent_id: "agent-1".into(),
809            success: true,
810            run_id: Some("run-1".into()),
811            tracking_task_id: Some("tracking-1".into()),
812            output: "Done".into(),
813            summary: Some("Task succeeded".into()),
814            tool_calls: vec![],
815            artifacts: vec![TaskArtifactRecord {
816                name: "summary.md".into(),
817                kind: "report".into(),
818                uri: Some("memory://summary".into()),
819                summary: Some("Delegation summary".into()),
820            }],
821            terminal_state_hint: Some(TaskTerminalStateHint::Completed),
822            duration_ms: 100,
823        };
824
825        let json = serde_json::to_string(&result).unwrap();
826        let parsed: TaskResult = serde_json::from_str(&json).unwrap();
827        assert_eq!(parsed.task_id, "task-1");
828        assert!(parsed.success);
829    }
830}