1pub mod cli;
12pub mod direct;
13pub mod simulated;
14
15use anyhow::Result;
16use async_trait::async_trait;
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19use std::time::Duration;
20use tokio::sync::mpsc;
21use tokio_util::sync::CancellationToken;
22
23use crate::commands::spawn::terminal::Harness;
24
25#[async_trait]
30pub trait AgentBackend: Send + Sync {
31 async fn execute(&self, request: AgentRequest) -> Result<AgentHandle>;
35}
36
37#[derive(Debug, Clone)]
39pub struct AgentRequest {
40 pub prompt: String,
42 pub system_prompt: Option<String>,
44 pub working_dir: PathBuf,
46 pub model: Option<String>,
48 pub provider: Option<String>,
50 pub max_turns: Option<usize>,
52 pub timeout: Option<Duration>,
54 pub reasoning_effort: Option<String>,
56}
57
58impl Default for AgentRequest {
59 fn default() -> Self {
60 Self {
61 prompt: String::new(),
62 system_prompt: None,
63 working_dir: std::env::current_dir().unwrap_or_default(),
64 model: None,
65 provider: None,
66 max_turns: None,
67 timeout: None,
68 reasoning_effort: None,
69 }
70 }
71}
72
73pub struct AgentHandle {
77 pub events: mpsc::Receiver<AgentEvent>,
79 pub cancel: CancellationToken,
81}
82
83impl AgentHandle {
84 pub async fn result(mut self) -> Result<AgentResult> {
89 let mut text_parts = Vec::new();
90 let mut tool_calls = Vec::new();
91 let mut status = AgentStatus::Completed;
92 let usage = None;
93
94 while let Some(event) = self.events.recv().await {
95 match event {
96 AgentEvent::TextDelta(delta) => text_parts.push(delta),
97 AgentEvent::TextComplete(text) => {
98 text_parts.clear();
99 text_parts.push(text);
100 }
101 AgentEvent::ToolCallStart { id, name } => {
102 tool_calls.push(ToolCallRecord {
103 id,
104 name,
105 output: String::new(),
106 });
107 }
108 AgentEvent::ToolCallEnd { id, output } => {
109 if let Some(record) = tool_calls.iter_mut().find(|r| r.id == id) {
110 record.output = output;
111 }
112 }
113 AgentEvent::Complete(result) => return Ok(result),
114 AgentEvent::Error(msg) => {
115 status = AgentStatus::Failed(msg);
116 break;
117 }
118 AgentEvent::ThinkingDelta(_) => {}
119 }
120 }
121
122 Ok(AgentResult {
123 text: text_parts.join(""),
124 status,
125 tool_calls,
126 usage,
127 })
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct AgentResult {
134 pub text: String,
136 pub status: AgentStatus,
138 pub tool_calls: Vec<ToolCallRecord>,
140 pub usage: Option<TokenUsage>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub enum AgentStatus {
147 Completed,
149 Failed(String),
151 Cancelled,
153 Timeout,
155}
156
157impl AgentStatus {
158 pub fn is_success(&self) -> bool {
160 matches!(self, AgentStatus::Completed)
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ToolCallRecord {
167 pub id: String,
169 pub name: String,
171 pub output: String,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct TokenUsage {
178 pub input_tokens: u64,
179 pub output_tokens: u64,
180}
181
182#[derive(Debug, Clone)]
187pub enum AgentEvent {
188 TextDelta(String),
190 TextComplete(String),
192 ToolCallStart { id: String, name: String },
194 ToolCallEnd { id: String, output: String },
196 ThinkingDelta(String),
198 Error(String),
200 Complete(AgentResult),
202}
203
204pub fn create_backend(harness: &Harness) -> Result<Box<dyn AgentBackend>> {
208 match harness {
209 #[cfg(feature = "direct-api")]
210 Harness::DirectApi => Ok(Box::new(direct::DirectApiBackend::new())),
211 _ => Ok(Box::new(cli::CliBackend::new(harness.clone())?)),
212 }
213}
214
215pub fn create_simulated_backend() -> Box<dyn AgentBackend> {
217 Box::new(simulated::SimulatedBackend)
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[tokio::test]
225 async fn test_agent_request_default() {
226 let req = AgentRequest::default();
227 assert!(req.prompt.is_empty());
228 assert!(req.model.is_none());
229 assert!(req.timeout.is_none());
230 }
231
232 #[tokio::test]
233 async fn test_agent_status_is_success() {
234 assert!(AgentStatus::Completed.is_success());
235 assert!(!AgentStatus::Failed("err".into()).is_success());
236 assert!(!AgentStatus::Cancelled.is_success());
237 assert!(!AgentStatus::Timeout.is_success());
238 }
239
240 #[tokio::test]
241 async fn test_simulated_backend() {
242 let backend = create_simulated_backend();
243 let req = AgentRequest {
244 prompt: "Hello world".into(),
245 ..Default::default()
246 };
247 let handle = backend.execute(req).await.unwrap();
248 let result = handle.result().await.unwrap();
249 assert!(result.status.is_success());
250 assert!(result.text.contains("Simulated"));
251 }
252}