1use std::borrow::Cow;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
10pub enum ToolStatus {
11 #[default]
13 Running,
14 Ok,
16 Failed,
18 Cancelled,
20}
21
22impl ToolStatus {
23 pub const fn icon(self) -> &'static str {
25 match self {
26 ToolStatus::Ok => "✓ wrote",
27 ToolStatus::Failed => "✕ write failed",
28 ToolStatus::Running => "⠋ writing",
29 ToolStatus::Cancelled => "✕ write cancelled",
30 }
31 }
32
33 pub const fn label(self) -> &'static str {
35 match self {
36 ToolStatus::Running => "running",
37 ToolStatus::Ok => "ok",
38 ToolStatus::Failed => "failed",
39 ToolStatus::Cancelled => "cancelled",
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
46pub enum ToolEvidenceKind {
47 Output,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
56pub enum AgentMessage {
57 System(String),
59 User(String),
61 Assistant(String),
63 ToolUse(ToolUseRequest),
65 ToolResult {
67 id: String,
69 output: ToolOutput,
71 },
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum ToolPermissionDecision {
77 Allow,
79 Reject,
81 Cancelled,
83}
84
85impl ToolPermissionDecision {
86 pub const fn outcome_label(self) -> &'static str {
88 match self {
89 Self::Allow => "allowed",
90 Self::Reject => "rejected",
91 Self::Cancelled => "cancelled",
92 }
93 }
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
101pub enum AgentEvent {
102 Started,
104 Status(String),
106 Usage {
108 input_tokens: u64,
110 output_tokens: u64,
112 },
113 AssistantDelta(String),
115 ReasoningDelta(String),
117 ToolStarted {
119 id: String,
121 name: String,
123 arguments: String,
125 },
126 ToolFinished {
128 id: String,
130 output: Vec<String>,
132 status: ToolStatus,
134 },
135 ModelMetadataLoaded(Vec<(String, String)>),
137 Retrying {
139 attempt: u32,
141 max_attempts: u32,
143 delay_ms: u64,
145 error: String,
147 },
148 Finished,
150 Failed(String),
152 Cancelled,
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
158pub struct ToolUseRequest {
159 pub name: String,
161 pub arguments: String,
163 pub tool_use_id: String,
165}
166
167impl ToolUseRequest {
168 pub fn new(name: impl Into<String>, arguments: impl Into<String>, tool_use_id: impl Into<String>) -> Self {
170 Self { name: name.into(), arguments: arguments.into(), tool_use_id: tool_use_id.into() }
171 }
172}
173
174#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
181pub struct ToolEvidenceMetadata {
182 pub identity: String,
184 pub kind: ToolEvidenceKind,
186 pub byte_count: usize,
188 pub content_hash: Option<String>,
190 pub artifact_handle: Option<String>,
192}
193
194impl ToolEvidenceMetadata {
195 pub fn for_lines(identity: impl Into<String>, lines: &[String]) -> Self {
197 Self {
198 identity: identity.into(),
199 kind: ToolEvidenceKind::Output,
200 byte_count: lines.join("\n").len(),
201 content_hash: None,
202 artifact_handle: None,
203 }
204 }
205}
206
207#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
209pub struct ToolDisplayProjection {
210 pub lines: Vec<String>,
212}
213
214impl ToolDisplayProjection {
215 pub fn new(lines: Vec<String>) -> Self {
217 Self { lines }
218 }
219}
220
221#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
223pub struct ToolModelProjection {
224 pub lines: Vec<String>,
226}
227
228impl ToolModelProjection {
229 pub fn new(lines: Vec<String>) -> Self {
231 Self { lines }
232 }
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct ToolOutput {
238 pub name: String,
240 pub status: ToolStatus,
242 pub evidence: ToolEvidenceMetadata,
244 pub display: ToolDisplayProjection,
246 pub model: ToolModelProjection,
248 pub error: Option<String>,
250}
251
252impl ToolOutput {
253 pub fn ok(name: impl Into<String>, output: Vec<String>) -> Self {
255 let name = name.into();
256 Self {
257 evidence: ToolEvidenceMetadata::for_lines(&name, &output),
258 name,
259 status: ToolStatus::Ok,
260 display: ToolDisplayProjection::new(output.clone()),
261 model: ToolModelProjection::new(output),
262 error: None,
263 }
264 }
265
266 pub fn failed(name: impl Into<String>, error: impl Into<String>) -> Self {
268 let name = name.into();
269 Self {
270 evidence: ToolEvidenceMetadata::for_lines(&name, &[]),
271 name,
272 status: ToolStatus::Failed,
273 display: ToolDisplayProjection::new(Vec::new()),
274 model: ToolModelProjection::new(Vec::new()),
275 error: Some(error.into()),
276 }
277 }
278
279 pub fn display_lines(&self) -> Vec<String> {
281 projection_lines(&self.display.lines, self.error.as_deref())
282 }
283
284 pub fn model_lines(&self) -> Vec<String> {
286 projection_lines(&self.model.lines, self.error.as_deref())
287 }
288}
289
290#[derive(Clone, Debug)]
292pub struct ToolDefinition {
293 pub name: Cow<'static, str>,
295 pub description: Cow<'static, str>,
297 pub input_schema: serde_json::Value,
299}
300
301impl ToolDefinition {
302 pub fn new(
304 name: impl Into<Cow<'static, str>>, description: impl Into<Cow<'static, str>>, input_schema: serde_json::Value,
305 ) -> Self {
306 Self { name: name.into(), description: description.into(), input_schema }
307 }
308}
309
310#[derive(Clone, Debug)]
317pub struct AgentTurn {
318 pub messages: Vec<AgentMessage>,
320 pub tools: Vec<ToolDefinition>,
322}
323
324impl AgentTurn {
325 pub fn new(messages: Vec<AgentMessage>, tools: Vec<ToolDefinition>) -> Self {
327 Self { messages, tools }
328 }
329}
330
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333pub struct RetryPolicy {
334 pub max_retries: u32,
336 pub base_delay: Duration,
338}
339
340impl RetryPolicy {
341 pub const fn new(max_retries: u32, base_delay: Duration) -> Self {
343 Self { max_retries, base_delay }
344 }
345
346 pub fn delay_for_attempt(self, attempt: u32) -> Duration {
348 self.base_delay * 2u32.saturating_pow(attempt.saturating_sub(1))
349 }
350}
351
352fn projection_lines(lines: &[String], error: Option<&str>) -> Vec<String> {
353 let mut lines = lines.to_vec();
354 let Some(error) = error.map(str::trim).filter(|error| !error.is_empty()) else {
355 return lines;
356 };
357 let error_line = format!("error: {error}");
358 if !lines.iter().any(|line| line == error || line == &error_line) {
359 lines.insert(0, error_line);
360 }
361 lines
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn permission_outcomes_have_stable_labels() {
370 assert_eq!(ToolPermissionDecision::Allow.outcome_label(), "allowed");
371 assert_eq!(ToolPermissionDecision::Reject.outcome_label(), "rejected");
372 assert_eq!(ToolPermissionDecision::Cancelled.outcome_label(), "cancelled");
373 }
374
375 #[test]
376 fn retry_delays_double_per_attempt() {
377 let policy = RetryPolicy::new(3, Duration::from_millis(25));
378 assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(25));
379 assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(100));
380 }
381
382 #[test]
383 fn tool_output_preserves_success_and_failure_states() {
384 let success = ToolOutput::ok("read", vec!["ok".to_string()]);
385 assert_eq!(success.status, ToolStatus::Ok);
386 assert_eq!(success.display.lines, vec!["ok"]);
387 assert_eq!(success.model.lines, vec!["ok"]);
388 assert_eq!(success.evidence.byte_count, 2);
389
390 let failure = ToolOutput::failed("read", "missing");
391 assert_eq!(failure.error.as_deref(), Some("missing"));
392 assert_eq!(failure.display_lines(), vec!["error: missing"]);
393 assert_eq!(failure.model_lines(), vec!["error: missing"]);
394 }
395
396 #[test]
397 fn display_and_model_projections_can_diverge_without_raw_output() {
398 let mut output = ToolOutput::ok("read", vec!["full result".to_string()]);
399 output.display.lines = vec!["shown to user".to_string()];
400 output.model.lines = vec!["sent to model".to_string()];
401
402 assert_eq!(output.display.lines, vec!["shown to user"]);
403 assert_eq!(output.model.lines, vec!["sent to model"]);
404 assert_eq!(output.evidence.artifact_handle, None);
405 }
406
407 #[test]
408 fn turn_keeps_provider_neutral_messages_and_tools_together() {
409 let request = ToolUseRequest::new("read_file", r#"{"path":"README.md"}"#, "call_1");
410 let turn = AgentTurn::new(
411 vec![
412 AgentMessage::User("inspect the readme".to_string()),
413 AgentMessage::ToolUse(request),
414 ],
415 vec![ToolDefinition::new(
416 "read_file",
417 "Read a file",
418 serde_json::json!({"type": "object"}),
419 )],
420 );
421
422 assert_eq!(turn.messages.len(), 2);
423 assert_eq!(turn.tools[0].name, "read_file");
424 }
425}