1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[serde(rename_all = "snake_case")]
55pub enum AgentRole {
56 Supervisor,
58 Researcher,
60 #[default]
62 Implementer,
63 Reviewer,
65 Tester,
67 SecurityReviewer,
69 RemoteWorker,
71 Custom(String),
73}
74
75impl AgentRole {
76 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
140#[serde(rename_all = "snake_case")]
141pub enum AgentExecutionMode {
142 #[default]
144 SharedWorkspace,
145 IsolatedWorkspace,
147 GitWorktree,
149 Remote,
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
155pub struct DelegationBrief {
156 pub objective: String,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub acceptance_criteria: Vec<String>,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub constraints: Vec<String>,
164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub deliverables: Vec<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub context_summary: Option<String>,
170}
171
172impl DelegationBrief {
173 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct RemoteAgentTarget {
220 pub url: String,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub name: Option<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub auth_token: Option<String>,
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 pub capabilities: Vec<String>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct AgentSpawnRequest {
236 pub id: String,
238 pub name: String,
240 #[serde(default)]
242 pub role: AgentRole,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub workspace_dir: Option<PathBuf>,
246 #[serde(default)]
248 pub execution_mode: AgentExecutionMode,
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
251 pub capabilities: Vec<String>,
252}
253
254impl AgentSpawnRequest {
255 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#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct TaskArtifactRecord {
271 pub name: String,
273 pub kind: String,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub uri: Option<String>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub summary: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct AgentEnvelope {
286 pub agent_id: String,
288 pub subject: String,
290 pub payload: serde_json::Value,
292}
293
294#[derive(Debug, Clone)]
296pub enum AgentCommand {
297 Shutdown,
299 Event(String),
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub enum AgentStatus {
306 Running,
308 Stopped,
310}
311
312impl AgentStatus {
313 pub fn as_str(&self) -> &'static str {
315 match self {
316 AgentStatus::Running => "running",
317 AgentStatus::Stopped => "stopped",
318 }
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct AgentInfo {
325 pub id: String,
327 pub name: String,
329 pub status: String,
331 pub last_activity: chrono::DateTime<chrono::Utc>,
333 #[serde(default)]
335 pub role: AgentRole,
336 #[serde(default, skip_serializing_if = "Vec::is_empty")]
338 pub capabilities: Vec<String>,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub workspace_dir: Option<PathBuf>,
342 #[serde(default)]
344 pub execution_mode: AgentExecutionMode,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct DelegatedTask {
350 pub id: String,
352 pub agent_id: String,
354 pub prompt: String,
356 pub context: Option<serde_json::Value>,
358 pub required_tools: Vec<String>,
360 pub priority: u8,
362 #[serde(default)]
364 pub session_id: Option<String>,
365 #[serde(default)]
367 pub directive_id: Option<String>,
368 #[serde(default)]
370 pub tracking_task_id: Option<String>,
371 #[serde(default)]
373 pub run_id: Option<String>,
374 #[serde(default)]
376 pub parent_task_id: Option<String>,
377 #[serde(default)]
379 pub depends_on: Vec<String>,
380 #[serde(default)]
382 pub role: Option<AgentRole>,
383 #[serde(default)]
385 pub delegation_brief: Option<DelegationBrief>,
386 #[serde(default)]
388 pub planning_only: bool,
389 #[serde(default)]
391 pub approval_required: bool,
392 #[serde(default)]
394 pub reviewer_required: bool,
395 #[serde(default)]
397 pub test_required: bool,
398 #[serde(default)]
400 pub workspace_dir: Option<PathBuf>,
401 #[serde(default)]
403 pub execution_mode: AgentExecutionMode,
404 #[serde(default)]
406 pub environment_id: Option<String>,
407 #[serde(default)]
409 pub remote_target: Option<RemoteAgentTarget>,
410 #[serde(default)]
412 pub memory_tags: Vec<String>,
413 #[serde(default)]
415 pub name: Option<String>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct TaskResult {
421 pub task_id: String,
423 pub agent_id: String,
425 pub success: bool,
427 #[serde(default)]
429 pub run_id: Option<String>,
430 #[serde(default)]
432 pub tracking_task_id: Option<String>,
433 pub output: String,
435 #[serde(default)]
437 pub summary: Option<String>,
438 pub tool_calls: Vec<OrchestratorToolCall>,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 pub artifacts: Vec<TaskArtifactRecord>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub terminal_state_hint: Option<TaskTerminalStateHint>,
446 pub duration_ms: u64,
448}
449
450#[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#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct OrchestratorToolCall {
467 pub tool_name: String,
469 pub input: serde_json::Value,
471 pub output: serde_json::Value,
473 pub success: bool,
475 pub duration_ms: u64,
477}
478
479#[async_trait::async_trait]
481pub trait AgentSpawner: Send + Sync {
482 async fn spawn_agent(&self, id: String, name: String);
484 async fn spawn_agent_with_request(&self, request: AgentSpawnRequest) {
486 self.spawn_agent(request.id, request.name).await;
487 }
488 async fn send_event(&self, id: &str, payload: String);
490 async fn load_state(&self, id: &str) -> Option<String>;
492 async fn shutdown_all(&self, grace_secs: u64);
494}
495
496struct 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#[derive(Clone)]
520pub struct AgentManager {
521 inner: Arc<Mutex<Inner>>,
522 #[allow(dead_code)]
523 db_path: PathBuf,
524}
525
526impl AgentManager {
527 pub fn new(db_path: PathBuf) -> Self {
529 Self {
530 inner: Arc::new(Mutex::new(Inner::default())),
531 db_path,
532 }
533 }
534
535 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 pub async fn spawn_agent_with_request(&self, request: AgentSpawnRequest) {
546 let (tx, mut rx) = mpsc::channel::<AgentCommand>(32);
547
548 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 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 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 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 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 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 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 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 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 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}