Skip to content

Commit da70490

Browse files
echobtfactorydroid
andauthored
feat(agents): implement background agents and async messaging system (#419)
This commit implements the background process architecture and asynchronous messaging system for the CLI as specified in AGENT_1_BACKGROUND_AGENTS.md. ## New Features ### Background Agent Execution (cortex-agents/src/background/) - BackgroundAgentManager: Manages concurrent background agents via tokio tasks - Spawn agents with configurable timeout and priority - Max concurrent agents limit (default: 5) - Graceful cancellation with cleanup - Event broadcasting for agent lifecycle ### Inter-Agent Async Messaging - AgentMessageBroker: Central message router for agents - AgentMailbox: Per-agent inbox/outbox for async communication - MessageContent types: Notify, Request/Response, Data, Delegate, TaskComplete - Support for broadcast and direct messaging ### Agent Events - AgentEvent enum: Started, Progress, ToolCall, Completed, Failed, Cancelled, TimedOut - AgentResult: Structured result with summary, output, tokens, files modified - AgentStatus: Initializing, Running, Completed, Failed, Cancelled, TimedOut ### TUI Integration - /tasks command (aliases: /bg, /background) for viewing background tasks - TasksView: Table-based display with status badges and navigation - ModalType::Tasks for the background tasks modal ## Technical Details - Uses tokio::select! for concurrent cancellation and timeout handling - Broadcast channels for event subscription - RAII-style cleanup to prevent zombie processes - Full test coverage for executor, messaging, and view components Implements: ORCHESTRATE/AGENT_1_BACKGROUND_AGENTS.md Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 631ba4f commit da70490

11 files changed

Lines changed: 1989 additions & 0 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
//! Event types for background agent operations.
2+
//!
3+
//! Provides event types that are broadcast when background agents
4+
//! change state, complete, or encounter errors.
5+
6+
use serde::{Deserialize, Serialize};
7+
use std::time::{Duration, Instant};
8+
9+
/// Status of a background agent.
10+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11+
pub enum AgentStatus {
12+
/// Agent is being initialized.
13+
Initializing,
14+
/// Agent is currently running.
15+
Running,
16+
/// Agent completed successfully.
17+
Completed,
18+
/// Agent failed with an error.
19+
Failed,
20+
/// Agent was cancelled by user.
21+
Cancelled,
22+
/// Agent timed out.
23+
TimedOut,
24+
}
25+
26+
impl Default for AgentStatus {
27+
fn default() -> Self {
28+
AgentStatus::Initializing
29+
}
30+
}
31+
32+
impl std::fmt::Display for AgentStatus {
33+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34+
match self {
35+
AgentStatus::Initializing => write!(f, "initializing"),
36+
AgentStatus::Running => write!(f, "running"),
37+
AgentStatus::Completed => write!(f, "completed"),
38+
AgentStatus::Failed => write!(f, "failed"),
39+
AgentStatus::Cancelled => write!(f, "cancelled"),
40+
AgentStatus::TimedOut => write!(f, "timed out"),
41+
}
42+
}
43+
}
44+
45+
/// Result of a background agent execution.
46+
#[derive(Debug, Clone, Serialize, Deserialize)]
47+
pub struct AgentResult {
48+
/// Summary of what the agent accomplished.
49+
pub summary: String,
50+
/// Detailed output from the agent.
51+
pub output: String,
52+
/// Whether the agent completed successfully.
53+
pub success: bool,
54+
/// Number of tokens used.
55+
pub tokens_used: Option<u64>,
56+
/// Duration of execution.
57+
#[serde(with = "duration_serde")]
58+
pub duration: Duration,
59+
/// Files that were modified (if any).
60+
pub files_modified: Vec<String>,
61+
/// Any errors encountered.
62+
pub errors: Vec<String>,
63+
}
64+
65+
impl AgentResult {
66+
/// Creates a new successful result.
67+
pub fn success(
68+
summary: impl Into<String>,
69+
output: impl Into<String>,
70+
duration: Duration,
71+
) -> Self {
72+
Self {
73+
summary: summary.into(),
74+
output: output.into(),
75+
success: true,
76+
tokens_used: None,
77+
duration,
78+
files_modified: Vec::new(),
79+
errors: Vec::new(),
80+
}
81+
}
82+
83+
/// Creates a new failed result.
84+
pub fn failure(error: impl Into<String>, duration: Duration) -> Self {
85+
let error_str = error.into();
86+
Self {
87+
summary: format!("Failed: {}", error_str),
88+
output: String::new(),
89+
success: false,
90+
tokens_used: None,
91+
duration,
92+
files_modified: Vec::new(),
93+
errors: vec![error_str],
94+
}
95+
}
96+
97+
/// Creates a cancelled result.
98+
pub fn cancelled(duration: Duration) -> Self {
99+
Self {
100+
summary: "Cancelled by user".to_string(),
101+
output: String::new(),
102+
success: false,
103+
tokens_used: None,
104+
duration,
105+
files_modified: Vec::new(),
106+
errors: vec!["Cancelled by user".to_string()],
107+
}
108+
}
109+
110+
/// Sets the tokens used.
111+
pub fn with_tokens(mut self, tokens: u64) -> Self {
112+
self.tokens_used = Some(tokens);
113+
self
114+
}
115+
116+
/// Adds modified files.
117+
pub fn with_files(mut self, files: Vec<String>) -> Self {
118+
self.files_modified = files;
119+
self
120+
}
121+
}
122+
123+
impl Default for AgentResult {
124+
fn default() -> Self {
125+
Self {
126+
summary: String::new(),
127+
output: String::new(),
128+
success: false,
129+
tokens_used: None,
130+
duration: Duration::ZERO,
131+
files_modified: Vec::new(),
132+
errors: Vec::new(),
133+
}
134+
}
135+
}
136+
137+
/// Events emitted by background agents.
138+
///
139+
/// These events are broadcast to all subscribers when agent state changes.
140+
#[derive(Debug, Clone)]
141+
pub enum AgentEvent {
142+
/// Agent execution started.
143+
Started {
144+
/// Unique agent ID.
145+
id: String,
146+
/// Task description.
147+
task: String,
148+
/// When the agent started.
149+
started_at: Instant,
150+
},
151+
152+
/// Agent made progress (e.g., tool call, step completion).
153+
Progress {
154+
/// Agent ID.
155+
id: String,
156+
/// Progress message.
157+
message: String,
158+
/// Optional percentage (0-100).
159+
percentage: Option<u8>,
160+
},
161+
162+
/// Agent is executing a tool.
163+
ToolCall {
164+
/// Agent ID.
165+
id: String,
166+
/// Tool name.
167+
tool_name: String,
168+
/// Tool arguments (may be truncated).
169+
arguments: String,
170+
},
171+
172+
/// Agent completed successfully.
173+
Completed {
174+
/// Agent ID.
175+
id: String,
176+
/// Execution result.
177+
result: AgentResult,
178+
},
179+
180+
/// Agent failed with an error.
181+
Failed {
182+
/// Agent ID.
183+
id: String,
184+
/// Error message.
185+
error: String,
186+
/// Duration before failure.
187+
duration: Duration,
188+
},
189+
190+
/// Agent was cancelled.
191+
Cancelled {
192+
/// Agent ID.
193+
id: String,
194+
/// Duration before cancellation.
195+
duration: Duration,
196+
},
197+
198+
/// Agent timed out.
199+
TimedOut {
200+
/// Agent ID.
201+
id: String,
202+
/// Timeout duration.
203+
timeout: Duration,
204+
},
205+
}
206+
207+
impl AgentEvent {
208+
/// Returns the agent ID for this event.
209+
pub fn agent_id(&self) -> &str {
210+
match self {
211+
AgentEvent::Started { id, .. } => id,
212+
AgentEvent::Progress { id, .. } => id,
213+
AgentEvent::ToolCall { id, .. } => id,
214+
AgentEvent::Completed { id, .. } => id,
215+
AgentEvent::Failed { id, .. } => id,
216+
AgentEvent::Cancelled { id, .. } => id,
217+
AgentEvent::TimedOut { id, .. } => id,
218+
}
219+
}
220+
221+
/// Returns true if this is a terminal event (completed, failed, cancelled, timed out).
222+
pub fn is_terminal(&self) -> bool {
223+
matches!(
224+
self,
225+
AgentEvent::Completed { .. }
226+
| AgentEvent::Failed { .. }
227+
| AgentEvent::Cancelled { .. }
228+
| AgentEvent::TimedOut { .. }
229+
)
230+
}
231+
232+
/// Returns the status implied by this event.
233+
pub fn status(&self) -> AgentStatus {
234+
match self {
235+
AgentEvent::Started { .. } => AgentStatus::Running,
236+
AgentEvent::Progress { .. } => AgentStatus::Running,
237+
AgentEvent::ToolCall { .. } => AgentStatus::Running,
238+
AgentEvent::Completed { .. } => AgentStatus::Completed,
239+
AgentEvent::Failed { .. } => AgentStatus::Failed,
240+
AgentEvent::Cancelled { .. } => AgentStatus::Cancelled,
241+
AgentEvent::TimedOut { .. } => AgentStatus::TimedOut,
242+
}
243+
}
244+
}
245+
246+
/// Serde support for Duration.
247+
mod duration_serde {
248+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
249+
use std::time::Duration;
250+
251+
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
252+
where
253+
S: Serializer,
254+
{
255+
duration.as_millis().serialize(serializer)
256+
}
257+
258+
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
259+
where
260+
D: Deserializer<'de>,
261+
{
262+
let millis = u64::deserialize(deserializer)?;
263+
Ok(Duration::from_millis(millis))
264+
}
265+
}
266+
267+
#[cfg(test)]
268+
mod tests {
269+
use super::*;
270+
271+
#[test]
272+
fn test_agent_status_display() {
273+
assert_eq!(AgentStatus::Running.to_string(), "running");
274+
assert_eq!(AgentStatus::Completed.to_string(), "completed");
275+
assert_eq!(AgentStatus::Failed.to_string(), "failed");
276+
assert_eq!(AgentStatus::Cancelled.to_string(), "cancelled");
277+
}
278+
279+
#[test]
280+
fn test_agent_result_success() {
281+
let result = AgentResult::success("Done", "Output", Duration::from_secs(10));
282+
assert!(result.success);
283+
assert_eq!(result.summary, "Done");
284+
assert!(result.errors.is_empty());
285+
}
286+
287+
#[test]
288+
fn test_agent_result_failure() {
289+
let result = AgentResult::failure("Something went wrong", Duration::from_secs(5));
290+
assert!(!result.success);
291+
assert!(result.summary.contains("Failed"));
292+
assert_eq!(result.errors.len(), 1);
293+
}
294+
295+
#[test]
296+
fn test_agent_event_is_terminal() {
297+
let started = AgentEvent::Started {
298+
id: "1".to_string(),
299+
task: "test".to_string(),
300+
started_at: Instant::now(),
301+
};
302+
assert!(!started.is_terminal());
303+
304+
let completed = AgentEvent::Completed {
305+
id: "1".to_string(),
306+
result: AgentResult::default(),
307+
};
308+
assert!(completed.is_terminal());
309+
310+
let failed = AgentEvent::Failed {
311+
id: "1".to_string(),
312+
error: "error".to_string(),
313+
duration: Duration::ZERO,
314+
};
315+
assert!(failed.is_terminal());
316+
}
317+
318+
#[test]
319+
fn test_agent_event_agent_id() {
320+
let event = AgentEvent::Progress {
321+
id: "test-agent".to_string(),
322+
message: "working".to_string(),
323+
percentage: Some(50),
324+
};
325+
assert_eq!(event.agent_id(), "test-agent");
326+
}
327+
}

0 commit comments

Comments
 (0)