Skip to main content

Module session

Module session 

Source
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§

MessageUpdate
A message that changed, and the message as it now stands.
ReasoningUpdate
Reasoning that changed, and the reasoning as it now stands.
RunStream
One run, as a stream of Updates.
Session
A conversation with an agent.
SessionBuilder
Builds a Session.
SubagentUpdate
A subagent that changed, and the subagent as it now stands.

Enums§

RunEnd
How a run ended.
Update
Something a view should react to.