Skip to main content

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}