Expand description
The high-level API: a conversation you send text to.
Thread and run are the protocol’s words; Session is this crate’s. The
wire carries a threadId and a runId and nothing else — there is no
session on it, and no thread object either, only an id — so a Session
sits over that id, adding the transport, the conversation so far, the
typed state and the tools. It is deliberately not called Thread:
borrowing the name would imply a protocol entity that does not exist.
RemoteAgent gives you events. A UI does not want events — it
wants “this message grew by three characters”, “the state changed, here it
is typed”, “the agent is waiting for you to approve something”. A Session
yields Updates instead of raw events.
Everything the protocol makes fiddly happens inside: chunk events are normalized, the stream is verified, deltas are folded into messages, and the next run automatically carries the conversation so far.
use ag_ui::client::{Session, Update, transport::ReplayTransport};
use ag_ui::{Event, TextMessageRole};
use futures_util::StreamExt;
let transport = ReplayTransport::new([
Event::run_started("thread-1", "run-1"),
Event::text_message_start("msg-1", TextMessageRole::Assistant),
Event::text_message_content("msg-1", "Sunny."),
Event::text_message_end("msg-1"),
Event::run_finished_success("thread-1", "run-1"),
]);
let mut session = Session::<_>::new(transport, "thread-1");
let mut run = session.send("what is the weather?");
while let Some(update) = run.next().await {
if let Update::Message(message) = update {
println!("{}: {:?}", message.id, message.change);
}
}
drop(run);
// The user's turn and the agent's reply are both in the thread now, so the
// next `send` carries them.
assert_eq!(session.messages().len(), 2);§One update is one event, and the order is the nesting
The stream is per event, not per entity. A reply that streams in forty
deltas is forty Update::Messages under one id; two tool calls in flight
— which a model produces whenever it asks for two things at once —
interleave, and only their ids separate them.
So whatever the run nested survives as arrival order and nothing else. An
agent that publishes state while a tool call is open (which
ag_ui::server’s handles support, and the protocol allows because
STATE_* is unordered) puts the STATE_* event between that call’s
TOOL_CALL_ARGS and its TOOL_CALL_END — and the Update::State that
comes out carries no mention of the call. That is not an omission to be
fixed by adding a field: under parallel calls two calls are open at once and
the wire itself does not say which one the state belongs to, so any
attribution would be invented. The ordering is the contract.
A renderer that draws in arrival order therefore shows what happened. One
that buffers by entity — collecting a call’s arguments so it can draw the
call on one line when it closes — is choosing to reorder: everything that
arrived while the call was open now draws before it. For a terminal that is
often the right trade, and examples/board-watch makes it deliberately and
pins the consequence in a test. It is only a bug when it is an accident.
Structs§
- Message
Update - A message that changed, and the message as it now stands.
- Reasoning
Update - Reasoning that changed, and the reasoning as it now stands.
- RunStream
- One run, as a stream of
Updates. - Session
- A conversation with an agent.
- Session
Builder - Builds a
Session. - Subagent
Update - A subagent that changed, and the subagent as it now stands.