ag_ui/server/emit/mod.rs
1//! Typestate handles that make protocol misuse a compile error.
2//!
3//! Every streaming construct in AG-UI is bracketed: `TEXT_MESSAGE_START` …
4//! `TEXT_MESSAGE_END`, `TOOL_CALL_START` … `TOOL_CALL_END`, `STEP_STARTED` …
5//! `STEP_FINISHED`. Handing an agent three raw `emit` calls per construct means
6//! trusting it to close what it opened, in order, on every path including the
7//! early return. This module hands out RAII handles instead:
8//!
9//! - creating a handle emits the opening event;
10//! - the handle borrows the [`RunContext`](crate::server::RunContext) mutably, so a
11//! second overlapping handle is a **borrow-check error**, not a runtime
12//! protocol violation;
13//! - `Drop` emits the terminator, so forgetting `end()` — or returning `Err`
14//! through a `?` halfway through a message — still produces a well-formed
15//! stream.
16//!
17//! ```compile_fail,E0499
18//! use ag_ui::server::RunContext;
19//!
20//! fn interleave(ctx: &mut RunContext<()>) {
21//! let mut first = ctx.assistant_message().unwrap();
22//! // error[E0499]: cannot borrow `*ctx` as mutable more than once at a time
23//! let mut second = ctx.assistant_message().unwrap();
24//! first.delta("a").unwrap();
25//! second.delta("b").unwrap();
26//! }
27//! ```
28//!
29//! # Why the emit path is synchronous
30//!
31//! `Drop` cannot be async, so a handle cannot `await` while emitting its
32//! terminator. `msg.delta(text)?` therefore does not take `.await`: emitters
33//! push into an unbounded channel and the transport drains it. An earlier draft
34//! copied `await`-ing emitters from the TypeScript and .NET SDKs; it cannot
35//! coexist with the `Drop` guarantee.
36//!
37//! # The escape hatch
38//!
39//! [`StepGuard`] dereferences to the run context — a step is a scope, and
40//! everything else nests inside it. The three streaming handles deliberately do
41//! not, because that is exactly what would let a second message open inside the
42//! first. They expose [`emit`](MessageHandle::emit) instead, for the unordered
43//! events (state, activity, custom) that may legally interleave with a message.
44//!
45//! # What an open handle can still reach
46//!
47//! A handle borrows two *fields* of the run context — the event sink and the
48//! state — rather than the context itself. So the state is reachable through
49//! the handle ([`state`](ToolCallHandle::state),
50//! [`state_mut`](ToolCallHandle::state_mut),
51//! [`publish_state`](ToolCallHandle::publish_state)) and a tool call can do its
52//! work between its arguments and its result: `STATE_*` is unordered, so a
53//! publish inside the brackets is a legal stream.
54//!
55//! Widening reach, not weakening the rule. The context stays exclusively
56//! borrowed for as long as the handle lives, so a second block is still a
57//! borrow-check error — including from inside an open call:
58//!
59//! ```compile_fail,E0499
60//! use ag_ui::server::RunContext;
61//!
62//! fn narrate(ctx: &mut RunContext<()>) {
63//! let mut call = ctx.tool_call("search").unwrap();
64//! // error[E0499]: cannot borrow `*ctx` as mutable more than once at a time
65//! let mut message = ctx.assistant_message().unwrap();
66//! call.args("{}").unwrap();
67//! }
68//! ```
69//!
70//! # What has no handle, and why that is the answer
71//!
72//! Two things an agent may legitimately put on the wire are
73//! [`RunContext::emit`](crate::server::RunContext::emit) territory, and the escape
74//! hatch is the supported path for both rather than a gap waiting for an API.
75//!
76//! The `*_CHUNK` family is unbracketed by definition: a chunk carries its own
77//! id and needs no start and no end, which is the point — it exists for
78//! provider adapters that cannot know a message ended until the next one
79//! begins. There is nothing for an RAII handle to close, and wrapping one
80//! around a self-contained event would only add a way to get it wrong.
81//!
82//! Interleaved parallel tool calls are the other. Two open [`ToolCallHandle`]s
83//! at once is a borrow-check error *by design*, so a provider streaming
84//! `args(a) args(b) args(a) end(a) end(b)` cannot be mirrored handle-for-call.
85//! Either accumulate each call and emit it whole once its arguments are
86//! complete — what `e2e/src/llm.rs` does, and the only mapping that cannot
87//! splice two calls' arguments into each other — or emit the interleaving
88//! yourself. The verifier keys everything by id, so it accepts the interleaved
89//! stream; what it will not let you do is close a call you never opened.
90//!
91//! # Subagents
92//!
93//! [`SubagentHandle`] is a scope in the sense [`StepGuard`] is: it dereferences
94//! to the run context, and everything opened through it — messages, tool
95//! calls, reasoning, steps, nested subagents — comes out carrying its
96//! `subagentRunId`. The attribution lives in the event sink rather than in
97//! the handles, which is why a [`MessageHandle`] opened inside a subagent
98//! needs no idea that it was: the sink tags every attributable event that
99//! arrives untagged while a scope is open, and leaves an event the agent
100//! tagged explicitly alone.
101//!
102//! That last clause is the concurrent case. Subagents that stream at once are
103//! the parallel-tool-call situation again: two open handles is a borrow-check
104//! error by design, so build each subagent's events with
105//! [`Event::with_subagent_run_id`] and [`emit`](crate::server::RunContext::emit)
106//! them interleaved, bracketed by [`Event::subagent_started`] and
107//! [`Event::subagent_finished_success`]. The verifier keys every entity by id
108//! and remembers who opened it, so the interleaving is accepted; what it will
109//! not let you do is continue one subagent's message under another's tag.
110//! Attribute every chunk when several subagents stream at once — a chunk
111//! that names neither a message nor a subagent can only be resolved when one
112//! stream is open.
113
114mod message;
115mod reasoning;
116mod step;
117mod subagent;
118mod tool;
119
120use crate::{Event, SubagentRunId};
121use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
122use futures_core::Stream;
123use futures_util::StreamExt as _;
124
125use crate::server::cancel::CancellationToken;
126use crate::server::error::{Error, Result};
127use crate::server::transform::TransformerChain;
128use crate::server::verify::Verifier;
129
130pub use message::MessageHandle;
131pub use reasoning::ReasoningHandle;
132pub use step::StepGuard;
133pub use subagent::SubagentHandle;
134pub use tool::ToolCallHandle;
135
136/// The write end of a run's event stream: transformers, then verification,
137/// then the channel.
138///
139/// Not public: the only way to reach one is through a
140/// [`RunContext`](crate::server::RunContext) or a handle, which is what keeps the
141/// ordering guarantees intact.
142pub(crate) struct EventSink {
143 tx: UnboundedSender<Event>,
144 chain: TransformerChain,
145 verifier: Verifier,
146 cancel: CancellationToken,
147 /// Whether a terminal event has gone out. Tracked here as well as in the
148 /// verifier so that turning the `verify` feature off cannot make the driver
149 /// emit a second `RUN_FINISHED`.
150 terminated: bool,
151 /// The subagent scope in force: every attributable event emitted untagged
152 /// while it is set goes out carrying it. See [`SubagentHandle`].
153 attribution: Option<SubagentRunId>,
154}
155
156impl std::fmt::Debug for EventSink {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("EventSink")
159 .field("transformers", &self.chain.len())
160 .field("cancelled", &self.cancel.is_cancelled())
161 .field("attribution", &self.attribution)
162 .finish()
163 }
164}
165
166impl EventSink {
167 pub(crate) fn new(
168 tx: UnboundedSender<Event>,
169 chain: TransformerChain,
170 cancel: CancellationToken,
171 ) -> Self {
172 Self {
173 tx,
174 chain,
175 verifier: Verifier::new(),
176 cancel,
177 terminated: false,
178 attribution: None,
179 }
180 }
181
182 /// Emits one event, unless the run was cancelled.
183 ///
184 /// Failing every emit after cancellation is what makes cancellation work
185 /// without any cooperation from the agent: the next `?` unwinds the run.
186 pub(crate) fn emit(&mut self, event: Event) -> Result<()> {
187 if self.cancel.is_cancelled() {
188 return Err(Error::Cancelled);
189 }
190 self.emit_forced(event)
191 }
192
193 /// Emits one event even after cancellation — used by the run driver for
194 /// `RUN_FINISHED` and `RUN_ERROR`, which must go out regardless.
195 pub(crate) fn emit_forced(&mut self, mut event: Event) -> Result<()> {
196 // Attribution is applied before the transformers, so a transformer
197 // sees the same tagged stream a consumer would. An event the agent
198 // tagged itself keeps its tag: that is how a hand-interleaved
199 // concurrent stream is written.
200 if let Some(id) = &self.attribution {
201 if event.event_type().is_attributable() && event.subagent_run_id().is_none() {
202 event.set_subagent_run_id(id.clone());
203 }
204 }
205 if self.chain.is_empty() {
206 return self.send(event);
207 }
208 for event in self.chain.transform(event) {
209 self.send(event)?;
210 }
211 Ok(())
212 }
213
214 /// The subagent scope in force, if any.
215 pub(crate) fn attribution(&self) -> Option<&SubagentRunId> {
216 self.attribution.as_ref()
217 }
218
219 /// Replaces the subagent scope and returns the previous one, so a nested
220 /// scope can restore it on close.
221 pub(crate) fn set_attribution(
222 &mut self,
223 attribution: Option<SubagentRunId>,
224 ) -> Option<SubagentRunId> {
225 std::mem::replace(&mut self.attribution, attribution)
226 }
227
228 fn send(&mut self, event: Event) -> Result<()> {
229 self.verifier.observe(&event)?;
230 self.terminated |= matches!(event, Event::RunFinished(_) | Event::RunError(_));
231 self.tx
232 .unbounded_send(event)
233 .map_err(|_| Error::Disconnected)
234 }
235
236 /// Whether a terminal event has already gone out.
237 pub(crate) fn is_terminated(&self) -> bool {
238 self.terminated
239 }
240
241 pub(crate) fn cancel_token(&self) -> &CancellationToken {
242 &self.cancel
243 }
244}
245
246/// The read end of a run's event stream.
247///
248/// Yielded by [`RunContext::new`](crate::server::RunContext::new) for agents under
249/// test. Transports get a [`Stream`] from
250/// [`Runner::run`](crate::server::Runner::run) instead.
251#[derive(Debug)]
252pub struct EventReceiver {
253 rx: UnboundedReceiver<Event>,
254}
255
256impl EventReceiver {
257 pub(crate) fn new(rx: UnboundedReceiver<Event>) -> Self {
258 Self { rx }
259 }
260
261 /// Takes every event emitted so far without waiting.
262 ///
263 /// The emit path is synchronous, so after calling an agent's code
264 /// everything it emitted is already queued. That makes this the whole
265 /// assertion story for a unit test:
266 ///
267 /// ```
268 /// # use ag_ui::{Event, RunAgentInput, TextMessageRole};
269 /// # use ag_ui::server::RunContext;
270 /// let (mut ctx, mut events) = RunContext::<()>::new(RunAgentInput::new("t", "r"))?;
271 /// ctx.say("hello")?;
272 /// assert_eq!(events.drain(), vec![
273 /// Event::text_message_start("r-msg-1", TextMessageRole::Assistant),
274 /// Event::text_message_content("r-msg-1", "hello"),
275 /// Event::text_message_end("r-msg-1"),
276 /// ]);
277 /// # Ok::<(), ag_ui::server::Error>(())
278 /// ```
279 pub fn drain(&mut self) -> Vec<Event> {
280 let mut events = Vec::new();
281 while let Ok(event) = self.rx.try_recv() {
282 events.push(event);
283 }
284 events
285 }
286
287 /// Closes the channel, so the next emit fails with
288 /// [`Error::Disconnected`].
289 ///
290 /// [`Error::Disconnected`]: crate::server::Error::Disconnected
291 pub fn close(&mut self) {
292 self.rx.close();
293 }
294}
295
296impl Stream for EventReceiver {
297 type Item = Event;
298
299 fn poll_next(
300 mut self: std::pin::Pin<&mut Self>,
301 cx: &mut std::task::Context<'_>,
302 ) -> std::task::Poll<Option<Event>> {
303 self.rx.poll_next_unpin(cx)
304 }
305}