#![allow(
missing_docs,
reason = "fields mirror pi's TypeScript types; contracts are noted where they differ from the name"
)]
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::present;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "camelCase")]
pub enum Message {
System(SystemMessage),
User(UserMessage),
Assistant(Box<AssistantMessage>),
ToolResult(ToolResultMessage),
BashExecution(BashExecutionMessage),
Custom(CustomMessage),
BranchSummary(BranchSummaryMessage),
CompactionSummary(CompactionSummaryMessage),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemMessage {
pub content: Content,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sections: Option<IndexMap<String, Option<String>>>,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools_added: Option<Vec<ToolDeclaration>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools_removed: Option<Vec<ToolReference>>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolDeclaration {
pub name: String,
pub description: String,
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constrained_sampling: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolReference {
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserMessage {
pub content: Content,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssistantMessage {
pub content: Vec<ContentBlock>,
pub api: String,
pub provider: String,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_thinking_level: Option<String>,
pub usage: Usage,
pub stop_reason: StopReason,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_stop_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deferred: Option<DeferredHandle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_turn: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<ThinkingLevel>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeferredHandle {
pub provider: String,
pub model_id: String,
pub api: String,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poll_after_ms: Option<u64>,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
pub data: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolResultMessage {
pub tool_call_id: String,
pub tool_name: String,
pub content: Vec<ContentBlock>,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
pub details: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
pub is_error: bool,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nested_calls: Option<NestedToolCalls>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct NestedToolCalls {
pub calls: Vec<NestedToolCall>,
pub complete: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NestedToolCall {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<Map<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments_bytes: Option<u64>,
pub status: NestedToolCallStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NestedToolCallStatus {
Ok,
Error,
Unfinished,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BashExecutionMessage {
pub command: String,
pub output: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i64>,
pub cancelled: bool,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_output_path: Option<String>,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_from_context: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomMessage {
pub custom_type: String,
pub content: Content,
pub display: bool,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
pub details: Option<Value>,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BranchSummaryMessage {
pub summary: String,
pub from_id: Option<String>,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactionSummaryMessage {
pub summary: String,
pub tokens_before: u64,
pub timestamp: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Content {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ContentBlock {
Text(TextContent),
Thinking(ThinkingContent),
ToolCall(ToolCall),
Image(ImageContent),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_signature: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThinkingContent {
pub thinking: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redacted: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Map<String, Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageContent {
pub data: String,
pub mime_type: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Usage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<u64>,
pub cost: Cost,
#[serde(
default,
rename = "cacheWrite1h",
skip_serializing_if = "Option::is_none"
)]
pub cache_write_1h: Option<u64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Cost {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
pub total: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StopReason {
Pending,
Stop,
Length,
ToolUse,
Error,
Aborted,
Deferred,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingLevel {
Off,
Minimal,
Low,
Medium,
High,
Xhigh,
Max,
}
impl ThinkingLevel {
pub const ALL: [ThinkingLevel; 7] = [
ThinkingLevel::Off,
ThinkingLevel::Minimal,
ThinkingLevel::Low,
ThinkingLevel::Medium,
ThinkingLevel::High,
ThinkingLevel::Xhigh,
ThinkingLevel::Max,
];
pub fn as_str(self) -> &'static str {
match self {
ThinkingLevel::Off => "off",
ThinkingLevel::Minimal => "minimal",
ThinkingLevel::Low => "low",
ThinkingLevel::Medium => "medium",
ThinkingLevel::High => "high",
ThinkingLevel::Xhigh => "xhigh",
ThinkingLevel::Max => "max",
}
}
pub fn parse(name: &str) -> Option<ThinkingLevel> {
ThinkingLevel::ALL
.into_iter()
.find(|level| level.as_str() == name)
}
}
impl Message {
pub fn timestamp(&self) -> u64 {
match self {
Message::System(message) => message.timestamp,
Message::User(message) => message.timestamp,
Message::Assistant(message) => message.timestamp,
Message::ToolResult(message) => message.timestamp,
Message::BashExecution(message) => message.timestamp,
Message::Custom(message) => message.timestamp,
Message::BranchSummary(message) => message.timestamp,
Message::CompactionSummary(message) => message.timestamp,
}
}
}
impl Usage {
pub fn combine(&self, other: &Usage) -> Usage {
let optional = |a: Option<u64>, b: Option<u64>| {
(a.is_some() || b.is_some()).then(|| a.unwrap_or(0) + b.unwrap_or(0))
};
Usage {
input: self.input + other.input,
output: self.output + other.output,
cache_read: self.cache_read + other.cache_read,
cache_write: self.cache_write + other.cache_write,
cache_write_1h: optional(self.cache_write_1h, other.cache_write_1h),
reasoning: optional(self.reasoning, other.reasoning),
total_tokens: Some(self.total_tokens.unwrap_or(0) + other.total_tokens.unwrap_or(0)),
cost: Cost {
input: self.cost.input + other.cost.input,
output: self.cost.output + other.cost.output,
cache_read: self.cost.cache_read + other.cost.cache_read,
cache_write: self.cost.cache_write + other.cost.cache_write,
total: self.cost.total + other.cost.total,
},
}
}
}
impl Content {
pub fn text(&self, separator: &str) -> String {
match self {
Content::Text(text) => text.clone(),
Content::Blocks(blocks) => blocks_text(blocks, separator),
}
}
pub fn into_blocks(self) -> Vec<ContentBlock> {
match self {
Content::Text(text) => vec![ContentBlock::text(text)],
Content::Blocks(blocks) => blocks,
}
}
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> ContentBlock {
ContentBlock::Text(TextContent {
text: text.into(),
text_signature: None,
})
}
pub fn thinking(thinking: impl Into<String>, signature: Option<String>) -> ContentBlock {
ContentBlock::Thinking(ThinkingContent {
thinking: thinking.into(),
thinking_signature: signature,
redacted: None,
})
}
pub fn tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: Map<String, Value>,
) -> ContentBlock {
ContentBlock::ToolCall(ToolCall {
id: id.into(),
name: name.into(),
arguments,
thought_signature: None,
namespace: None,
})
}
}
pub fn blocks_text(blocks: &[ContentBlock], separator: &str) -> String {
blocks
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(separator)
}
impl SystemMessage {
pub fn text(&self) -> String {
let mut parts = vec![self.content.text("\n")];
for text in self
.sections
.iter()
.flatten()
.filter_map(|(_, text)| text.clone())
{
parts.push(text);
}
parts.retain(|part| !part.is_empty());
parts.join("\n\n")
}
pub fn render_update(&self) -> String {
let mut parts = Vec::new();
let text = self.content.text("\n");
if !text.is_empty() {
parts.push(text);
}
for (name, value) in self.sections.iter().flatten() {
parts.push(match value {
None => format!("Removed system prompt section \"{name}\"."),
Some(value) => format!("Updated system prompt section \"{name}\":\n\n{value}"),
});
}
parts.join("\n\n")
}
}