1#![allow(
9 missing_docs,
10 reason = "variants mirror pi's TypeScript types; contracts are noted where they differ from the name"
11)]
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::message::{ContentBlock, Message, ThinkingLevel, ToolCall, ToolResultMessage, Usage};
17use crate::session::FileEntry;
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
25#[serde(
26 tag = "type",
27 rename_all = "snake_case",
28 rename_all_fields = "camelCase"
29)]
30pub enum AssistantMessageEvent {
31 TextStart {
32 content_index: usize,
33 },
34 TextDelta {
35 content_index: usize,
36 delta: String,
37 },
38 TextEnd {
39 content_index: usize,
40 content: String,
41 },
42 ThinkingStart {
43 content_index: usize,
44 },
45 ThinkingDelta {
46 content_index: usize,
47 delta: String,
48 },
49 ThinkingEnd {
50 content_index: usize,
51 content: String,
52 },
53 ToolcallStart {
54 content_index: usize,
55 id: String,
56 tool_name: String,
57 },
58 ToolcallDelta {
59 content_index: usize,
60 delta: String,
61 },
62 ToolcallEnd {
63 content_index: usize,
64 #[serde(with = "tool_call_block")]
66 tool_call: ToolCall,
67 },
68}
69
70mod tool_result_messages {
72 use serde::{Deserialize, Deserializer, Serialize, Serializer};
73
74 use crate::message::{Message, ToolResultMessage};
75
76 pub fn serialize<S: Serializer>(
77 results: &[ToolResultMessage],
78 serializer: S,
79 ) -> Result<S::Ok, S::Error> {
80 let messages: Vec<Message> = results.iter().cloned().map(Message::ToolResult).collect();
81 messages.serialize(serializer)
82 }
83
84 pub fn deserialize<'de, D: Deserializer<'de>>(
85 deserializer: D,
86 ) -> Result<Vec<ToolResultMessage>, D::Error> {
87 Vec::<Message>::deserialize(deserializer)?
88 .into_iter()
89 .map(|message| match message {
90 Message::ToolResult(result) => Ok(result),
91 _ => Err(serde::de::Error::custom("expected a toolResult message")),
92 })
93 .collect()
94 }
95}
96
97mod tool_call_block {
99 use serde::{Deserialize, Deserializer, Serialize, Serializer};
100
101 use crate::message::{ContentBlock, ToolCall};
102
103 pub fn serialize<S: Serializer>(call: &ToolCall, serializer: S) -> Result<S::Ok, S::Error> {
104 ContentBlock::ToolCall(call.clone()).serialize(serializer)
105 }
106
107 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ToolCall, D::Error> {
108 match ContentBlock::deserialize(deserializer)? {
109 ContentBlock::ToolCall(call) => Ok(call),
110 _ => Err(serde::de::Error::custom("expected a toolCall block")),
111 }
112 }
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ToolResult {
119 pub content: Vec<ContentBlock>,
121 #[serde(
123 default,
124 deserialize_with = "crate::present",
125 skip_serializing_if = "Option::is_none"
126 )]
127 pub details: Option<Value>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub structured_content: Option<Value>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub usage: Option<Usage>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub is_error: Option<bool>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub terminate: Option<bool>,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub enum SummarySource {
143 Compaction,
145 BranchSummary,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "lowercase")]
152pub enum CompactionReason {
153 Manual,
154 Threshold,
155 Overflow,
156}
157
158#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct CompactionResult {
162 pub summary: String,
163 pub first_kept_entry_id: String,
164 pub tokens_before: u64,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub estimated_tokens_after: Option<u64>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub usage: Option<Usage>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub details: Option<Value>,
171}
172
173#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
175#[serde(
176 tag = "type",
177 rename_all = "snake_case",
178 rename_all_fields = "camelCase"
179)]
180pub enum AgentEvent {
181 AgentStart,
182 AgentEnd {
183 messages: Vec<Message>,
184 will_retry: bool,
185 },
186 AgentSettled,
188 TurnStart,
189 TurnEnd {
190 message: Message,
191 #[serde(with = "tool_result_messages")]
193 tool_results: Vec<ToolResultMessage>,
194 },
195 MessageStart {
196 message: Message,
197 },
198 MessageUpdate {
199 usage: Usage,
200 assistant_message_event: AssistantMessageEvent,
201 },
202 MessageEnd {
203 message: Message,
204 },
205 ToolExecutionStart {
206 tool_call_id: String,
207 tool_name: String,
208 args: Value,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 parent_tool_call_id: Option<String>,
211 },
212 ToolExecutionUpdate {
213 tool_call_id: String,
214 tool_name: String,
215 args: Value,
216 partial_result: ToolResult,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 parent_tool_call_id: Option<String>,
219 },
220 ToolExecutionEnd {
221 tool_call_id: String,
222 tool_name: String,
223 result: ToolResult,
224 is_error: bool,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 parent_tool_call_id: Option<String>,
227 },
228 QueueUpdate {
229 steering: Vec<String>,
230 follow_up: Vec<String>,
231 },
232 CompactionStart {
233 reason: CompactionReason,
234 },
235 CompactionEnd {
236 reason: CompactionReason,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 result: Option<CompactionResult>,
239 aborted: bool,
240 will_retry: bool,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 error_message: Option<String>,
243 },
244 EntryAppended {
245 entry: FileEntry,
246 },
247 SessionInfoChanged {
248 name: Option<String>,
249 },
250 ThinkingLevelChanged {
251 level: ThinkingLevel,
252 },
253 AutoRetryStart {
254 attempt: u32,
255 max_attempts: u32,
256 delay_ms: u64,
257 error_message: String,
258 },
259 AutoRetryEnd {
260 success: bool,
261 attempt: u32,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 final_error: Option<String>,
264 },
265 SummarizationRetryScheduled {
268 attempt: u32,
269 max_attempts: u32,
270 delay_ms: u64,
271 error_message: String,
272 },
273 SummarizationRetryAttemptStart {
275 source: SummarySource,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 reason: Option<CompactionReason>,
278 },
279 SummarizationRetryFinished,
281 BashExecutionUpdate {
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 id: Option<String>,
284 delta: String,
285 },
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::json;
292
293 #[test]
294 fn message_update_matches_pi_json_mode() {
295 let line = r#"{"type":"message_update","usage":{"input":12,"output":6,"cacheRead":0,"cacheWrite":0,"totalTokens":18,"cost":{"input":0.000036,"output":0.00009,"cacheRead":0,"cacheWrite":0,"total":0.000126},"cacheWrite1h":0},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}"#;
296 let event: AgentEvent = serde_json::from_str(line).unwrap();
297 assert_eq!(json::to_string(&event).unwrap(), line);
298 }
299
300 #[test]
301 fn toolcall_start_carries_id_and_name() {
302 let event = AssistantMessageEvent::ToolcallStart {
303 content_index: 1,
304 id: "call_1".into(),
305 tool_name: "read".into(),
306 };
307 assert_eq!(
308 json::to_string(&event).unwrap(),
309 r#"{"type":"toolcall_start","contentIndex":1,"id":"call_1","toolName":"read"}"#
310 );
311 }
312
313 #[test]
314 fn unit_variants_are_bare_types() {
315 assert_eq!(
316 json::to_string(&AgentEvent::AgentSettled).unwrap(),
317 r#"{"type":"agent_settled"}"#
318 );
319 }
320}