Skip to main content

ag_ui/server/
context.rs

1//! What an agent is handed for one run.
2
3use std::future::Future;
4
5use crate::{
6    Context, Event, Message, MessageId, ResumeEntry, RunAgentInput, RunId, StepName, SubagentRunId,
7    SubagentStartedEvent, TextMessageRole, ThreadId, Tool, ToolCallId,
8};
9use futures_channel::mpsc;
10use futures_util::future::{Either, select};
11use serde_json::Value;
12
13use crate::server::agent::AgentState;
14use crate::server::cancel::{CancellationToken, Cancelled};
15use crate::server::emit::{
16    EventReceiver, EventSink, MessageHandle, ReasoningHandle, StepGuard, SubagentHandle,
17    ToolCallHandle,
18};
19use crate::server::error::{Error, Result};
20use crate::server::state::RunState;
21use crate::server::transform::TransformerChain;
22
23/// The request, the state, the event sink and the cancellation flag — one
24/// run's whole world.
25///
26/// An agent gets `&mut RunContext<S>` and emits through it. Every emitter takes
27/// `&mut self`, which is what makes two overlapping messages a borrow-check
28/// error rather than a protocol violation discovered by a confused frontend.
29///
30/// ```
31/// # use ag_ui::RunAgentInput;
32/// # use ag_ui::server::RunContext;
33/// # let (mut ctx, _events) = RunContext::<()>::new(RunAgentInput::new("thread-1", "run-1"))?;
34/// assert_eq!(ctx.thread_id().as_str(), "thread-1");
35/// assert!(ctx.messages().is_empty());
36///
37/// let mut message = ctx.assistant_message()?;
38/// message.delta("Hello")?;
39/// message.end()?;
40/// # Ok::<(), ag_ui::server::Error>(())
41/// ```
42#[derive(Debug)]
43pub struct RunContext<S> {
44    input: RunAgentInput,
45    state: RunState<S>,
46    sink: EventSink,
47    next_message: u64,
48    next_tool_call: u64,
49    next_subagent: u64,
50}
51
52impl<S: AgentState> RunContext<S> {
53    /// Builds a context and the receiving half of its event stream.
54    ///
55    /// This is the harness for unit-testing an [`Agent`](crate::server::Agent) without
56    /// the run driver: call the agent's body, then assert on
57    /// [`EventReceiver::drain`]. Nothing emits `RUN_STARTED` here — that is the
58    /// driver's job, and skipping it lets a test exercise one method in
59    /// isolation.
60    pub fn new(input: RunAgentInput) -> Result<(Self, EventReceiver)> {
61        let (tx, rx) = mpsc::unbounded();
62        let sink = EventSink::new(tx, TransformerChain::new(), CancellationToken::new());
63        let state = decode_state(&input.state)?;
64        Ok((Self::from_parts(input, state, sink), EventReceiver::new(rx)))
65    }
66
67    /// Assembles a context from an already-decoded state.
68    ///
69    /// The run driver decodes first so that a state that does not fit `S` is
70    /// reported through the sink it still owns, as a `RUN_ERROR`.
71    pub(crate) fn from_parts(input: RunAgentInput, state: S, sink: EventSink) -> Self {
72        Self {
73            input,
74            state: RunState::new(state),
75            sink,
76            next_message: 0,
77            next_tool_call: 0,
78            next_subagent: 0,
79        }
80    }
81
82    /// The typed state, as of the last publish.
83    pub fn state(&self) -> &S {
84        self.state.get()
85    }
86
87    /// The typed state, mutably. Nothing is emitted until you call
88    /// [`publish_state`](Self::publish_state).
89    pub fn state_mut(&mut self) -> &mut S {
90        self.state.get_mut()
91    }
92
93    /// Replaces the state and publishes the change.
94    ///
95    /// The first publish of a run is a `STATE_SNAPSHOT`; later ones are a
96    /// `STATE_DELTA` unless the patch would be no smaller than the snapshot.
97    /// See [`StateManager`](crate::server::StateManager).
98    pub fn set_state(&mut self, state: &S) -> Result<()> {
99        self.state.replace(&mut self.sink, state)
100    }
101
102    /// Mutates the state in place and publishes the change.
103    ///
104    /// ```
105    /// # use ag_ui::RunAgentInput;
106    /// # use ag_ui::server::RunContext;
107    /// # use serde::{Deserialize, Serialize};
108    /// #[derive(Default, Serialize, Deserialize)]
109    /// struct Draft { revision: u32 }
110    ///
111    /// # let (mut ctx, _events) = RunContext::<Draft>::new(RunAgentInput::new("t", "r"))?;
112    /// ctx.update_state(|draft| draft.revision += 1)?;
113    /// assert_eq!(ctx.state().revision, 1);
114    /// # Ok::<(), ag_ui::server::Error>(())
115    /// ```
116    pub fn update_state(&mut self, update: impl FnOnce(&mut S)) -> Result<()> {
117        update(self.state.get_mut());
118        self.publish_state()
119    }
120
121    /// Publishes whatever [`state_mut`](Self::state_mut) left behind.
122    ///
123    /// A no-op when nothing changed since the last publish.
124    pub fn publish_state(&mut self) -> Result<()> {
125        self.state.publish(&mut self.sink)
126    }
127}
128
129impl<S> RunContext<S> {
130    /// The whole request, for anything the accessors do not cover.
131    pub fn input(&self) -> &RunAgentInput {
132        &self.input
133    }
134
135    /// The conversation this run belongs to.
136    pub fn thread_id(&self) -> &ThreadId {
137        &self.input.thread_id
138    }
139
140    /// This run's id.
141    pub fn run_id(&self) -> &RunId {
142        &self.input.run_id
143    }
144
145    /// The run that spawned this one, for nested agents.
146    pub fn parent_run_id(&self) -> Option<&RunId> {
147        self.input.parent_run_id.as_ref()
148    }
149
150    /// Conversation history, oldest first.
151    pub fn messages(&self) -> &[Message] {
152        &self.input.messages
153    }
154
155    /// What the user said last, as text.
156    ///
157    /// The turn an agent is almost always answering. Non-text parts of a
158    /// multimodal message are dropped — see
159    /// [`UserContent::to_text`](https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/message/enum.UserContent.html#method.to_text); reach into
160    /// [`RunContext::messages`] directly if the images matter.
161    ///
162    /// `None` when the history holds no user message at all, which is distinct
163    /// from a user who sent an empty one.
164    ///
165    /// ```
166    /// # use ag_ui::{RunAgentInput, Message};
167    /// # use ag_ui::server::RunContext;
168    /// let mut input = RunAgentInput::new("thread-1", "run-1");
169    /// input.messages = vec![Message::user("msg-1", "add milk")];
170    /// let (ctx, _events) = RunContext::<()>::new(input)?;
171    ///
172    /// assert_eq!(ctx.last_user_text().as_deref(), Some("add milk"));
173    /// # Ok::<(), ag_ui::server::Error>(())
174    /// ```
175    pub fn last_user_text(&self) -> Option<String> {
176        self.input
177            .messages
178            .iter()
179            .rev()
180            .find_map(|message| match message {
181                Message::User(user) => Some(user.content.to_text()),
182                _ => None,
183            })
184    }
185
186    /// Tools the client is offering for this run.
187    pub fn tools(&self) -> &[Tool] {
188        &self.input.tools
189    }
190
191    /// One offered tool by name.
192    pub fn tool(&self, name: &str) -> Option<&Tool> {
193        self.input.tools.iter().find(|tool| tool.name == name)
194    }
195
196    /// Ambient context entries.
197    pub fn context(&self) -> &[Context] {
198        &self.input.context
199    }
200
201    /// Arbitrary passthrough properties, opaque to the protocol.
202    pub fn forwarded_props(&self) -> &Value {
203        &self.input.forwarded_props
204    }
205
206    /// Answers to the interrupts a previous run paused on.
207    ///
208    /// Empty unless this request resumes a paused run.
209    pub fn resume(&self) -> &[ResumeEntry] {
210        self.input.resume.as_deref().unwrap_or_default()
211    }
212
213    /// The answer to one interrupt, by its [`Interrupt::id`].
214    ///
215    /// [`Interrupt::id`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/outcome/struct.Interrupt.html#structfield.id
216    pub fn resume_for(&self, interrupt_id: &str) -> Option<&ResumeEntry> {
217        self.resume()
218            .iter()
219            .find(|entry| entry.interrupt_id == interrupt_id)
220    }
221
222    /// Whether this request resumes a paused run.
223    pub fn is_resume(&self) -> bool {
224        self.input.is_resume()
225    }
226
227    /// Whether the run has been cancelled.
228    pub fn is_cancelled(&self) -> bool {
229        self.sink.cancel_token().is_cancelled()
230    }
231
232    /// [`Error::Cancelled`] once the run is cancelled, for use with `?`.
233    pub fn check_cancelled(&self) -> Result<()> {
234        if self.is_cancelled() {
235            return Err(Error::Cancelled);
236        }
237        Ok(())
238    }
239
240    /// A handle a transport can trip on client disconnect.
241    pub fn cancel_token(&self) -> CancellationToken {
242        self.sink.cancel_token().clone()
243    }
244
245    /// Resolves once the run is cancelled.
246    pub fn cancelled(&self) -> Cancelled {
247        self.sink.cancel_token().cancelled()
248    }
249
250    /// Races `future` against cancellation, returning `None` if cancellation
251    /// won.
252    ///
253    /// The way to make a long model call interruptible:
254    ///
255    /// ```
256    /// # use ag_ui::RunAgentInput;
257    /// # use ag_ui::server::{Error, RunContext};
258    /// # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
259    /// # rt.block_on(async {
260    /// # let (ctx, _events) = RunContext::<()>::new(RunAgentInput::new("t", "r"))?;
261    /// let answer = ctx
262    ///     .until_cancelled(async { "the model's reply" })
263    ///     .await
264    ///     .ok_or(Error::Cancelled)?;
265    /// assert_eq!(answer, "the model's reply");
266    /// # Ok::<(), Error>(())
267    /// # })?;
268    /// # Ok::<(), Error>(())
269    /// ```
270    /// Deliberately not an `async fn`: that would capture `&self` in the
271    /// returned future, and a future holding a borrow of the run context is
272    /// only `Send` if the context is `Sync` — which it is not, since a
273    /// transformer only has to be `Send`.
274    pub fn until_cancelled<F: Future>(&self, future: F) -> impl Future<Output = Option<F::Output>> {
275        let cancelled = self.cancelled();
276        async move {
277            futures_util::pin_mut!(future, cancelled);
278            match select(future, cancelled).await {
279                Either::Left((output, _)) => Some(output),
280                Either::Right(((), _)) => None,
281            }
282        }
283    }
284
285    /// Emits an event as-is — the escape hatch under the typed emitters.
286    ///
287    /// Everything the handles emit goes through here, so a raw event is
288    /// transformed and verified like any other.
289    pub fn emit(&mut self, event: Event) -> Result<()> {
290        self.sink.emit(event)
291    }
292
293    /// A fresh message id, unique within the run.
294    ///
295    /// Derived from the run id and a counter rather than a UUID: the protocol
296    /// asks for opaque strings, this crate takes no `uuid` dependency, and a
297    /// deterministic id makes a recorded stream diffable. Pass your own id to
298    /// [`message_with_id`](Self::message_with_id) when you need one.
299    pub fn new_message_id(&mut self) -> MessageId {
300        self.next_message += 1;
301        MessageId::new(format!("{}-msg-{}", self.id_prefix(), self.next_message))
302    }
303
304    /// A fresh tool call id, unique within the run.
305    pub fn new_tool_call_id(&mut self) -> ToolCallId {
306        self.next_tool_call += 1;
307        ToolCallId::new(format!("{}-call-{}", self.id_prefix(), self.next_tool_call))
308    }
309
310    /// A fresh subagent invocation id, unique within the run.
311    ///
312    /// Derived like the others, from the run id and a counter — so, like
313    /// message ids, it is unique across runs only while run ids are. A
314    /// resuming run that continues a *suspended* subagent should reuse the
315    /// suspended id instead — see [`subagent_with`](Self::subagent_with).
316    pub fn new_subagent_run_id(&mut self) -> SubagentRunId {
317        self.next_subagent += 1;
318        SubagentRunId::new(format!("{}-sub-{}", self.id_prefix(), self.next_subagent))
319    }
320
321    /// The subagent everything emitted right now is attributed to — `None`
322    /// outside any [`subagent`](Self::subagent) scope.
323    pub fn subagent_run_id(&self) -> Option<&SubagentRunId> {
324        self.sink.attribution()
325    }
326
327    /// Replaces the attribution scope and returns the previous one. Only the
328    /// subagent handle calls this, on the way in and on the way out.
329    pub(crate) fn set_attribution(
330        &mut self,
331        attribution: Option<SubagentRunId>,
332    ) -> Option<SubagentRunId> {
333        self.sink.set_attribution(attribution)
334    }
335
336    /// Announces a subagent under a fresh id and scopes everything emitted
337    /// through the returned handle to it — `SUBAGENT_STARTED` now,
338    /// `SUBAGENT_FINISHED` when the handle drops.
339    ///
340    /// `name` is the subagent's reusable type or name, for display; the id is
341    /// this invocation's alone. See [`SubagentHandle`].
342    pub fn subagent(&mut self, name: impl Into<String>) -> Result<SubagentHandle<'_, S>> {
343        let id = self.new_subagent_run_id();
344        self.subagent_with(SubagentStartedEvent::new(id, name))
345    }
346
347    /// The same, from an announcement you built — for a description, an
348    /// explicit id, or the agents-as-tools links.
349    ///
350    /// A `parent_subagent_run_id` left absent is filled from the enclosing
351    /// scope, so nesting needs no help. An explicit id is how a resuming run
352    /// continues a subagent that suspended: announce the id the suspended
353    /// invocation had, and a client transitions its group from waiting back
354    /// to running rather than drawing a second one.
355    ///
356    /// ```
357    /// # use ag_ui::{Event, EventType, RunAgentInput, SubagentStartedEvent};
358    /// # use ag_ui::server::RunContext;
359    /// # let (mut ctx, mut events) = RunContext::<()>::new(RunAgentInput::new("t", "r"))?;
360    /// let mut call = ctx.tool_call("task")?;
361    /// call.args(r#"{"brief":"find sources"}"#)?;
362    /// let (call_id, result_id) = (call.id().clone(), call.result_message_id().clone());
363    /// call.end()?;                                   // the client sees the call close…
364    ///
365    /// let announce = SubagentStartedEvent::new("researcher-7", "researcher")
366    ///     .with_parent_tool_call(call_id.clone());
367    /// let mut researcher = ctx.subagent_with(announce)?;
368    /// researcher.say("Three sources found.")?;       // …then the subagent it spawned…
369    /// researcher.finish()?;
370    ///
371    /// ctx.emit(Event::tool_call_result(result_id, call_id, "3 sources"))?;  // …then its result
372    /// let types: Vec<_> = events.drain().iter().map(Event::event_type).collect();
373    /// assert_eq!(types[3], EventType::SubagentStarted);
374    /// assert_eq!(types[7], EventType::SubagentFinished);
375    /// assert_eq!(types[8], EventType::ToolCallResult);
376    /// # Ok::<(), ag_ui::server::Error>(())
377    /// ```
378    pub fn subagent_with(
379        &mut self,
380        started: SubagentStartedEvent,
381    ) -> Result<SubagentHandle<'_, S>> {
382        SubagentHandle::start(self, started)
383    }
384
385    fn id_prefix(&self) -> &str {
386        if self.input.run_id.is_empty() {
387            "run"
388        } else {
389            self.input.run_id.as_str()
390        }
391    }
392
393    /// Opens an assistant message under a fresh id — `TEXT_MESSAGE_START`.
394    pub fn assistant_message(&mut self) -> Result<MessageHandle<'_, S>> {
395        self.message(TextMessageRole::Assistant)
396    }
397
398    /// Opens a message with the given role under a fresh id.
399    pub fn message(&mut self, role: TextMessageRole) -> Result<MessageHandle<'_, S>> {
400        let id = self.new_message_id();
401        self.message_with_id(id, role)
402    }
403
404    /// Opens a message under an id you choose.
405    pub fn message_with_id(
406        &mut self,
407        id: impl Into<MessageId>,
408        role: TextMessageRole,
409    ) -> Result<MessageHandle<'_, S>> {
410        // Two disjoint field borrows, not a borrow of the context: the handle
411        // reaches the state without being able to open a second block.
412        MessageHandle::start(&mut self.sink, &mut self.state, id.into(), role)
413    }
414
415    /// Emits a whole assistant message — start, content, end — and returns its
416    /// id.
417    pub fn say(&mut self, text: impl Into<String>) -> Result<MessageId> {
418        let mut message = self.message(TextMessageRole::Assistant)?;
419        message.delta(text)?;
420        let id = message.id().clone();
421        message.end()?;
422        Ok(id)
423    }
424
425    /// Opens a reasoning block under a fresh id — `REASONING_START`.
426    pub fn reasoning(&mut self) -> Result<ReasoningHandle<'_, S>> {
427        let id = self.new_message_id();
428        self.reasoning_with_id(id)
429    }
430
431    /// Opens a reasoning block under an id you choose.
432    pub fn reasoning_with_id(
433        &mut self,
434        id: impl Into<MessageId>,
435    ) -> Result<ReasoningHandle<'_, S>> {
436        ReasoningHandle::start(&mut self.sink, &mut self.state, id.into())
437    }
438
439    /// Emits a whole reasoning block in one call and returns its id.
440    pub fn think(&mut self, text: impl Into<String>) -> Result<MessageId> {
441        let mut reasoning = self.reasoning()?;
442        reasoning.delta(text)?;
443        let id = reasoning.id().clone();
444        reasoning.end()?;
445        Ok(id)
446    }
447
448    /// Opens a call to `name` under a fresh id — `TOOL_CALL_START`.
449    pub fn tool_call(&mut self, name: &str) -> Result<ToolCallHandle<'_, S>> {
450        let id = self.new_tool_call_id();
451        self.tool_call_with_id(id, name)
452    }
453
454    /// Opens a call to `name` under an id you choose.
455    pub fn tool_call_with_id(
456        &mut self,
457        id: impl Into<ToolCallId>,
458        name: &str,
459    ) -> Result<ToolCallHandle<'_, S>> {
460        let id = id.into();
461        let result_message_id = self.new_message_id();
462        ToolCallHandle::start(
463            &mut self.sink,
464            &mut self.state,
465            id,
466            name,
467            None,
468            result_message_id,
469        )
470    }
471
472    /// Opens a named step — `STEP_STARTED`.
473    ///
474    /// The returned guard dereferences to this context, and emits
475    /// `STEP_FINISHED` when it drops.
476    pub fn step(&mut self, name: impl Into<StepName>) -> Result<StepGuard<'_, S>> {
477        StepGuard::start(self, name.into())
478    }
479
480    /// Whether a terminal event has already gone out.
481    pub(crate) fn is_terminated(&self) -> bool {
482        self.sink.is_terminated()
483    }
484
485    /// Recovers the sink so the run driver can emit the terminal event through
486    /// the same transformers and the same verifier the agent used.
487    pub(crate) fn into_sink(self) -> EventSink {
488        self.sink
489    }
490}
491
492/// Reads `RunAgentInput::state` as `S`.
493///
494/// An absent state — JSON `null`, or the empty object clients send for "no
495/// state yet" — becomes `S::default()` rather than a deserialization error, so
496/// a stateless agent (`State = ()`) works against every client.
497pub(crate) fn decode_state<S: AgentState>(value: &Value) -> Result<S> {
498    let empty = value.is_null() || value.as_object().is_some_and(serde_json::Map::is_empty);
499    if empty {
500        return Ok(S::default());
501    }
502    Ok(serde_json::from_value(value.clone())?)
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use serde::{Deserialize, Serialize};
509    use serde_json::json;
510
511    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
512    struct Counter {
513        clicks: u32,
514    }
515
516    fn context<S: AgentState>(input: RunAgentInput) -> (RunContext<S>, EventReceiver) {
517        RunContext::new(input).expect("state should decode")
518    }
519
520    #[test]
521    fn ids_are_derived_from_the_run_id() {
522        let (mut ctx, _events) = context::<()>(RunAgentInput::new("t", "run-7"));
523        assert_eq!(ctx.new_message_id().as_str(), "run-7-msg-1");
524        assert_eq!(ctx.new_message_id().as_str(), "run-7-msg-2");
525        assert_eq!(ctx.new_tool_call_id().as_str(), "run-7-call-1");
526    }
527
528    #[test]
529    fn the_last_user_turn_is_the_one_returned_and_absence_is_not_emptiness() {
530        let (ctx, _events) = context::<()>(RunAgentInput::new("t", "r"));
531        assert_eq!(ctx.last_user_text(), None, "no user message is not \"\"");
532
533        let mut input = RunAgentInput::new("t", "r");
534        input.messages = vec![
535            Message::user("m-1", "add milk"),
536            Message::assistant("m-2", "milk it is"),
537            Message::user("m-3", "and bread"),
538        ];
539        let (ctx, _events) = context::<()>(input);
540        assert_eq!(ctx.last_user_text().as_deref(), Some("and bread"));
541    }
542
543    #[test]
544    fn an_empty_state_object_decodes_to_the_default() {
545        let mut input = RunAgentInput::new("t", "r");
546        input.state = json!({});
547        let (ctx, _events) = context::<Counter>(input);
548        assert_eq!(ctx.state(), &Counter::default());
549    }
550
551    #[test]
552    fn typed_state_comes_from_the_input() {
553        let mut input = RunAgentInput::new("t", "r");
554        input.state = json!({"clicks": 3});
555        let (ctx, _events) = context::<Counter>(input);
556        assert_eq!(ctx.state().clicks, 3);
557    }
558
559    #[test]
560    fn a_state_that_does_not_fit_is_an_error() {
561        let mut input = RunAgentInput::new("t", "r");
562        input.state = json!({"clicks": "three"});
563        let error = RunContext::<Counter>::new(input).expect_err("should not decode");
564        assert!(matches!(error, Error::Json(_)), "{error}");
565    }
566
567    #[test]
568    fn resume_entries_are_addressable_by_interrupt_id() {
569        let mut input = RunAgentInput::new("t", "r");
570        input.resume = Some(vec![ResumeEntry::resolved("i-1", json!(true))]);
571        let (ctx, _events) = context::<()>(input);
572        assert!(ctx.is_resume());
573        assert_eq!(ctx.resume().len(), 1);
574        assert!(ctx.resume_for("i-1").is_some());
575        assert!(ctx.resume_for("i-2").is_none());
576    }
577
578    #[test]
579    fn a_cancelled_run_fails_every_emit() {
580        let (mut ctx, _events) = context::<()>(RunAgentInput::new("t", "r"));
581        ctx.cancel_token().cancel();
582        assert!(ctx.is_cancelled());
583        let error = ctx.say("too late").expect_err("emit should fail");
584        assert!(error.is_cancelled(), "{error}");
585    }
586
587    #[test]
588    fn a_dropped_receiver_disconnects_the_run() {
589        let (mut ctx, events) = context::<()>(RunAgentInput::new("t", "r"));
590        drop(events);
591        let error = ctx.say("nobody home").expect_err("emit should fail");
592        assert!(error.is_disconnected(), "{error}");
593    }
594}