mod custom_message;
pub mod message_codec;
mod model;
pub use custom_message::*;
pub use message_codec::{
MessageSlot, SerializedCustomMessage, SerializedMessages, clone_messages_for_send,
restore_messages, restore_single_custom, serialize_messages,
};
pub use model::*;
use std::collections::HashMap;
use std::fmt;
use std::ops::{Add, AddAssign};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text { text: String },
Thinking {
thinking: String,
signature: Option<String>,
},
ToolCall {
id: String,
name: String,
arguments: serde_json::Value,
partial_json: Option<String>,
},
Image { source: ImageSource },
Extension {
type_name: String,
data: serde_json::Value,
},
}
impl ContentBlock {
pub fn extract_text(blocks: &[Self]) -> String {
let mut result = String::new();
for block in blocks {
if let Self::Text { text } = block {
result.push_str(text);
}
}
result
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 { media_type: String, data: String },
Url { url: String, media_type: String },
File {
path: std::path::PathBuf,
media_type: String,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserMessage {
pub content: Vec<ContentBlock>,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_hint: Option<crate::context_cache::CacheHint>,
}
impl UserMessage {
#[must_use]
pub fn new(content: Vec<ContentBlock>) -> Self {
Self {
content,
timestamp: crate::util::now_timestamp(),
cache_hint: None,
}
}
#[must_use]
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
#[must_use]
pub fn with_cache_hint(mut self, cache_hint: crate::context_cache::CacheHint) -> Self {
self.cache_hint = Some(cache_hint);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub content: Vec<ContentBlock>,
pub provider: String,
pub model_id: String,
pub usage: Usage,
pub cost: Cost,
pub stop_reason: StopReason,
pub error_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_kind: Option<crate::stream_error_kind::StreamErrorKind>,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_hint: Option<crate::context_cache::CacheHint>,
}
impl AssistantMessage {
#[must_use]
pub fn new(
content: Vec<ContentBlock>,
provider: impl Into<String>,
model_id: impl Into<String>,
) -> Self {
Self {
content,
provider: provider.into(),
model_id: model_id.into(),
usage: Usage::default(),
cost: Cost::default(),
stop_reason: StopReason::Stop,
error_message: None,
error_kind: None,
timestamp: crate::util::now_timestamp(),
cache_hint: None,
}
}
#[must_use]
pub fn with_usage(mut self, usage: Usage) -> Self {
self.usage = usage;
self
}
#[must_use]
pub fn with_cost(mut self, cost: Cost) -> Self {
self.cost = cost;
self
}
#[must_use]
pub const fn with_stop_reason(mut self, stop_reason: StopReason) -> Self {
self.stop_reason = stop_reason;
self
}
#[must_use]
pub fn with_error_message(mut self, error_message: impl Into<String>) -> Self {
self.error_message = Some(error_message.into());
self
}
#[must_use]
pub const fn with_error_kind(
mut self,
error_kind: crate::stream_error_kind::StreamErrorKind,
) -> Self {
self.error_kind = Some(error_kind);
self
}
#[must_use]
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
#[must_use]
pub fn with_cache_hint(mut self, cache_hint: crate::context_cache::CacheHint) -> Self {
self.cache_hint = Some(cache_hint);
self
}
#[must_use]
pub fn has_visible_content(&self) -> bool {
self.content.iter().any(|block| match block {
ContentBlock::Text { text } => !text.trim().is_empty(),
ContentBlock::Thinking { .. } => false,
_ => true,
})
}
#[must_use]
pub fn is_reasoning_only(&self) -> bool {
!self.has_visible_content()
&& self
.content
.iter()
.any(|block| matches!(block, ContentBlock::Thinking { .. }))
}
}
impl Default for AssistantMessage {
fn default() -> Self {
Self::new(Vec::new(), String::new(), String::new())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolResultMessage {
pub tool_call_id: String,
pub content: Vec<ContentBlock>,
pub is_error: bool,
pub timestamp: u64,
#[serde(default)]
pub details: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_hint: Option<crate::context_cache::CacheHint>,
}
impl ToolResultMessage {
#[must_use]
pub fn new(tool_call_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
Self {
tool_call_id: tool_call_id.into(),
content,
is_error: false,
timestamp: crate::util::now_timestamp(),
details: serde_json::Value::Null,
cache_hint: None,
}
}
#[must_use]
pub const fn with_is_error(mut self, is_error: bool) -> Self {
self.is_error = is_error;
self
}
#[must_use]
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = details;
self
}
#[must_use]
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
#[must_use]
pub fn with_cache_hint(mut self, cache_hint: crate::context_cache::CacheHint) -> Self {
self.cache_hint = Some(cache_hint);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "snake_case")]
pub enum LlmMessage {
User(UserMessage),
Assistant(AssistantMessage),
ToolResult(ToolResultMessage),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Usage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
pub total: u64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, u64>,
}
impl Usage {
pub fn merge(&mut self, other: &Self) {
*self += other.clone();
}
#[must_use]
pub const fn with_input(mut self, input: u64) -> Self {
self.input = input;
self
}
#[must_use]
pub const fn with_output(mut self, output: u64) -> Self {
self.output = output;
self
}
#[must_use]
pub const fn with_cache_read(mut self, cache_read: u64) -> Self {
self.cache_read = cache_read;
self
}
#[must_use]
pub const fn with_cache_write(mut self, cache_write: u64) -> Self {
self.cache_write = cache_write;
self
}
#[must_use]
pub const fn with_total(mut self, total: u64) -> Self {
self.total = total;
self
}
#[must_use]
pub fn with_extra(mut self, extra: HashMap<String, u64>) -> Self {
self.extra = extra;
self
}
}
impl Add for Usage {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl AddAssign for Usage {
fn add_assign(&mut self, rhs: Self) {
self.input += rhs.input;
self.output += rhs.output;
self.cache_read += rhs.cache_read;
self.cache_write += rhs.cache_write;
self.total += rhs.total;
for (k, v) in rhs.extra {
*self.extra.entry(k).or_insert(0) += v;
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Cost {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
pub total: f64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, f64>,
}
impl Cost {
#[must_use]
pub fn is_zero(&self) -> bool {
self.input == 0.0
&& self.output == 0.0
&& self.cache_read == 0.0
&& self.cache_write == 0.0
&& self.total == 0.0
&& self.extra.values().all(|v| *v == 0.0)
}
#[must_use]
pub const fn with_input(mut self, input: f64) -> Self {
self.input = input;
self
}
#[must_use]
pub const fn with_output(mut self, output: f64) -> Self {
self.output = output;
self
}
#[must_use]
pub const fn with_cache_read(mut self, cache_read: f64) -> Self {
self.cache_read = cache_read;
self
}
#[must_use]
pub const fn with_cache_write(mut self, cache_write: f64) -> Self {
self.cache_write = cache_write;
self
}
#[must_use]
pub const fn with_total(mut self, total: f64) -> Self {
self.total = total;
self
}
#[must_use]
pub fn with_extra(mut self, extra: HashMap<String, f64>) -> Self {
self.extra = extra;
self
}
}
impl Add for Cost {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl AddAssign for Cost {
fn add_assign(&mut self, rhs: Self) {
self.input += rhs.input;
self.output += rhs.output;
self.cache_read += rhs.cache_read;
self.cache_write += rhs.cache_write;
self.total += rhs.total;
for (k, v) in rhs.extra {
*self.extra.entry(k).or_insert(0.0) += v;
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
Stop,
Length,
ToolUse,
Aborted,
Error,
Transfer,
}
#[non_exhaustive]
pub struct AgentResult {
pub messages: Vec<AgentMessage>,
pub stop_reason: StopReason,
pub usage: Usage,
pub cost: Cost,
pub error: Option<String>,
pub transfer_signal: Option<crate::transfer::TransferSignal>,
}
impl AgentResult {
#[must_use]
pub fn new(messages: Vec<AgentMessage>, stop_reason: StopReason) -> Self {
Self {
messages,
stop_reason,
usage: Usage::default(),
cost: Cost::default(),
error: None,
transfer_signal: None,
}
}
#[must_use]
pub fn with_usage(mut self, usage: Usage) -> Self {
self.usage = usage;
self
}
#[must_use]
pub fn with_cost(mut self, cost: Cost) -> Self {
self.cost = cost;
self
}
#[must_use]
pub fn with_error(mut self, error: impl Into<String>) -> Self {
self.error = Some(error.into());
self
}
#[must_use]
pub fn with_transfer_signal(
mut self,
transfer_signal: crate::transfer::TransferSignal,
) -> Self {
self.transfer_signal = Some(transfer_signal);
self
}
pub fn assistant_text(&self) -> String {
self.messages
.iter()
.rev()
.find_map(|msg| match msg {
AgentMessage::Llm(LlmMessage::Assistant(a)) => Some(a),
_ => None,
})
.map(|a| ContentBlock::extract_text(&a.content))
.unwrap_or_default()
}
}
impl fmt::Debug for AgentResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AgentResult")
.field("messages", &self.messages)
.field("stop_reason", &self.stop_reason)
.field("usage", &self.usage)
.field("cost", &self.cost)
.field("error", &self.error)
.field("transfer_signal", &self.transfer_signal)
.finish()
}
}
#[non_exhaustive]
pub struct AgentContext {
pub system_prompt: String,
pub messages: Vec<AgentMessage>,
pub tools: Vec<Arc<dyn crate::tool::AgentTool>>,
}
impl AgentContext {
#[must_use]
pub fn new(
system_prompt: impl Into<String>,
messages: Vec<AgentMessage>,
tools: Vec<Arc<dyn crate::tool::AgentTool>>,
) -> Self {
Self {
system_prompt: system_prompt.into(),
messages,
tools,
}
}
#[must_use]
pub fn try_clone(&self) -> Option<Self> {
let messages = self
.messages
.iter()
.map(AgentMessage::try_clone)
.collect::<Option<Vec<_>>>()?;
Some(Self {
system_prompt: self.system_prompt.clone(),
messages,
tools: self.tools.clone(),
})
}
#[must_use]
pub fn clone_for_send(&self) -> Self {
Self {
system_prompt: self.system_prompt.clone(),
messages: clone_messages_for_send(&self.messages),
tools: self.tools.clone(),
}
}
}
impl fmt::Debug for AgentContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AgentContext")
.field("system_prompt", &self.system_prompt)
.field("messages", &self.messages)
.field("tools", &format_args!("[{} tool(s)]", self.tools.len()))
.finish()
}
}
fn serialize_arc_vec<S, T>(value: &Arc<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: Serialize,
{
value.as_ref().serialize(serializer)
}
fn deserialize_arc_vec<'de, D, T>(deserializer: D) -> Result<Arc<Vec<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
let v = Vec::<T>::deserialize(deserializer)?;
Ok(Arc::new(v))
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnSnapshot {
pub turn_index: usize,
#[serde(
serialize_with = "serialize_arc_vec",
deserialize_with = "deserialize_arc_vec"
)]
pub messages: Arc<Vec<Arc<LlmMessage>>>,
pub usage: Usage,
pub cost: Cost,
pub stop_reason: StopReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_delta: Option<crate::StateDelta>,
}
impl TurnSnapshot {
#[must_use]
pub fn new(
turn_index: usize,
messages: Arc<Vec<Arc<LlmMessage>>>,
stop_reason: StopReason,
) -> Self {
Self {
turn_index,
messages,
usage: Usage::default(),
cost: Cost::default(),
stop_reason,
state_delta: None,
}
}
#[must_use]
pub fn with_usage(mut self, usage: Usage) -> Self {
self.usage = usage;
self
}
#[must_use]
pub fn with_cost(mut self, cost: Cost) -> Self {
self.cost = cost;
self
}
#[must_use]
pub fn with_state_delta(mut self, state_delta: crate::StateDelta) -> Self {
self.state_delta = Some(state_delta);
self
}
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ContentBlock>();
assert_send_sync::<ImageSource>();
assert_send_sync::<UserMessage>();
assert_send_sync::<AssistantMessage>();
assert_send_sync::<ToolResultMessage>();
assert_send_sync::<LlmMessage>();
assert_send_sync::<AgentMessage>();
assert_send_sync::<Usage>();
assert_send_sync::<Cost>();
assert_send_sync::<StopReason>();
assert_send_sync::<ThinkingLevel>();
assert_send_sync::<ThinkingLevelSet>();
assert_send_sync::<ThinkingBudgets>();
assert_send_sync::<ModelCapabilities>();
assert_send_sync::<ModelSpec>();
assert_send_sync::<AgentResult>();
assert_send_sync::<AgentContext>();
assert_send_sync::<TurnSnapshot>();
assert_send_sync::<CustomMessageRegistry>();
assert_send_sync::<crate::error::DowncastError>();
};
#[cfg(test)]
#[path = "tests.rs"]
mod tests;