Skip to main content

ag_ui/
message.rs

1//! The message union and its multimodal content parts.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::JsonObject;
7use crate::ids::{MessageId, SubagentRunId, ToolCallId};
8use crate::tool::ToolCall;
9
10/// Every role the protocol defines.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
15pub enum Role {
16    /// Out-of-band instructions from the application developer.
17    Developer,
18    /// System prompt.
19    System,
20    /// Model output.
21    Assistant,
22    /// End-user input.
23    User,
24    /// The result of a tool call.
25    Tool,
26    /// A structured progress update rendered by the client.
27    Activity,
28    /// Model reasoning / chain-of-thought.
29    Reasoning,
30}
31
32impl Role {
33    /// The role string as it appears on the wire.
34    pub const fn as_str(&self) -> &'static str {
35        match self {
36            Self::Developer => "developer",
37            Self::System => "system",
38            Self::Assistant => "assistant",
39            Self::User => "user",
40            Self::Tool => "tool",
41            Self::Activity => "activity",
42            Self::Reasoning => "reasoning",
43        }
44    }
45}
46
47impl std::fmt::Display for Role {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.write_str(self.as_str())
50    }
51}
52
53/// Where the bytes of a multimodal part live.
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(tag = "type", rename_all = "lowercase")]
56#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
57#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
58pub enum InputContentSource {
59    /// Inline data, typically base64.
60    Data {
61        /// The encoded payload.
62        value: String,
63        /// MIME type of the payload, for example `image/png`.
64        #[serde(rename = "mimeType")]
65        mime_type: String,
66    },
67    /// A URL the consumer fetches itself.
68    Url {
69        /// The URL.
70        value: String,
71        /// MIME type, when the producer knows it.
72        #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
73        mime_type: Option<String>,
74    },
75}
76
77/// A plain-text content part.
78#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
79#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
80#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
81pub struct TextInputContent {
82    /// The text.
83    pub text: String,
84}
85
86/// An image, audio, video or document content part.
87///
88/// The four modalities share a shape, so they share a struct; which one it is
89/// is carried by the [`InputContent`] variant.
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
93pub struct MediaInputContent {
94    /// Where the bytes are.
95    pub source: InputContentSource,
96    /// Producer-defined extras (dimensions, page counts, alt text, …).
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub metadata: Option<Value>,
99}
100
101impl MediaInputContent {
102    /// Builds a media part from a source, with no metadata.
103    pub fn new(source: InputContentSource) -> Self {
104        Self {
105            source,
106            metadata: None,
107        }
108    }
109}
110
111/// The legacy `binary` content part.
112///
113/// Superseded by the modality-specific parts, but still accepted: at least one
114/// of `id`, `url` or `data` must be set. That constraint is a runtime rule in
115/// the upstream schema and is not encoded in this type — see
116/// [`BinaryInputContent::has_payload`].
117#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
120#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
121pub struct BinaryInputContent {
122    /// MIME type of the payload.
123    pub mime_type: String,
124    /// Reference to a previously uploaded blob.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub id: Option<String>,
127    /// URL to fetch the payload from.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub url: Option<String>,
130    /// Inline payload, typically base64.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub data: Option<String>,
133    /// Original file name, for display.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub filename: Option<String>,
136}
137
138impl BinaryInputContent {
139    /// Whether the part carries a payload in any of the three accepted forms.
140    pub fn has_payload(&self) -> bool {
141        self.id.is_some() || self.url.is_some() || self.data.is_some()
142    }
143}
144
145/// One part of a multimodal user message.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "type", rename_all = "lowercase")]
148#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
149#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
150pub enum InputContent {
151    /// Text.
152    Text(TextInputContent),
153    /// An image.
154    Image(MediaInputContent),
155    /// An audio clip.
156    Audio(MediaInputContent),
157    /// A video clip.
158    Video(MediaInputContent),
159    /// A document, for example a PDF.
160    Document(MediaInputContent),
161    /// The legacy catch-all binary part.
162    Binary(BinaryInputContent),
163}
164
165impl InputContent {
166    /// Builds a text part.
167    pub fn text(text: impl Into<String>) -> Self {
168        Self::Text(TextInputContent { text: text.into() })
169    }
170}
171
172/// The body of a user message: either a bare string or an ordered list of
173/// multimodal parts.
174#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
175#[serde(untagged)]
176#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
177#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
178pub enum UserContent {
179    /// Plain text.
180    Text(String),
181    /// Multimodal parts.
182    Parts(Vec<InputContent>),
183}
184
185impl UserContent {
186    /// The text, when the content is a plain string.
187    ///
188    /// `None` for multimodal content, even when every part of it happens to be
189    /// text: this borrows, and joining parts cannot. Reach for
190    /// [`UserContent::to_text`] when any text will do.
191    pub fn as_text(&self) -> Option<&str> {
192        match self {
193            Self::Text(text) => Some(text),
194            Self::Parts(_) => None,
195        }
196    }
197
198    /// Every text part, in order, joined with newlines.
199    ///
200    /// Non-text parts are dropped rather than described: an agent that does not
201    /// handle images wants the caption, not a placeholder it has to strip.
202    ///
203    /// ```
204    /// use ag_ui::{InputContent, InputContentSource, MediaInputContent, UserContent};
205    ///
206    /// let plain = UserContent::from("what is the weather?");
207    /// assert_eq!(plain.as_text(), Some("what is the weather?"));
208    /// assert_eq!(plain.to_text(), "what is the weather?");
209    ///
210    /// let image = MediaInputContent::new(InputContentSource::Url {
211    ///     value: "https://example.com/cat.png".into(),
212    ///     mime_type: None,
213    /// });
214    /// let mixed = UserContent::from(vec![
215    ///     InputContent::text("what is this?"),
216    ///     InputContent::Image(image),
217    ///     InputContent::text("be brief"),
218    /// ]);
219    /// assert_eq!(mixed.as_text(), None);
220    /// assert_eq!(mixed.to_text(), "what is this?\nbe brief");
221    /// ```
222    pub fn to_text(&self) -> String {
223        match self {
224            Self::Text(text) => text.clone(),
225            Self::Parts(parts) => parts
226                .iter()
227                .filter_map(|part| match part {
228                    InputContent::Text(part) => Some(part.text.as_str()),
229                    _ => None,
230                })
231                .collect::<Vec<_>>()
232                .join("\n"),
233        }
234    }
235}
236
237impl Default for UserContent {
238    fn default() -> Self {
239        Self::Text(String::new())
240    }
241}
242
243impl From<String> for UserContent {
244    fn from(value: String) -> Self {
245        Self::Text(value)
246    }
247}
248
249impl From<&str> for UserContent {
250    fn from(value: &str) -> Self {
251        Self::Text(value.to_owned())
252    }
253}
254
255impl From<Vec<InputContent>> for UserContent {
256    fn from(value: Vec<InputContent>) -> Self {
257        Self::Parts(value)
258    }
259}
260
261/// Out-of-band instructions from the application developer.
262#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
265#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
266pub struct DeveloperMessage {
267    /// Message id.
268    pub id: MessageId,
269    /// The instructions.
270    pub content: String,
271    /// Optional display name for the author.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub name: Option<String>,
274    /// Opaque provider payload for zero-data-retention modes.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub encrypted_value: Option<String>,
277    /// The subagent that produced this message; absent means the parent
278    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
279    #[serde(
280        default,
281        deserialize_with = "crate::serde_util::reject_null",
282        skip_serializing_if = "Option::is_none"
283    )]
284    pub subagent_run_id: Option<SubagentRunId>,
285    /// Extra information, open by key. Absent or an object — a JSON `null`
286    /// is rejected. See [`crate::metadata`].
287    #[serde(
288        default,
289        deserialize_with = "crate::serde_util::reject_null",
290        skip_serializing_if = "Option::is_none"
291    )]
292    #[cfg_attr(
293        feature = "schemars",
294        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
295    )]
296    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
297    pub metadata: Option<JsonObject>,
298}
299
300/// The system prompt.
301#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
304#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
305pub struct SystemMessage {
306    /// Message id.
307    pub id: MessageId,
308    /// The prompt.
309    pub content: String,
310    /// Optional display name for the author.
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub name: Option<String>,
313    /// Opaque provider payload for zero-data-retention modes.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub encrypted_value: Option<String>,
316    /// The subagent that produced this message; absent means the parent
317    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
318    #[serde(
319        default,
320        deserialize_with = "crate::serde_util::reject_null",
321        skip_serializing_if = "Option::is_none"
322    )]
323    pub subagent_run_id: Option<SubagentRunId>,
324    /// Extra information, open by key. Absent or an object — a JSON `null`
325    /// is rejected. See [`crate::metadata`].
326    #[serde(
327        default,
328        deserialize_with = "crate::serde_util::reject_null",
329        skip_serializing_if = "Option::is_none"
330    )]
331    #[cfg_attr(
332        feature = "schemars",
333        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
334    )]
335    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
336    pub metadata: Option<JsonObject>,
337}
338
339/// Model output, optionally requesting tool calls.
340#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
343#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
344pub struct AssistantMessage {
345    /// Message id.
346    pub id: MessageId,
347    /// The reply text. Absent when the turn is tool calls only.
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub content: Option<String>,
350    /// Optional display name for the author.
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub name: Option<String>,
353    /// Opaque provider payload for zero-data-retention modes.
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub encrypted_value: Option<String>,
356    /// Tool calls the assistant wants executed.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub tool_calls: Option<Vec<ToolCall>>,
359    /// The subagent that produced this message; absent means the parent
360    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
361    #[serde(
362        default,
363        deserialize_with = "crate::serde_util::reject_null",
364        skip_serializing_if = "Option::is_none"
365    )]
366    pub subagent_run_id: Option<SubagentRunId>,
367    /// Extra information, open by key. Absent or an object — a JSON `null`
368    /// is rejected. See [`crate::metadata`].
369    #[serde(
370        default,
371        deserialize_with = "crate::serde_util::reject_null",
372        skip_serializing_if = "Option::is_none"
373    )]
374    #[cfg_attr(
375        feature = "schemars",
376        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
377    )]
378    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
379    pub metadata: Option<JsonObject>,
380}
381
382/// End-user input, possibly multimodal.
383#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
386#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
387pub struct UserMessage {
388    /// Message id.
389    pub id: MessageId,
390    /// Text or multimodal parts.
391    pub content: UserContent,
392    /// Optional display name for the author.
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub name: Option<String>,
395    /// Opaque provider payload for zero-data-retention modes.
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub encrypted_value: Option<String>,
398    /// The subagent that produced this message; absent means the parent
399    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
400    #[serde(
401        default,
402        deserialize_with = "crate::serde_util::reject_null",
403        skip_serializing_if = "Option::is_none"
404    )]
405    pub subagent_run_id: Option<SubagentRunId>,
406    /// Extra information, open by key. Absent or an object — a JSON `null`
407    /// is rejected. See [`crate::metadata`].
408    #[serde(
409        default,
410        deserialize_with = "crate::serde_util::reject_null",
411        skip_serializing_if = "Option::is_none"
412    )]
413    #[cfg_attr(
414        feature = "schemars",
415        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
416    )]
417    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
418    pub metadata: Option<JsonObject>,
419}
420
421/// The result of a tool call, fed back to the model.
422#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
425#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
426pub struct ToolMessage {
427    /// Message id.
428    pub id: MessageId,
429    /// The result, already rendered to a string.
430    pub content: String,
431    /// The call this result answers.
432    pub tool_call_id: ToolCallId,
433    /// Set when the tool failed; `content` then holds whatever partial output
434    /// there was.
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub error: Option<String>,
437    /// Opaque provider payload for zero-data-retention modes.
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub encrypted_value: Option<String>,
440    /// The subagent that *executed* the call; absent means the parent agent.
441    /// Attributed independently of the call it answers — see
442    /// [`ToolCallResultEvent`](crate::event::ToolCallResultEvent). A JSON
443    /// `null` is rejected.
444    #[serde(
445        default,
446        deserialize_with = "crate::serde_util::reject_null",
447        skip_serializing_if = "Option::is_none"
448    )]
449    pub subagent_run_id: Option<SubagentRunId>,
450    /// Extra information, open by key. Absent or an object — a JSON `null`
451    /// is rejected. See [`crate::metadata`].
452    #[serde(
453        default,
454        deserialize_with = "crate::serde_util::reject_null",
455        skip_serializing_if = "Option::is_none"
456    )]
457    #[cfg_attr(
458        feature = "schemars",
459        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
460    )]
461    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
462    pub metadata: Option<JsonObject>,
463}
464
465/// A structured progress update the client renders as it likes.
466#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
469#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
470pub struct ActivityMessage {
471    /// Message id.
472    pub id: MessageId,
473    /// Client-defined activity discriminator, for example `"web_search"`.
474    pub activity_type: String,
475    /// The activity payload.
476    #[cfg_attr(
477        feature = "schemars",
478        schemars(with = "std::collections::BTreeMap<String, serde_json::Value>")
479    )]
480    #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
481    pub content: JsonObject,
482    /// The subagent that produced this message; absent means the parent
483    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
484    #[serde(
485        default,
486        deserialize_with = "crate::serde_util::reject_null",
487        skip_serializing_if = "Option::is_none"
488    )]
489    pub subagent_run_id: Option<SubagentRunId>,
490    /// Extra information, open by key. Absent or an object — a JSON `null`
491    /// is rejected. See [`crate::metadata`].
492    #[serde(
493        default,
494        deserialize_with = "crate::serde_util::reject_null",
495        skip_serializing_if = "Option::is_none"
496    )]
497    #[cfg_attr(
498        feature = "schemars",
499        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
500    )]
501    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
502    pub metadata: Option<JsonObject>,
503}
504
505/// Model reasoning, shown separately from the reply.
506#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
507#[serde(rename_all = "camelCase")]
508#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
509#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
510pub struct ReasoningMessage {
511    /// Message id.
512    pub id: MessageId,
513    /// The reasoning text. Empty when the provider only returns an encrypted
514    /// blob.
515    pub content: String,
516    /// Opaque provider payload for zero-data-retention modes.
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub encrypted_value: Option<String>,
519    /// The subagent that produced this message; absent means the parent
520    /// agent. A JSON `null` is rejected — see [`crate::event::subagent`].
521    #[serde(
522        default,
523        deserialize_with = "crate::serde_util::reject_null",
524        skip_serializing_if = "Option::is_none"
525    )]
526    pub subagent_run_id: Option<SubagentRunId>,
527    /// Extra information, open by key. Absent or an object — a JSON `null`
528    /// is rejected. See [`crate::metadata`].
529    #[serde(
530        default,
531        deserialize_with = "crate::serde_util::reject_null",
532        skip_serializing_if = "Option::is_none"
533    )]
534    #[cfg_attr(
535        feature = "schemars",
536        schemars(with = "Option<std::collections::BTreeMap<String, serde_json::Value>>")
537    )]
538    #[cfg_attr(feature = "utoipa", schema(value_type = Option<Object>))]
539    pub metadata: Option<JsonObject>,
540}
541
542/// A message in a thread, discriminated by its `role`.
543#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
544#[serde(tag = "role", rename_all = "lowercase")]
545#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
546#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
547pub enum Message {
548    /// See [`DeveloperMessage`].
549    Developer(DeveloperMessage),
550    /// See [`SystemMessage`].
551    System(SystemMessage),
552    /// See [`AssistantMessage`].
553    Assistant(AssistantMessage),
554    /// See [`UserMessage`].
555    User(UserMessage),
556    /// See [`ToolMessage`].
557    Tool(ToolMessage),
558    /// See [`ActivityMessage`].
559    Activity(ActivityMessage),
560    /// See [`ReasoningMessage`].
561    Reasoning(ReasoningMessage),
562}
563
564impl Message {
565    /// Builds a user message carrying plain text.
566    pub fn user(id: impl Into<MessageId>, content: impl Into<UserContent>) -> Self {
567        Self::User(UserMessage {
568            id: id.into(),
569            content: content.into(),
570            ..Default::default()
571        })
572    }
573
574    /// Builds an assistant message carrying plain text.
575    pub fn assistant(id: impl Into<MessageId>, content: impl Into<String>) -> Self {
576        Self::Assistant(AssistantMessage {
577            id: id.into(),
578            content: Some(content.into()),
579            ..Default::default()
580        })
581    }
582
583    /// Builds a system message.
584    pub fn system(id: impl Into<MessageId>, content: impl Into<String>) -> Self {
585        Self::System(SystemMessage {
586            id: id.into(),
587            content: content.into(),
588            ..Default::default()
589        })
590    }
591
592    /// Builds a developer message.
593    pub fn developer(id: impl Into<MessageId>, content: impl Into<String>) -> Self {
594        Self::Developer(DeveloperMessage {
595            id: id.into(),
596            content: content.into(),
597            ..Default::default()
598        })
599    }
600
601    /// Builds a tool result message.
602    pub fn tool(
603        id: impl Into<MessageId>,
604        tool_call_id: impl Into<ToolCallId>,
605        content: impl Into<String>,
606    ) -> Self {
607        Self::Tool(ToolMessage {
608            id: id.into(),
609            content: content.into(),
610            tool_call_id: tool_call_id.into(),
611            ..Default::default()
612        })
613    }
614
615    /// The message id, whatever the role.
616    pub const fn id(&self) -> &MessageId {
617        match self {
618            Self::Developer(m) => &m.id,
619            Self::System(m) => &m.id,
620            Self::Assistant(m) => &m.id,
621            Self::User(m) => &m.id,
622            Self::Tool(m) => &m.id,
623            Self::Activity(m) => &m.id,
624            Self::Reasoning(m) => &m.id,
625        }
626    }
627
628    /// The role, whatever the variant.
629    pub const fn role(&self) -> Role {
630        match self {
631            Self::Developer(_) => Role::Developer,
632            Self::System(_) => Role::System,
633            Self::Assistant(_) => Role::Assistant,
634            Self::User(_) => Role::User,
635            Self::Tool(_) => Role::Tool,
636            Self::Activity(_) => Role::Activity,
637            Self::Reasoning(_) => Role::Reasoning,
638        }
639    }
640
641    /// The subagent that produced the message, whatever the role. `None`
642    /// means the parent agent. See [`crate::event::subagent`].
643    pub const fn subagent_run_id(&self) -> Option<&SubagentRunId> {
644        match self {
645            Self::Developer(m) => m.subagent_run_id.as_ref(),
646            Self::System(m) => m.subagent_run_id.as_ref(),
647            Self::Assistant(m) => m.subagent_run_id.as_ref(),
648            Self::User(m) => m.subagent_run_id.as_ref(),
649            Self::Tool(m) => m.subagent_run_id.as_ref(),
650            Self::Activity(m) => m.subagent_run_id.as_ref(),
651            Self::Reasoning(m) => m.subagent_run_id.as_ref(),
652        }
653    }
654
655    /// Sets or clears the subagent the message is attributed to.
656    pub fn set_subagent_run_id(&mut self, subagent_run_id: Option<SubagentRunId>) {
657        match self {
658            Self::Developer(m) => m.subagent_run_id = subagent_run_id,
659            Self::System(m) => m.subagent_run_id = subagent_run_id,
660            Self::Assistant(m) => m.subagent_run_id = subagent_run_id,
661            Self::User(m) => m.subagent_run_id = subagent_run_id,
662            Self::Tool(m) => m.subagent_run_id = subagent_run_id,
663            Self::Activity(m) => m.subagent_run_id = subagent_run_id,
664            Self::Reasoning(m) => m.subagent_run_id = subagent_run_id,
665        }
666    }
667
668    /// The message's metadata, whatever the role. See [`crate::metadata`].
669    pub const fn metadata(&self) -> Option<&JsonObject> {
670        match self {
671            Self::Developer(m) => m.metadata.as_ref(),
672            Self::System(m) => m.metadata.as_ref(),
673            Self::Assistant(m) => m.metadata.as_ref(),
674            Self::User(m) => m.metadata.as_ref(),
675            Self::Tool(m) => m.metadata.as_ref(),
676            Self::Activity(m) => m.metadata.as_ref(),
677            Self::Reasoning(m) => m.metadata.as_ref(),
678        }
679    }
680
681    /// The metadata slot, for a consumer merging an event's metadata into
682    /// the message it builds — see
683    /// [`merge_metadata_into`](crate::metadata::merge_metadata_into).
684    pub fn metadata_mut(&mut self) -> &mut Option<JsonObject> {
685        match self {
686            Self::Developer(m) => &mut m.metadata,
687            Self::System(m) => &mut m.metadata,
688            Self::Assistant(m) => &mut m.metadata,
689            Self::User(m) => &mut m.metadata,
690            Self::Tool(m) => &mut m.metadata,
691            Self::Activity(m) => &mut m.metadata,
692            Self::Reasoning(m) => &mut m.metadata,
693        }
694    }
695}
696
697macro_rules! message_from {
698    ($($ty:ident => $variant:ident),* $(,)?) => {
699        $(
700            impl From<$ty> for Message {
701                fn from(value: $ty) -> Self {
702                    Self::$variant(value)
703                }
704            }
705        )*
706    };
707}
708
709message_from! {
710    DeveloperMessage => Developer,
711    SystemMessage => System,
712    AssistantMessage => Assistant,
713    UserMessage => User,
714    ToolMessage => Tool,
715    ActivityMessage => Activity,
716    ReasoningMessage => Reasoning,
717}