Skip to content

OpenAI Agents SDK (Python) integration - #2210

Open
abdelrahmanzied wants to merge 95 commits into
ag-ui-protocol:mainfrom
abdelrahmanzied:feat/openai-agents
Open

OpenAI Agents SDK (Python) integration#2210
abdelrahmanzied wants to merge 95 commits into
ag-ui-protocol:mainfrom
abdelrahmanzied:feat/openai-agents

Conversation

@abdelrahmanzied

Copy link
Copy Markdown

Fixes #167

Summary

Adds an OpenAI Agents SDK (Python) integration for the AG-UI protocol. It
translates AG-UI requests into OpenAI Agents SDK input and converts streamed SDK
output into ordered AG-UI events while preserving message, tool-call, reasoning,
thread, and run IDs.

What's included

Python package (ag-ui-openai-agents)

  • AGUITranslator, the recommended API for developers who need full control
    over the OpenAI Agents SDK runner, transport, and orchestration. The
    integration only translates between OpenAI Agents SDK data and AG-UI events.
  • OpenAIAgentsAgent, an optional wrapper for standard streamed runs.
  • add_openai_agents_fastapi_endpoint, an optional FastAPI SSE endpoint wrapper.
  • AG-UI lifecycle, text, tool, reasoning, step, state, snapshot, custom, and
    error event translation.
  • Backend tools, frontend-owned tools, hosted tools, handoffs, agents as tools,
    guardrails, backend approval flows, and supported multimodal input.
  • Stable event correlation and ID reuse between streamed events and
    MESSAGES_SNAPSHOT.
  • Developer documentation covering public APIs, event/message mapping,
    lifecycle behavior, IDs, examples, and troubleshooting.

Examples and Dojo

Adds a runnable FastAPI example server and OpenAI Agents (Python) Dojo support
for nine features:

  • AG-UI Docs Copilot — Answers AG-UI<>OpenAI Agents SDK questions using backend documentation tools.
  • Agentic Chat — Streams a standard multi-turn conversation.
  • Backend Tool Rendering — Runs a backend weather tool and renders its result.
  • Human in the Loop — Executes a frontend-owned tool and returns its result to the agent.
  • Human in the Loop Approval — Pauses a backend tool for approval, then resumes the run.
  • Tool-based Generative UI — Renders structured UI from streamed tool arguments.
  • Subagents — Uses a supervisor to call specialist agents as tools.
  • Custom Lifecycle Events — Sends token usage through an AG-UI custom event.
  • Dynamic System Prompt — Changes agent instructions using context supplied by the frontend.

Each listed Dojo feature has integration-specific Playwright coverage using
LLMock, so the E2E suite does not require live OpenAI calls or API credentials.

TypeScript client

Adds the repository-required TypeScript HTTP client package,
@ag-ui/openai-agents, which exposes OpenAIAgentsHttpAgent using the base
HttpAgent. The package is private because it adds no integration-specific
client behavior; the integration runs in Python.

CI

  • Registers the Python package in the Python unit-test workflow.
  • Registers the integration and its service in the Dojo E2E workflow.

Architecture

AG-UI RunAgentInput
        |
        v
AGUITranslator.to_openai()
        |
        v
OpenAI Agents SDK Runner.run_streamed(...)
        |
        v
AGUITranslator.to_agui()
        |
        v
AG-UI SSE event stream

Test plan

  • Python package tests: 134 passed
  • Python example tests: 20 passed
  • TypeScript client tests: 2 passed
  • TypeScript typecheck and build
  • Dojo E2E: 11 passed across all nine features with Node 22 and LLMock
  • GitHub Actions checks

Reviewer note

PyPI release enrollment is intentionally not included. Please confirm whether maintainers want to add ag-ui-openai-agents to the release configuration in this PR or in a maintainer-owned follow-up.

…penAI Agents SDK

- Configure `pyproject.toml` with project metadata and dependencies.
- Include required `.python-version` file.
- Add `README.md` with installation instructions.
- Generate `uv.lock` file with dependency resolution.
…ing parts

- Change tool_result_id from random `toolresult_<hex>` to deterministic `{call_id}-result`,
  linking result message visually and functionally to its tool call. Matches claude-agent-sdk pattern.
- Change reasoning part id separator from `/` to `-` (e.g., `rs_abc-1` not `rs_abc/1`).
  Hyphen never appears in wire ids, so suffix clearly marks our derivations.
- new_tool_result_id(call_id) now takes call_id param; determinism aids dedup/replay safety.
- Reasoning fallback generation via new_reasoning_id() was already in place (rs_<hex> prefix).

All generated id shapes now mimic their native OpenAI counterparts; hyphen-suffixed
ids are transparently ours. Pass-through chain unchanged (call_id → fc_ → generated).
…ubpackage

- Create public interfaces: AGUITranslator (streamed) and AGUINonStreamingTranslator (sync/async)
- Move per-direction translation logic into engine/ subpackage for better separation of concerns
- Flatten public module structure at package root to align with sibling integrations
- Maintain backward compatibility: translator/ subpackage imports resolve via translator.py
- Update README to serve as integration usage documentation

This restructuring improves maintainability by isolating implementation details while keeping the public API simple and discoverable.
Add emit_state_snapshot (default True): after RUN_STARTED, echo run_input.state as a STATE_SNAPSHOT including empty state
@abdelrahmanzied
abdelrahmanzied requested a review from a team as a code owner July 19, 2026 16:34
@NathanTarbert

NathanTarbert commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Hey @abdelrahmanzied, thanks for this. It's a clean, complete integration, and the checklist was met from the contributing guide.

One thing to confirm: the new ALL_SERVICES block in run-dojo-everything.js doesn't set an explicit env var on the dojo/dojo-dev entries pointing at port 8024. The env.ts default (http://localhost:8024) should cover it - just want to confirm the dojo reaches the agent in CI without it.

On the PyPI enrollment note, we'll handle that maintainer-side, nothing needed from you.

Nice contribution. 🚀

@abdelrahmanzied

Copy link
Copy Markdown
Author

Hey @abdelrahmanzied, thanks for this. It's a clean, complete integration, and the checklist was met from the contributing guide.

One thing to confirm: the new ALL_SERVICES block in run-dojo-everything.js doesn't set an explicit env var on the dojo/dojo-dev entries pointing at port 8024. The env.ts default (http://localhost:8024) should cover it - just want to confirm the dojo reaches the agent in CI without it.

On the PyPI enrollment note, we'll handle that maintainer-side, nothing needed from you.

Nice contribution. 🚀

Thanks for your kind feedback 😊

Yes, I can confirm that Dojo reaches the OpenAI Agents backend in the CI-style E2E setup using the existing env.ts default at http://localhost:8024, all E2E tests passed locally successfully.

Thanks for clarifying the PyPI enrollment as well!

@NathanTarbert

Copy link
Copy Markdown
Contributor

Great, @abdelrahmanzied, I've pinged engineering to get their final review.

AlemTuzlak and others added 2 commits July 28, 2026 16:00
…-protocol#2210) (#1)

* fix(openai-agents): CR round-1 fixes (correctness, robustness, tests)

Engine:
- translate(): read run_input.resume via getattr (defensive, matches
  parent_run_id and the documented contract)
- outbound: reuse the real item id on lazy text-open instead of a
  generated one (honors the id-reuse invariant; avoids snapshot/stream
  id divergence)
- outbound: buffer function-call args that stream before output_item.added
  instead of opening under a throwaway id, so the run-item commit no longer
  emits a duplicate TOOL_CALL_START/ARGS/END; finalize flushes orphans
- outbound: surface REASONING_ENCRYPTED_VALUE on the run-item skip path so
  encrypted reasoning replay data is not dropped
- helpers.to_string(): serialize pydantic models via model_dump()
- helpers.__all__: add new_reasoning_id
- translate_content(): drop empty/None content so empty turns are omitted

Wrapper / endpoint:
- OpenAIAgentsAgent: add context= injection point (falls back to AG-UI
  context); dedup client tools against each other
- endpoint: log re-raised run errors instead of leaking an ASGI traceback;
  unique per-route name/operation_id so multiple agents can mount on one app

Examples/dojo:
- example DEFAULT_MODEL fallback -> gpt-5.5
- ApprovalCard dark-mode variants; consumerAgentId on docs_copilot & subagents
- subagents e2e: scope delegation-log assertions to the log container (were
  matching the always-rendered team chips)

Tests: covering tests for every behavioral change; snapshot test now guards
against duplicate id emission.

* fix(openai-agents): CR round-2 fixes (placeholder-id correlation, endpoint, ids)

Confirmation round found a cluster of bugs the round-1 fixes exposed for
non-native (placeholder / FAKE_RESPONSES_ID) backends, plus a few pre-existing:

Outbound translator:
- _is_real_id no longer misclassifies internal '__idx_' placeholder window keys
  as wire ids (add _is_placeholder_key guard): lazy text-open and the arg-buffer
  paths no longer leak placeholder keys onto the wire as message/tool_call ids
- _window_key active-set now includes _pending_tool_args, so a placeholder window
  holding only buffered args is recognized and reused instead of split in two
- run-item commit clears the correlated arg buffer (by item id, else oldest FIFO)
  so finalize no longer re-emits a phantom empty-name tool call
- finalize drops orphaned (never-committed) arg buffers instead of emitting them
- REASONING_ENCRYPTED_VALUE dedup keyed on the stable phase id (raw + skip paths)
- reasoning delta-first reuses the real wire id (mirrors output_item.added)

Wrapper / endpoint:
- endpoint surfaces a terminal RUN_ERROR when setup fails before to_agui (was an
  empty 200); slug uniqueness guard so paths normalizing to one slug don't collide
- translator honors an explicit empty run_error_message (only None -> str(exc))
- helpers.to_string logs a model_dump() failure instead of swallowing silently

Examples/dojo:
- run-dojo-everything.js OPENAI_CHAT_MODEL_ID default and commented model refs
  in agents.ts -> gpt-5.5

Tests: placeholder-backend coverage (arg flush/no-split, commit no-phantom, clean
text id, reasoning id reuse), endpoint setup-error + slug-collision, empty
run_error_message, and the text 'skip' branch now actually exercised in snapshot.

* fix(openai-agents): CR round-3 fixes (SDK-floor compat, tool errors, buffer safety)

Second confirmation round surfaced two regressions from the round-2 buffer
fixes plus several pre-existing bugs:

Outbound translator:
- translate_tool_call_output_item reads call_id defensively (getattr + raw
  fallback): ToolCallOutputItem.call_id does not exist on the declared
  openai-agents>=0.8.4 floor, so a direct access turned every tool result into
  a RUN_ERROR on the minimum supported SDK
- remove the blind FIFO buffer-drop in the tool-call 'new' branch: it could
  evict a *different* still-streaming call's buffered args; an uncorrelated
  placeholder buffer is harmlessly dropped by finalize instead

Inbound translator:
- translate_tool_message surfaces ToolMessage.error when content is empty
  (was silently sending a blank tool result the model can't recover from)
- translate_document_content synthesizes a filename for base64 file_data
  (the Responses API requires one; mirrors _binary_as_file)

Endpoint:
- fallback now keys on RUN_STARTED: emits a well-formed RUN_STARTED+RUN_ERROR
  only on a true setup failure (run never started); stays silent (logs only)
  once the run started, so an explicit emit_run_error=False is honored and the
  terminal is never a lone RUN_ERROR without RUN_STARTED

Dojo: OPENAI_AGENTS_PYTHON_URL wired into the dojo env blocks for parity.

Tests: inbound message-type coverage (system/developer/assistant/reasoning/
tool-with-error/activity/context), defensive tool-output call_id, document
filename, and caplog level consistency.

* fix(openai-agents): CR round-4 fixes (filename synthesis, context/reasoning edges)

Third confirmation round confirmed the round-3 fixes correct and surfaced a few
small pre-existing edges plus one polish item on the round-3 endpoint change:

- _binary_as_file synthesizes a filename for base64 file_data (mirrors
  translate_document_content); a filename-less binary file part was otherwise
  rejected by the Responses API. Comment on the document path corrected.
- agent.py normalizes an empty ambient context to None (SDK default) instead of
  passing [], so agents branching on 'ctx.context is None' behave correctly.
- endpoint setup-failure fallback RUN_STARTED now carries parent_run_id,
  matching the normal to_agui lifecycle path.
- translate_context drops items missing either description or value (no more
  half-empty 'label: ' / ': value' lines).
- translate_reasoning_message omits the summary entry entirely when content is
  empty (an empty summary_text is rejected by some backends).

Tests updated/added for each; the prior test asserting the old (buggy)
no-filename binary behavior now asserts the synthesized filename.

* fix(openai-agents): CR round-5 fixes (SkipValidation on messages, refusal, mime)

Fourth confirmation round confirmed the round-4 fixes correct; three reviewers
independently traced two real bugs to one root cause -- TranslatedInput.messages
being pydantic-validated against the Responses input union:

- Audio input crashed to_openai(): input_audio is a valid Chat-Completions shape
  but not a Responses input-item member, so constructing TranslatedInput raised
  ValidationError and failed the WHOLE request (not just the audio part).
- A reasoning item's summary (SDK-typed Iterable) was materialized as a one-shot
  pydantic ValidatorIterator that empties after a single read -- silent reasoning-
  replay data loss if messages is read more than once.

Fix: mark TranslatedInput.messages SkipValidation (mirrors tools, same forward-ref
rationale). The translator already guarantees well-formed shapes (e.g. images
always get detail) and the SDK validates what it consumes at run time, so the
removed strict validation was a redundant safety net that rejected otherwise-
valid multimodal input. Audio now translates and reasoning summary stays a list.

Also:
- translate_message_output_item concatenates text + refusal into the snapshot
  (extract_text ignores refusals, so a fallback dropped a streamed refusal).
- _audio_format_from_mime strips mime parameters ('audio/wav; codecs=1') before
  matching, so a parameterized-but-valid type isn't silently dropped.
- endpoint comment clarifies the setup-failure terminal is emitted even when
  emit_run_error=False (a silent empty 200 is worse than a terminal error).

Tests: audio-survives-to_openai, reasoning-summary-stable-list, text+refusal
snapshot, mime-parameter parsing; the obsolete reject-image-without-detail test
is replaced with a SkipValidation behavior test.
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.

Add OpenAI Agents SDK Support

3 participants