Skip to main content

ag_ui/event/
activity.rs

1//! Structured progress updates: `ACTIVITY_SNAPSHOT`, `ACTIVITY_DELTA`.
2//!
3//! Activities are how an agent reports what it is *doing* — searching, reading
4//! files, waiting on an API — in a shape the client renders itself, rather than
5//! as prose in the reply.
6
7use serde::{Deserialize, Serialize};
8
9use crate::JsonObject;
10use crate::event::BaseEvent;
11use crate::ids::{MessageId, SubagentRunId};
12use crate::patch::PatchOperation;
13
14/// Publishes the full content of an activity.
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
18#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
19pub struct ActivitySnapshotEvent {
20    /// Timestamp and raw provider event.
21    #[serde(flatten)]
22    pub base: BaseEvent,
23    /// The activity message being written.
24    pub message_id: MessageId,
25    /// Client-defined activity discriminator, for example `"web_search"`.
26    pub activity_type: String,
27    /// The activity payload.
28    #[cfg_attr(
29        feature = "schemars",
30        schemars(with = "std::collections::BTreeMap<String, serde_json::Value>")
31    )]
32    #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
33    pub content: JsonObject,
34    /// Whether this replaces the existing content (the default) or merges into
35    /// it.
36    #[serde(default = "default_replace")]
37    pub replace: bool,
38    /// The subagent that produced this event; absent means the parent agent.
39    /// A JSON `null` is rejected — see [`crate::event::subagent`].
40    #[serde(
41        default,
42        deserialize_with = "crate::serde_util::reject_null",
43        skip_serializing_if = "Option::is_none"
44    )]
45    pub subagent_run_id: Option<SubagentRunId>,
46}
47
48/// The upstream schema defaults `replace` to `true`, so an omitted field means
49/// "replace" rather than "merge".
50const fn default_replace() -> bool {
51    true
52}
53
54impl Default for ActivitySnapshotEvent {
55    fn default() -> Self {
56        Self {
57            base: BaseEvent::default(),
58            message_id: MessageId::default(),
59            activity_type: String::new(),
60            content: JsonObject::new(),
61            replace: default_replace(),
62            subagent_run_id: None,
63        }
64    }
65}
66
67impl ActivitySnapshotEvent {
68    /// Publishes an activity payload, replacing any previous content.
69    pub fn new(
70        message_id: impl Into<MessageId>,
71        activity_type: impl Into<String>,
72        content: JsonObject,
73    ) -> Self {
74        Self {
75            base: BaseEvent::default(),
76            message_id: message_id.into(),
77            activity_type: activity_type.into(),
78            content,
79            replace: true,
80            subagent_run_id: None,
81        }
82    }
83}
84
85/// Mutates an activity's content with a JSON Patch document.
86#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
89#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
90pub struct ActivityDeltaEvent {
91    /// Timestamp and raw provider event.
92    #[serde(flatten)]
93    pub base: BaseEvent,
94    /// The activity message being patched.
95    pub message_id: MessageId,
96    /// Client-defined activity discriminator.
97    pub activity_type: String,
98    /// RFC 6902 operations, applied in order to the activity content.
99    pub patch: Vec<PatchOperation>,
100    /// The subagent that produced this event; absent means the parent agent.
101    /// A JSON `null` is rejected — see [`crate::event::subagent`].
102    #[serde(
103        default,
104        deserialize_with = "crate::serde_util::reject_null",
105        skip_serializing_if = "Option::is_none"
106    )]
107    pub subagent_run_id: Option<SubagentRunId>,
108}
109
110impl ActivityDeltaEvent {
111    /// Patches an activity's content.
112    pub fn new(
113        message_id: impl Into<MessageId>,
114        activity_type: impl Into<String>,
115        patch: impl Into<Vec<PatchOperation>>,
116    ) -> Self {
117        Self {
118            base: BaseEvent::default(),
119            message_id: message_id.into(),
120            activity_type: activity_type.into(),
121            patch: patch.into(),
122            subagent_run_id: None,
123        }
124    }
125}