AgentSession

class AgentSession(val agent: AbstractAgent, val tools: ToolRegistry? = null, onWarning: (String) -> Unit = {})

One agent, one thread, one transcript.

Runs an upstream AbstractAgent and folds every event it produces into a UiTranscript that outlives the run. agui-core's foldToTranscript starts a fresh reducer per collection, which is right for one run and wrong for a conversation: a second run folded that way would begin from an empty transcript and the first answer would vanish from the screen. This holds a single UiTranscriptReducer for as long as the session lives and feeds every run into it, so the transcript grows the way the thread does. Upstream's threadId is fixed per agent, so a session is a thread.

Events are taken from AbstractAgent.runAgentObservable rather than runAgent: the latter collects on an internal Dispatchers.Default scope and reports failure only to a logger, so a caller can neither observe the events nor learn that the run failed. The observable rethrows after upstream's own HttpAgent has already turned transport failures into RUN_ERROR, and it still runs upstream's chunk transform and event verifier first -- so a stream that violates the protocol's state machine surfaces as a throw, and lands here as RunState.Failed.

The reducer is not thread-safe and asks to be fed from one coroutine. run and send hold a mutex for the length of a run, so two callers who race to start one take turns; the second waits for the first to end rather than interleaving its events into the same transcript.

This does not dispose the agent. AbstractAgent.dispose cancels its scope and closes an HTTP client it created itself, and the agent may be shared with something outside this session; the owner that constructed it closes it.

Frontend tools

A session given a ToolRegistry executes the tools in it on the client. Every run declares the registry's tools to the server, after whatever the caller's RunAgentParameters.tools names; when the agent calls one, a ToolRunner -- inserted between the agent's stream and the reducer -- assembles the call from its events, runs the executor, and hands the ToolMessage back here. The result is folded into the transcript as a TOOL_CALL_RESULT, so the call that was drawn AWAITING_RESULT is drawn COMPLETE, and once the run has finished it is sent back in a run of its own: the same tools, context and forwardedProps, a new run id, and the thread's history with the tool's answer placed directly after the assistant message that made the call -- where a model backend requires it, even when the agent said more after calling. That run may call another tool, which is answered the same way, until a run ends without calling one. run and send suspend for the whole exchange and return the state the last run ended in.

Neither upstream's ClientToolResponseHandler nor its ToolExecutionManager is used. The handler answers a tool by starting a second run inside itself and collecting it there, which is a run this session never sees: its events would not reach the transcript, and it would not take the lock the runs below take turns on. The manager is what ToolRunner replaces, for the reasons its note gives. Here the run that carries an answer is started by the same code that started the one it answers.

A tool the agent calls that the registry does not hold is left alone, so a backend tool's events fold exactly as they do with no registry at all.

A run that stops to ask -- RUN_FINISHED with an interrupt outcome, an approval or a choice only a human can give -- is not answered by a tool result, and not by the next run or send either. The protocol lets no run start on the thread until every interrupt of the one that stopped has been answered or abandoned in the input's resume list, and this class holds that line: resume is the one call that starts a run while the thread is interrupted, and run and send throw rather than start one the server would refuse -- or worse, one it would accept and proceed past the question with. The interrupts to answer are on the transcript (RunState.Finished.interrupts) and, until a run has ended cleanly since, on pendingInterrupts. A tool result folded while the thread is interrupted is kept the way a failed run's is, and goes out with the resume.

A result whose run failed -- a RUN_ERROR from the agent, or a stream that threw or was cancelled while the tool was executing -- is not sent then, because there is no finished run to answer. It is kept, and goes out with the next run or send on this session, ahead of the turn that call adds. The alternative was dropping it, which would leave the agent's history holding a call with no result, and that is a history most model backends refuse to continue. A tool cancelled mid-execution has a result too: upstream's AbstractToolExecutor catches the cancellation as it would any exception and hands back its own failure report (Tool execution failed: …), which is kept and sent for the same reason -- the history then holds an answer to the call, even if the answer is that it was stopped. A tool cancelled before it ran at all is answered by the runner with the fact that it did not run, for the same reason.

The reducer is fed only from the collecting coroutine. The runner executes tools on jobs of its own, so each result goes on a channel and the collector folds it -- before the next event, or after the stream ends -- rather than the job folding it from wherever it happens to be running.

Parameters

agent

the upstream agent to run. Its threadId names the thread this transcript is of.

tools

the tools this client executes, or null for a session that executes none. Declared on every run through this session, and answered as described above.

onWarning

see UiTranscriptReducer; this class adds one case of its own, a RUN_ERROR that arrived after the run had already ended and was therefore not folded. Last, so a call that passes it as a trailing lambda still can.

Constructors

Link copied to clipboard
constructor(agent: AbstractAgent, tools: ToolRegistry? = null, onWarning: (String) -> Unit = {})

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
val agent: AbstractAgent
Link copied to clipboard

The interrupts the thread is waiting on: those of the last run that ended cleanly, when it stopped to ask. Empty on a thread that is not waiting; while it is not empty, run and send throw unless a failed resume left its answers owed, in which case they retry it.

Link copied to clipboard
val tools: ToolRegistry?
Link copied to clipboard
val transcript: StateFlow<UiTranscript>

The transcript as of the last event, one value per event -- plus one per send, which puts the message on screen before the run that carries it has produced anything.

Functions

Link copied to clipboard
suspend fun resume(entries: List<UiResumeEntry>, parameters: RunAgentParameters? = null): RunState

Answers what the last run stopped to ask for, and runs the agent.

Link copied to clipboard
suspend fun run(parameters: RunAgentParameters? = null): RunState

Runs the agent once and folds its events into transcript, suspending until the run ends.

Link copied to clipboard
suspend fun send(message: UserMessage, parameters: RunAgentParameters? = null): RunState

Appends message to the transcript, sends it with the thread's history, and runs the agent.

suspend fun send(text: String, parameters: RunAgentParameters? = null): RunState

Sends one line of text as a user turn, under a generated message id.