Shared State
Create a two-way connection between your UI and agent state.
What is shared state?#
Agentic Copilots maintain a shared state that seamlessly connects your UI with the agent's execution. This shared state system allows you to:
- Display the agent's current progress and intermediate results
- Update the agent's state through UI interactions
- React to state changes in real-time across your application
When should I use this?#
Use shared state when you want to facilitate collaboration between your agent and the user. Updates flow both ways — the agent's outputs are automatically reflected in the UI, and any inputs the user updates in the UI are automatically reflected in the agent's execution.
Reading agent state#
Subscribe a component to the agent's state with useAgent. Any time the agent
mutates its state — for example via a tool call — the hook fires and your UI
re-renders with the new values.
// Subscribe the component to agent state changes. Any time the agent // mutates its state (e.g. via its `set_notes` tool) this hook fires, // we re-render, and the sidebar panels reflect the new values. const { agent } = useAgent({ agentId: "shared-state-read-write", updates: [UseAgentUpdate.OnStateChanged], });The returned agent.state is just a plain object. Read it like any other
piece of React state and render the parts you care about — agent-written
notes, structured outputs, progress indicators, anything the agent has put
there.
Writing agent state#
The same agent object exposes a setState setter. Calling it from a UI
event handler pushes the new value into shared state, and the agent reads it
back on its next turn — so the UI's writes visibly steer the model.
// WRITE: every edit in the sidebar goes straight into agent state. // On the agent's next turn, `_inject_preferences` reads this back out // of state and prepends a preferences SystemMessage — so the UI's // writes visibly steer the model. const handlePreferencesChange = (next: Preferences) => { agent.setState({ ...(agentState as object | undefined), preferences: next, notes: agentState?.notes ?? [], } as RWAgentState); };This is what makes the channel two-way: the UI doesn't just observe the agent, it can hand the agent fresh inputs (preferences, selections, partial work) without going through the chat thread.
Rendering shared state in the UI#
Because agent.state is plain React data, the UI layer is whatever you'd
normally build. The demo on this page wires the agent's outputs into a
small card component and feeds user edits back through setState.
// Read-side render: this card reflects the agent-authored `notes` slice// of shared state. The parent page passes `state.notes` in; we never// touch agent state ourselves — we just render it. The Clear button is// a small write-back, exposed as an `onClear` prop.export function NotesCard({ notes, onClear }: NotesCardProps) { return ( <Card data-testid="notes-card" className="w-full"> <CardHeader> <div className="flex items-start justify-between gap-3"> <div className="space-y-1.5"> <CardTitle>Agent Scratch pad</CardTitle> <CardDescription> The agent writes here via its{" "} <code className="font-mono text-[11px] text-[#010507]"> set_notes </code>{" "} tool. The UI re-renders from shared state. </CardDescription> </div> {notes.length > 0 && ( <Button type="button" onClick={onClear} data-testid="notes-clear-button" variant="destructive" size="sm" className="uppercase tracking-[0.14em] text-[10px]" > Clear </Button> )} </div> </CardHeader> <CardContent> {notes.length === 0 ? ( <div data-testid="notes-empty" className="text-sm text-[#838389] italic min-h-[160px] flex items-center justify-center text-center px-4 border border-dashed border-[#E9E9EF] rounded-xl bg-[#FAFAFC]" > the agent will make observations about you and note them here! </div> ) : ( <ul data-testid="notes-list" className="space-y-2 text-sm text-[#010507]" > {notes.map((note, i) => ( <li key={i} data-testid="note-item" className="flex gap-2 rounded-lg border border-[#E9E9EF] bg-[#FAFAFC] px-3 py-2" > <span className="text-[#838389] font-mono text-xs leading-5 select-none"> {String(i + 1).padStart(2, "0")} </span> <span className="flex-1">{note}</span> </li> ))} </ul> )} </CardContent> </Card> );}Streaming partial state updates#
By default, agent state only updates between node transitions, so a long-running tool call appears as one big burst at the end. State streaming forwards a specific tool argument straight into a state key as it's being generated, so the UI can watch the answer assemble token-by-token.
from __future__ import annotations
from ag_ui_adk import AGUIToolset
from ag_ui_adk.config import PredictStateMapping
from google.adk.agents import LlmAgent
from google.adk.tools import ToolContext
from agents.shared_chat import get_model, stop_on_terminal_text
def write_document(tool_context: ToolContext, document: str) -> dict:
"""Write a document into shared state.
Whenever the user asks you to write or draft anything (essay, poem,
email, summary, etc.), call this tool with the full content as a
single string. The UI renders state["document"] live as you type.
Argument name `document` mirrors langgraph-python's `write_document`
signature so the shared D5 fixture (`tool_argument="document"`) and
the LGP-aligned PredictStateMapping below stay in lock-step.
"""
tool_context.state["document"] = document
return {"status": "ok", "length": len(document)}
_INSTRUCTION = (
"You are a collaborative writing assistant. Whenever the user asks "
"you to write, draft, or revise any piece of text, ALWAYS call the "
"`write_document` tool with the full content as a single string. "
"Never paste the document into a chat message directly — the document "
"belongs in shared state and the UI renders it live as you type."
)
shared_state_streaming_agent = LlmAgent(
name="SharedStateStreamingAgent",
model=get_model(),
instruction=_INSTRUCTION,
tools=[write_document, AGUIToolset()],
after_model_callback=stop_on_terminal_text,
)
SHARED_STATE_STREAMING_PREDICT_STATE = [
PredictStateMapping(
state_key="document",
tool="write_document",
tool_argument="document",
emit_confirm_tool=False,
stream_tool_call=True,
),
]
See State streaming for the full walkthrough,
including the corresponding useAgent subscription on the frontend.
Read-only context#
When the value is UI-owned and the agent should read it but never write
it back — current user, selected record, scroll position — reach for
useAgentContext instead of full shared state. It publishes values as a
one-way UI → agent channel that auto-unregisters on unmount.
See Agent read-only context for the full pattern.
