migrating-openai-agents-sdk-to-pydantic-ai

Migrate Python OpenAI Agents SDK applications to Pydantic AI and, when warranted, Pydantic AI Harness. Use for `agents.Agent`, `Runner`, function tools, handoffs, guardrails, sessions, human approval, streaming, or `SandboxAgent`. Do not use for applications built directly on the OpenAI Responses API without the Agents SDK runtime.

Install
npx skills add 'https://github.com/pydantic/pydantic-ai/tree/main/pydantic_ai_slim/pydantic_ai/.agents/skills/migrating-openai-agents-sdk-to-pydantic-ai'
Download bundle ↓
main · 9e9fdc4Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

references/CONCEPT-MAPPING.md

references/CONCEPT-MAPPING.mdBrowse 4 files
View on GitHub
← Back to SKILL.md

Concept Mapping

Use this reference only for features present in the active source path. Confirm the source and target behavior against the installed versions before editing code.

Core agent and tools

OpenAI Agents SDKFirst Pydantic AI seamWhat to verify
Agent, Runner.run() / run_sync()reusable Agent, run() / run_sync()input, output, errors, model requests, and tool loop
static or callable instructionsinstructions or @agent.instructionsdynamic values and what becomes model-visible
RunContextWrapper.contexttyped deps and RunContext.depstrusted identity and clients never become tool arguments
@function_tool / FunctionTool@agent.tool, @agent.tool_plain, Tool, or a toolsetname, description, schema, validation, retries, error shape, and side effects
output_typeoutput_type, optionally NativeOutput or ToolOutputprovider transport, validation, retry, and public wire shape
tool_use_behavior terminal resultoutput function or ToolOutputwhich call wins, whether sibling tools execute, and terminal value
ModelSettings and RunConfigagent/run model, model_settings, UsageLimits, and hooksprecedence, provider support, retry/counting, and failure behavior
ModelBehaviorError / tool input correctionPydantic validation plus ModelRetry where the model can correct the callretry budget and caller-visible terminal error
Agent.clone(...) variantsconstruct an explicit configured variant; use Agent.override(...) only for a scoped temporary overrideshared tool containers, concurrent isolation, and variant lifetime

OpenAI hosted tools do not all share one replacement:

  • Map web search, file search, code execution, image generation, and hosted MCP to Pydantic AI native tools only when the model profile supports them and their outputs/events are sufficient.
  • Map local function tools and local MCP servers to Pydantic AI tools or MCPToolset.
  • Treat OpenAI ComputerTool, ShellTool, ApplyPatchTool, and SandboxAgent as execution-boundary decisions. Harness FileSystem, Shell, Coder, and ModalSandbox are candidates, not drop-in equivalents. Preserve workspace, process, network, secret, and cleanup boundaries with real integration tests.
  • OpenAI hosted execution and skills may be provider-owned. Harness skills expose SKILL.md guidance to the model; they do not imply the same hosted container, file materialization, or provider tool behavior.

Orchestration

OpenAI exposes two distinct LLM-directed patterns:

Source behaviorCandidate targetSemantic difference
Agent.as_tool()a Pydantic AI tool that runs a specialist, or Harness SubAgentsmanager remains in control in both designs, but history, usage, limits, approval, and event propagation require explicit tests
handoffapplication router, a typed routing output followed by another agent run, or an explicit orchestration layerOpenAI changes the active agent inside the same run; a Pydantic AI tool call normally returns to the caller

Do not replace a handoff with a subagent merely because both invoke a specialist. Inventory these contracts first:

  • whether the specialist or manager writes the final answer;
  • whether the receiving agent sees full, filtered, or nested history;
  • whether handoff metadata is model-generated and validated;
  • which input/output guardrails run;
  • lifecycle event names and ordering;
  • whether last_agent selects the next conversational turn;
  • parent/child usage, limits, retries, and cancellation.

Use plain Python for deterministic routing. Use pydantic-graph only when explicit typed nodes, branching, or persisted workflow state remain useful.

Guardrails and hooks

Harness provides InputGuardrail, OutputGuardrail, and ToolGuardrail, but matching names are not parity evidence.

OpenAI behaviorRequired decision or proof
input guardrails apply to the first agent onlyidentify the actual public input boundary and test handoff/subagent paths
input guardrails run in parallel by defaultchoose sequential blocking when no model/tool work may start, or accept and test speculative work
output guardrails apply to the final agent onlyprove the same terminal boundary and sanitization/error contract
tool guardrails wrap eligible local tools, not all hosted tools or handoffsenumerate tools actually covered and enforce security below the model layer
tripwires raise SDK-specific exceptionspreserve the API error shape with an adapter or accept a documented change
RunHooks span a run and AgentHooks scope to one agentmap observations to Pydantic AI hooks/capabilities and test order across delegation
call_model_input_filter replaces model input immediately before a requestuse a focused model-request hook or message-history processor and prove every request shape

Guardrails are policy, not authentication or isolation. Authorization belongs inside application services and protected tools, using authenticated dependencies.

State, sessions, and approval

Choose one state strategy per observed contract:

OpenAI sourceMeaningTarget owner
result.to_input_list()caller-managed replay-ready conversation inputcore message_history, serialized with ModelMessagesTypeAdapter; application owns storage
SessionSDK loads, merges, and persists client-managed historyapplication history repository, or Harness StepPersistence only when its settled snapshots/event/effect semantics are wanted
previous_response_idOpenAI Responses server-side chainOpenAIResponsesModelSettings.openai_previous_response_id; verify storage/ZDR and reasoning continuity
OpenAI conversation_idOpenAI Conversations API stateOpenAIResponsesModelSettings.openai_conversation_id; do not confuse it with Pydantic AI's correlation conversation_id
RunState plus interruptionsresumable paused execution with approval decisionscore deferred tools with inline HandleDeferredToolCalls or stored DeferredToolRequests/DeferredToolResults, Harness step persistence, or a durable integration according to crash/replay requirements

Pydantic AI's conversation_id groups runs and traces; it is not itself a message store. Message history preserves conversation context, not arbitrary workflow/checkpoint state.

For human approval:

  1. Use deferred tools or raise ApprovalRequired based on the call and trusted dependencies.
  2. Choose the flow per pending call: resolve with HandleDeferredToolCalls when the decision is available during the same call; otherwise include DeferredToolRequests in output_type, store the paused messages and complete request—or an equivalent pending-action record with category, validated arguments, and metadata—at an authenticated server-side boundary, and resume in a later run with DeferredToolResults, a new run ID, and the same conversation ID. A handler may resolve some calls and let the rest bubble up.
  3. Re-check authorization and idempotency inside the tool before the side effect.
  4. Test approve, deny, foreign/unknown ID, stale schema, and duplicate decisions. For the later-run flow, also test duplicate resume and process restart to the extent the source promised them.

Use a Pydantic AI durable integration when the source path promises replay or crash recovery across model/tool steps. Use Harness StepPersistence when its snapshot, event-log, continuation/fork, and effect-ledger contract fits. Neither follows merely from the word "session."

Streaming and observability

OpenAI run_streamed().stream_events() can expose raw Responses events, run-item events, and agent lifecycle events. Pydantic AI exposes model deltas, tool/lifecycle events, and final results through several APIs; raw event types and completion timing differ.

Choose the smallest Pydantic AI streaming surface that includes the required lifecycle, then adapt it to the existing public schema. run_stream() commits the first matching output and may skip tool calls emitted alongside or after it; use a loop-completing event or graph surface unless that terminal behavior is part of the source contract. Test incremental delivery, order, IDs, tool and final events, approval interruption, cancellation, early consumer exit, and terminal errors with the real client boundary.

OpenAI tracing is enabled by default and has OpenAI-specific span/export behavior. Pydantic AI uses OpenTelemetry and integrates directly with Logfire. Retaining an existing exporter, dual-running temporarily, and switching to Logfire have different dashboard, alert, privacy, retention, and cost consequences. Trace similarity can corroborate a test, but cannot prove state, authorization, exactly-once side effects, or public streaming delivery.

Primary references: OpenAI Agents SDK, agents, running agents, orchestration, sessions, guardrails, human-in-the-loop, streaming, and Pydantic AI comparisons.

Referenced from SKILL.md