Skip to main content

ag_ui/event/
lifecycle.rs

1//! Run and step lifecycle: `RUN_*` and `STEP_*`.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::event::BaseEvent;
7use crate::ids::{RunId, StepName, SubagentRunId, ThreadId};
8use crate::input::RunAgentInput;
9use crate::outcome::RunOutcome;
10use crate::token_usage::TokenUsage;
11
12/// The first event of every run.
13#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
17pub struct RunStartedEvent {
18    /// Timestamp and raw provider event.
19    #[serde(flatten)]
20    pub base: BaseEvent,
21    /// The conversation this run belongs to.
22    pub thread_id: ThreadId,
23    /// The run that is starting.
24    pub run_id: RunId,
25    /// The run that spawned this one, for nested / delegated agents.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub parent_run_id: Option<RunId>,
28    /// The request that started the run, echoed so a recorded stream replays
29    /// without the original HTTP body.
30    ///
31    /// Boxed: it is the largest payload in the protocol and is usually absent,
32    /// so inlining it would bloat every [`Event`](crate::event::Event).
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub input: Option<Box<RunAgentInput>>,
35}
36
37impl RunStartedEvent {
38    /// Starts a run.
39    pub fn new(thread_id: impl Into<ThreadId>, run_id: impl Into<RunId>) -> Self {
40        Self {
41            base: BaseEvent::default(),
42            thread_id: thread_id.into(),
43            run_id: run_id.into(),
44            parent_run_id: None,
45            input: None,
46        }
47    }
48}
49
50/// The last event of a run that did not error.
51///
52/// "Finished" includes *paused*: an [`RunOutcome::Interrupt`] outcome means the
53/// agent is waiting on human input and the run continues on the next request.
54#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
57#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
58pub struct RunFinishedEvent {
59    /// Timestamp and raw provider event.
60    #[serde(flatten)]
61    pub base: BaseEvent,
62    /// The conversation this run belongs to.
63    pub thread_id: ThreadId,
64    /// The run that finished.
65    pub run_id: RunId,
66    /// Agent-defined return value.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub result: Option<Value>,
69    /// How the run ended. Absent from producers that predate the interrupt
70    /// protocol, which consumers read as success. A JSON `null` also reads as
71    /// absent, for producers that serialize `None` rather than omitting it.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub outcome: Option<RunOutcome>,
74    /// Token usage, one entry per `(provider, model)` the run invoked.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub usage: Option<Vec<TokenUsage>>,
77}
78
79impl RunFinishedEvent {
80    /// Finishes a run without declaring an outcome (legacy shape).
81    pub fn new(thread_id: impl Into<ThreadId>, run_id: impl Into<RunId>) -> Self {
82        Self {
83            base: BaseEvent::default(),
84            thread_id: thread_id.into(),
85            run_id: run_id.into(),
86            result: None,
87            outcome: None,
88            usage: None,
89        }
90    }
91
92    /// Sets the outcome.
93    #[must_use]
94    pub fn with_outcome(mut self, outcome: RunOutcome) -> Self {
95        self.outcome = Some(outcome);
96        self
97    }
98
99    /// Sets the return value.
100    #[must_use]
101    pub fn with_result(mut self, result: impl Into<Value>) -> Self {
102        self.result = Some(result.into());
103        self
104    }
105
106    /// Sets the token usage.
107    #[must_use]
108    pub fn with_usage(mut self, usage: impl Into<Vec<TokenUsage>>) -> Self {
109        self.usage = Some(usage.into());
110        self
111    }
112}
113
114/// The run failed. No further events follow.
115#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
118#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
119pub struct RunErrorEvent {
120    /// Timestamp and raw provider event.
121    #[serde(flatten)]
122    pub base: BaseEvent,
123    /// What went wrong, for a human.
124    pub message: String,
125    /// Machine-readable error code.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub code: Option<String>,
128    /// Partial usage for a run that failed after some model calls completed.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub usage: Option<Vec<TokenUsage>>,
131}
132
133impl RunErrorEvent {
134    /// Fails the run with a message.
135    pub fn new(message: impl Into<String>) -> Self {
136        Self {
137            base: BaseEvent::default(),
138            message: message.into(),
139            code: None,
140            usage: None,
141        }
142    }
143
144    /// Sets the error code.
145    #[must_use]
146    pub fn with_code(mut self, code: impl Into<String>) -> Self {
147        self.code = Some(code.into());
148        self
149    }
150}
151
152/// Opens a named step within a run.
153#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
156#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
157pub struct StepStartedEvent {
158    /// Timestamp and raw provider event.
159    #[serde(flatten)]
160    pub base: BaseEvent,
161    /// The step that is starting.
162    pub step_name: StepName,
163    /// The subagent that opened the step; absent means the parent agent. A
164    /// JSON `null` is rejected — see [`crate::event::subagent`]. Steps are
165    /// scoped to the agent that opened them: a subagent cannot close the
166    /// parent's step, or a sibling's, so the same name may be open under two
167    /// owners at once.
168    #[serde(
169        default,
170        deserialize_with = "crate::serde_util::reject_null",
171        skip_serializing_if = "Option::is_none"
172    )]
173    pub subagent_run_id: Option<SubagentRunId>,
174}
175
176impl StepStartedEvent {
177    /// Starts a step.
178    pub fn new(step_name: impl Into<StepName>) -> Self {
179        Self {
180            base: BaseEvent::default(),
181            step_name: step_name.into(),
182            subagent_run_id: None,
183        }
184    }
185}
186
187/// Closes a named step.
188#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
191#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
192pub struct StepFinishedEvent {
193    /// Timestamp and raw provider event.
194    #[serde(flatten)]
195    pub base: BaseEvent,
196    /// The step that finished.
197    pub step_name: StepName,
198    /// The subagent that closes the step; absent means the parent agent. A
199    /// JSON `null` is rejected — see [`crate::event::subagent`]. Must match
200    /// the owner that opened it, as on [`StepStartedEvent`].
201    #[serde(
202        default,
203        deserialize_with = "crate::serde_util::reject_null",
204        skip_serializing_if = "Option::is_none"
205    )]
206    pub subagent_run_id: Option<SubagentRunId>,
207}
208
209impl StepFinishedEvent {
210    /// Finishes a step.
211    pub fn new(step_name: impl Into<StepName>) -> Self {
212        Self {
213            base: BaseEvent::default(),
214            step_name: step_name.into(),
215            subagent_run_id: None,
216        }
217    }
218}