Skip to main content

ag_ui/client/
session.rs

1//! The high-level API: a conversation you send text to.
2//!
3//! *Thread* and *run* are the protocol's words; `Session` is this crate's. The
4//! wire carries a `threadId` and a `runId` and nothing else — there is no
5//! session on it, and no thread object either, only an id — so a [`Session`]
6//! sits *over* that id, adding the transport, the conversation so far, the
7//! typed state and the tools. It is deliberately not called `Thread`:
8//! borrowing the name would imply a protocol entity that does not exist.
9//!
10//! [`RemoteAgent`] gives you events. A UI does not want events — it
11//! wants "this message grew by three characters", "the state changed, here it
12//! is typed", "the agent is waiting for you to approve something". A [`Session`]
13//! yields [`Update`]s instead of raw events.
14//!
15//! Everything the protocol makes fiddly happens inside: chunk events are
16//! normalized, the stream is verified, deltas are folded into messages, and the
17//! next run automatically carries the conversation so far.
18//!
19//! ```
20//! use ag_ui::client::{Session, Update, transport::ReplayTransport};
21//! use ag_ui::{Event, TextMessageRole};
22//! use futures_util::StreamExt;
23//!
24//! # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
25//! # rt.block_on(async {
26//! let transport = ReplayTransport::new([
27//!     Event::run_started("thread-1", "run-1"),
28//!     Event::text_message_start("msg-1", TextMessageRole::Assistant),
29//!     Event::text_message_content("msg-1", "Sunny."),
30//!     Event::text_message_end("msg-1"),
31//!     Event::run_finished_success("thread-1", "run-1"),
32//! ]);
33//!
34//! let mut session = Session::<_>::new(transport, "thread-1");
35//! let mut run = session.send("what is the weather?");
36//! while let Some(update) = run.next().await {
37//!     if let Update::Message(message) = update {
38//!         println!("{}: {:?}", message.id, message.change);
39//!     }
40//! }
41//! drop(run);
42//!
43//! // The user's turn and the agent's reply are both in the thread now, so the
44//! // next `send` carries them.
45//! assert_eq!(session.messages().len(), 2);
46//! # });
47//! ```
48//!
49//! # One update is one event, and the order is the nesting
50//!
51//! The stream is per *event*, not per entity. A reply that streams in forty
52//! deltas is forty [`Update::Message`]s under one id; two tool calls in flight
53//! — which a model produces whenever it asks for two things at once —
54//! interleave, and only their ids separate them.
55//!
56//! So whatever the run *nested* survives as arrival order and nothing else. An
57//! agent that publishes state while a tool call is open (which
58//! [`ag_ui::server`]'s handles support, and the protocol allows because
59//! `STATE_*` is unordered) puts the `STATE_*` event between that call's
60//! `TOOL_CALL_ARGS` and its `TOOL_CALL_END` — and the [`Update::State`] that
61//! comes out carries no mention of the call. That is not an omission to be
62//! fixed by adding a field: under parallel calls two calls are open at once and
63//! the wire itself does not say which one the state belongs to, so any
64//! attribution would be invented. **The ordering is the contract.**
65//!
66//! A renderer that draws in arrival order therefore shows what happened. One
67//! that buffers by entity — collecting a call's arguments so it can draw the
68//! call on one line when it closes — is choosing to reorder: everything that
69//! arrived while the call was open now draws before it. For a terminal that is
70//! often the right trade, and `examples/board-watch` makes it deliberately and
71//! pins the consequence in a test. It is only a bug when it is an accident.
72//!
73//! [`ag_ui::server`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/server/index.html
74
75use std::collections::VecDeque;
76use std::pin::Pin;
77use std::task::{Context as TaskContext, Poll};
78
79use crate::{
80    Context, Event, Interrupt, Message, MessageId, ReasoningMessage, ResumeEntry, RunAgentInput,
81    RunId, RunOutcome, ThreadId, Tool,
82};
83use futures_core::Stream;
84use serde::de::DeserializeOwned;
85use serde_json::Value;
86
87use crate::client::agent::RemoteAgent;
88use crate::client::apply::{
89    Applier, Changed, MessageChangeKind, ReasoningChangeKind, Subagent, SubagentChangeKind,
90};
91use crate::client::chunks::ChunkNormalizer;
92use crate::client::error::Error;
93use crate::client::interrupts::InterruptExt;
94use crate::client::transport::{EventStream, Transport};
95use crate::client::verify::Verifier;
96
97/// Something a view should react to.
98///
99/// One [`Update`] is one redraw. `S` is the caller's state type; it is
100/// [`serde_json::Value`] unless a [`Session`] is asked for something better.
101#[derive(Debug)]
102#[non_exhaustive]
103pub enum Update<S = Value> {
104    /// A message was created, appended to, or completed.
105    Message(MessageUpdate),
106    /// `MESSAGES_SNAPSHOT` replaced the conversation. Messages may have
107    /// disappeared, so redraw all of it.
108    Messages(Vec<Message>),
109    /// The application state changed, and here it is in the caller's type.
110    ///
111    /// Carries no association with whatever was open when it arrived — a tool
112    /// call, a message — because the wire carries none either. Where it lands
113    /// in the stream is the only nesting there is; see the [module
114    /// docs](self).
115    State(S),
116    /// Reasoning text arrived. Kept separate from the reply.
117    Reasoning(ReasoningUpdate),
118    /// A subagent was announced, resumed, finished, suspended or failed.
119    ///
120    /// The lifecycle only: what a subagent *says* arrives as ordinary
121    /// [`Update::Message`]s and [`Update::Reasoning`]s whose messages carry
122    /// its `subagent_run_id`, so a view groups by that and uses this for the
123    /// group's header and status. See [`Session::subagents`].
124    Subagent(SubagentUpdate),
125    /// The run paused and needs a human. Answer it with
126    /// [`Session::resume`] — one update per pending interrupt.
127    Interrupt(Interrupt),
128    /// Something went wrong: a malformed stream, a patch that would not apply,
129    /// a transport failure, a `RUN_ERROR`.
130    ///
131    /// Not necessarily fatal — a run survives a patch it could not apply. When
132    /// it is fatal, the matching [`Update::Done`] follows.
133    Error(Error),
134    /// The run ended, and how. Always the last update of a run, on every path
135    /// out: the agent finishing, the agent failing, and the transport dying
136    /// mid-sentence.
137    Done(RunEnd),
138}
139
140/// A message that changed, and the message as it now stands.
141#[derive(Clone, Debug, PartialEq)]
142pub struct MessageUpdate {
143    /// Index into [`Session::messages`].
144    pub index: usize,
145    /// The message's id.
146    pub id: MessageId,
147    /// What this event did to it — the text delta, the tool call, the close.
148    pub change: MessageChangeKind,
149    /// The whole message, assembled so far.
150    pub message: Message,
151}
152
153/// Reasoning that changed, and the reasoning as it now stands.
154#[derive(Clone, Debug, PartialEq)]
155pub struct ReasoningUpdate {
156    /// The reasoning message's id.
157    pub id: MessageId,
158    /// What this event did to it.
159    pub change: ReasoningChangeKind,
160    /// The accumulated reasoning text.
161    pub text: String,
162}
163
164/// A subagent that changed, and the subagent as it now stands.
165#[derive(Clone, Debug, PartialEq)]
166pub struct SubagentUpdate {
167    /// Index into [`Session::subagents`].
168    pub index: usize,
169    /// The invocation's id — what the messages it produced carry.
170    pub run_id: crate::SubagentRunId,
171    /// What this event did to it.
172    pub change: SubagentChangeKind,
173    /// The whole entry, status included.
174    pub subagent: Subagent,
175}
176
177/// How a run ended.
178///
179/// Exhaustive, unlike [`Update`] and unlike every error in this workspace: a
180/// run ends in one of three ways because the protocol says so — `RUN_FINISHED`
181/// with a success outcome, `RUN_FINISHED` with an interrupt outcome, or
182/// `RUN_ERROR` (which the truncated-stream case is reported as). A fourth would
183/// be a wire-contract change, and `docs/DESIGN.md` explains why those are meant
184/// to be compile errors for consumers rather than something a `_` arm swallows.
185/// This is the match a front-end most wants the compiler's help with — it
186/// decides whether the input goes live again.
187#[derive(Clone, Debug, PartialEq)]
188pub enum RunEnd {
189    /// The agent finished.
190    ///
191    /// Meaning "the agent said the run succeeded", not "nothing went wrong".
192    /// The two come apart: a protocol violation the verifier caught, or a state
193    /// patch that would not apply, arrives as an [`Update::Error`] and the run
194    /// carries on to end here — those are the *client's* diagnostics, and the
195    /// agent is neither told nor asked. A view that routes on
196    /// [`Update::Done`] alone will call such a run clean; track the errors as
197    /// they arrive if the difference matters to you.
198    Success {
199        /// The agent's return value, if it sent one.
200        result: Option<Value>,
201    },
202    /// The agent paused for human input. The same interrupts arrived
203    /// individually as [`Update::Interrupt`], and are on
204    /// [`Session::interrupts`] until the next run.
205    Interrupted {
206        /// What the agent is waiting for.
207        interrupts: Vec<Interrupt>,
208    },
209    /// The run failed, or the transport stopped before it could finish. The
210    /// matching [`Update::Error`] came first.
211    Failed {
212        /// What went wrong, for a human.
213        message: String,
214        /// The machine-readable code, when the agent sent one.
215        code: Option<String>,
216    },
217}
218
219/// A conversation with an agent.
220///
221/// Holds the thread id, the messages both sides have said, and the application
222/// state. `S` is the type the state deserializes into, and it is inferred from
223/// whatever the caller does with an [`Update::State`] — a `Session` handed to a
224/// function expecting `Session<T, MyState>`, or a match arm that keeps the
225/// state in a typed local, needs no turbofish at all. Spell it only when
226/// nothing else names it: `Session::<_>::new(transport, thread)` falls back to
227/// the [`serde_json::Value`] default, and `Session::<_, MyState>::new(…)` pins
228/// it by hand.
229///
230/// To stream updates, `S` must be `Deserialize + Clone + Unpin` — an
231/// [`Update::State`] carries the state by value, so a view can hold it after
232/// the run has moved on. `#[derive(Clone, Deserialize)]` on a plain struct is
233/// all that takes.
234#[derive(Debug)]
235pub struct Session<T, S = Value> {
236    agent: RemoteAgent<T>,
237    thread_id: ThreadId,
238    applier: Applier,
239    state: Option<S>,
240    tools: Vec<Tool>,
241    context: Vec<Context>,
242    forwarded_props: Value,
243    verify: bool,
244    interrupts: Vec<Interrupt>,
245    runs: u64,
246    messages_sent: u64,
247    next_run_id: Option<RunId>,
248}
249
250impl<T, S> Session<T, S> {
251    /// A new conversation over `transport`.
252    ///
253    /// The `T: Transport` bound is checked *here*, at the construction site,
254    /// rather than on [`Session`] itself — so something that is not a transport
255    /// is an error on this line instead of on the first
256    /// [`send`](Session::send), which is usually in another file:
257    ///
258    /// ```compile_fail,E0277
259    /// use ag_ui::client::Session;
260    ///
261    /// // error[E0277]: the trait bound `str: Transport` is not satisfied,
262    /// // and the note lists the types that do implement it.
263    /// let session = Session::<_>::new("http://localhost:8080/agent", "thread-1");
264    /// ```
265    ///
266    /// (A URL is the mistake worth catching: it is what a transport is *made
267    /// from*, so it reads plausible.) The bound stays off the struct because a
268    /// bound there is viral — every application helper naming `Session<T, S>`
269    /// would have to repeat it, including ones that only read
270    /// [`messages`](Session::messages). `tests/bounds.rs` is what says so.
271    pub fn new(transport: T, thread_id: impl Into<ThreadId>) -> Self
272    where
273        T: Transport,
274    {
275        Self::builder(transport, thread_id).build()
276    }
277
278    /// A builder, for seeding history, tools, context, or turning verification
279    /// off.
280    ///
281    /// Takes the same `T: Transport` bound as [`new`](Session::new), and for
282    /// the same reason. Everything after it — including
283    /// [`build`](SessionBuilder::build) — is unbounded.
284    pub fn builder(transport: T, thread_id: impl Into<ThreadId>) -> SessionBuilder<T, S>
285    where
286        T: Transport,
287    {
288        SessionBuilder::new(transport, thread_id)
289    }
290
291    /// The conversation this session is part of.
292    pub fn thread_id(&self) -> &ThreadId {
293        &self.thread_id
294    }
295
296    /// The assembled conversation, oldest first — everything the user sent and
297    /// everything the agent has said across every run.
298    pub fn messages(&self) -> &[Message] {
299        self.applier.messages()
300    }
301
302    /// The application state in the caller's type, once the agent has published
303    /// one that deserializes.
304    pub fn state(&self) -> Option<&S> {
305        self.state.as_ref()
306    }
307
308    /// The application state as raw JSON. Always current, even when the typed
309    /// view is not.
310    pub fn raw_state(&self) -> &Value {
311        self.applier.state()
312    }
313
314    /// The reasoning messages, kept out of the transcript.
315    pub fn reasoning(&self) -> &[ReasoningMessage] {
316        self.applier.reasoning()
317    }
318
319    /// What the agent is waiting for, if the last run paused.
320    pub fn interrupts(&self) -> &[Interrupt] {
321        &self.interrupts
322    }
323
324    /// The subagent invocations announced so far, across runs — a suspended
325    /// one stays until the run that resumes it announces it again.
326    pub fn subagents(&self) -> &[Subagent] {
327        self.applier.subagents()
328    }
329
330    /// One subagent invocation by id — what a view does with the
331    /// [`Message::subagent_run_id`] on a message it is about to draw.
332    pub fn subagent(&self, run_id: &crate::SubagentRunId) -> Option<&Subagent> {
333        self.applier.subagent(run_id)
334    }
335
336    /// The applier underneath, for a view that wants the raw materialised
337    /// state.
338    pub fn applier(&self) -> &Applier {
339        &self.applier
340    }
341
342    /// The low-level agent underneath.
343    pub fn agent(&self) -> &RemoteAgent<T> {
344        &self.agent
345    }
346
347    /// Appends a message without starting a run — a tool result computed on the
348    /// client, or history loaded from a store.
349    pub fn push_message(&mut self, message: Message) {
350        self.applier.push_message(message);
351    }
352
353    /// Replaces the state without going through the agent.
354    pub fn set_state(&mut self, state: impl Into<Value>) {
355        self.applier.set_state(state);
356    }
357
358    /// Offers a different set of tools from the next run on.
359    pub fn set_tools(&mut self, tools: impl Into<Vec<Tool>>) {
360        self.tools = tools.into();
361    }
362
363    /// Names the next run explicitly, instead of the generated
364    /// `{thread}-run-{n}`.
365    ///
366    /// Servers that key resumption on a run id need this; most do not.
367    pub fn set_next_run_id(&mut self, run_id: impl Into<RunId>) {
368        self.next_run_id = Some(run_id.into());
369    }
370
371    /// Builds the next request: the conversation so far, the state so far, and
372    /// a freshly minted run id.
373    fn input(&mut self, resume: Option<Vec<ResumeEntry>>) -> RunAgentInput {
374        RunAgentInput {
375            thread_id: self.thread_id.clone(),
376            run_id: self.next_run_id(),
377            parent_run_id: None,
378            state: self.applier.state().clone(),
379            messages: self.applier.messages().to_vec(),
380            tools: self.tools.clone(),
381            context: self.context.clone(),
382            forwarded_props: self.forwarded_props.clone(),
383            resume,
384        }
385    }
386
387    fn next_run_id(&mut self) -> RunId {
388        if let Some(run_id) = self.next_run_id.take() {
389            return run_id;
390        }
391        self.runs += 1;
392        RunId::new(format!("{}-run-{}", self.thread_id, self.runs))
393    }
394
395    fn next_message_id(&mut self) -> MessageId {
396        self.messages_sent += 1;
397        MessageId::new(format!("{}-msg-{}", self.thread_id, self.messages_sent))
398    }
399}
400
401impl<T: Transport, S> Session<T, S> {
402    /// Sends the user's turn and streams what the agent does about it.
403    ///
404    /// The message is appended to the conversation before the request goes out,
405    /// so it is in [`Session::messages`] whatever happens to the run.
406    pub fn send(&mut self, text: impl Into<String>) -> RunStream<'_, T, S> {
407        let id = self.next_message_id();
408        self.push_message(Message::user(id, text.into()));
409        self.start(None)
410    }
411
412    /// Sends a message of any role and streams the run.
413    pub fn send_message(&mut self, message: Message) -> RunStream<'_, T, S> {
414        self.push_message(message);
415        self.start(None)
416    }
417
418    /// Starts a run without adding anything — after pushing a tool result, or
419    /// to let an agent continue on its own.
420    pub fn run(&mut self) -> RunStream<'_, T, S> {
421        self.start(None)
422    }
423
424    /// Answers one interrupt and resumes the paused run.
425    ///
426    /// The answer's shape is up to the agent; when the interrupt carried a
427    /// `responseSchema`, `payload` should satisfy it.
428    pub fn resume(
429        &mut self,
430        interrupt: &Interrupt,
431        payload: impl Into<Value>,
432    ) -> RunStream<'_, T, S> {
433        self.resume_many([interrupt.resolve(payload)])
434    }
435
436    /// Declines one interrupt and resumes the paused run.
437    pub fn cancel(&mut self, interrupt: &Interrupt) -> RunStream<'_, T, S> {
438        self.resume_many([interrupt.cancel()])
439    }
440
441    /// Answers several interrupts at once — a run can pause on more than one.
442    ///
443    /// Any interrupt left unanswered is dropped: the resumed run supersedes the
444    /// paused one, and the agent only sees what is in this request. Use
445    /// [`ResumeBuilder`](crate::client::interrupts::ResumeBuilder) to answer them all.
446    pub fn resume_many(
447        &mut self,
448        entries: impl IntoIterator<Item = ResumeEntry>,
449    ) -> RunStream<'_, T, S> {
450        self.start(Some(entries.into_iter().collect()))
451    }
452
453    fn start(&mut self, resume: Option<Vec<ResumeEntry>>) -> RunStream<'_, T, S> {
454        self.interrupts.clear();
455        let input = self.input(resume);
456        // The stream is `'static`, so this borrow of the agent ends here and
457        // the session is free to be borrowed mutably for the run.
458        let events = self.agent.run(input);
459        let verifier = self.verify.then(Verifier::new);
460        RunStream {
461            session: self,
462            events,
463            normalizer: ChunkNormalizer::new(),
464            verifier,
465            expanded: Vec::new(),
466            ready: VecDeque::new(),
467            done: false,
468        }
469    }
470}
471
472/// Builds a [`Session`].
473#[derive(Debug)]
474pub struct SessionBuilder<T, S = Value> {
475    transport: T,
476    thread_id: ThreadId,
477    messages: Vec<Message>,
478    state: Value,
479    tools: Vec<Tool>,
480    context: Vec<Context>,
481    forwarded_props: Value,
482    verify: bool,
483    marker: std::marker::PhantomData<fn() -> S>,
484}
485
486impl<T, S> SessionBuilder<T, S> {
487    /// A builder for a conversation over `transport`.
488    ///
489    /// The one bounded method here: see [`Session::new`] for why the check
490    /// belongs on the constructor and not on the type.
491    pub fn new(transport: T, thread_id: impl Into<ThreadId>) -> Self
492    where
493        T: Transport,
494    {
495        Self {
496            transport,
497            thread_id: thread_id.into(),
498            messages: Vec::new(),
499            state: Value::Object(serde_json::Map::new()),
500            tools: Vec::new(),
501            context: Vec::new(),
502            forwarded_props: Value::Null,
503            verify: true,
504            marker: std::marker::PhantomData,
505        }
506    }
507
508    /// Seeds the conversation with existing history.
509    #[must_use]
510    pub fn messages(mut self, messages: impl Into<Vec<Message>>) -> Self {
511        self.messages = messages.into();
512        self
513    }
514
515    /// Seeds the application state.
516    #[must_use]
517    pub fn state(mut self, state: impl Into<Value>) -> Self {
518        self.state = state.into();
519        self
520    }
521
522    /// Offers tools on every run.
523    ///
524    /// The client's responsibility, not the agent's: there is no discovery in
525    /// AG-UI. See the [crate docs](crate#tools-are-yours-to-offer).
526    #[must_use]
527    pub fn tools(mut self, tools: impl Into<Vec<Tool>>) -> Self {
528        self.tools = tools.into();
529        self
530    }
531
532    /// Sets the ambient context entries sent on every run.
533    #[must_use]
534    pub fn context(mut self, context: impl Into<Vec<Context>>) -> Self {
535        self.context = context.into();
536        self
537    }
538
539    /// Sets the passthrough properties sent on every run.
540    #[must_use]
541    pub fn forwarded_props(mut self, props: impl Into<Value>) -> Self {
542        self.forwarded_props = props.into();
543        self
544    }
545
546    /// Turns [protocol verification](crate::client::verify) on or off. On by default.
547    ///
548    /// Off is for producers whose quirks you have decided to live with; the
549    /// applier stays tolerant either way, so what you lose is the diagnosis,
550    /// not the conversation.
551    #[must_use]
552    pub fn verify(mut self, verify: bool) -> Self {
553        self.verify = verify;
554        self
555    }
556
557    /// Builds the session.
558    pub fn build(self) -> Session<T, S> {
559        Session {
560            agent: RemoteAgent::new(self.transport),
561            thread_id: self.thread_id,
562            applier: Applier::new()
563                .with_messages(self.messages)
564                .with_state(self.state),
565            state: None,
566            tools: self.tools,
567            context: self.context,
568            forwarded_props: self.forwarded_props,
569            verify: self.verify,
570            interrupts: Vec::new(),
571            runs: 0,
572            messages_sent: 0,
573            next_run_id: None,
574        }
575    }
576}
577
578/// One run, as a stream of [`Update`]s.
579///
580/// Borrows the session mutably: the conversation and the state are being
581/// updated as the stream is polled, which is what makes
582/// [`Session::messages`] correct the moment the run ends.
583///
584/// Every run ends with exactly one [`Update::Done`], and the stream ends
585/// there. A transport that stops early is reported as an [`Update::Error`] —
586/// naming the truncation when the body simply stopped, and the transport's own
587/// failure when it broke — followed by [`RunEnd::Failed`]: a view that
588/// re-enables its input on `Done` must not be left waiting by a dropped
589/// connection. Turning verification off changes how precisely the truncation is
590/// described, not whether it is reported.
591pub struct RunStream<'a, T, S = Value> {
592    session: &'a mut Session<T, S>,
593    events: EventStream,
594    normalizer: ChunkNormalizer,
595    verifier: Option<Verifier>,
596    expanded: Vec<Event>,
597    ready: VecDeque<Update<S>>,
598    done: bool,
599}
600
601impl<T, S> std::fmt::Debug for RunStream<'_, T, S> {
602    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603        f.debug_struct("RunStream")
604            .field("pending", &self.ready.len())
605            .field("done", &self.done)
606            .finish_non_exhaustive()
607    }
608}
609
610impl<T, S> RunStream<'_, T, S> {
611    /// The session as it stands mid-run, for a view that needs more than the
612    /// update in hand — the subagent a message's owner id names, the state
613    /// so far, the messages before this one.
614    ///
615    /// Read-only: the stream holds the mutable borrow until it is dropped,
616    /// which is what makes every update consistent with what this returns.
617    pub fn session(&self) -> &Session<T, S> {
618        self.session
619    }
620}
621
622impl<T, S> RunStream<'_, T, S>
623where
624    S: DeserializeOwned + Clone,
625{
626    /// Runs one event through normalize → verify → apply.
627    fn ingest(&mut self, event: Event) {
628        // Reuse the buffer across events; it is the same shape every time.
629        let mut expanded = std::mem::take(&mut self.expanded);
630        expanded.clear();
631        let outcome = self.normalizer.normalize(event, &mut expanded);
632        for event in expanded.drain(..) {
633            self.handle(event);
634        }
635        self.expanded = expanded;
636        if let Err(error) = outcome {
637            self.ready.push_back(Update::Error(error));
638        }
639    }
640
641    fn handle(&mut self, event: Event) {
642        if let Some(verifier) = &mut self.verifier {
643            if let Err(error) = verifier.verify(&event) {
644                self.ready.push_back(Update::Error(error));
645                // Do not apply an event the producer should not have sent: a
646                // clear error beats state assembled from a broken stream. The
647                // exception is an event that ends the run — the run is over
648                // either way, and a caller that never hears so waits forever.
649                if !matches!(event, Event::RunFinished(_) | Event::RunError(_)) {
650                    return;
651                }
652            }
653        }
654        match self.session.applier.apply(&event) {
655            Ok(changed) => self.emit(changed),
656            Err(error) => self.ready.push_back(Update::Error(error)),
657        }
658    }
659
660    fn emit(&mut self, changed: Changed) {
661        match changed {
662            Changed::Nothing | Changed::RunStarted { .. } => {}
663
664            Changed::Message(change) => {
665                if let Some(message) = self.session.applier.messages().get(change.index) {
666                    let update = MessageUpdate {
667                        index: change.index,
668                        id: change.id,
669                        change: change.kind,
670                        message: message.clone(),
671                    };
672                    self.ready.push_back(Update::Message(update));
673                }
674            }
675
676            Changed::MessagesReplaced => {
677                let messages = self.session.applier.messages().to_vec();
678                self.ready.push_back(Update::Messages(messages));
679            }
680
681            Changed::State => match self.session.applier.state_as::<S>() {
682                Ok(state) => {
683                    self.session.state = Some(state.clone());
684                    self.ready.push_back(Update::State(state));
685                }
686                // The raw state is still updated and correct; only the typed
687                // view is out of date, and that is worth saying out loud.
688                Err(error) => self.ready.push_back(Update::Error(error)),
689            },
690
691            Changed::Reasoning(change) => {
692                let text = self
693                    .session
694                    .applier
695                    .reasoning_text(&change.id)
696                    .unwrap_or_default()
697                    .to_owned();
698                let update = ReasoningUpdate {
699                    id: change.id,
700                    change: change.kind,
701                    text,
702                };
703                self.ready.push_back(Update::Reasoning(update));
704            }
705
706            Changed::Subagent(change) => {
707                if let Some(subagent) = self.session.applier.subagents().get(change.index) {
708                    let update = SubagentUpdate {
709                        index: change.index,
710                        run_id: change.run_id,
711                        change: change.kind,
712                        subagent: subagent.clone(),
713                    };
714                    self.ready.push_back(Update::Subagent(update));
715                }
716            }
717
718            Changed::RunFinished { outcome, result } => {
719                self.done = true;
720                match outcome {
721                    RunOutcome::Success => {
722                        self.ready
723                            .push_back(Update::Done(RunEnd::Success { result }));
724                    }
725                    RunOutcome::Interrupt { interrupts } => {
726                        self.session.interrupts.clone_from(&interrupts);
727                        for interrupt in &interrupts {
728                            self.ready.push_back(Update::Interrupt(interrupt.clone()));
729                        }
730                        self.ready
731                            .push_back(Update::Done(RunEnd::Interrupted { interrupts }));
732                    }
733                }
734            }
735
736            Changed::RunError { message, code } => {
737                self.done = true;
738                self.ready.push_back(Update::Error(Error::Run {
739                    message: message.clone(),
740                    code: code.clone(),
741                }));
742                self.ready
743                    .push_back(Update::Done(RunEnd::Failed { message, code }));
744            }
745        }
746    }
747
748    /// The transport stopped sending. Close what the producer left open, then
749    /// report a truncated stream.
750    fn end_of_stream(&mut self) {
751        self.done = true;
752        self.close_open_streams();
753
754        // Getting here means no terminal event was applied, because applying
755        // one queues the run's `Done` and stops the stream before this. So the
756        // run ended without saying how, and this is where that is said.
757        let error = match self.verifier.as_ref().map(Verifier::finish) {
758            // The verifier says it more precisely: it knows whether the run
759            // ever started.
760            Some(Err(error)) => error,
761            // Unverified, or verified and somehow tidy: either way the producer
762            // never sent a terminal event. Turning verification off buys a
763            // caller a producer's quirks, not silence about a dead run.
764            _ => Error::protocol(TRUNCATED),
765        };
766        self.fail(error);
767    }
768
769    /// Emits the terminators the normalizer still owes.
770    ///
771    /// A view that hides its typing indicator on [`MessageChangeKind::Ended`]
772    /// would otherwise spin forever on a message the producer never closed.
773    fn close_open_streams(&mut self) {
774        let mut expanded = std::mem::take(&mut self.expanded);
775        expanded.clear();
776        self.normalizer.finish(&mut expanded);
777        for event in expanded.drain(..) {
778            self.handle(event);
779        }
780        self.expanded = expanded;
781    }
782
783    /// The transport itself failed. Nothing more will arrive on this run.
784    fn transport_failed(&mut self, error: Error) {
785        self.done = true;
786        self.close_open_streams();
787        self.fail(error);
788    }
789
790    /// Ends a run that stopped without the agent saying how: the error, and
791    /// then the [`Update::Done`] every run owes its caller.
792    ///
793    /// Both in that order, on every path, so that [`RunEnd::Failed`] always has
794    /// the matching [`Update::Error`] in front of it.
795    fn fail(&mut self, error: Error) {
796        let message = error.to_string();
797        self.ready.push_back(Update::Error(error));
798        self.ready.push_back(Update::Done(RunEnd::Failed {
799            message,
800            code: None,
801        }));
802    }
803}
804
805/// What a run that simply stopped is reported as, when there is no verifier to
806/// describe it more precisely.
807const TRUNCATED: &str = "the stream ended before RUN_FINISHED or RUN_ERROR";
808
809// `S: Unpin` because the queued updates hold an `S` and this stream is polled
810// through a `&mut`. Every type that deserializes from JSON is `Unpin` in
811// practice — self-referential state would not survive `serde` anyway.
812impl<T, S> Stream for RunStream<'_, T, S>
813where
814    S: DeserializeOwned + Clone + Unpin,
815{
816    type Item = Update<S>;
817
818    fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
819        // Every field is `Unpin`: the boxed event stream is a `Pin<Box<…>>` and
820        // everything else is plain data.
821        let this = self.get_mut();
822        loop {
823            if let Some(update) = this.ready.pop_front() {
824                return Poll::Ready(Some(update));
825            }
826            if this.done {
827                return Poll::Ready(None);
828            }
829            match this.events.as_mut().poll_next(cx) {
830                Poll::Pending => return Poll::Pending,
831                Poll::Ready(Some(Ok(event))) => this.ingest(event),
832                // A broken transport cannot recover, so this ends the run.
833                Poll::Ready(Some(Err(error))) => this.transport_failed(error),
834                Poll::Ready(None) => this.end_of_stream(),
835            }
836        }
837    }
838}