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}