pydantic_ai.ui.ag_ui
AG-UI protocol integration for Pydantic AI agents.
Bases: UIEventStream[RunAgentInput, BaseEvent, AgentDepsT, OutputDataT]
UI event stream transformer for the Agent-User Interaction (AG-UI) protocol.
The AG-UI thread ID to report on RUN_STARTED and RUN_FINISHED.
A run_input takes precedence: when one is given, its
thread ID replaces whatever was passed here, with a UserWarning. Without a run input, set it to
the ID the conversation already has in your own transport, or leave it to default to a new UUID —
but note that the default is minted per stream, so a conversation that spans more than one run
needs to pass its own.
This identifies the conversation to the frontend. It is what
AGUIAdapter maps onto the agent’s conversation_id on the
request path, so passing the conversation ID the agent run itself uses keeps the frontend and
the agent’s traces correlated.
Type: str Default: field(default_factory=_generate_id)
The AG-UI run ID to report on RUN_STARTED and RUN_FINISHED.
A run_input takes precedence: when one is given, its
run ID replaces whatever was passed here, with a UserWarning. Without a run input, set it to
the ID the run already has in your own transport, or leave it to default to a new UUID.
This is the protocol’s run ID, not the agent run ID that
UIAdapter.run_stream() takes as run_id; the two are
never wired together.
Type: str Default: field(default_factory=_generate_id)
Who owns the system prompt, as UIAdapter.manage_system_prompt.
With 'server', SystemPromptParts delivered by ctx.enqueue()
are left out of the enqueued messages sent to the frontend.
Type: Literal[‘server’, ‘client’] Default: 'server'
Whether enqueued messages sent to the frontend keep FilePart and UploadedFile items, as AGUIAdapter.preserve_file_data.
Type: bool Default: False
@async
def handle_event(event: NativeEvent) -> AsyncIterator[BaseEvent]
Override to set timestamps on all AG-UI events.
AsyncIterator[BaseEvent]
Bases: UIAdapter[RunAgentInput, Message, BaseEvent, AgentDepsT, OutputDataT]
UI adapter for the Agent-User Interaction (AG-UI) protocol.
AG-UI protocol version controlling behavior thresholds.
Accepts any version string (e.g. '0.1.11'). Defaults to the version detected from
the installed ag-ui-protocol package.
Known thresholds:
< 0.1.11: emitsTHINKING_*events during streaming, dropsThinkingPartfromdump_messagesoutput.>= 0.1.11: emitsREASONING_*events with encrypted metadata during streaming, and includesThinkingPartasReasoningMessageindump_messagesoutput for full round-trip fidelity of thinking signatures and provider metadata.>= 0.1.15: emits typed multimodal input content (ImageInputContent,AudioInputContent,VideoInputContent,DocumentInputContent) instead of genericBinaryInputContent.>= 0.1.19: emitsACTIVITY_SNAPSHOTevents for tool availability, compaction, enqueued messages and MCP Apps during streaming.
load_messages always accepts ReasoningMessage and multimodal content types regardless
of this setting, and build_run_input skips inbound content types the installed
ag-ui-protocol predates rather than rejecting the request.
Type: str Default: DEFAULT_AG_UI_VERSION
Whether to round-trip FilePart and UploadedFile through reserved pydantic_ai_*
activity messages.
Defaults to False. AG-UI has no native representation for agent-generated files
(FilePart) or uploaded-file references
(UploadedFile), so when this is True they are
serialized as sidecar activity messages on dump_messages and reconstructed on
load_messages. A frontend only completes the round-trip if it sends these activity
messages back on the next request, which @ag-ui/client doesn’t do; see
Records carried as activity messages.
This is a representation setting, not a security one: honoring a reconstructed inbound
UploadedFile still requires
allow_uploaded_files, which the shared
sanitize_messages step enforces regardless of this flag. Multimodal tool-return files are
unaffected — they ride inline in ToolMessage.content.
Type: bool Default: False
Pydantic AI messages from the AG-UI run input.
Type: list[ModelMessage]
Toolset representing frontend tools from the AG-UI run input.
Type: AbstractToolset[AgentDepsT] | None
Frontend state from the AG-UI run input.
Conversation ID from the AG-UI RunAgentInput.threadId.
Translate AG-UI RunAgentInput.resume[] into Pydantic AI DeferredToolResults.
See docs.ag-ui.com/concepts/interrupts.
Each ResumeEntry is mapped to an approval keyed by the original tool_call_id.
The payload is validated against the same Pydantic model whose JSON schema is
advertised on Interrupt.response_schema, and the mapping is deny-by-default:
approval requires a payload that validates with approved=True, so a malformed
client response cannot accidentally execute a tool that requires human approval.
(Approval guards against the model acting without human sign-off; it is not an
authorization boundary against the client — see
Trust model for client-submitted messages.)
The full mapping, including which payloads raise instead of denying, is documented under Tool approval (interrupts).
Raises UserError for a payload that fails validation, and for a ResumeEntry.interrupt_id
that carries no recognizable prefix. Both are resolved inside the run stream (see
run_stream_native), so they reach the client
as a RunErrorEvent rather than escaping the streaming response.
Returns None when resume is missing or empty, or when the installed
ag-ui-protocol predates the interrupt lifecycle.
Type: DeferredToolResults | None
@classmethod
def build_run_input(cls, body: bytes) -> RunAgentInput
Build an AG-UI run input object from the request body.
A message role or input content type introduced by a protocol version newer than the
installed ag-ui-protocol is skipped with a warning rather than failing the whole request,
per the backwards-compatibility policy in pydantic_ai/ui/AGENTS.md. Only items the
installed models cannot dispatch at all are skipped: a body that is invalid for any other
reason still raises, so a client bug isn’t converted into silent misbehavior.
RunAgentInput
def build_event_stream(
) -> UIEventStream[RunAgentInput, BaseEvent, AgentDepsT, OutputDataT]
Build an AG-UI event stream transformer.
UIEventStream[RunAgentInput, BaseEvent, AgentDepsT, OutputDataT]
@async
@classmethod
def from_request(
cls,
request: Request,
*,
agent: AbstractAgent[AgentDepsT, OutputDataT],
ag_ui_version: str = DEFAULT_AG_UI_VERSION,
preserve_file_data: bool = False,
manage_system_prompt: Literal['server', 'client'] = 'server',
allowed_file_url_schemes: frozenset[str] = frozenset({'http', 'https'}),
allowed_file_url_force_download: frozenset[ForceDownloadMode] = frozenset(),
allow_uploaded_files: bool = False,
allowed_content_types: frozenset[str] | None = DEFAULT_ALLOWED_CONTENT_TYPES,
**kwargs: Any,
) -> AGUIAdapter[AgentDepsT, OutputDataT]
Extends from_request with AG-UI-specific parameters.
AGUIAdapter[AgentDepsT, OutputDataT]
@classmethod
def load_messages(
cls,
messages: Sequence[Message],
*,
preserve_file_data: bool = False,
) -> list[ModelMessage]
Transform AG-UI messages into Pydantic AI messages.
@classmethod
def dump_messages(
cls,
messages: Sequence[ModelMessage],
*,
ag_ui_version: str = DEFAULT_AG_UI_VERSION,
preserve_file_data: bool = False,
) -> list[Message]
Transform Pydantic AI messages into AG-UI messages.
Note: The round-trip dump_messages -> load_messages is not fully lossless:
ModelRequest.metadataand top-levelModelResponse.provider_detailsare lost. AG-UI has no trusted message-level carrier for framework or provider state; general client-controlled metadata must not be restored as server-side state. The one thingload_messageskeeps in the reserved__pydantic_ai__namespace is the AG-UI message id, which comes back on the last message dumped from eachModelMessage.TextPart.id,.provider_name,.provider_detailsare lost.ToolCallPart.id,.provider_name,.provider_detailsare lost.ToolCallPart.argsandNativeToolCallPart.argsthat don’t parse as a JSON object are rewritten to'{"INVALID_JSON":"<raw args>"}'(seeargs_as_json_str), so the raw string is no longer recoverable as args on reload. Unlike the live event stream, which emits them verbatim so streamed fragments stay concatenable, history has to hold a sendable value. Args that do parse are dumped as compact JSON, the form the model adapters send, so whitespace in the model’s raw string doesn’t survive.NativeToolCallPart.id,.provider_detailsare lost (only.provider_namesurvives via the prefixed tool call ID).NativeToolReturnPart.provider_detailsis lost.tool_kindis lost whenag_ui_version < '0.1.11'(before itsencrypted_valuecarrier existed), so typed tool parts reload as their base classes.- A failed, denied or interrupted (native) tool return keeps its
tool_kind, but reloads as a plainToolReturnPart/NativeToolReturnPart: only successful returns are promoted to typed parts. - A non-
'success'outcomeon a (native) tool return survives via theencrypted_valuecarrier from 0.1.11 (ToolMessagehas no outcome slot). Below that,'failed'survives viaToolMessage.error,'denied'and'retried'reload as'failed', and'interrupted'reloads as'success'. RetryFeedbackPartdumps as a message in the voice the model is shown it in — aUserMessagefor a'validation_error'or'model_retry', aSystemMessagefor'no_output'feedback in the framework’s own words — with the part itself in that message’sencrypted_value; it reloads as aRetryFeedbackPartonly from there, so a message the client wrote stays a plain prompt part. Below 0.1.11 the carrier doesn’t exist, so it reloads as that plain part holding the rendered text, and the dump warns.RetryPromptPartis dumped as the part it always meant, so a tool-bound one becomes aToolMessageand reloads as aToolReturnPartwithoutcome='retried', and a tool-less one takes theRetryFeedbackPartpath above.- Each
UserPromptPartis dumped as its ownUserMessage, so a request’s prompts reload as the same separate parts rather than merged into one. - A
NativeToolReturnPartis always emitted directly after itsNativeToolCallPart, so any part that originally sat between them — e.g. aCompactionPart— reloads after the pair instead. Provider adapters emit compaction parts outside call/return pairs, so this only affects hand-constructed histories. CachePointandUploadedFilecontent items are dropped (unlesspreserve_file_data=True).FileUrl.force_downloadis dropped whenag_ui_version < '0.1.15'(before typed multimodal content gained a metadata carrier).ThinkingPartis dropped whenag_ui_version='0.1.10'.FilePartis silently dropped unlesspreserve_file_data=True.UploadedFilein a multi-itemUserPromptPartis split into a separate activity message whenpreserve_file_data=True, which reloads as a separateUserPromptPart.MultiModalContentitems inToolReturnPart/NativeToolReturnPart.contentalways round-trip, regardless ofpreserve_file_data: the full content (files as base64/URL dicts) is serialized inline into the JSONToolMessage.contentand rehydrated on reload through theToolReturnContentunion. The same serialization is used for both history (dump_messages) and the live event stream (ToolCallResultEvent.content), so files survive either round-trip.- An MCP Apps record in
ToolReturnPart.metadatais dumped as anmcp-appsactivity message after the tool result, whichload_messagesignores, so the record is not restored. - Part ordering within a
ModelResponsemay change when text follows tool calls.
list[Message] — A list of AG-UI Message objects.
messages : Sequence[ModelMessage]
A sequence of ModelMessage objects to convert.
ag_ui_version : str Default: DEFAULT_AG_UI_VERSION
AG-UI protocol version controlling ThinkingPart emission.
preserve_file_data : bool Default: False
Whether to include FilePart and UploadedFile items as ActivityMessages.
(Multimodal tool-return files always ride inline in ToolMessage.content and are unaffected.)
The default AG-UI version, auto-detected from the installed ag-ui-protocol package.
Type: str Default: detect_ag_ui_version()