1#![allow(
8 missing_docs,
9 reason = "fields mirror pi's TypeScript types; contracts are noted where they differ from the name"
10)]
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16use crate::present;
17
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "role", rename_all = "camelCase")]
21pub enum Message {
22 System(SystemMessage),
23 User(UserMessage),
24 Assistant(Box<AssistantMessage>),
25 ToolResult(ToolResultMessage),
26 BashExecution(BashExecutionMessage),
28 Custom(CustomMessage),
30 BranchSummary(BranchSummaryMessage),
32 CompactionSummary(CompactionSummaryMessage),
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct SystemMessage {
41 pub content: Content,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub sections: Option<IndexMap<String, Option<String>>>,
45 pub timestamp: u64,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub tools_added: Option<Vec<ToolDeclaration>>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub tools_removed: Option<Vec<ToolReference>>,
51}
52
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct ToolDeclaration {
57 pub name: String,
58 pub description: String,
59 pub parameters: Value,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub constrained_sampling: Option<Value>,
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct ToolReference {
69 pub name: String,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct UserMessage {
74 pub content: Content,
75 pub timestamp: u64,
77}
78
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct AssistantMessage {
84 pub content: Vec<ContentBlock>,
85 pub api: String,
86 pub provider: String,
87 pub model: String,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub provider_thinking_level: Option<String>,
91 pub usage: Usage,
92 pub stop_reason: StopReason,
93 pub timestamp: u64,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub response_id: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub response_model: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub raw_stop_reason: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub deferred: Option<DeferredHandle>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub error_message: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub diagnostics: Option<Vec<Value>>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub end_turn: Option<bool>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub thinking_level: Option<ThinkingLevel>,
115}
116
117#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct DeferredHandle {
121 pub provider: String,
122 pub model_id: String,
123 pub api: String,
124 pub id: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub expires_at: Option<u64>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub poll_after_ms: Option<u64>,
129 #[serde(
130 default,
131 deserialize_with = "present",
132 skip_serializing_if = "Option::is_none"
133 )]
134 pub data: Option<Value>,
135}
136
137#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct ToolResultMessage {
142 pub tool_call_id: String,
143 pub tool_name: String,
144 pub content: Vec<ContentBlock>,
145 #[serde(
146 default,
147 deserialize_with = "present",
148 skip_serializing_if = "Option::is_none"
149 )]
150 pub details: Option<Value>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub usage: Option<Usage>,
154 pub is_error: bool,
155 pub timestamp: u64,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub nested_calls: Option<NestedToolCalls>,
159}
160
161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163pub struct NestedToolCalls {
164 pub calls: Vec<NestedToolCall>,
165 pub complete: bool,
167}
168
169#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct NestedToolCall {
172 pub id: String,
173 pub name: String,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub arguments: Option<Map<String, Value>>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub arguments_bytes: Option<u64>,
179 pub status: NestedToolCallStatus,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub duration_ms: Option<u64>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub error: Option<String>,
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "lowercase")]
188pub enum NestedToolCallStatus {
189 Ok,
190 Error,
191 Unfinished,
192}
193
194#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct BashExecutionMessage {
198 pub command: String,
199 pub output: String,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub exit_code: Option<i64>,
203 pub cancelled: bool,
204 pub truncated: bool,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub full_output_path: Option<String>,
207 pub timestamp: u64,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub exclude_from_context: Option<bool>,
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct CustomMessage {
217 pub custom_type: String,
218 pub content: Content,
219 pub display: bool,
220 #[serde(
221 default,
222 deserialize_with = "present",
223 skip_serializing_if = "Option::is_none"
224 )]
225 pub details: Option<Value>,
226 pub timestamp: u64,
228}
229
230#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct BranchSummaryMessage {
233 pub summary: String,
234 pub from_id: Option<String>,
235 pub timestamp: u64,
237}
238
239#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct CompactionSummaryMessage {
242 pub summary: String,
243 pub tokens_before: u64,
244 pub timestamp: u64,
246}
247
248#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
250#[serde(untagged)]
251pub enum Content {
252 Text(String),
253 Blocks(Vec<ContentBlock>),
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
260#[serde(tag = "type", rename_all = "camelCase")]
261pub enum ContentBlock {
262 Text(TextContent),
263 Thinking(ThinkingContent),
264 ToolCall(ToolCall),
265 Image(ImageContent),
266}
267
268#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct TextContent {
271 pub text: String,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub text_signature: Option<String>,
275}
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct ThinkingContent {
280 pub thinking: String,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub thinking_signature: Option<String>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub redacted: Option<bool>,
287}
288
289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct ToolCall {
292 pub id: String,
293 pub name: String,
294 pub arguments: Map<String, Value>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub thought_signature: Option<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub namespace: Option<String>,
301}
302
303#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct ImageContent {
306 pub data: String,
308 pub mime_type: String,
309}
310
311#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct Usage {
315 pub input: u64,
316 pub output: u64,
317 pub cache_read: u64,
318 pub cache_write: u64,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub reasoning: Option<u64>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub total_tokens: Option<u64>,
325 pub cost: Cost,
326 #[serde(
328 default,
329 rename = "cacheWrite1h",
330 skip_serializing_if = "Option::is_none"
331 )]
332 pub cache_write_1h: Option<u64>,
333}
334
335#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct Cost {
339 pub input: f64,
340 pub output: f64,
341 pub cache_read: f64,
342 pub cache_write: f64,
343 pub total: f64,
344}
345
346#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub enum StopReason {
349 Pending,
350 Stop,
351 Length,
352 ToolUse,
353 Error,
354 Aborted,
355 Deferred,
356}
357
358#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
360#[serde(rename_all = "lowercase")]
361pub enum ThinkingLevel {
362 Off,
363 Minimal,
364 Low,
365 Medium,
366 High,
367 Xhigh,
368 Max,
369}
370
371impl ThinkingLevel {
372 pub const ALL: [ThinkingLevel; 7] = [
374 ThinkingLevel::Off,
375 ThinkingLevel::Minimal,
376 ThinkingLevel::Low,
377 ThinkingLevel::Medium,
378 ThinkingLevel::High,
379 ThinkingLevel::Xhigh,
380 ThinkingLevel::Max,
381 ];
382
383 pub fn as_str(self) -> &'static str {
385 match self {
386 ThinkingLevel::Off => "off",
387 ThinkingLevel::Minimal => "minimal",
388 ThinkingLevel::Low => "low",
389 ThinkingLevel::Medium => "medium",
390 ThinkingLevel::High => "high",
391 ThinkingLevel::Xhigh => "xhigh",
392 ThinkingLevel::Max => "max",
393 }
394 }
395
396 pub fn parse(name: &str) -> Option<ThinkingLevel> {
398 ThinkingLevel::ALL
399 .into_iter()
400 .find(|level| level.as_str() == name)
401 }
402}
403
404impl Message {
405 pub fn timestamp(&self) -> u64 {
407 match self {
408 Message::System(message) => message.timestamp,
409 Message::User(message) => message.timestamp,
410 Message::Assistant(message) => message.timestamp,
411 Message::ToolResult(message) => message.timestamp,
412 Message::BashExecution(message) => message.timestamp,
413 Message::Custom(message) => message.timestamp,
414 Message::BranchSummary(message) => message.timestamp,
415 Message::CompactionSummary(message) => message.timestamp,
416 }
417 }
418}
419
420impl Usage {
421 pub fn combine(&self, other: &Usage) -> Usage {
424 let optional = |a: Option<u64>, b: Option<u64>| {
425 (a.is_some() || b.is_some()).then(|| a.unwrap_or(0) + b.unwrap_or(0))
426 };
427 Usage {
428 input: self.input + other.input,
429 output: self.output + other.output,
430 cache_read: self.cache_read + other.cache_read,
431 cache_write: self.cache_write + other.cache_write,
432 cache_write_1h: optional(self.cache_write_1h, other.cache_write_1h),
433 reasoning: optional(self.reasoning, other.reasoning),
434 total_tokens: Some(self.total_tokens.unwrap_or(0) + other.total_tokens.unwrap_or(0)),
435 cost: Cost {
436 input: self.cost.input + other.cost.input,
437 output: self.cost.output + other.cost.output,
438 cache_read: self.cost.cache_read + other.cost.cache_read,
439 cache_write: self.cost.cache_write + other.cost.cache_write,
440 total: self.cost.total + other.cost.total,
441 },
442 }
443 }
444}
445
446impl Content {
447 pub fn text(&self, separator: &str) -> String {
449 match self {
450 Content::Text(text) => text.clone(),
451 Content::Blocks(blocks) => blocks_text(blocks, separator),
452 }
453 }
454
455 pub fn into_blocks(self) -> Vec<ContentBlock> {
457 match self {
458 Content::Text(text) => vec![ContentBlock::text(text)],
459 Content::Blocks(blocks) => blocks,
460 }
461 }
462}
463
464impl ContentBlock {
465 pub fn text(text: impl Into<String>) -> ContentBlock {
467 ContentBlock::Text(TextContent {
468 text: text.into(),
469 text_signature: None,
470 })
471 }
472
473 pub fn thinking(thinking: impl Into<String>, signature: Option<String>) -> ContentBlock {
475 ContentBlock::Thinking(ThinkingContent {
476 thinking: thinking.into(),
477 thinking_signature: signature,
478 redacted: None,
479 })
480 }
481
482 pub fn tool_call(
484 id: impl Into<String>,
485 name: impl Into<String>,
486 arguments: Map<String, Value>,
487 ) -> ContentBlock {
488 ContentBlock::ToolCall(ToolCall {
489 id: id.into(),
490 name: name.into(),
491 arguments,
492 thought_signature: None,
493 namespace: None,
494 })
495 }
496}
497
498pub fn blocks_text(blocks: &[ContentBlock], separator: &str) -> String {
500 blocks
501 .iter()
502 .filter_map(|block| match block {
503 ContentBlock::Text(text) => Some(text.text.as_str()),
504 _ => None,
505 })
506 .collect::<Vec<_>>()
507 .join(separator)
508}
509
510impl SystemMessage {
511 pub fn text(&self) -> String {
513 let mut parts = vec![self.content.text("\n")];
514 for text in self
515 .sections
516 .iter()
517 .flatten()
518 .filter_map(|(_, text)| text.clone())
519 {
520 parts.push(text);
521 }
522 parts.retain(|part| !part.is_empty());
523 parts.join("\n\n")
524 }
525
526 pub fn render_update(&self) -> String {
528 let mut parts = Vec::new();
529 let text = self.content.text("\n");
530 if !text.is_empty() {
531 parts.push(text);
532 }
533 for (name, value) in self.sections.iter().flatten() {
534 parts.push(match value {
535 None => format!("Removed system prompt section \"{name}\"."),
536 Some(value) => format!("Updated system prompt section \"{name}\":\n\n{value}"),
537 });
538 }
539 parts.join("\n\n")
540 }
541}