ag_ui/client/mod.rs
1//! Consume a remote [AG-UI] agent: turn its event stream into messages and
2//! state.
3//!
4//! An AG-UI run arrives as deltas — a message opens, text arrives a fragment at
5//! a time, tool arguments accumulate as partial JSON, state moves by RFC 6902
6//! patch, and the run may pause to ask a human something. This crate is the
7//! consumer half of that protocol: the state machines that fold a stream back
8//! into a conversation, the wire-format decoder that feeds them, and two levels
9//! of API over the top.
10//!
11//! ```
12//! use ag_ui::client::{RunEnd, Session, Update, transport::ReplayTransport};
13//! use ag_ui::{Event, PatchOperation, TextMessageRole};
14//! use futures_util::StreamExt;
15//! use serde::Deserialize;
16//!
17//! /// The agent's state, in your own type.
18//! #[derive(Clone, Debug, Deserialize, PartialEq)]
19//! struct Weather {
20//! checked: bool,
21//! }
22//!
23//! # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
24//! # rt.block_on(async {
25//! // A transport that replays a scripted run, so this example needs no network.
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", "It is "),
30//! Event::text_message_content("msg-1", "sunny."),
31//! Event::text_message_end("msg-1"),
32//! Event::state_delta(vec![PatchOperation::add("/checked", true)]),
33//! Event::run_finished_success("thread-1", "run-1"),
34//! ]);
35//!
36//! let mut session = Session::new(transport, "thread-1");
37//! let mut weather = None;
38//! let mut ended = None;
39//!
40//! let mut run = session.send("what is the weather?");
41//! while let Some(update) = run.next().await {
42//! match update {
43//! Update::Message(message) => println!("{:?}", message.change),
44//! // The state arrives already typed — and this is where the type
45//! // comes from, so `Session` needs no turbofish.
46//! Update::State(state) => weather = Some(state),
47//! Update::Error(error) => eprintln!("{error}"),
48//! Update::Done(end) => ended = Some(end),
49//! _ => {}
50//! }
51//! }
52//! drop(run);
53//!
54//! assert!(matches!(ended, Some(RunEnd::Success { .. })));
55//! assert_eq!(session.messages().len(), 2);
56//! assert_eq!(weather, Some(Weather { checked: true }));
57//! // The raw JSON is always there too, whether or not it fits the type.
58//! assert_eq!(session.raw_state()["checked"], true);
59//! # });
60//! ```
61//!
62//! # Two levels
63//!
64//! [`RemoteAgent`] is the low level:
65//! [`agent.run(params)`](RemoteAgent::run) gives you the events exactly as the
66//! agent sent them, unassembled. That is what a proxy, a recorder or a bridge
67//! to another protocol wants.
68//!
69//! It is called `RemoteAgent` and not `Agent` because the other half of this
70//! SDK already owns that word from the other side:
71//! [`crate::server::Agent`] is the trait you implement to *be* an agent, and an
72//! agent that calls another agent imports both.
73//!
74//! [`crate::server::Agent`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/server/agent/trait.Agent.html
75//!
76//! [`Session`] is the high level: a thread, its accumulated messages, and typed
77//! state. [`session.send(text)`](Session::send) yields [`Update`]s — "this
78//! message grew", "the state changed", "the agent is waiting on you" — with
79//! chunk normalization, protocol verification and delta application already
80//! done.
81//!
82//! # Tools are yours to offer
83//!
84//! AG-UI has no tool discovery and no negotiation. The tool set travels on
85//! every request, from the client, and an agent cannot ask for one it was not
86//! sent — so offering none to an agent that needs one does not produce a
87//! missing-tool error from this crate. It produces the *agent's* own error
88//! ("the client offered no add_task tool", or whatever that agent says),
89//! arriving as an ordinary failed run, which reads like a bug in the agent and
90//! is not one. A client written against no particular agent therefore has to be
91//! configured with a tool set the way it is configured with a URL:
92//! [`SessionBuilder::tools`], or [`Session::set_tools`] from the next run on.
93//!
94//! # The pieces underneath
95//!
96//! - [`apply`] — the event applier. Deltas in, materialised messages and state
97//! out, plus a report of what changed so a view can redraw one row.
98//! - [`chunks`] — normalizes `*_CHUNK` events into explicit start/content/end
99//! triples. Chunks carry their id only on the first one, so this stage
100//! remembers.
101//! - [`verify`] — the ordering rules, checked client-side as the TypeScript SDK
102//! does. A malformed stream produces one clear error instead of a confused UI.
103//! - [`interrupts`] — the human-in-the-loop round trip.
104//! - [`transport`] — where events come from: [`Transport`], an SSE decoder, a
105//! `reqwest` client, and a replay transport for tests.
106//!
107//! # Executor-agnostic, transport-agnostic
108//!
109//! Only [`transport`] is async. Everything else — application, normalization,
110//! verification — is a plain synchronous state machine you can drive from a
111//! loop, a test, or an event handler.
112//!
113//! The one async layer is a trait, so a wasm frontend or a non-tokio runtime
114//! substitutes its own. `cargo check --no-default-features` is a CI job
115//! precisely to keep that true: it must not pull in `reqwest` or tokio.
116//!
117//! # Features
118//!
119// Gated because a feature list names items the current build may not have, and
120// a link to one of those is a rustdoc error rather than a dead link. See
121// `doc-features` in CI.
122# and",
125 doc = " [`HttpAgent`], backed by `reqwest`. Disable it for wasm or for a custom",
126 doc = " transport, and the dependency disappears with it."
127)]
128#![cfg_attr(
129 not(feature = "http"),
130 doc = "- `http` *(default, off in this build)* — `transport::HttpTransport` and",
131 doc = " `HttpAgent`, backed by `reqwest`. Off, as here, the dependency disappears",
132 doc = " with it and you bring your own transport."
133)]
134//!
135//! [AG-UI]: https://docs.ag-ui.com
136
137pub mod agent;
138pub mod apply;
139pub mod chunks;
140pub mod error;
141pub mod interrupts;
142pub mod session;
143pub mod transport;
144pub mod verify;
145
146pub use agent::{RemoteAgent, RunParams};
147pub use apply::{
148 Applier, Changed, MessageChange, MessageChangeKind, ReasoningChange, ReasoningChangeKind,
149 Subagent, SubagentChange, SubagentChangeKind, SubagentStatus,
150};
151pub use chunks::{ChunkNormalizer, normalize_all};
152pub use error::{Error, Result};
153pub use interrupts::{InterruptExt, ResumeBuilder, interrupts_of, resume_run};
154pub use session::{
155 MessageUpdate, ReasoningUpdate, RunEnd, RunStream, Session, SessionBuilder, SubagentUpdate,
156 Update,
157};
158pub use transport::{EventStream, Transport};
159pub use verify::{Verifier, verify_all};
160
161#[cfg(feature = "http")]
162pub use agent::{HttpAgent, HttpAgentBuilder};