Skip to content

Agent

Construction: a ToolRegistry plus an AgentConfig in, a pydantic_ai.Agent out. See Concepts for what gets composed.

AgentConfig

AgentConfig dataclass

Resolved construction parameters for a Pydantic-AI Agent.

Bundles everything build_agent needs so the call site passes one record instead of a long keyword list. A transport resolves these from its own configuration and hands the record down; toolsets and capabilities arrive already resolved to instances (never dotted paths — this substrate resolves nothing from settings).

model instance-attribute

model: Any

The Pydantic-AI model (a model string or Model instance).

instructions class-attribute instance-attribute

instructions: str | None = None

System/instructions prompt for the agent.

audit_logger class-attribute instance-attribute

audit_logger: AuditLogger | None = None

Wraps every server-side tool call for timing and success/failure records. None means no auditing.

audit_ip_address class-attribute instance-attribute

audit_ip_address: str | None = None

Client IP stamped onto every audit event this agent records (the view fills it from the driving request). None leaves the field unset.

model_settings class-attribute instance-attribute

model_settings: dict[str, Any] | None = None

Pydantic-AI ModelSettings (temperature, max_tokens, …).

retries class-attribute instance-attribute

retries: int | None = None

Default tool/output retry budget.

toolsets class-attribute instance-attribute

toolsets: Sequence[Any] | None = None

Extra Pydantic-AI toolsets composed alongside the registry tools.

capabilities class-attribute instance-attribute

capabilities: Sequence[Any] | None = None

Pydantic-AI capabilities passed to the Agent.

tool_guard class-attribute instance-attribute

tool_guard: ToolGuardConfig | None = None

Server-side destructive-tool approval policy. When set and enabled, build_agent composes a ToolGuard built from the registry's destructive tools; None or disabled leaves the agent ungated.

tool_failure class-attribute instance-attribute

tool_failure: ToolFailureConfig = field(default_factory=ToolFailureConfig)

What an unhandled tool exception costs. On by default, so a raising tool fails its own call and the run carries on, with the exception still reaching the audit logger and the Python logger. A plain record rather than ... | None because there is no third state, and None would have to mean "on" to keep that default — backwards next to tool_guard.

AttachmentInlineConfig

AttachmentInlineConfig dataclass

Which attachment types read_attachment hands back as file content.

Tunes build_attachment_toolset. media_types governs the binary attachments, where the choice is between attaching the bytes for the model to look at and returning a one-line note about a file it will never see; AttachmentInlineConfig(media_types=frozenset()) switches inlining off entirely. A textual attachment is decoded and returned as text without consulting that allowlist.

max_bytes governs both. A decoded text file reaches the provider and stays in the run's history exactly as inlined bytes do -- decoding changes the encoding, not the cost -- so a file over the limit is described rather than returned whichever branch it takes.

media_types is an allowlist rather than "everything that is not text" because bytes a provider cannot interpret make it reject the whole request: a broad rule would trade a model that cannot read your PDF for a run that does not start. max_bytes sits far below any sane upload cap because what it bounds is one request, not a stored thread — the bytes ride in a synthetic user message that stays in the run's history, so every further model request ships the file again, base64 and all.

None of it reaches the client: the bytes never travel on the event stream, and a follow-up question about the same file is answered by reading the attachment again, server-side. The decision is made on AttachmentRef.mime, which is client-declared, so a mislabelled file costs a rejected request rather than a disclosure — the store is owner-scoped either way, and the model only reaches files the acting user uploaded.

media_types class-attribute instance-attribute

media_types: frozenset[str] = frozenset(
    {"application/pdf", "image/png", "image/jpeg", "image/gif", "image/webp"}
)

Content types whose bytes are attached to the tool result; anything outside the set falls back to the one-line note.

max_bytes class-attribute instance-attribute

max_bytes: int = 4 * 1024 * 1024

Largest file, in bytes, returned rather than described -- text and binary alike. Measured against the bytes the store returns, not the declared AttachmentRef.size.

AgentDeps

AgentDeps dataclass

Per-run dependencies handed to the agent as ctx.deps.

Pydantic-AI's seam for threading request-scoped values into a run: tools, toolsets and capabilities read them off RunContext.deps instead of closing over a request, which is what lets one agent serve many requests. djangorestframework-pydantic-ai's SpecToolset already reads ctx.deps.user as its default user extractor, so a run given these deps binds the acting user with nothing passed at the call site.

For more per-run context, subclass this and build it in your transport's deps_factory (AGUIServer(deps_factory=...) for django-ag-ui); the fields below are the ones the framework itself reads. The factory belongs to the transport rather than to AgentConfig because the deps are per-request and the config is not — an agent built once serves every run.

user instance-attribute

user: Any

The acting Django user (request.user), or None for an unauthenticated run. Any because it is a Django boundary: a User, an AnonymousUser, or a project's own model.

Required, with no default. Pydantic-AI types deps as AgentDepsT = None and never validates it, so nothing downstream would catch a run built without one. A registry tool does not fail closed the way a spec tool does: it runs with no user context, audit records it with the fields it has, and the answer reads like any other. Saying user=None is one word, and it is a decision rather than an omission.

ip_address class-attribute instance-attribute

ip_address: str | None = None

The client IP this run was driven from, stamped onto every audit event AuditCapability records. Per-run, so it belongs here rather than on the capability's constructor — a transport that closes over an IP has an agent good for one request. The constructor argument remains the right home for a value that is genuinely fixed for the endpoint, and an unset run falls back to it.

state class-attribute instance-attribute

state: Any = None

AG-UI shared state for this run, inbound only.

Present so the deps satisfy StateHandler: the UI adapter validates the client's RunAgentInput.state into this field, and drops the state with a UserWarning when the deps type does not match. Nothing emits STATE_SNAPSHOT / STATE_DELTA back — a tool has to return those itself as ToolReturn metadata.

None keeps the default transport-agnostic: the adapter passes a raw mapping through unvalidated. Seed it with a Pydantic model instance to get validation, which the adapter runs against type(deps.state).

progress class-attribute instance-attribute

progress: Callable[..., None] | None = None

Where a spec's progress(...) calls go for this run, or None.

A drf-services ProgressReporter: a callable (progress, *, total, message, meta) -> None. SpecToolset reads this field by name and forwards it into the dispatch pool, so a service that declares a progress parameter reports to whatever the caller passed. Typed structurally rather than as the Protocol, which lives in an optional dependency.

Nothing here constructs one. Where a report should go is a transport's decision — an SSE frame, a task record, a log line — and a substrate that picked one would have chosen a transport it does not own. None is the honest default and costs nothing: drf-services substitutes its no-op, so a service declaring progress runs unchanged whether or not anyone is listening. A transport that wants the reports on the wire supplies the sink from its deps_factory and emits the events itself.

build_agent

build_agent

build_agent(registry: ToolRegistry, config: AgentConfig) -> Agent[AgentDeps, Any]

Build a Pydantic-AI Agent from a registry and an AgentConfig.

Each registry tool is registered as a plain Pydantic-AI tool, and config supplies the model, the toolsets and capabilities composed alongside them, and the policies below. Frontend tools declared in the AG-UI RunAgentInput are merged by the adapter, not registered here.

Three capabilities are added from config on request: an AuditCapability that times and records every tool the agent runs — registry tools and composed toolsets alike — a ToolGuard that flips destructive tools to require approval, and a ToolFailurePolicy, on unless turned off, so a raising tool fails its own call rather than the whole run. Each declares its position through get_ordering and pydantic-ai sorts them, so the list needs no pre-ordering.

A destructive tool is not confirmed unless a config asks for it. The approval interrupt exists only when config.tool_guard is enabled, and even then it reaches a tool only if that tool's source declares the mutation — a registry @tool(destructive=True), an MCP readOnlyHint of False, an x-destructive schema stamp, or an explicit require_approval name. With no tool_guard, every server-side tool here runs the moment the model calls it. The browser's own confirmation card is no substitute: it is driven by a client-registered tool's schema and never sees a tool that executes server-side. A transport describing this agent to the model should say which of the two it configured.

The agent is typed Agent[AgentDeps, ...], so every run must be given AgentDeps through deps=.

build_tool_catalog

build_tool_catalog

build_tool_catalog(
    registry: ToolRegistry,
    *,
    drf_mcp_server: Any = None,
    service_specs: dict[str, Any] | None = None,
) -> list[dict[str, Any]]

The agent's server-tool catalog for the frontend to label tool-call cards.

Server-side tools execute server-side, so their JSON Schema never reaches the browser and a web component cannot read an x-summary off it. This catalog is that channel: the component fetches it and maps tool name to label.

Each entry is {"name", "summary", "description"?}. summary is always present, resolved from @tool(summary=...) for registry tools and from display_name then title for drf-mcp ones, falling back to a prettified name. description is carried through when the source has one.

The catalog covers these sources and no others. A tool attached through a transport's capabilities= / toolsets= is not listed and its card falls back to a prettified name — a degraded label, not a broken call. Enumerating those is not possible here: pydantic-ai exposes tool names only through AbstractToolset.get_tools, which is async and needs a RunContext, while this runs at configuration time with no run in sight. Route spec tools through a transport's service_specs= to keep labels.

Parameters:

Name Type Description Default
registry ToolRegistry

The @tool registry, listed first; it wins name collisions.

required
drf_mcp_server Any

A drf-mcp server whose registered tools are appended.

None
service_specs dict[str, Any] | None

A name -> spec mapping whose tools are appended, each described by its service or selector callable's docstring.

None

Returns:

Type Description
list[dict[str, Any]]

One entry per tool, in that source order.

AgentFactoryFn

AgentFactoryFn

Bases: Protocol

The escape-hatch signature for a transport's agent_factory= argument.

A callable of this shape fully replaces build_agent, giving a project complete control over Agent construction. It receives the server-side tool registry and the calling transport's own resolved config record, which is untyped here because this substrate owns no settings namespace; each transport documents the concrete type its users receive.

The returned agent must be built with deps_type=AgentDeps. Transports hand every run an AgentDeps, and that is how the acting user reaches spec tools and how AG-UI state reaches deps.state. A factory that omits it produces an agent whose tools see no user.