showcase/shell-docs/src/content/docs/integrations/pydantic-ai/generative-ui/state-rendering.mdx
<IframeSwitcher id="agent-state-example" exampleUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/agentic_generative_ui?sidebar=false&chatDefaultOpen=false" codeUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/agentic_generative_ui?view=code&sidebar=false&codeLayout=tabs" exampleLabel="Demo" codeLabel="Code" height="700px" />
All Pydantic AI Agents are stateful. This means that as your agent progresses through nodes, a state object is passed between them perserving the overall state of a session. CopilotKit allows you to render this state in your application with custom UI components, which we call Agentic Generative UI.
Rendering the state of your agent in the UI is useful when you want to provide the user with feedback about the overall state of a session. A great example of this is a situation where a user and an agent are working together to solve a problem. The agent can store a draft in its state which is then rendered in the UI.
Create your Pydantic AI agent with a stateful structure. Here's a complete example that tracks searches:
```python title="agent.py"
from textwrap import dedent
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.ag_ui import StateDeps
from ag_ui.core import StateSnapshotEvent, EventType
class Search(BaseModel):
query: str
done: bool
class AgentState(BaseModel):
searches: list[Search] = Field(default_factory=list)
agent = Agent("openai:gpt-5.4-mini", deps_type=StateDeps[AgentState])
@agent.tool
async def add_search(
ctx: RunContext[StateDeps[AgentState]], new_query: str
) -> StateSnapshotEvent:
"""Add a search to the agent's list of searches."""
new_search = Search(query=new_query, done=False)
searches = ctx.deps.state.searches
searches.append(new_search)
agent_state = AgentState(searches=searches)
ctx.deps.state = agent_state
return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=agent_state)
@agent.tool
async def run_searches(ctx: RunContext[StateDeps[AgentState]]) -> StateSnapshotEvent:
"""Run the searches in the agent's state."""
searches = ctx.deps.state.searches
for search in searches:
await asyncio.sleep(1)
search.done = True
agent_state = AgentState(searches=searches)
ctx.deps.state = agent_state
return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=agent_state)
@agent.instructions()
async def search_instructions(ctx: RunContext[StateDeps[AgentState]]) -> str:
"""Instructions for the search agent."""
return dedent(
f"""
You are a helpful assistant for storing searches.
IMPORTANT:
- Use the `add_search` tool to add a search to the agent's state
- After using the `add_search` tool, YOU MUST ALWAYS use the `run_searches` tool to run the searches
- ONLY USE THE `add_search` TOOL ONCE FOR A GIVEN QUERY
Current searches:
{ctx.deps.state.model_dump_json(indent=2)}
"""
)
app = agent.to_ag_ui(deps=StateDeps(AgentState()))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
```
```tsx title="app/page.tsx"
// ...
// ...
// Define the state of the agent, should match the state of your Pydantic AI Agent.
type AgentState = {
searches: {
query: string;
done: boolean;
}[];
};
function YourMainContent() {
// ...
// [!code highlight:13]
// styles omitted for brevity
useAgent({
agentId: "my_agent",
render: ({ state }) => (
<div>
{state.searches?.map((search, index) => (
<div key={index}>
{search.done ? "✅" : "❌"} {search.query}{search.done ? "" : "..."}
</div>
))}
</div>
),
});
// ...
return <div>...</div>;
}
```
<Callout type="warn" title="Important">
The `name` parameter must exactly match the agent name you defined in your CopilotRuntime configuration (e.g., `my_agent` from the quickstart).
</Callout>
```tsx title="app/page.tsx"
// ...
// Define the state of the agent, should match the state of your Pydantic AI Agent.
type AgentState = {
searches: {
query: string;
done: boolean;
}[];
};
function YourMainContent() {
// ...
// [!code highlight:3]
const { agent } = useAgent({
agentId: "my_agent",
})
// ...
return (
<div>
<div className="flex flex-col gap-2 mt-4">
{agent.state?.searches?.map((search, index) => (
<div key={index} className="flex flex-row">
{search.done ? "✅" : "❌"} {search.query}
</div>
))}
</div>
</div>
)
}
```
<Callout type="warn" title="Important">
The `name` parameter must exactly match the agent name you defined in your CopilotRuntime configuration (e.g., `my_agent` from the quickstart).
</Callout>
You've now created a component that will render the agent's state in the chat.
<video
src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/agentic-generative-ui.mp4"
className="rounded-lg shadow-xl"
loop
playsInline
controls
autoPlay
muted
/>