1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct AgentContext {
15 pub segments: Vec<(String, String)>,
18 pub tokens_used: u64,
20}
21
22impl AgentContext {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn push(mut self, key: impl Into<String>, content: impl Into<String>) -> Self {
28 self.segments.push((key.into(), content.into()));
29 self
30 }
31
32 pub fn render(&self) -> String {
35 if self.segments.is_empty() {
36 return String::new();
37 }
38 let mut out = String::new();
39 for (key, content) in &self.segments {
40 out.push_str("### ");
41 out.push_str(key);
42 out.push('\n');
43 out.push_str(content.trim());
44 out.push_str("\n\n");
45 }
46 out.trim_end().to_string()
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AgentTask {
53 pub id: String,
54 pub description: String,
55 pub context: AgentContext,
56 pub workdir: Option<PathBuf>,
58 pub timeout_secs: u64,
61}
62
63impl AgentTask {
64 pub fn new(description: impl Into<String>) -> Self {
65 Self {
66 id: uuid::Uuid::new_v4().to_string(),
67 description: description.into(),
68 context: AgentContext::default(),
69 workdir: None,
70 timeout_secs: 300,
71 }
72 }
73
74 pub fn with_context(mut self, ctx: AgentContext) -> Self {
75 self.context = ctx;
76 self
77 }
78
79 pub fn with_workdir(mut self, workdir: impl Into<PathBuf>) -> Self {
80 self.workdir = Some(workdir.into());
81 self
82 }
83
84 pub fn with_timeout_secs(mut self, secs: u64) -> Self {
85 self.timeout_secs = secs;
86 self
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Artifact {
93 pub kind: String,
95 pub reference: String,
97 #[serde(default)]
99 pub summary: Option<String>,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum AgentTaskStatus {
106 Running,
107 Succeeded,
108 Failed,
109 Cancelled,
110 TimedOut,
111}
112
113impl AgentTaskStatus {
114 pub fn is_terminal(self) -> bool {
115 !matches!(self, AgentTaskStatus::Running)
116 }
117 pub fn is_success(self) -> bool {
118 matches!(self, AgentTaskStatus::Succeeded)
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AgentResult {
125 pub task_id: String,
126 pub status: AgentTaskStatus,
127 pub summary: String,
129 #[serde(default)]
131 pub artifacts: Vec<Artifact>,
132 #[serde(default)]
134 pub stdout: String,
135 #[serde(default)]
137 pub stderr: String,
138 pub exit_code: Option<i32>,
140 pub started_at: DateTime<Utc>,
141 pub completed_at: DateTime<Utc>,
142}
143
144impl AgentResult {
145 pub fn duration_ms(&self) -> i64 {
146 (self.completed_at - self.started_at)
147 .num_milliseconds()
148 .max(0)
149 }
150}
151
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
155pub struct AgentCapabilities {
156 #[serde(default)]
158 pub tags: Vec<String>,
159 #[serde(default)]
161 pub languages: Vec<String>,
162 #[serde(default = "default_concurrency")]
164 pub max_concurrency: u32,
165 #[serde(default)]
167 pub needs_network: bool,
168}
169
170fn default_concurrency() -> u32 {
171 1
172}
173
174#[derive(Debug, Error)]
175pub enum AgentError {
176 #[error("delegate '{0}' not found in registry")]
177 NotFound(String),
178 #[error("delegate failed to launch: {0}")]
179 Launch(String),
180 #[error("delegate timed out after {secs}s: {task_id}")]
181 Timeout { task_id: String, secs: u64 },
182 #[error("delegate returned non-zero exit {code}: {stderr}")]
183 NonZeroExit { code: i32, stderr: String },
184 #[error("delegate output unparseable: {0}")]
185 Parse(String),
186 #[error("delegate cancelled: {0}")]
187 Cancelled(String),
188 #[error("IO error: {0}")]
189 Io(String),
190 #[error("other: {0}")]
191 Other(String),
192}
193
194#[async_trait]
196pub trait AgentDelegate: Send + Sync {
197 fn name(&self) -> &str;
199
200 fn capabilities(&self) -> AgentCapabilities {
202 AgentCapabilities::default()
203 }
204
205 async fn delegate(&self, task: AgentTask) -> Result<AgentResult, AgentError>;
208
209 async fn cancel(&self, _task_id: &str) -> Result<(), AgentError> {
211 Ok(())
212 }
213
214 async fn status(&self, _task_id: &str) -> Result<AgentTaskStatus, AgentError> {
217 Ok(AgentTaskStatus::Running)
218 }
219
220 async fn health_check(&self) -> bool {
224 true
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn context_render_round_trips() {
234 let ctx = AgentContext::new()
235 .push("memory", "fact A\nfact B")
236 .push("conventions", "use tabs");
237 let rendered = ctx.render();
238 assert!(rendered.contains("### memory"));
239 assert!(rendered.contains("fact A"));
240 assert!(rendered.contains("### conventions"));
241 assert!(rendered.contains("use tabs"));
242 }
243
244 #[test]
245 fn context_render_empty_returns_empty() {
246 assert!(AgentContext::new().render().is_empty());
247 }
248
249 #[test]
250 fn agent_task_new_generates_uuid() {
251 let a = AgentTask::new("a");
252 let b = AgentTask::new("b");
253 assert_ne!(a.id, b.id);
254 assert_eq!(a.timeout_secs, 300);
255 }
256
257 #[test]
258 fn task_status_terminality() {
259 assert!(!AgentTaskStatus::Running.is_terminal());
260 assert!(AgentTaskStatus::Succeeded.is_terminal());
261 assert!(AgentTaskStatus::Failed.is_terminal());
262 assert!(AgentTaskStatus::TimedOut.is_terminal());
263 assert!(AgentTaskStatus::Cancelled.is_terminal());
264 assert!(AgentTaskStatus::Succeeded.is_success());
265 assert!(!AgentTaskStatus::Failed.is_success());
266 }
267}