Skip to main content

yapi_types/
event.rs

1//! Streaming events, in the shape pi's JSON and RPC modes print them.
2//!
3//! Mirrors `AssistantMessageEvent` in `packages/ai/src/types.ts`, `AgentEvent` in
4//! `packages/agent/src/types.ts` and `AgentSessionEvent` with `toJsonEvent` in
5//! `packages/coding-agent/src/modes/json-event.ts` in pi `v1.0.0`. pi's in-process
6//! events also carry the live partial message; on the wire it is replaced by the
7//! cumulative usage, and `toolcall_start` gains the call's id and tool name.
8#![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/// One step of a streamed assistant message, as carried by `message_update`.
20///
21/// `contentIndex` addresses the message's `content`. Text and thinking blocks start
22/// empty and grow by deltas until the authoritative `*_end`; tool-call arguments
23/// stream as JSON text.
24#[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        /// The finished call, as its content block (with `type: "toolCall"`).
65        #[serde(with = "tool_call_block")]
66        tool_call: ToolCall,
67    },
68}
69
70/// Serializes tool results as tagged messages.
71mod 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
97/// Serializes a tool call as its tagged content block.
98mod 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/// What a tool returns: content for the model, details for the UI and session.
116#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ToolResult {
119    /// Text and images sent to the model.
120    pub content: Vec<ContentBlock>,
121    /// Tool-specific data, stored with the result but not sent to the model.
122    #[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    /// Ends the run after this batch when every result in it asks to.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub terminate: Option<bool>,
137}
138
139/// What a summary request summarizes.
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub enum SummarySource {
143    /// The context, for a compaction.
144    Compaction,
145    /// An abandoned branch, for tree navigation.
146    BranchSummary,
147}
148
149/// Why a compaction ran.
150#[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/// What a compaction produced.
159#[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/// An agent session event, one JSON line in JSON mode.
174#[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    /// The session is idle: no run, retry or queued work remains.
187    AgentSettled,
188    TurnStart,
189    TurnEnd {
190        message: Message,
191        /// Tool results, as messages (with `role: "toolResult"`).
192        #[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    /// A compaction or branch summary request failed and is retried after
266    /// a delay.
267    SummarizationRetryScheduled {
268        attempt: u32,
269        max_attempts: u32,
270        delay_ms: u64,
271        error_message: String,
272    },
273    /// The retried summary request starts.
274    SummarizationRetryAttemptStart {
275        source: SummarySource,
276        #[serde(default, skip_serializing_if = "Option::is_none")]
277        reason: Option<CompactionReason>,
278    },
279    /// Retrying a summary request ended.
280    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}