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#
Install the LangGraph Python SDK
uv add copilotkitpoetry add copilotkitpip install copilotkit --extra-index-url https://copilotkit.gateway.scarf.sh/simple/conda install copilotkit -c copilotkit-channelWire CopilotKit middleware into your graph
Shared state flows between your UI and your agent through agent.setState
on the frontend and state.get(...) in your graph nodes. Attach
CopilotKitMiddleware to your create_agent call so CopilotKit-specific
state is picked up alongside your own.
graph = create_agent(
model=ChatOpenAI(model="gpt-5.4"),
tools=[set_notes],
middleware=[CopilotKitMiddleware(), PreferencesInjectorMiddleware()],
state_schema=AgentState,
system_prompt=(
"You are a helpful, concise assistant. "
"The user's preferences are supplied via shared state and will be "
"added as a system message at the start of every turn. Always "
"respect them. "
"When the user asks you to remember something, or when you observe "
"something worth surfacing in the UI, call `set_notes` with the "
"FULL updated list of short note strings (existing notes + new)."
),
)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, `PreferencesInjectorMiddleware` reads this // back out of state and adds it to the system prompt — so the UI's // writes visibly steer the model. const handlePreferencesChange = (next: Preferences) => { agent.setState({ preferences: next, notes, // preserve what the agent has written } 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 ( <div data-testid="notes-card" className="w-full max-w-md p-6 bg-white rounded-2xl shadow-sm border border-[#DBDBE5] space-y-4" > <div className="flex items-start justify-between gap-3"> <div> <h2 className="text-xl font-semibold text-[#010507]">Agent notes</h2> <p className="text-xs text-[#57575B] mt-1"> 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. </p> </div> {notes.length > 0 && ( <button type="button" onClick={onClear} data-testid="notes-clear-button" className="text-[10px] uppercase tracking-[0.14em] font-medium text-[#57575B] hover:text-[#FA5F67] border border-[#DBDBE5] hover:border-[#FA5F67] rounded-full px-2.5 py-1 transition-colors" > Clear </button> )} </div> {notes.length === 0 ? ( <div data-testid="notes-empty" className="text-sm text-[#838389] italic pt-1" > No notes yet. Ask the agent to remember something. </div> ) : ( <ul data-testid="notes-list" className="list-disc list-inside space-y-1 text-sm text-[#010507]" > {notes.map((note, i) => ( <li key={i} data-testid="note-item"> {note} </li> ))} </ul> )} </div> );}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.
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.
