use std::path::PathBuf;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::ToolCall;
use crate::llm::TokenUsage;
use crate::message::ContentPart;
use crate::model::Model;
use crate::tool::BackgroundRequest;
use crate::tool::InterruptPayload;
#[derive(Debug, Clone, Default)]
pub enum AgentInput {
User {
content: String,
context: Option<String>,
parts: Option<Vec<ContentPart>>,
},
#[default]
Resume,
}
impl AgentInput {
pub fn user(content: impl Into<String>) -> Self {
Self::User {
content: content.into(),
context: None,
parts: None,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TurnUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub cache_read_tokens: u32,
pub cache_creation_tokens: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum CompactionReason {
OutputLength,
ContextThreshold(u32),
}
#[derive(Debug)]
pub enum TurnStatus {
Continue { content: String, usage: TurnUsage },
Done { content: String, usage: TurnUsage },
ToolCalls {
content: String,
tool_calls: Vec<ToolCall>,
usage: TurnUsage,
},
Interrupted {
tool_call_id: String,
tool_name: String,
output: String,
payload: Option<Arc<dyn InterruptPayload>>,
content: String,
usage: TurnUsage,
},
CompactionNeeded {
content: String,
usage: TurnUsage,
reason: CompactionReason,
},
}
impl TurnStatus {
pub fn content_and_usage(&self) -> (&str, &TurnUsage) {
match self {
Self::Continue { content, usage }
| Self::Done { content, usage }
| Self::ToolCalls { content, usage, .. }
| Self::Interrupted { content, usage, .. }
| Self::CompactionNeeded { content, usage, .. } => (content, usage),
}
}
}
pub type TurnResult = anyhow::Result<TurnStatus>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Source {
pub url: String,
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum AgentEvent {
ReasoningDelta(String),
TextDelta(String),
ToolStart {
name: String,
id: String,
humanized: String,
},
ToolBackgroundReady {
id: String,
#[serde(skip)]
request: Option<BackgroundRequest>,
},
ToolResult {
name: String,
id: String,
output: String,
success: bool,
elapsed_ms: u64,
sources: Vec<Source>,
},
ToolMedia {
id: String,
parts: Vec<ContentPart>,
files: Vec<PathBuf>,
},
Attachment {
filename: String,
media_type: String,
data: Vec<u8>,
},
AudioChunk { data: Vec<u8>, format: String },
Status(String),
Compacted(String),
FileDiff {
path: String,
diff: Vec<crate::tool::DiffLine>,
},
Usage(TokenUsage),
Done(String),
Cancelled,
Error(String),
ToolProgress { id: String, line: String },
SpawnedEvent {
id: String,
task: String,
model: Option<Model>,
event: Box<AgentEvent>,
},
Interrupted {
tool_call_id: String,
tool_name: String,
output: String,
#[serde(skip)]
payload: Option<Arc<dyn InterruptPayload>>,
},
CompactionNeeded { reason: CompactionReason },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recorded_tool_events_do_not_restore_live_background_controls() {
let event = AgentEvent::ToolBackgroundReady {
id: "command".into(),
request: Some(BackgroundRequest::default()),
};
let serialized = serde_json::to_string(&event).unwrap();
let replayed: AgentEvent = serde_json::from_str(&serialized).unwrap();
assert!(matches!(
replayed,
AgentEvent::ToolBackgroundReady { request: None, .. }
));
}
#[test]
fn agent_input_user_shorthand() {
let input = AgentInput::user("hello");
match input {
AgentInput::User {
content,
context,
parts,
} => {
assert_eq!(content, "hello");
assert!(context.is_none());
assert!(parts.is_none());
}
_ => panic!("expected User variant"),
}
}
#[test]
fn agent_input_default_is_resume() {
let input = AgentInput::default();
assert!(matches!(input, AgentInput::Resume));
}
#[test]
fn turn_status_content_and_usage_done() {
let status = TurnStatus::Done {
content: "response".into(),
usage: TurnUsage {
prompt_tokens: 10,
completion_tokens: 20,
..TurnUsage::default()
},
};
let (content, usage) = status.content_and_usage();
assert_eq!(content, "response");
assert_eq!(usage.prompt_tokens, 10);
assert_eq!(usage.completion_tokens, 20);
}
#[test]
fn turn_status_content_and_usage_tool_calls() {
let status = TurnStatus::ToolCalls {
content: "thinking".into(),
tool_calls: vec![],
usage: TurnUsage {
prompt_tokens: 5,
completion_tokens: 15,
..TurnUsage::default()
},
};
let (content, usage) = status.content_and_usage();
assert_eq!(content, "thinking");
assert_eq!(usage.completion_tokens, 15);
}
#[test]
fn turn_usage_default_is_zero() {
let usage = TurnUsage::default();
assert_eq!(usage.prompt_tokens, 0);
assert_eq!(usage.completion_tokens, 0);
}
}