Skip to main content

ag_ui/server/
mod.rs

1//! Host an [AG-UI] agent in Rust.
2//!
3//! AG-UI is the protocol between a user-facing application and an agent
4//! backend: a POST carrying [`RunAgentInput`],
5//! answered by a stream of typed events. This crate is the server half —
6//! implement [`Agent`], hand it to [`run()`], and you have a stream a transport
7//! can serialize. [`ag-ui-axum`] mounts it on a router; nothing here depends on
8//! a web framework, an executor or an LLM client.
9//!
10//! ```
11//! use ag_ui::{Event, EventType, RunAgentInput, RunOutcome};
12//! use ag_ui::server::{Agent, Result, RunContext, run};
13//! use futures_util::StreamExt;
14//! use serde::{Deserialize, Serialize};
15//!
16//! /// State the client mirrors and the agent updates.
17//! #[derive(Default, Serialize, Deserialize)]
18//! struct Draft {
19//!     revision: u32,
20//!     title: String,
21//! }
22//!
23//! struct Editor;
24//!
25//! impl Agent for Editor {
26//!     type State = Draft;
27//!
28//!     async fn run(&self, ctx: &mut RunContext<Draft>) -> Result<RunOutcome> {
29//!         // A step brackets a phase of the run. Its guard emits
30//!         // STEP_FINISHED on drop, so an early `?` cannot skip it.
31//!         let mut step = ctx.step("draft")?;
32//!
33//!         // Reasoning the client can render, in its own REASONING_* block.
34//!         step.think("The user wants a title.")?;
35//!
36//!         // A message streams as TEXT_MESSAGE_START / _CONTENT* / _END.
37//!         let mut message = step.assistant_message()?;
38//!         message.delta("Naming it ")?;
39//!         message.delta("\"Q3 plan\".")?;
40//!         message.end()?;
41//!
42//!         // Publishing state diffs against the last snapshot and sends
43//!         // whichever of STATE_SNAPSHOT / STATE_DELTA is smaller.
44//!         step.update_state(|draft| {
45//!             draft.revision += 1;
46//!             draft.title = "Q3 plan".into();
47//!         })?;
48//!
49//!         drop(step); // or just let it fall out of scope
50//!         Ok(RunOutcome::Success)
51//!     }
52//! }
53//!
54//! # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
55//! # rt.block_on(async {
56//! let input = RunAgentInput::new("thread-1", "run-1");
57//! let events: Vec<Event> = run(Editor, input)
58//!     .map(|event| event.expect("the stream should not break"))
59//!     .collect()
60//!     .await;
61//!
62//! let types: Vec<EventType> = events.iter().map(Event::event_type).collect();
63//! assert_eq!(
64//!     types,
65//!     [
66//!         EventType::RunStarted,
67//!         EventType::StepStarted,
68//!         EventType::ReasoningStart,
69//!         EventType::ReasoningMessageStart,
70//!         EventType::ReasoningMessageContent,
71//!         EventType::ReasoningMessageEnd,
72//!         EventType::ReasoningEnd,
73//!         EventType::TextMessageStart,
74//!         EventType::TextMessageContent,
75//!         EventType::TextMessageContent,
76//!         EventType::TextMessageEnd,
77//!         EventType::StateSnapshot,
78//!         EventType::StepFinished,
79//!         EventType::RunFinished,
80//!     ]
81//! );
82//! # });
83//! ```
84//!
85//! # The four things that shape this API
86//!
87//! **Protocol misuse should not compile.** Event ordering is enforced by
88//! [typestate handles](emit) that borrow the run context mutably, so
89//! interleaving two messages is a borrow-check error. The handles emit their
90//! terminating event on `Drop`, so it cannot be forgotten. What the borrow
91//! checker cannot catch — raw [`emit`](RunContext::emit) calls — a runtime
92//! [ordering verifier](verify) catches, on by default.
93//!
94//! **The emit path is synchronous.** `Drop` cannot be async, so a handle cannot
95//! `await` while emitting its terminator: `msg.delta(text)?` takes no `.await`.
96//! Emitters push into an unbounded channel and the transport drains it.
97//!
98//! **Executor-agnostic.** `futures` primitives throughout, no tokio in the
99//! dependency list, no `spawn`. [`CancellationToken`] is an `AtomicBool` and a
100//! waker list rather than `tokio_util`'s. Polling the stream is what runs the
101//! agent.
102//!
103//! **One extension point.** Everything that observes or rewrites the stream is
104//! a [`StreamTransformer`]. There is no parallel builder of callbacks; the
105//! hooks other SDKs expose that way are built-in transformers here —
106//! [`FilterToolCalls`], [`ToolResultToState`].
107//!
108//! # Human in the loop
109//!
110//! Return [`RunOutcome::Interrupt`] to pause
111//! a run. The client answers, and the next request carries the answers in
112//! [`RunContext::resume`]:
113//!
114//! ```
115//! # use ag_ui::{Interrupt, ResumeStatus, RunOutcome};
116//! # use ag_ui::server::{Agent, Result, RunContext};
117//! # struct Approver;
118//! impl Agent for Approver {
119//!     type State = ();
120//!
121//!     async fn run(&self, ctx: &mut RunContext<()>) -> Result<RunOutcome> {
122//!         match ctx.resume_for("delete-everything") {
123//!             None => Ok(RunOutcome::interrupt(vec![Interrupt::new(
124//!                 "delete-everything",
125//!                 "tool_approval",
126//!             )])),
127//!             Some(answer) if answer.status == ResumeStatus::Resolved => {
128//!                 ctx.say("Done.")?;
129//!                 Ok(RunOutcome::Success)
130//!             }
131//!             Some(_) => {
132//!                 ctx.say("Cancelled.")?;
133//!                 Ok(RunOutcome::Success)
134//!             }
135//!         }
136//!     }
137//! }
138//! ```
139//!
140//! # Subagents
141//!
142//! An agent that delegates opens a [`RunContext::subagent`] scope. Everything
143//! emitted through the handle — text, tool calls, reasoning, steps, nested
144//! subagents — comes out attributed to that invocation, bracketed by
145//! `SUBAGENT_STARTED` and `SUBAGENT_FINISHED`, so a client can group the
146//! output by who produced it:
147//!
148//! ```
149//! # use ag_ui::RunOutcome;
150//! # use ag_ui::server::{Agent, Result, RunContext};
151//! # struct Supervisor;
152//! impl Agent for Supervisor {
153//!     type State = ();
154//!
155//!     async fn run(&self, ctx: &mut RunContext<()>) -> Result<RunOutcome> {
156//!         let mut planner = ctx.subagent("planner")?;
157//!         planner.say("Two tasks: scope, then risks.")?;
158//!         {
159//!             let mut estimator = planner.subagent("estimator")?;   // nested
160//!             estimator.say("About a day each.")?;
161//!         }                                                          // SUBAGENT_FINISHED
162//!         planner.finish_with(serde_json::json!({ "tasks": 2 }))?;
163//!
164//!         ctx.say("Plan ready.")?;                                   // the parent's own
165//!         Ok(RunOutcome::Success)
166//!     }
167//! }
168//! ```
169//!
170//! What a consumer sees is a producer-side choice, because a client older than
171//! subagent support fails while decoding the lifecycle events. The default
172//! sends the stream as emitted; [`SubagentVisibility::inline`] flattens it to
173//! the pre-subagent shape and [`SubagentVisibility::hidden`] keeps only the
174//! parent's own events — both are ordinary transformers.
175//!
176//! # Features
177//!
178//! - `verify` *(default)* — the ordering state machine. Off, the whole
179//!   verifier is a zero-sized type whose checks compile away.
180//!
181//! [AG-UI]: https://docs.ag-ui.com
182//! [`ag-ui-axum`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/axum/index.html
183//
184// `ag-ui-core` items are spelled as absolute links to the published rustdoc
185// rather than as intra-doc paths. `cargo doc --no-deps` — which is what CI and
186// the Pages deploy both run — cannot emit a path into a crate it is not
187// documenting, and it does not warn: a cross-crate intra-doc link silently
188// becomes literal `[text]`, and the `[text](path)` form silently becomes an
189// href of `path`, which renders as a link and 404s. See the `doc-links` job.
190//! [`RunAgentInput`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/input/struct.RunAgentInput.html
191//! [`RunOutcome::Interrupt`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/outcome/enum.RunOutcome.html#variant.Interrupt
192
193pub mod agent;
194pub mod cancel;
195pub mod context;
196pub mod emit;
197pub mod error;
198pub mod run;
199pub mod state;
200pub mod transform;
201pub mod verify;
202
203pub use agent::{Agent, AgentState, BoxAgent, DynAgent};
204pub use cancel::{CancellationToken, Cancelled};
205pub use context::RunContext;
206pub use emit::{
207    EventReceiver, MessageHandle, ReasoningHandle, StepGuard, SubagentHandle, ToolCallHandle,
208};
209pub use error::{Error, Result, Rule, VerificationError};
210pub use run::{Runner, run};
211pub use state::{StateManager, StatePublish};
212pub use transform::{
213    FilterToolCalls, StreamTransformer, SubagentFilter, SubagentVisibility, ToolResultToState,
214    TransformerChain,
215};