ag_ui/server/run.rs
1//! The driver: an [`Agent`] plus a [`RunAgentInput`](https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/input/struct.RunAgentInput.html) in, a [`Stream`] of
2//! events out.
3//!
4//! The stream owns the agent's future and polls it itself, so this crate needs
5//! no executor of its own — no `tokio::spawn`, nothing to configure, and the
6//! same code runs on wasm. Draining the stream *is* running the agent.
7//!
8//! ```
9//! # use ag_ui::{Event, RunAgentInput, RunOutcome};
10//! # use ag_ui::server::{Agent, Result, RunContext, run};
11//! # use futures_util::StreamExt;
12//! struct Greeter;
13//!
14//! impl Agent for Greeter {
15//! type State = ();
16//! async fn run(&self, ctx: &mut RunContext<()>) -> Result<RunOutcome> {
17//! ctx.say("hello")?;
18//! Ok(RunOutcome::Success)
19//! }
20//! }
21//!
22//! # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
23//! # rt.block_on(async {
24//! let events: Vec<Event> = run(Greeter, RunAgentInput::new("thread-1", "run-1"))
25//! .map(|event| event.expect("the stream should not break"))
26//! .collect()
27//! .await;
28//!
29//! assert_eq!(events.first().map(Event::event_type), Some(ag_ui::EventType::RunStarted));
30//! assert_eq!(events.last().map(Event::event_type), Some(ag_ui::EventType::RunFinished));
31//! # });
32//! ```
33
34use std::future::Future;
35use std::pin::Pin;
36use std::task::{Context, Poll};
37
38use crate::{
39 Event, RunAgentInput, RunErrorEvent, RunFinishedEvent, RunId, RunOutcome, RunStartedEvent,
40 ThreadId,
41};
42use futures_channel::mpsc::{self, UnboundedReceiver};
43use futures_core::Stream;
44use futures_util::StreamExt as _;
45
46use crate::server::agent::Agent;
47use crate::server::cancel::CancellationToken;
48use crate::server::context::{RunContext, decode_state};
49use crate::server::emit::EventSink;
50use crate::server::error::{Error, Result};
51use crate::server::transform::{StreamTransformer, TransformerChain};
52
53/// Runs `agent` against `input` with no transformers and a fresh cancellation
54/// token.
55///
56/// [`Runner`] is the same thing with knobs.
57pub fn run<A: Agent>(agent: A, input: RunAgentInput) -> impl Stream<Item = Result<Event>> + Send {
58 Runner::new(agent).run(input)
59}
60
61/// A configured run: the agent, its transformer chain and its cancellation
62/// token.
63///
64/// ```
65/// # use ag_ui::{RunAgentInput, RunOutcome};
66/// # use ag_ui::server::{Agent, FilterToolCalls, Result, RunContext, Runner};
67/// # struct MyAgent;
68/// # impl Agent for MyAgent {
69/// # type State = ();
70/// # async fn run(&self, _ctx: &mut RunContext<()>) -> Result<RunOutcome> { Ok(RunOutcome::Success) }
71/// # }
72/// let runner = Runner::new(MyAgent).transformer(FilterToolCalls::deny(["internal_debug"]));
73/// let token = runner.cancellation_token(); // hand this to the transport
74/// let stream = runner.run(RunAgentInput::new("thread-1", "run-1"));
75/// # let _ = (token, stream);
76/// ```
77pub struct Runner<A> {
78 agent: A,
79 chain: TransformerChain,
80 cancel: CancellationToken,
81 echo_input: bool,
82}
83
84// Hand-written so that `Runner` is printable whatever the agent is: requiring
85// `A: Debug` would make the bound viral through every wrapper that holds one.
86impl<A> std::fmt::Debug for Runner<A> {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("Runner")
89 .field("agent", &std::any::type_name::<A>())
90 .field("chain", &self.chain)
91 .field("cancel", &self.cancel)
92 .field("echo_input", &self.echo_input)
93 .finish()
94 }
95}
96
97impl<A> Runner<A> {
98 /// Wraps an agent.
99 pub fn new(agent: A) -> Self {
100 Self {
101 agent,
102 chain: TransformerChain::new(),
103 cancel: CancellationToken::new(),
104 echo_input: false,
105 }
106 }
107
108 /// Appends a transformer to the chain. See [`StreamTransformer`].
109 #[must_use]
110 pub fn transformer(mut self, transformer: impl StreamTransformer + 'static) -> Self {
111 self.chain.push(transformer);
112 self
113 }
114
115 /// Replaces the whole transformer chain.
116 #[must_use]
117 pub fn transformers(mut self, chain: TransformerChain) -> Self {
118 self.chain = chain;
119 self
120 }
121
122 /// Uses an existing cancellation token instead of the fresh one.
123 #[must_use]
124 pub fn cancellation(mut self, token: CancellationToken) -> Self {
125 self.cancel = token;
126 self
127 }
128
129 /// A handle on this run's cancellation, for the transport to trip when the
130 /// client disconnects.
131 pub fn cancellation_token(&self) -> CancellationToken {
132 self.cancel.clone()
133 }
134
135 /// Echoes the request back on `RUN_STARTED`, so a recorded stream replays
136 /// without the original HTTP body. Off by default — it is the largest
137 /// payload in the protocol.
138 #[must_use]
139 pub fn echo_input(mut self, echo: bool) -> Self {
140 self.echo_input = echo;
141 self
142 }
143}
144
145impl<A: Agent> Runner<A> {
146 /// Starts the run.
147 ///
148 /// The returned stream emits `RUN_STARTED` first and exactly one of
149 /// `RUN_FINISHED` / `RUN_ERROR` last — including when the agent body does
150 /// nothing, and when it returns `Err` through a `?`.
151 ///
152 /// A panic inside the agent is not caught; it unwinds through whoever is
153 /// polling the stream, as it would through any other future. Use the
154 /// transport's own panic handling if you need a response body for that
155 /// case.
156 pub fn run(self, input: RunAgentInput) -> impl Stream<Item = Result<Event>> + Send {
157 let Self {
158 agent,
159 chain,
160 cancel,
161 echo_input,
162 } = self;
163 let (tx, rx) = mpsc::unbounded();
164 let sink = EventSink::new(tx, chain, cancel);
165 RunStream {
166 driver: Some(Box::pin(drive(agent, input, sink, echo_input))),
167 rx,
168 }
169 }
170}
171
172/// Emits `RUN_STARTED`, runs the agent, and emits the terminal event.
173///
174/// Takes the sink by value and only hands it to the context for the duration of
175/// the agent's call: whatever happens in there, the terminal event still goes
176/// through the same transformers and the same verifier.
177async fn drive<A: Agent>(agent: A, input: RunAgentInput, mut sink: EventSink, echo_input: bool) {
178 let thread_id = input.thread_id.clone();
179 let run_id = input.run_id.clone();
180
181 let mut started = RunStartedEvent::new(thread_id.clone(), run_id.clone());
182 started.parent_run_id = input.parent_run_id.clone();
183 if echo_input {
184 started.input = Some(Box::new(input.clone()));
185 }
186 if sink.emit_forced(started.into()).is_err() {
187 // Nobody is listening, or RUN_STARTED was rejected. Either way there is
188 // no run to report on.
189 return;
190 }
191
192 // Decoded before the match so the borrow of `input` ends here: the context
193 // takes the input by value on the next line.
194 let decoded = decode_state::<A::State>(&input.state);
195 let outcome = match decoded {
196 Ok(state) => {
197 let mut ctx = RunContext::from_parts(input, state, sink);
198 let outcome = agent.run(&mut ctx).await;
199 if ctx.is_terminated() {
200 // The agent emitted its own terminal event through `emit`.
201 return;
202 }
203 sink = ctx.into_sink();
204 outcome
205 }
206 Err(error) => Err(error),
207 };
208
209 terminate(&mut sink, outcome, &thread_id, &run_id);
210}
211
212/// Emits exactly one terminal event.
213fn terminate(
214 sink: &mut EventSink,
215 outcome: Result<RunOutcome>,
216 thread_id: &ThreadId,
217 run_id: &RunId,
218) {
219 let event = match outcome {
220 Ok(outcome) => match outcome.validate() {
221 Ok(()) => RunFinishedEvent::new(thread_id.clone(), run_id.clone())
222 .with_outcome(outcome)
223 .into(),
224 Err(error) => error_event(&Error::Protocol(error)),
225 },
226 Err(error) => error_event(&error),
227 };
228
229 let Err(rejected) = sink.emit_forced(event) else {
230 return;
231 };
232 // `RUN_FINISHED` can be rejected — by the verifier, for a message the agent
233 // left open. A run that ends with no terminal event at all is worse than
234 // one that ends badly, so say what went wrong instead. `RUN_ERROR` is
235 // exempt from the open-at-finish rule for exactly this reason.
236 if !rejected.is_disconnected() && !sink.is_terminated() {
237 let _ = sink.emit_forced(error_event(&rejected));
238 }
239}
240
241fn error_event(error: &Error) -> Event {
242 RunErrorEvent::new(error.to_string())
243 .with_code(error.code())
244 .into()
245}
246
247/// The stream half: drains queued events, and polls the agent when there are
248/// none.
249///
250/// The driver future is boxed so the stream is `Unpin` and needs no unsafe
251/// projection — one allocation per run.
252struct RunStream<F> {
253 driver: Option<Pin<Box<F>>>,
254 rx: UnboundedReceiver<Event>,
255}
256
257impl<F: Future<Output = ()>> Stream for RunStream<F> {
258 type Item = Result<Event>;
259
260 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
261 let this = self.get_mut();
262 loop {
263 // Events first: everything the agent has already emitted goes out
264 // before it is polled again, which is what keeps a slow agent's
265 // early output flowing.
266 match this.rx.poll_next_unpin(cx) {
267 Poll::Ready(Some(event)) => return Poll::Ready(Some(Ok(event))),
268 // The sender lives inside the driver future, so this only
269 // happens once the driver has been dropped below.
270 Poll::Ready(None) => return Poll::Ready(None),
271 Poll::Pending => {}
272 }
273
274 let Some(driver) = this.driver.as_mut() else {
275 return Poll::Pending;
276 };
277 match driver.as_mut().poll(cx) {
278 // Dropping the future drops the sink, which closes the channel
279 // once the events still queued behind it have been drained.
280 Poll::Ready(()) => this.driver = None,
281 Poll::Pending => return Poll::Pending,
282 }
283 }
284 }
285}