requires_approval=True: Pydantic AI defers that call, AgentOS asks a person, and the run continues with the approval, the edited arguments or the rejection.
Tested with
pydantic-ai-slim 2.62.0 and Python 3.13, using the conformance kit: 22 checks passed, including an approved, an edited and a rejected refund.The bundle
vendor/ folder; see SDKs. PYDANTIC_AI_NO_BANNER=1 keeps Pydantic AI’s start-up banner out of the container’s log.
How the model is wired
OpenAIProvider takes an OpenAI client, so the agent hands it one made from inv.llm.openai_kwargs(asynchronous=True): the gateway’s base URL, the run token in the x-agentos-run-token header, and an HTTP client that never sends the token anywhere else. The model name from config.models reaches the gateway exactly as written (anthropic/claude-sonnet-4-5); Pydantic AI shows it as openai:anthropic/claude-sonnet-4-5 in its own logs, but sends the name unchanged.
The agent is built inside the handler, on every invocation, because the token belongs to one invocation. The handler is async, which the SDK supports, so it uses the async state and ask calls (aget, aput, aask). With max_retries=0 a guardrail block isn’t retried; agentos.guardrail_block(error) finds it inside Pydantic AI’s ModelRequestRejectedError, and the run fails with blocked_by_guardrail. See Guardrails.
How the approval works
1
Pydantic AI defers the call
The agent’s
output_type includes DeferredToolRequests, so a call to issue_refund ends the run with those requests instead of running the tool. The handler keeps the conversation in the run’s state (ModelMessagesTypeAdapter, as JSON) and asks one AgentOS hold with an item per call: the item’s id is the tool call id and its data the call’s arguments.2
A person decides
The run is held until someone decides in the Review queue or through the Holds API.
3
The run continues
The handler loads the conversation and runs the agent again with
deferred_tool_results: True for an approval, ToolApproved(override_args=…) for an edit, ToolDenied(comment) for a rejection. Pydantic AI runs the approved calls and the model answers. If the model asks for another refund, the same steps repeat.Run it
1
Zip and deploy
Put the SDK wheel in
vendor/ (see SDKs), set config.models.main to a model from your Models page, then zip the folder’s contents and upload them:2
Ask a question
3
Ask for a refund
GET …/holds/h_12).Test it before you deploy
The conformance kit builds the bundle and runs it against a mock platform whose model followscall:<tool> {arguments} in a message, so these cases drive the tools and every decision without a real model:
conformance.yaml
Things to know
- The conversation travels in run state. A run’s state value can be at most 1 MiB, so a very long conversation needs trimming before it is kept.
- Code before the ask runs once. A start is invoked once; the resume continues from the kept conversation, so the model isn’t asked the first question again.
- An expired ask has no decided items, so the example keeps the waiting call ids in run state and denies every one of them; the model then explains that nothing was refunded. Change
decisions()if expiry should mean something else for you.
Next steps
Human approvals
Everything an ask can carry: several items, details, files, an expiry.
More frameworks
The same agent in other frameworks and languages.

