Skip to content

feat(adapters): OpenAI Agents SDK, and Pydantic AI verified as already covered - #146

Merged
imran-siddique merged 1 commit into
mainfrom
feat/openai-agents-adapter
Aug 27, 2026
Merged

feat(adapters): OpenAI Agents SDK, and Pydantic AI verified as already covered#146
imran-siddique merged 1 commit into
mainfrom
feat/openai-agents-adapter

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Addresses #96. It asked to pick the next adapters by adoption. I measured rather than assumed, and the measurement changed the plan from two adapters to one adapter and one verification.

Adoption

PyPI downloads per month, with GitHub stars as an independent second signal.

Framework PyPI/mo Stars
LangGraph 70.3M covered
OpenAI Agents SDK 39.7M 29.0k built here
Strands 37.1M 7.0k next
Claude Agent SDK 32.9M 8.0k next
CrewAI 30.8M 57.7k next
Google ADK 21.6M covered
Pydantic AI 137M / 13M 19.5k already covered

OpenAI Agents SDK: first-party

The SDK ships agents.tracing.TracingProcessor, which runs in the agent's own process. That is the LangChain shape, not the transcription shape, so records carry no origin block: absence means self, and self is the truth.

Routing a first-party observation through agentrust-trace-adapters would force third-party-control-plane onto a record about the operator's own execution. A worse description, not a safer one.

enforcement_mode defaults to declared. Guardrails exist and can stop a run, but they are the operator's own code, not a policy engine evaluating a bundle. Recording a tripwire as enforcement is the overclaim that value exists to prevent.

Payloads stay out. FunctionSpanData carries input/output, GenerationSpanData carries the whole message list. A test runs a real agent with a secret in the tool input and asserts it appears nowhere in the record. What goes in is identity: which tools ran, in what order, and which agents handed off to which — because in a multi-agent run the tool order alone does not say who was holding it.

The processor keys everything by trace_id. The SDK runs agents concurrently, and a record whose transcript describes two runs while its subject names one is wrong rather than incomplete.

Pydantic AI: no adapter needed, and here is the proof

It instruments through OpenTelemetry, not a bespoke callback, and emits the GenAI conventions otel-genai already maps. Rather than assert that, the new interop test runs a real released agent with a tool call and maps its actual spans.

Three findings, recorded rather than smoothed over:

  • No gen_ai.tool.type, so the transcript carries None there. Worth knowing before someone compares transcripts across frameworks and finds a field populated in one and not another.
  • gen_ai.agent.call.id where the conventions say gen_ai.agent.id. Exactly the drift UNMAPPED_ATTRIBUTES exists to surface, and it costs nothing since both are already unmapped.
  • It emits tool arguments, tool results and both message lists BY DEFAULT. All four already excluded as payloads. A real framework shipping them on by default is the argument for that exclusion, and there is now a test asserting the framework really does emit them.

Interop tests use only what a user can reach

Both run against pinned released packages with no network. The OpenAI one implements the SDK's own Model interface for a scripted two-turn run rather than borrowing a fixture from its test suite, which is not shipped in the wheel.

Both get their own CI job so a failure is legible in the checks list rather than buried.

Also worth noting

Scope item 1 of #96 ("LangGraph is not mentioned anywhere") is already closed by test_langgraph_interop.py. Details on the issue.

51 tests across the two directories. Ruff gate clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

…y covered

Issue #96 asked which frameworks need an adapter next, and said to pick by
adoption. Measured rather than assumed, and the measurement changed the plan
from two adapters to one adapter and one verification.

Adoption, PyPI downloads per month with stars as a second signal:

  LangGraph 70.3M        already covered
  OpenAI Agents 39.7M    29.0k stars   uncovered  <- built here
  Strands 37.1M          7.0k
  Claude Agent SDK 32.9M 8.0k
  CrewAI 30.8M           57.7k
  Google ADK 21.6M       already covered
  Pydantic AI 137M/13M   19.5k         already covered, see below

OpenAI Agents SDK: a first-party adapter

  The SDK ships agents.tracing.TracingProcessor, which runs in the agent's own
  process. So this is the LangChain shape, not the transcription shape: records
  carry no origin block, because absence means self and self is the truth.
  Routing a first-party observation through agentrust-trace-adapters would force
  third-party-control-plane onto a record about the operator's own execution,
  which is a worse description rather than a safer one.

  enforcement_mode defaults to declared. Guardrails exist and can stop a run,
  but they are the operator's own code, not a policy engine evaluating a bundle,
  and recording a tripwire as enforcement is the overclaim that value exists to
  prevent.

  Payloads stay out. FunctionSpanData carries input and output and
  GenerationSpanData carries the whole message list; none of it is hashed into
  the transcript. A test runs a real agent with a secret in the tool input and
  asserts it appears nowhere in the record. What goes in is identity: which
  tools ran, in what order, and which agents handed off to which, because in a
  multi-agent run the tool order alone does not say who was holding the run.

  The processor keys everything by trace_id. The SDK runs agents concurrently,
  and a record whose transcript describes two runs while its subject names one
  is wrong rather than incomplete.

Pydantic AI: no adapter needed

  It instruments through OpenTelemetry rather than a bespoke callback, and emits
  the GenAI conventions integrations/otel-genai already maps. Rather than assert
  that, a new interop test runs a real released agent with a tool call and maps
  its actual spans.

  Three findings recorded rather than smoothed over. It does not emit
  gen_ai.tool.type, so the transcript carries None there; worth knowing before
  somebody compares transcripts across frameworks. It emits gen_ai.agent.call.id
  where the conventions say gen_ai.agent.id, which is exactly the drift
  UNMAPPED_ATTRIBUTES exists to surface, and costs nothing because both are
  already unmapped. And it emits tool arguments, tool results and both message
  lists BY DEFAULT, all four already excluded as payloads. A real framework
  shipping them on by default is the argument for that exclusion, not against
  it, and there is now a test asserting the framework really does emit them.

Both interop tests run against pinned released packages with no network: the
OpenAI one implements the SDK's own Model interface for a scripted two-turn run
rather than borrowing a fixture from its test suite, which is not in the wheel.
Both get their own CI job so a failure is legible in the checks list.

51 tests across the two directories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak
@imran-siddique
imran-siddique merged commit d969820 into main Aug 27, 2026
31 checks passed
@imran-siddique
imran-siddique deleted the feat/openai-agents-adapter branch August 27, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant