1use crate::contract::backend::{
7 AgentBackend, AgentCapabilities, AgentResult, AgentStatus, AgentTask, BackendError, LogRef,
8 RunContext,
9};
10use crate::contract::ids::TokenUsage;
11use serde_json::Value;
12use std::path::Path;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::Duration;
16
17#[derive(Debug, Clone)]
18pub struct CallRecord {
19 pub seq: u64,
20 pub agent_name: Option<String>,
21 pub thread_id: Option<String>,
22 pub prompt: String,
23}
24
25pub struct CrashBackend {
27 canned: Value,
28 crash_after: u64,
29 count: AtomicU64,
30}
31
32impl CrashBackend {
33 pub fn new(canned: Value, crash_after: u64) -> Self {
34 Self { canned, crash_after, count: AtomicU64::new(0) }
35 }
36}
37
38#[async_trait::async_trait]
39impl AgentBackend for CrashBackend {
40 fn id(&self) -> &'static str { "crash" }
41 fn capabilities(&self) -> AgentCapabilities {
42 AgentCapabilities { streaming: true, mcp_injection: false, structured_output: false, models: vec![] }
43 }
44 async fn run(&self, task: AgentTask, _ctx: RunContext) -> Result<AgentResult, BackendError> {
45 let n = self.count.fetch_add(1, Ordering::SeqCst) + 1;
46 eprintln!("[crash-backend] agent #{n}: {}", task.name.as_deref().unwrap_or("?"));
47 if n >= self.crash_after {
48 eprintln!("[crash-backend] exiting after {n} calls");
49 std::process::exit(1);
50 }
51 Ok(AgentResult {
52 agent_id: task.agent_id, status: AgentStatus::Ok, output: self.canned.clone(),
53 thread_id: task.thread_id.clone(), findings: vec![], tokens_used: TokenUsage::default(),
54 artifacts: vec![], logs: LogRef::default(),
55 })
56 }
57 fn as_any(&self) -> &dyn std::any::Any { self }
58}
59
60#[derive(Clone)]
62pub struct CountingBackend {
63 canned: Value,
64 calls: Arc<Mutex<Vec<String>>>,
65}
66
67impl CountingBackend {
68 pub fn new(canned: Value) -> Self {
69 Self { canned, calls: Arc::new(Mutex::new(Vec::new())) }
70 }
71 pub fn dispatched_names(&self) -> Vec<String> { self.calls.lock().unwrap().clone() }
72 pub fn total_calls(&self) -> usize { self.calls.lock().unwrap().len() }
73}
74
75#[async_trait::async_trait]
76impl AgentBackend for CountingBackend {
77 fn id(&self) -> &'static str { "counting" }
78 fn capabilities(&self) -> AgentCapabilities {
79 AgentCapabilities { streaming: true, mcp_injection: false, structured_output: false, models: vec![] }
80 }
81 async fn run(&self, task: AgentTask, _ctx: RunContext) -> Result<AgentResult, BackendError> {
82 self.calls.lock().unwrap().push(task.name.clone().unwrap_or_default());
83 Ok(AgentResult {
84 agent_id: task.agent_id, status: AgentStatus::Ok, output: self.canned.clone(),
85 thread_id: task.thread_id.clone(), findings: vec![], tokens_used: TokenUsage::default(),
86 artifacts: vec![], logs: LogRef::default(),
87 })
88 }
89 fn as_any(&self) -> &dyn std::any::Any { self }
90}
91
92#[derive(Clone)]
94pub struct SharedBackend {
95 canned: Value,
96 call_count: Arc<AtomicU64>,
97 pub block_on: Arc<Mutex<Option<u64>>>,
98 pub fail_on: Arc<Mutex<Option<u64>>>,
99 calls: Arc<Mutex<Vec<CallRecord>>>,
100}
101
102impl SharedBackend {
103 pub fn new(canned: Value) -> Self {
104 Self {
105 canned, call_count: Arc::new(AtomicU64::new(0)),
106 block_on: Arc::new(Mutex::new(None)), fail_on: Arc::new(Mutex::new(None)),
107 calls: Arc::new(Mutex::new(Vec::new())),
108 }
109 }
110 pub fn with_block_on(self, n: u64) -> Self { *self.block_on.lock().unwrap() = Some(n); self }
111 pub fn with_fail_on(self, n: u64) -> Self { *self.fail_on.lock().unwrap() = Some(n); self }
112 pub fn total_calls(&self) -> usize { self.calls.lock().unwrap().len() }
113 pub fn calls_snapshot(&self) -> Vec<CallRecord> { self.calls.lock().unwrap().clone() }
114 pub fn dispatched_names(&self) -> Vec<String> {
115 self.calls.lock().unwrap().iter().map(|c| c.agent_name.clone().unwrap_or_default()).collect()
116 }
117 pub fn mirror(&self) -> Self {
118 Self {
119 canned: self.canned.clone(), call_count: self.call_count.clone(),
120 block_on: self.block_on.clone(), fail_on: self.fail_on.clone(), calls: self.calls.clone(),
121 }
122 }
123}
124
125#[async_trait::async_trait]
126impl AgentBackend for SharedBackend {
127 fn id(&self) -> &'static str { "shared" }
128 fn capabilities(&self) -> AgentCapabilities {
129 AgentCapabilities { streaming: true, mcp_injection: false, structured_output: false, models: vec![] }
130 }
131 async fn run(&self, task: AgentTask, ctx: RunContext) -> Result<AgentResult, BackendError> {
132 let seq = self.call_count.fetch_add(1, Ordering::SeqCst) + 1;
133 self.calls.lock().unwrap().push(CallRecord {
134 seq, agent_name: task.name.clone(), thread_id: task.thread_id.clone(), prompt: task.prompt.clone(),
135 });
136 if self.fail_on.lock().unwrap().map(|n| n == seq).unwrap_or(false) {
137 return Err(BackendError::Execution("simulated failure".into()));
138 }
139 if self.block_on.lock().unwrap().map(|n| n == seq).unwrap_or(false) {
140 ctx.cancel.cancelled().await;
141 return Err(BackendError::Cancelled);
142 }
143 Ok(AgentResult {
144 agent_id: task.agent_id, status: AgentStatus::Ok, output: self.canned.clone(),
145 thread_id: task.thread_id.clone(), findings: vec![], tokens_used: TokenUsage::default(),
146 artifacts: vec![], logs: LogRef::default(),
147 })
148 }
149 fn as_any(&self) -> &dyn std::any::Any { self }
150}
151
152pub async fn wait_for_calls(backend: &SharedBackend, n: usize, timeout_ms: u64) {
153 let deadline = tokio::time::sleep(Duration::from_millis(timeout_ms));
154 tokio::pin!(deadline);
155 loop {
156 if backend.total_calls() >= n { return; }
157 tokio::select! {
158 _ = &mut deadline => panic!("timeout waiting for {n} agent calls (got {})", backend.total_calls()),
159 _ = tokio::time::sleep(Duration::from_millis(25)) => {}
160 }
161 }
162}
163
164pub async fn read_checkpoint(base: &Path, run_dir: &str) -> Value {
165 let path = base.join(run_dir).join("checkpoint.json");
166 match tokio::fs::read(&path).await {
167 Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or(Value::Null),
168 Err(_) => Value::Null,
169 }
170}
171
172pub fn completed_span_names(cp: &Value) -> Vec<String> {
173 cp.get("phase_state")
174 .and_then(|ps| ps.get("completed_spans"))
175 .and_then(|cs| cs.as_array())
176 .map(|arr| arr.iter().filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(String::from)).collect())
177 .unwrap_or_default()
178}