Skip to main content

ag_ui/event/
reasoning.rs

1//! Model reasoning: `REASONING_*`, plus the deprecated `THINKING_*` events
2//! they replaced.
3
4use serde::{Deserialize, Serialize};
5
6use crate::event::BaseEvent;
7use crate::ids::{MessageId, SubagentRunId};
8
9/// Opens a reasoning block. Reasoning messages inside it are bracketed by
10/// `REASONING_MESSAGE_START` / `REASONING_MESSAGE_END`, and the block closes
11/// with `REASONING_END`.
12#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
16pub struct ReasoningStartEvent {
17    /// Timestamp and raw provider event.
18    #[serde(flatten)]
19    pub base: BaseEvent,
20    /// The message this reasoning belongs to.
21    pub message_id: MessageId,
22    /// The subagent that produced this event; absent means the parent agent.
23    /// A JSON `null` is rejected — see [`crate::event::subagent`].
24    #[serde(
25        default,
26        deserialize_with = "crate::serde_util::reject_null",
27        skip_serializing_if = "Option::is_none"
28    )]
29    pub subagent_run_id: Option<SubagentRunId>,
30}
31
32impl ReasoningStartEvent {
33    /// Opens a reasoning block for `message_id`.
34    pub fn new(message_id: impl Into<MessageId>) -> Self {
35        Self {
36            base: BaseEvent::default(),
37            message_id: message_id.into(),
38            subagent_run_id: None,
39        }
40    }
41}
42
43/// The single role a reasoning message may declare.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
46#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
47pub enum ReasoningRole {
48    /// `"reasoning"`.
49    #[default]
50    #[serde(rename = "reasoning")]
51    Reasoning,
52}
53
54/// Opens a reasoning message.
55#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
59pub struct ReasoningMessageStartEvent {
60    /// Timestamp and raw provider event.
61    #[serde(flatten)]
62    pub base: BaseEvent,
63    /// Id of the reasoning message being opened.
64    pub message_id: MessageId,
65    /// Always [`ReasoningRole::Reasoning`]. Required, unlike the optional role
66    /// on `TEXT_MESSAGE_START`.
67    pub role: ReasoningRole,
68    /// The subagent that produced this event; absent means the parent agent.
69    /// A JSON `null` is rejected — see [`crate::event::subagent`].
70    #[serde(
71        default,
72        deserialize_with = "crate::serde_util::reject_null",
73        skip_serializing_if = "Option::is_none"
74    )]
75    pub subagent_run_id: Option<SubagentRunId>,
76}
77
78impl ReasoningMessageStartEvent {
79    /// Opens a reasoning message.
80    pub fn new(message_id: impl Into<MessageId>) -> Self {
81        Self {
82            base: BaseEvent::default(),
83            message_id: message_id.into(),
84            role: ReasoningRole::Reasoning,
85            subagent_run_id: None,
86        }
87    }
88}
89
90/// Appends a chunk of reasoning text.
91#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
94#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
95pub struct ReasoningMessageContentEvent {
96    /// Timestamp and raw provider event.
97    #[serde(flatten)]
98    pub base: BaseEvent,
99    /// The reasoning message being appended to.
100    pub message_id: MessageId,
101    /// The text to append.
102    pub delta: String,
103    /// The subagent that produced this event; absent means the parent agent.
104    /// A JSON `null` is rejected — see [`crate::event::subagent`].
105    #[serde(
106        default,
107        deserialize_with = "crate::serde_util::reject_null",
108        skip_serializing_if = "Option::is_none"
109    )]
110    pub subagent_run_id: Option<SubagentRunId>,
111}
112
113impl ReasoningMessageContentEvent {
114    /// Appends `delta` to the reasoning message.
115    pub fn new(message_id: impl Into<MessageId>, delta: impl Into<String>) -> Self {
116        Self {
117            base: BaseEvent::default(),
118            message_id: message_id.into(),
119            delta: delta.into(),
120            subagent_run_id: None,
121        }
122    }
123}
124
125/// Closes a reasoning message.
126#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
129#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
130pub struct ReasoningMessageEndEvent {
131    /// Timestamp and raw provider event.
132    #[serde(flatten)]
133    pub base: BaseEvent,
134    /// The reasoning message being closed.
135    pub message_id: MessageId,
136    /// The subagent that produced this event; absent means the parent agent.
137    /// A JSON `null` is rejected — see [`crate::event::subagent`].
138    #[serde(
139        default,
140        deserialize_with = "crate::serde_util::reject_null",
141        skip_serializing_if = "Option::is_none"
142    )]
143    pub subagent_run_id: Option<SubagentRunId>,
144}
145
146impl ReasoningMessageEndEvent {
147    /// Closes the reasoning message.
148    pub fn new(message_id: impl Into<MessageId>) -> Self {
149        Self {
150            base: BaseEvent::default(),
151            message_id: message_id.into(),
152            subagent_run_id: None,
153        }
154    }
155}
156
157/// A self-contained reasoning update: start, content and end folded into one.
158#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
161#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
162pub struct ReasoningMessageChunkEvent {
163    /// Timestamp and raw provider event.
164    #[serde(flatten)]
165    pub base: BaseEvent,
166    /// The reasoning message this chunk belongs to.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub message_id: Option<MessageId>,
169    /// The text to append.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub delta: Option<String>,
172    /// The subagent that produced this event; absent means the parent agent.
173    /// A JSON `null` is rejected — see [`crate::event::subagent`]. Under
174    /// concurrency a chunk that omits its `message_id` is resolved within the
175    /// sending subagent's own stream, so attribute every chunk when several
176    /// subagents stream at once.
177    #[serde(
178        default,
179        deserialize_with = "crate::serde_util::reject_null",
180        skip_serializing_if = "Option::is_none"
181    )]
182    pub subagent_run_id: Option<SubagentRunId>,
183}
184
185impl ReasoningMessageChunkEvent {
186    /// Builds a reasoning chunk.
187    pub fn new(message_id: Option<MessageId>, delta: Option<String>) -> Self {
188        Self {
189            base: BaseEvent::default(),
190            message_id,
191            delta,
192            subagent_run_id: None,
193        }
194    }
195}
196
197/// Closes a reasoning block.
198#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
201#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
202pub struct ReasoningEndEvent {
203    /// Timestamp and raw provider event.
204    #[serde(flatten)]
205    pub base: BaseEvent,
206    /// The message whose reasoning block is closing.
207    pub message_id: MessageId,
208    /// The subagent that produced this event; absent means the parent agent.
209    /// A JSON `null` is rejected — see [`crate::event::subagent`].
210    #[serde(
211        default,
212        deserialize_with = "crate::serde_util::reject_null",
213        skip_serializing_if = "Option::is_none"
214    )]
215    pub subagent_run_id: Option<SubagentRunId>,
216}
217
218impl ReasoningEndEvent {
219    /// Closes the reasoning block for `message_id`.
220    pub fn new(message_id: impl Into<MessageId>) -> Self {
221        Self {
222            base: BaseEvent::default(),
223            message_id: message_id.into(),
224            subagent_run_id: None,
225        }
226    }
227}
228
229/// What an encrypted reasoning signature belongs to.
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
231#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
232#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
233pub enum ReasoningEncryptedValueSubtype {
234    /// The signature covers a tool call.
235    #[serde(rename = "tool-call")]
236    ToolCall,
237    /// The signature covers a message.
238    #[default]
239    #[serde(rename = "message")]
240    Message,
241}
242
243impl ReasoningEncryptedValueSubtype {
244    /// The subtype string as it appears on the wire.
245    pub const fn as_str(&self) -> &'static str {
246        match self {
247            Self::ToolCall => "tool-call",
248            Self::Message => "message",
249        }
250    }
251}
252
253/// Carries a provider's opaque reasoning signature.
254///
255/// Under zero-data-retention the provider returns no readable reasoning, only a
256/// blob that must be replayed on the next request for the model to continue
257/// coherently.
258#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
261#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
262pub struct ReasoningEncryptedValueEvent {
263    /// Timestamp and raw provider event.
264    #[serde(flatten)]
265    pub base: BaseEvent,
266    /// Whether `entity_id` names a tool call or a message.
267    pub subtype: ReasoningEncryptedValueSubtype,
268    /// The tool call or message the blob belongs to.
269    pub entity_id: String,
270    /// The opaque blob.
271    pub encrypted_value: String,
272    /// The subagent that produced this event; absent means the parent agent.
273    /// A JSON `null` is rejected — see [`crate::event::subagent`].
274    #[serde(
275        default,
276        deserialize_with = "crate::serde_util::reject_null",
277        skip_serializing_if = "Option::is_none"
278    )]
279    pub subagent_run_id: Option<SubagentRunId>,
280}
281
282impl ReasoningEncryptedValueEvent {
283    /// Attaches an encrypted reasoning blob to an entity.
284    pub fn new(
285        subtype: ReasoningEncryptedValueSubtype,
286        entity_id: impl Into<String>,
287        encrypted_value: impl Into<String>,
288    ) -> Self {
289        Self {
290            base: BaseEvent::default(),
291            subtype,
292            entity_id: entity_id.into(),
293            encrypted_value: encrypted_value.into(),
294            subagent_run_id: None,
295        }
296    }
297}
298
299// The five THINKING_* payloads below are deprecated, but `#[deprecated]` is
300// suppressed when the `utoipa` feature is on: utoipa 5.5's derive emits a
301// `.deprecated()` call on the `AllOf` builder it uses for `#[serde(flatten)]`
302// structs and tagged-enum variants, and that builder has no such method, so the
303// crate would not compile. The deprecation stays unconditional on the
304// `Event::thinking_*` constructors, which utoipa never sees.
305//
306// None of them carries `subagentRunId`: the family predates subagents and is
307// excluded from the attribution table upstream, so accepting the field here
308// would let TypeScript-shaped payloads through that Python and .NET reject.
309
310/// Opens a thinking block.
311#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
314#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
315#[cfg_attr(
316    not(feature = "utoipa"),
317    deprecated(note = "use the REASONING_* events: ReasoningStartEvent")
318)]
319pub struct ThinkingStartEvent {
320    /// Timestamp and raw provider event.
321    #[serde(flatten)]
322    pub base: BaseEvent,
323    /// Heading to show above the block.
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub title: Option<String>,
326}
327
328impl ThinkingStartEvent {
329    /// Opens a thinking block with an optional title.
330    pub fn new(title: Option<String>) -> Self {
331        Self {
332            base: BaseEvent::default(),
333            title,
334        }
335    }
336}
337
338/// Closes a thinking block.
339#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
342#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
343#[cfg_attr(
344    not(feature = "utoipa"),
345    deprecated(note = "use the REASONING_* events: ReasoningEndEvent")
346)]
347pub struct ThinkingEndEvent {
348    /// Timestamp and raw provider event.
349    #[serde(flatten)]
350    pub base: BaseEvent,
351}
352
353/// Opens a thinking text message.
354#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
357#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
358#[cfg_attr(
359    not(feature = "utoipa"),
360    deprecated(note = "use the REASONING_* events: ReasoningMessageStartEvent")
361)]
362pub struct ThinkingTextMessageStartEvent {
363    /// Timestamp and raw provider event.
364    #[serde(flatten)]
365    pub base: BaseEvent,
366}
367
368/// Appends a chunk of thinking text.
369///
370/// Unlike its replacement it carries no message id — a thinking block could
371/// only ever have one message in flight, which is why the event was replaced.
372#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
375#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
376#[cfg_attr(
377    not(feature = "utoipa"),
378    deprecated(note = "use the REASONING_* events: ReasoningMessageContentEvent")
379)]
380pub struct ThinkingTextMessageContentEvent {
381    /// Timestamp and raw provider event.
382    #[serde(flatten)]
383    pub base: BaseEvent,
384    /// The text to append.
385    pub delta: String,
386}
387
388impl ThinkingTextMessageContentEvent {
389    /// Appends `delta` to the open thinking message.
390    pub fn new(delta: impl Into<String>) -> Self {
391        Self {
392            base: BaseEvent::default(),
393            delta: delta.into(),
394        }
395    }
396}
397
398/// Closes a thinking text message.
399#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
402#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
403#[cfg_attr(
404    not(feature = "utoipa"),
405    deprecated(note = "use the REASONING_* events: ReasoningMessageEndEvent")
406)]
407pub struct ThinkingTextMessageEndEvent {
408    /// Timestamp and raw provider event.
409    #[serde(flatten)]
410    pub base: BaseEvent,
411}