use std::borrow::Cow;
use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum ToolStatus {
#[default]
Running,
Ok,
Failed,
Cancelled,
}
impl ToolStatus {
pub const fn icon(self) -> &'static str {
match self {
ToolStatus::Ok => "✓ wrote",
ToolStatus::Failed => "✕ write failed",
ToolStatus::Running => "⠋ writing",
ToolStatus::Cancelled => "✕ write cancelled",
}
}
pub const fn label(self) -> &'static str {
match self {
ToolStatus::Running => "running",
ToolStatus::Ok => "ok",
ToolStatus::Failed => "failed",
ToolStatus::Cancelled => "cancelled",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum ToolEvidenceKind {
Output,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AgentMessage {
System(String),
User(String),
Assistant(String),
ToolUse(ToolUseRequest),
ToolResult {
id: String,
output: ToolOutput,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ToolPermissionDecision {
Allow,
Reject,
Cancelled,
}
impl ToolPermissionDecision {
pub const fn outcome_label(self) -> &'static str {
match self {
Self::Allow => "allowed",
Self::Reject => "rejected",
Self::Cancelled => "cancelled",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AgentEvent {
Started,
Status(String),
Usage {
input_tokens: u64,
output_tokens: u64,
},
AssistantDelta(String),
ReasoningDelta(String),
ToolStarted {
id: String,
name: String,
arguments: String,
},
ToolFinished {
id: String,
output: Vec<String>,
status: ToolStatus,
},
ModelMetadataLoaded(Vec<(String, String)>),
Retrying {
attempt: u32,
max_attempts: u32,
delay_ms: u64,
error: String,
},
Finished,
Failed(String),
Cancelled,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolUseRequest {
pub name: String,
pub arguments: String,
pub tool_use_id: String,
}
impl ToolUseRequest {
pub fn new(name: impl Into<String>, arguments: impl Into<String>, tool_use_id: impl Into<String>) -> Self {
Self { name: name.into(), arguments: arguments.into(), tool_use_id: tool_use_id.into() }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ToolEvidenceMetadata {
pub identity: String,
pub kind: ToolEvidenceKind,
pub byte_count: usize,
pub content_hash: Option<String>,
pub artifact_handle: Option<String>,
}
impl ToolEvidenceMetadata {
pub fn for_lines(identity: impl Into<String>, lines: &[String]) -> Self {
Self {
identity: identity.into(),
kind: ToolEvidenceKind::Output,
byte_count: lines.join("\n").len(),
content_hash: None,
artifact_handle: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ToolDisplayProjection {
pub lines: Vec<String>,
}
impl ToolDisplayProjection {
pub fn new(lines: Vec<String>) -> Self {
Self { lines }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ToolModelProjection {
pub lines: Vec<String>,
}
impl ToolModelProjection {
pub fn new(lines: Vec<String>) -> Self {
Self { lines }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolOutput {
pub name: String,
pub status: ToolStatus,
pub evidence: ToolEvidenceMetadata,
pub display: ToolDisplayProjection,
pub model: ToolModelProjection,
pub error: Option<String>,
}
impl ToolOutput {
pub fn ok(name: impl Into<String>, output: Vec<String>) -> Self {
let name = name.into();
Self {
evidence: ToolEvidenceMetadata::for_lines(&name, &output),
name,
status: ToolStatus::Ok,
display: ToolDisplayProjection::new(output.clone()),
model: ToolModelProjection::new(output),
error: None,
}
}
pub fn failed(name: impl Into<String>, error: impl Into<String>) -> Self {
let name = name.into();
Self {
evidence: ToolEvidenceMetadata::for_lines(&name, &[]),
name,
status: ToolStatus::Failed,
display: ToolDisplayProjection::new(Vec::new()),
model: ToolModelProjection::new(Vec::new()),
error: Some(error.into()),
}
}
pub fn display_lines(&self) -> Vec<String> {
projection_lines(&self.display.lines, self.error.as_deref())
}
pub fn model_lines(&self) -> Vec<String> {
projection_lines(&self.model.lines, self.error.as_deref())
}
}
#[derive(Clone, Debug)]
pub struct ToolDefinition {
pub name: Cow<'static, str>,
pub description: Cow<'static, str>,
pub input_schema: serde_json::Value,
}
impl ToolDefinition {
pub fn new(
name: impl Into<Cow<'static, str>>, description: impl Into<Cow<'static, str>>, input_schema: serde_json::Value,
) -> Self {
Self { name: name.into(), description: description.into(), input_schema }
}
}
#[derive(Clone, Debug)]
pub struct AgentTurn {
pub messages: Vec<AgentMessage>,
pub tools: Vec<ToolDefinition>,
}
impl AgentTurn {
pub fn new(messages: Vec<AgentMessage>, tools: Vec<ToolDefinition>) -> Self {
Self { messages, tools }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryPolicy {
pub max_retries: u32,
pub base_delay: Duration,
}
impl RetryPolicy {
pub const fn new(max_retries: u32, base_delay: Duration) -> Self {
Self { max_retries, base_delay }
}
pub fn delay_for_attempt(self, attempt: u32) -> Duration {
self.base_delay * 2u32.saturating_pow(attempt.saturating_sub(1))
}
}
fn projection_lines(lines: &[String], error: Option<&str>) -> Vec<String> {
let mut lines = lines.to_vec();
let Some(error) = error.map(str::trim).filter(|error| !error.is_empty()) else {
return lines;
};
let error_line = format!("error: {error}");
if !lines.iter().any(|line| line == error || line == &error_line) {
lines.insert(0, error_line);
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permission_outcomes_have_stable_labels() {
assert_eq!(ToolPermissionDecision::Allow.outcome_label(), "allowed");
assert_eq!(ToolPermissionDecision::Reject.outcome_label(), "rejected");
assert_eq!(ToolPermissionDecision::Cancelled.outcome_label(), "cancelled");
}
#[test]
fn retry_delays_double_per_attempt() {
let policy = RetryPolicy::new(3, Duration::from_millis(25));
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(25));
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(100));
}
#[test]
fn tool_output_preserves_success_and_failure_states() {
let success = ToolOutput::ok("read", vec!["ok".to_string()]);
assert_eq!(success.status, ToolStatus::Ok);
assert_eq!(success.display.lines, vec!["ok"]);
assert_eq!(success.model.lines, vec!["ok"]);
assert_eq!(success.evidence.byte_count, 2);
let failure = ToolOutput::failed("read", "missing");
assert_eq!(failure.error.as_deref(), Some("missing"));
assert_eq!(failure.display_lines(), vec!["error: missing"]);
assert_eq!(failure.model_lines(), vec!["error: missing"]);
}
#[test]
fn display_and_model_projections_can_diverge_without_raw_output() {
let mut output = ToolOutput::ok("read", vec!["full result".to_string()]);
output.display.lines = vec!["shown to user".to_string()];
output.model.lines = vec!["sent to model".to_string()];
assert_eq!(output.display.lines, vec!["shown to user"]);
assert_eq!(output.model.lines, vec!["sent to model"]);
assert_eq!(output.evidence.artifact_handle, None);
}
#[test]
fn turn_keeps_provider_neutral_messages_and_tools_together() {
let request = ToolUseRequest::new("read_file", r#"{"path":"README.md"}"#, "call_1");
let turn = AgentTurn::new(
vec![
AgentMessage::User("inspect the readme".to_string()),
AgentMessage::ToolUse(request),
],
vec![ToolDefinition::new(
"read_file",
"Read a file",
serde_json::json!({"type": "object"}),
)],
);
assert_eq!(turn.messages.len(), 2);
assert_eq!(turn.tools[0].name, "read_file");
}
}