Spring AI
ag-ui-spring-ai adapts a Spring AI ChatClient into an AG-UI Agent. Any model Spring AI supports (OpenAI, Anthropic, Ollama and others) can then drive an AG-UI front end, along with your advisors, chat memory, default prompts and tools.
All three are at version
2.0.0 and target Spring AI 2.x. Add one of the two starters, not both.
Zero-code setup
ChatClient.Builder for the model. The starter builds a SpringAiAgent from it and serves it at /agent. This happens only when you have not defined an Agent bean yourself.
Shared state can be enabled with properties:
Agent bean yourself. The endpoint auto-configuration still applies:
SpringAiAgent
Event mapping
A run produces this sequence:RUN_ERROR event instead of signalling onError.
Reasoning
Reasoning is detected from inline<think>...</think> tags in the streamed text. This is the provider-neutral convention that reasoning models use. Text inside the tags becomes REASONING_* events, and text outside becomes TEXT_MESSAGE_* events. Tags that are split across chunks are reassembled. Models that don’t use the tags are unaffected.
A ReasoningMessage in the incoming history is sent back to the model as an assistant message wrapped in the same tags, so reasoning survives the round trip. Blank reasoning messages are dropped.
Tools
The agent distinguishes two kinds of tools:- Client tools come from
RunAgentInput.tools. They are advertised to the model, and calls to them are emitted asTOOL_CALL_*events for the front end to execute. The front end sends the result back as aToolMessageon the next run. - Backend tools registered with the
SpringAiAgentbuilder’s.tools(...)are executed on the server. When the model calls one, the agent emits theTOOL_CALL_*events, runs the tool through Spring AI’sToolCallingManager, emitsTOOL_CALL_RESULT, and prompts the model again.
ChatClient builder’s defaultTools(...) are picked up by the adapter only when client tools, shared state, explicit backend tools or interrupt tools activate its tool loop. Otherwise, the adapter leaves tool handling to the ChatClient and does not guarantee tool-call or tool-result events. Register backend tools with the SpringAiAgent builder’s .tools(...) when you need those events.
If the model calls both kinds in one turn, the agent emits the backend results and then ends the run so the front end can handle the client calls. While the agent controls the loop, it turns off the ChatClient’s own tool execution so that tools don’t run twice.
Each TOOL_CALL_RESULT gets its own message ID with role: tool. Front ends such as CopilotKit key messages by ID. Reusing the assistant message’s ID would overwrite the tool call, and any generative UI rendered from it.
Providers that leave out tool-call IDs, such as Ollama, get a generated ID.
Shared state
WhenshareState(true) is set, the agent:
- Emits a
STATE_SNAPSHOTwithRunAgentInput.stateat the start of the run. - Adds the current state to the prompt and advertises an
update_statetool that takes the complete new state. - Intercepts the model’s
update_statecalls and emits them as state events. They are not exposed as tool calls.
DELTA mode the agent computes the RFC 6902 patch against the run’s input state itself, so the result doesn’t depend on the model’s ability to write JSON Patch.
Interrupts
WithemitInterruptOutcome(true), a run that stops with pending client tool calls ends with a RunFinishedEvent that carries an InterruptOutcome. The outcome holds one Interrupt (reason "tool_call") per pending call. The TOOL_CALL_* events are still emitted, so clients that ignore outcomes keep working.
Tools passed to interruptTools(...) always pause the run, whatever that flag is set to. Each one produces an Interrupt with reason "input_required" and the tool’s parameters as its responseSchema. On the next run, the client’s Resume entries are passed back to the model as the tools’ results. See Interrupts.
Messages snapshot
WithemitMessagesSnapshot(true), the agent emits a MESSAGES_SNAPSHOT just before RUN_FINISHED. It contains the input messages plus the assistant and backend tool-result messages produced in this run. Streamed reasoning is not included. The snapshot is additional to the streamed events, which still carry the same messages.