1use std::fmt;
18use std::future::Future;
19use std::pin::Pin;
20
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24use tracing::warn;
25
26use crate::error::AgentError;
27use crate::operations::agent::{Model, PermissionMode};
28
29pub type InvokeFuture<'a> =
31 Pin<Box<dyn Future<Output = Result<AgentOutput, AgentError>> + Send + 'a>>;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct AgentConfig {
41 pub system_prompt: Option<String>,
43
44 pub prompt: String,
46
47 #[serde(default = "default_model")]
52 pub model: String,
53
54 #[serde(default)]
56 pub allowed_tools: Vec<String>,
57
58 pub max_turns: Option<u32>,
60
61 pub max_budget_usd: Option<f64>,
63
64 pub working_dir: Option<String>,
66
67 pub mcp_config: Option<String>,
69
70 #[serde(default)]
72 pub permission_mode: PermissionMode,
73
74 #[serde(alias = "output_schema")]
77 pub json_schema: Option<String>,
78
79 pub resume_session_id: Option<String>,
84
85 #[serde(default)]
92 pub verbose: bool,
93}
94
95fn default_model() -> String {
96 Model::SONNET.to_string()
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct AgentOutput {
105 pub value: Value,
108
109 pub session_id: Option<String>,
111
112 pub cost_usd: Option<f64>,
114
115 pub input_tokens: Option<u64>,
117
118 pub output_tokens: Option<u64>,
120
121 pub model: Option<String>,
123
124 pub duration_ms: u64,
126
127 pub debug_messages: Option<Vec<DebugMessage>>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
161#[non_exhaustive]
162pub struct DebugMessage {
163 pub text: Option<String>,
165
166 pub tool_calls: Vec<DebugToolCall>,
168
169 pub stop_reason: Option<String>,
171}
172
173impl fmt::Display for DebugMessage {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 if let Some(ref text) = self.text {
176 writeln!(f, "[assistant] {text}")?;
177 }
178 for tc in &self.tool_calls {
179 write!(f, "{tc}")?;
180 }
181 Ok(())
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
189#[non_exhaustive]
190pub struct DebugToolCall {
191 pub name: String,
193
194 pub input: Value,
196}
197
198impl fmt::Display for DebugToolCall {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 writeln!(f, " [tool_use] {} -> {}", self.name, self.input)
201 }
202}
203
204impl AgentConfig {
205 pub fn new(prompt: &str) -> Self {
207 Self {
208 system_prompt: None,
209 prompt: prompt.to_string(),
210 model: Model::SONNET.to_string(),
211 allowed_tools: Vec::new(),
212 max_turns: None,
213 max_budget_usd: None,
214 working_dir: None,
215 mcp_config: None,
216 permission_mode: PermissionMode::Default,
217 json_schema: None,
218 resume_session_id: None,
219 verbose: false,
220 }
221 }
222
223 pub fn system_prompt(mut self, prompt: &str) -> Self {
225 self.system_prompt = Some(prompt.to_string());
226 self
227 }
228
229 pub fn model(mut self, model: &str) -> Self {
231 self.model = model.to_string();
232 self
233 }
234
235 pub fn max_budget_usd(mut self, budget: f64) -> Self {
237 self.max_budget_usd = Some(budget);
238 self
239 }
240
241 pub fn max_turns(mut self, turns: u32) -> Self {
243 self.max_turns = Some(turns);
244 self
245 }
246
247 pub fn allow_tool(mut self, tool: &str) -> Self {
249 self.allowed_tools.push(tool.to_string());
250 self
251 }
252
253 pub fn working_dir(mut self, dir: &str) -> Self {
255 self.working_dir = Some(dir.to_string());
256 self
257 }
258
259 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
261 self.permission_mode = mode;
262 self
263 }
264
265 pub fn verbose(mut self, enabled: bool) -> Self {
267 self.verbose = enabled;
268 self
269 }
270
271 pub fn output<T: JsonSchema>(mut self) -> Self {
278 let schema = schemars::schema_for!(T);
279 self.json_schema = match serde_json::to_string(&schema) {
280 Ok(s) => Some(s),
281 Err(e) => {
282 warn!(
283 error = %e,
284 type_name = std::any::type_name::<T>(),
285 "failed to serialize JSON schema, structured output disabled"
286 );
287 None
288 }
289 };
290 self
291 }
292
293 pub fn output_schema_raw(mut self, schema: &str) -> Self {
295 self.json_schema = Some(schema.to_string());
296 self
297 }
298
299 pub fn mcp_config(mut self, config: &str) -> Self {
301 self.mcp_config = Some(config.to_string());
302 self
303 }
304
305 pub fn resume(mut self, session_id: &str) -> Self {
307 self.resume_session_id = Some(session_id.to_string());
308 self
309 }
310}
311
312impl AgentOutput {
313 pub fn new(value: Value) -> Self {
315 Self {
316 value,
317 session_id: None,
318 cost_usd: None,
319 input_tokens: None,
320 output_tokens: None,
321 model: None,
322 duration_ms: 0,
323 debug_messages: None,
324 }
325 }
326}
327
328pub trait AgentProvider: Send + Sync {
351 fn invoke<'a>(&'a self, config: &'a AgentConfig) -> InvokeFuture<'a>;
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use serde_json::json;
364
365 fn full_config() -> AgentConfig {
366 AgentConfig {
367 system_prompt: Some("you are helpful".to_string()),
368 prompt: "do stuff".to_string(),
369 model: Model::OPUS.to_string(),
370 allowed_tools: vec!["Read".to_string(), "Write".to_string()],
371 max_turns: Some(10),
372 max_budget_usd: Some(2.5),
373 working_dir: Some("/tmp".to_string()),
374 mcp_config: Some("{}".to_string()),
375 permission_mode: PermissionMode::Auto,
376 json_schema: Some(r#"{"type":"object"}"#.to_string()),
377 resume_session_id: None,
378 verbose: false,
379 }
380 }
381
382 #[test]
383 fn agent_config_serialize_deserialize_roundtrip() {
384 let config = full_config();
385 let json = serde_json::to_string(&config).unwrap();
386 let back: AgentConfig = serde_json::from_str(&json).unwrap();
387
388 assert_eq!(back.system_prompt, Some("you are helpful".to_string()));
389 assert_eq!(back.prompt, "do stuff");
390 assert_eq!(back.allowed_tools, vec!["Read", "Write"]);
391 assert_eq!(back.max_turns, Some(10));
392 assert_eq!(back.max_budget_usd, Some(2.5));
393 assert_eq!(back.working_dir, Some("/tmp".to_string()));
394 assert_eq!(back.mcp_config, Some("{}".to_string()));
395 assert_eq!(back.json_schema, Some(r#"{"type":"object"}"#.to_string()));
396 }
397
398 #[test]
399 fn agent_config_with_all_optional_fields_none() {
400 let config = AgentConfig {
401 system_prompt: None,
402 prompt: "hello".to_string(),
403 model: Model::HAIKU.to_string(),
404 allowed_tools: vec![],
405 max_turns: None,
406 max_budget_usd: None,
407 working_dir: None,
408 mcp_config: None,
409 permission_mode: PermissionMode::Default,
410 json_schema: None,
411 resume_session_id: None,
412 verbose: false,
413 };
414 let json = serde_json::to_string(&config).unwrap();
415 let back: AgentConfig = serde_json::from_str(&json).unwrap();
416
417 assert_eq!(back.system_prompt, None);
418 assert_eq!(back.prompt, "hello");
419 assert!(back.allowed_tools.is_empty());
420 assert_eq!(back.max_turns, None);
421 assert_eq!(back.max_budget_usd, None);
422 assert_eq!(back.working_dir, None);
423 assert_eq!(back.mcp_config, None);
424 assert_eq!(back.json_schema, None);
425 }
426
427 #[test]
428 fn agent_output_serialize_deserialize_roundtrip() {
429 let output = AgentOutput {
430 value: json!({"key": "value"}),
431 session_id: Some("sess-abc".to_string()),
432 cost_usd: Some(0.01),
433 input_tokens: Some(500),
434 output_tokens: Some(200),
435 model: Some("claude-sonnet".to_string()),
436 duration_ms: 3000,
437 debug_messages: None,
438 };
439 let json = serde_json::to_string(&output).unwrap();
440 let back: AgentOutput = serde_json::from_str(&json).unwrap();
441
442 assert_eq!(back.value, json!({"key": "value"}));
443 assert_eq!(back.session_id, Some("sess-abc".to_string()));
444 assert_eq!(back.cost_usd, Some(0.01));
445 assert_eq!(back.input_tokens, Some(500));
446 assert_eq!(back.output_tokens, Some(200));
447 assert_eq!(back.model, Some("claude-sonnet".to_string()));
448 assert_eq!(back.duration_ms, 3000);
449 }
450
451 #[test]
452 fn agent_config_new_has_correct_defaults() {
453 let config = AgentConfig::new("test prompt");
454 assert_eq!(config.prompt, "test prompt");
455 assert_eq!(config.system_prompt, None);
456 assert_eq!(config.model, Model::SONNET);
457 assert!(config.allowed_tools.is_empty());
458 assert_eq!(config.max_turns, None);
459 assert_eq!(config.max_budget_usd, None);
460 assert_eq!(config.working_dir, None);
461 assert_eq!(config.mcp_config, None);
462 assert!(matches!(config.permission_mode, PermissionMode::Default));
463 assert_eq!(config.json_schema, None);
464 assert_eq!(config.resume_session_id, None);
465 assert!(!config.verbose);
466 }
467
468 #[test]
469 fn agent_output_new_has_correct_defaults() {
470 let output = AgentOutput::new(json!("test"));
471 assert_eq!(output.value, json!("test"));
472 assert_eq!(output.session_id, None);
473 assert_eq!(output.cost_usd, None);
474 assert_eq!(output.input_tokens, None);
475 assert_eq!(output.output_tokens, None);
476 assert_eq!(output.model, None);
477 assert_eq!(output.duration_ms, 0);
478 assert!(output.debug_messages.is_none());
479 }
480
481 #[test]
482 fn agent_config_resume_session_roundtrip() {
483 let mut config = AgentConfig::new("test");
484 config.resume_session_id = Some("sess-xyz".to_string());
485 let json = serde_json::to_string(&config).unwrap();
486 let back: AgentConfig = serde_json::from_str(&json).unwrap();
487 assert_eq!(back.resume_session_id, Some("sess-xyz".to_string()));
488 }
489
490 #[test]
491 fn agent_output_debug_does_not_panic() {
492 let output = AgentOutput {
493 value: json!(null),
494 session_id: None,
495 cost_usd: None,
496 input_tokens: None,
497 output_tokens: None,
498 model: None,
499 duration_ms: 0,
500 debug_messages: None,
501 };
502 let debug_str = format!("{:?}", output);
503 assert!(!debug_str.is_empty());
504 }
505}