AG-UI × OpenAI Agents SDK
Connect an OpenAI Agents SDK agent to any AG-UI client. The package translates AG-UI requests into OpenAI Agents SDK input and translates the OpenAI Agents SDK's stream back into ordered AG-UI events.
You keep your existing OpenAI agent, tools, handoffs, guardrails, model settings, and server architecture. This integration owns only the protocol boundary.
AG-UI RunAgentInput
│
▼
AGUITranslator.to_openai()
│ OpenAI Agents SDK messages, client-tool proxies, request metadata
▼
Runner.run_streamed(...)
│ OpenAI Agents SDK stream
▼
AGUITranslator.to_agui()
│
▼
AG-UI lifecycle, text, tool, reasoning, step, state, and snapshot events
Requirements
- Python 3.10 or newer
ag-ui-protocol >= 1.0.0(AG-UI 1.0)openai-agents >= 0.23.1(tested on 0.23.1 and 0.24.0)- An
OPENAI_API_KEYfor live OpenAI runs
Installation
With uv:
uv add ag-ui-openai-agents
Or with pip:
pip install ag-ui-openai-agents
Configuration
Set the OpenAI API key in the environment where your server runs:
export OPENAI_API_KEY="sk-..."
AGUITranslator does not read environment variables or change OpenAI Agents
SDK settings. With the recommended translator integration, your application
owns the OpenAI Agents SDK runner and its tracing configuration.
If you use the OpenAIAgentsAgent wrapper or
add_openai_agents_fastapi_endpoint, you can optionally set
OPENAI_AGENTS_DISABLE_TRACING=true to disable OpenAI Agents SDK tracing. It
controls tracing only; it does not override the agent, model, or RunConfig.
| OpenAI Agents SDK variable | Required | Purpose |
|---|---|---|
OPENAI_API_KEY |
For the default OpenAI provider | Authenticates OpenAI requests. A custom provider may use different credentials. |
OPENAI_AGENTS_DISABLE_TRACING |
No | Optional when using the wrapper; set to true to disable OpenAI Agents SDK tracing. |
Model selection, retries, and other run behavior remain OpenAI Agents SDK
settings. Pass a model to Agent(...) and, when needed, pass a RunConfig to
OpenAIAgentsAgent or Runner.run_streamed(...).
The bundled example server additionally reads:
| Variable | Required | Purpose |
|---|---|---|
OPENAI_DEFAULT_MODEL |
No | Overrides the model used by all examples. |
HOST |
No | Example server bind address; defaults to 0.0.0.0. |
PORT |
No | Example server port; defaults to 8031. |
Keep secrets in your deployment secret manager or an uncommitted .env file.
Choose an integration level
| Need | Use |
|---|---|
| Existing agent and custom server logic | AGUITranslator (recommended) |
| Fixed agent with less orchestration code | OpenAIAgentsAgent |
| Ready-made FastAPI SSE route | OpenAIAgentsAgent + add_openai_agents_fastapi_endpoint |
| Change one message or event mapping | Custom inbound/outbound engine subclass |
Start with AGUITranslator. It leaves the OpenAI run and HTTP transport under
your control. Use the wrapper only when its standard behavior fits your server.
Quick start: use the translator (recommended)
This is the same small architecture used by the ag_ui_docs_copilot example:
the agent owns its instructions and tools, while the endpoint only translates
the request, runs the OpenAI Agents SDK, and encodes the AG-UI stream. It accepts an AG-UI
request, runs a normal streaming OpenAI agent, and returns Server-Sent Events
(SSE).
from agents import Agent, Runner
from ag_ui.core import RunAgentInput
from ag_ui.encoder import EventEncoder
from ag_ui_openai_agents import AGUITranslator
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
app = FastAPI(title="AG-UI Docs Copilot")
copilot_agent = Agent(
name="AG-UI Docs Copilot",
instructions="Answer AG-UI developer questions clearly and concisely.",
)
translator = AGUITranslator() # Reusable across requests.
@app.post("/")
async def run_ag_ui_docs_copilot(
body: RunAgentInput, request: Request
) -> StreamingResponse:
encoder = EventEncoder(accept=request.headers.get("accept"))
async def stream():
translated = translator.to_openai(body)
result = Runner.run_streamed(
copilot_agent,
input=translated.messages,
context=translated.context,
)
async for event in translator.to_agui(result, body):
yield encoder.encode(event)
return StreamingResponse(stream(), media_type=encoder.get_content_type())
The complete implementation adds the two documentation lookup tools to
copilot_agent; each tool reads one README section on demand. The full agent
and its endpoint are in
examples/agents_examples/ag_ui_docs_copilot.py.
That file is intentionally linear: RunAgentInput → to_openai → Runner.run_streamed → to_agui → EventEncoder.
Run the app, then send an AG-UI request:
curl -N -X POST http://localhost:8000/ \
-H 'Content-Type: application/json' \
-d '{
"thread_id": "thread-1",
"run_id": "run-1",
"messages": [
{"id": "user-1", "role": "user", "content": "Say hello in one sentence."}
],
"tools": [],
"state": {},
"context": [],
"forwarded_props": null
}'
A text-only run normally produces:
RUN_STARTED
TEXT_MESSAGE_START
TEXT_MESSAGE_CONTENT ...
TEXT_MESSAGE_END
MESSAGES_SNAPSHOT
RUN_FINISHED
Quick start: use the FastAPI shortcut
OpenAIAgentsAgent performs the same to_openai → run_streamed → to_agui
flow. The endpoint helper adds an AG-UI POST route, SSE encoding, content
negotiation, and a health route.
from agents import Agent
from ag_ui_openai_agents import (
OpenAIAgentsAgent,
add_openai_agents_fastapi_endpoint,
)
from fastapi import FastAPI
app = FastAPI()
agent = OpenAIAgentsAgent(
Agent(name="assistant", instructions="Be helpful and concise."),
)
add_openai_agents_fastapi_endpoint(app, agent, "/agent")
This registers:
POST /agent— acceptsRunAgentInputand streams encoded AG-UI events.GET /agent/health— returns the wrapped agent's name and health status.
Pass include_health=False if your application already owns health checks.
The helper adds routes; it does not start a server, store sessions, or manage
authentication.
Public API
from ag_ui_openai_agents import (
AGUITranslator,
OpenAIAgentsAgent,
TranslatedInput,
add_openai_agents_fastapi_endpoint,
resume_tool_calls, # see Backend tool approval
shared_state, # see Shared state
)
AGUITranslator
The main, stateless public API. One instance can serve concurrent requests.
translator = AGUITranslator()
to_openai(run_input)
Converts one RunAgentInput into TranslatedInput:
| Field | Behavior |
|---|---|
messages |
Converted to the OpenAI Agents SDK's input format. |
tools |
Converted to request-scoped OpenAI Agents SDK FunctionTool proxies. |
thread_id, run_id, parent_run_id |
Preserved unchanged. |
state, context, forwarded_props, resume |
Preserved for application orchestration. |
to_openai does not run the agent. It also does not automatically put state,
context, or forwarded_props into the model prompt.
to_agui(events, run_input, ...)
Accepts either the OpenAI Agents SDK's RunResultStreaming or its stream_events() async
iterator and yields AG-UI BaseEvent objects.
async for event in translator.to_agui(result, run_input):
...
| Option | Default | Purpose |
|---|---|---|
start_custom_event |
None |
Emit a CustomEvent after RUN_STARTED. |
initial_state |
None |
Emit an initial STATE_SNAPSHOT. |
final_state |
None |
Emit a final STATE_SNAPSHOT after streaming. |
emit_messages_snapshot |
True |
Emit the complete message history before finishing. |
end_custom_event |
None |
Emit a CustomEvent immediately before RUN_FINISHED. |
emit_run_error |
True |
Emit RUN_ERROR before re-raising ordinary errors. |
run_error_message |
None |
Replace exception text with a client-safe message. |
resumed |
None |
What resume_tool_calls returned: its TOOL_CALL_RESULT events are emitted, and its tools' usage counted. |
shared_state |
None |
The run's SharedState; each change streams as STATE_SNAPSHOT (see Shared state). |
State sources and custom-event values may be static values, zero-argument
functions, or zero-argument async functions. They are resolved at their
documented lifecycle position. None skips a state snapshot; {} emits one.
OpenAIAgentsAgent
A convenience wrapper around a fixed OpenAI Agents SDK Agent:
wrapped = OpenAIAgentsAgent(
agent,
name="public-name",
description="Optional description",
run_config=run_config,
initial_state=lambda: load_state(),
final_state=lambda: load_state(),
run_error_message="Agent run failed",
)
async for event in wrapped.run_streamed(run_input):
...
The wrapper:
- answers tool calls the history left open, such as a paused approval (see Backend tool approval);
- makes the request's state the run's
SharedState(see Shared state); - translates the request and appends the AG-UI context to the instructions;
- adds non-conflicting client tools to a per-request agent clone;
- passes
translated.contexttoRunner.run_streamed(context=...); - runs the OpenAI Agents SDK in streaming mode;
- translates the result and emits lifecycle/snapshot events.
The wrapper exposes the same lifecycle controls as AGUITranslator.to_agui():
| Option | Purpose |
|---|---|
start_custom_event |
Emit a custom event after RUN_STARTED. |
initial_state |
Emit the initial STATE_SNAPSHOT. |
final_state |
Emit the final STATE_SNAPSHOT. |
emit_messages_snapshot |
Enable or disable MESSAGES_SNAPSHOT. |
end_custom_event |
Emit a custom event before RUN_FINISHED. |
emit_run_error |
Enable or disable RUN_ERROR. |
run_error_message |
Set the client-visible RUN_ERROR.message. |
It does not expose the RunResultStreaming object. Use the translator directly
when a custom event must inspect the run result, or when you need custom
orchestration.
The shared agent is never mutated. If a client tool has the same name as a server tool, the server tool wins and the client tool is ignored with a warning.
Client tools and the context block go to the wrapped agent only. Agents it
hands off to keep their own tools and instructions: a handoff(...) holds
its target inside a closure, so the wrapper cannot swap in a per-request
copy. Give a specialist the context it needs through its own (dynamic)
instructions, for example by reading shared_state() or the SDK context.
Use AGUITranslator directly when you need the RunResultStreaming, custom
branching, resume logic, a different transport, or per-request OpenAI Agents SDK options.
add_openai_agents_fastapi_endpoint
add_openai_agents_fastapi_endpoint(
app,
wrapped_agent,
path="/agent",
include_health=True,
)
The helper respects the request's Accept header through AG-UI's
EventEncoder and returns the matching streaming content type.
What happens during a run
to_openaitranslates message history and client-declared tools.- Your code—or
OpenAIAgentsAgent—startsRunner.run_streamed. - A fresh outbound engine is created for that run.
RUN_STARTEDis emitted using the request's IDs andprotocol_version: "1.0".- OpenAI Agents SDK events are correlated into AG-UI text, reasoning, tool, and step windows.
- Any open windows are closed when the OpenAI Agents SDK stream ends.
- Optional final state and the default
MESSAGES_SNAPSHOTare emitted. - The run ends with
RUN_FINISHED, orRUN_ERRORfor an ordinary failure.
RUN_FINISHED carries the run's outcome and usage:
outcomeis{"type": "cancelled"}when the OpenAI Agents SDK run was cancelled withresult.cancel(). Otherwise it is{"type": "success"}, and when the run stopped on client-owned tool calls,pending_tool_call_idsnames them in call order. Hosted tool calls are never listed: the application does not answer them.usagehas one entry per model, summing the token counts of everyresponse.completed/response.incompletein the run. It is left out when no model call reported usage.RUN_ERRORcarries the usage accrued before the failure. Calls the stream does not show are added as one entry without a model: those of sub-agents run as tools (Agent.as_tool()), from the SDK's own run total, and those of tools run while resuming approvals, fromresume_tool_calls(pass it asto_agui(..., resumed=...)).
Successful lifecycle order:
RUN_STARTED
start_custom_event optional
STATE_SNAPSHOT optional initial state
... STEP / REASONING / TEXT / TOOL events ...
STATE_SNAPSHOT optional final state
MESSAGES_SNAPSHOT enabled by default
end_custom_event optional
RUN_FINISHED
On an ordinary error, open event windows are closed, RUN_ERROR is emitted
when enabled, and the original exception is re-raised for server logging.
Final state, messages snapshot, end custom event, and RUN_FINISHED are not
emitted on the error path.
The client-visible error text is configurable with run_error_message:
async for event in translator.to_agui(
result,
run_input,
run_error_message="The agent could not complete this request.",
):
yield encoder.encode(event)
This changes RUN_ERROR.message only. The original exception is still raised
after the event so the server can log the detailed failure. Use
run_error_message in production to keep internal exception details out of the
client response while preserving the original exception for server logs.
If a consumer stops reading early and passed a RunResultStreaming, the
translator cancels that owned OpenAI Agents SDK run. If the caller passes only a bare async
iterator, cancellation remains the caller's responsibility.
Feature support
| Feature | Support | Notes |
|---|---|---|
| Streaming assistant text and refusals | Yes | Emits one ordered text window per OpenAI Agents SDK message item. |
| Backend function tools | Yes | Tool call arguments and results are streamed. |
| Hosted tools | Yes | OpenAI Agents SDK hosted tools are surfaced as tool calls; some do not expose streamed arguments. Calls the provider runs itself (web and file search, code interpreter, image generation) get a TOOL_CALL_RESULT with their final status, and none of these hosted calls are replayed to the model on later turns. They are recognised by name in the history, so do not give a function tool a hosted call type's name (web_search_call, file_search_call, …). |
| Providers that reuse tool-call ids | No | AG-UI clients treat a reused id as the earlier call, so a later turn's call (and its <call_id>-result message) overwrites the earlier one in the client. Resuming approvals stays safe (calls are matched by position), but use a provider with unique call ids, as OpenAI's are. |
| Frontend/client-owned tools | Yes | Request tools become OpenAI Agents SDK FunctionTool proxies and complete in a later AG-UI request. |
| Handoffs | Yes | Handoff calls/results map to tools; agent changes map to steps. |
| Agents as tools | Yes | Nested specialist calls appear as ordinary tool calls/results. |
| Reasoning | Yes | Summary/text streams; encrypted content supports replay. |
| Guardrails | Yes | OpenAI Agents SDK tripwire exceptions surface through RUN_ERROR. |
| State snapshots | Yes | Tools change state mid-run through shared_state(); optional initial/final state sources. |
| Agent context | Yes | OpenAIAgentsAgent appends it to the instructions. |
| Messages snapshot | Yes | Enabled by default with stable streamed IDs. |
| Custom lifecycle events | Yes | Start/end CustomEvent hooks. |
| Multimodal input | Partial | Text, images, documents, and some audio; see below. |
| Multimodal output | No | AG-UI currently has no matching image/audio output event. |
Backend approval (needs_approval) |
Yes | AG-UI interrupt on RUN_FINISHED; resumed from resume entries, no server state. |
| MCP approval request | Yes | Emitted as CUSTOM named mcp_approval_request. |
Message mapping: AG-UI → OpenAI
to_openai keeps message order. One AG-UI message may produce multiple OpenAI Agents SDK
items, especially an assistant message containing tool calls.
| AG-UI input | OpenAI Agents SDK input | ID behavior |
|---|---|---|
SystemMessage |
message, role system |
AG-UI message ID is not sent. |
DeveloperMessage |
message, role developer |
AG-UI message ID is not sent. |
UserMessage |
message, role user, with translated content parts |
AG-UI message ID is not sent. |
AssistantMessage.content |
Assistant input message | AG-UI message ID is not sent; empty text is omitted. |
AssistantMessage.tool_calls[] |
One function_call per tool call |
ToolCall.id → call_id unchanged. |
ToolMessage |
function_call_output |
tool_call_id → call_id unchanged. |
ReasoningMessage |
reasoning item |
message.id → reasoning.id; emitted only with encrypted_value. |
ActivityMessage |
Not mapped | Dropped with a debug log. |
RunAgentInput.tools[] |
OpenAI Agents SDK FunctionTool proxies |
Tool name and JSON Schema are preserved. |
The Responses input format has no tool role. Tool history is represented by
function_call and function_call_output items joined by the same call_id.
Unknown message types are skipped rather than failing the entire request.
Event mapping: OpenAI → AG-UI
| OpenAI Agents SDK source | AG-UI output |
|---|---|
| Message item plus text/refusal deltas | TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END |
| Function, hosted-tool, or handoff call | TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END |
| Function/handoff output, hosted MCP call result | TOOL_CALL_RESULT |
| Reasoning summary/text | REASONING_START → REASONING_MESSAGE_START → REASONING_MESSAGE_CONTENT* → REASONING_MESSAGE_END → REASONING_END |
| Reasoning encrypted content | REASONING_ENCRYPTED_VALUE |
| Agent update/handoff target | STEP_FINISHED for the previous agent, then STEP_STARTED |
| MCP approval request | CUSTOM with name mcp_approval_request |
| MCP tool listing/approval response | Not emitted; server bookkeeping only. |
| Run start/success/failure | RUN_STARTED, RUN_FINISHED, or RUN_ERROR |
response.completed / response.incomplete usage |
usage on RUN_FINISHED / RUN_ERROR |
response.output_text.done closes one text part, not necessarily the complete
message item. The translator therefore closes a text window on the item-level
done event, the completed run item, or finalization. This keeps multi-part text
under one AG-UI message ID.
ID mapping and guarantees
| Source | AG-UI ID |
|---|---|
OpenAI Agents SDK message item id |
message_id |
Function/hosted tool call_id |
tool_call_id |
Text the tool call followed (else a generated msg_...) |
TOOL_CALL_START.parent_message_id |
Hosted tool without call_id |
Item id, then generated call_... fallback |
Tool result call_id |
tool_call_id; result message_id is <call_id>-result |
Reasoning item id |
Reasoning phase message_id |
| Additional reasoning parts | <phase_id>-1, <phase_id>-2, ... |
AG-UI request thread_id / run_id |
Same lifecycle event fields, unchanged |
Rules:
- A real OpenAI Agents SDK ID always wins.
- If the OpenAI Agents SDK omits an ID or uses its placeholder ID, the integration generates
a stable ID for that item (
msg_...,call_..., orrs_...). - One resolved ID is reused for every start/content/end event in that window.
- The same IDs are reused in
MESSAGES_SNAPSHOT, preventing duplicate client messages when streamed output is reconciled with final history. - Existing input history keeps its original AG-UI IDs in the snapshot.
- Reasoning messages in
MESSAGES_SNAPSHOTkeep the ids of theirREASONING_MESSAGE_STARTevents. - A tool call's
parent_message_idis the assistant message it belongs to: the text streamed just before it, or a generatedmsg_...when it came first. Calls in a row share that message; reasoning, a tool result or an agent change starts a new one. The snapshot groups text and calls the same way, so it matches the messages the client built from the stream. - Internal output-index correlation keys never appear on AG-UI events.
Messages and state snapshots
MESSAGES_SNAPSHOT is emitted before RUN_FINISHED by default. It contains:
- the original
RunAgentInput.messageswith their existing IDs; and - reasoning, assistant messages, tool calls, and tool results produced during this run, using the same IDs already streamed to the client.
Each streamed reasoning part is a ReasoningMessage placed before the first
text or tool call that started after it; the first part of a reasoning item
also carries its encrypted_value. Clients replace their messages with the
snapshot, so this keeps the reasoning the run showed, and the history it sends
next replays encrypted reasoning in the order the Responses API requires.
Disable it only when your application emits its own authoritative history:
async for event in translator.to_agui(
result,
run_input,
emit_messages_snapshot=False,
):
...
State remains application-owned. Supply snapshots explicitly:
state = dict(run_input.state or {})
initial_state = dict(state)
result = Runner.run_streamed(
agent,
input=translated.messages,
context=state,
)
async for event in translator.to_agui(
result,
run_input,
initial_state=initial_state,
final_state=lambda: dict(state),
):
...
The final factory runs after streaming, so it can observe changes made by tools or hooks.
Context and forwarded properties
state, context, forwarded_props, and resume pass through on
TranslatedInput; their meaning remains application-specific.
Be aware of two uses of “context”:
RunAgentInput.contextis the AG-UI list sent by the client.Runner.run_streamed(context=...)is the OpenAI Agents SDK dependency object available throughRunContextWrapper.
OpenAIAgentsAgent appends the AG-UI context to the agent's instructions on
every model call, under Context from the application: as
description: value lines, so the model sees what the page shared. Pass
context_in_instructions=False when the agent uses the context itself. When no
SDK context is given, the wrapper also passes the AG-UI context list into the
OpenAI Agents SDK context slot, so dynamic instructions or tools can read the
list, as the dynamic_system_prompt example does. With the direct translator,
translator.translate_context(items) renders the same lines for you to add.
Shared state
RunAgentInput.state is the client's state. Tools (and dynamic instructions)
read and change it during the run through shared_state(); each change is
streamed as a STATE_SNAPSHOT before the next event, so it reaches the client
before the tool's result:
from agents import function_tool
from ag_ui_openai_agents import shared_state
@function_tool
def add_item(name: str) -> str:
state = shared_state()
state.update(items=[*state.value.get("items", []), name])
return f"Added {name}."
OpenAIAgentsAgent sets this up for every request. With the translator, start
the SDK run inside use_shared_state (the SDK run's task copies the context it
was created in) and pass the same object to to_agui:
from ag_ui_openai_agents import SharedState, use_shared_state
state = SharedState(run_input.state)
with use_shared_state(state):
result = Runner.run_streamed(agent, input=translated.messages)
async for event in translator.to_agui(result, run_input, shared_state=state):
...
update(**changes) merges top-level keys into a dict state; set(value)
replaces the state. The request's own state is never mutated.
Tools and human-in-the-loop
Backend tools
Normal OpenAI Agents SDK @function_tool tools stay on the server. Their calls and results
become AG-UI tool events without special integration code.
Frontend-owned tools
Tools declared in RunAgentInput.tools belong to the client. to_openai
creates OpenAI Agents SDK FunctionTool proxies so the model can call them, but the frontend
must execute them.
Recommended flow:
- Merge translated client tools onto a per-request agent clone.
- Configure OpenAI Agents SDK
StopAtToolsfor those names when the run should stop as soon as the model calls one. - Render/execute the tool in the client.
- Send its result as a
ToolMessagein the next AG-UI request. to_openaiconverts that result tofunction_call_output, preserving thecall_id, and the agent continues from the conversation history.
from agents import Agent, StopAtTools
agent = Agent(
name="planner",
instructions="Use generate_steps when asked to make a plan.",
tool_use_behavior=StopAtTools(stop_at_tool_names=["generate_steps"]),
)
This pattern covers frontend tool rendering, tool-based generative UI, and client-owned human approval.
A client tool call stops the run as soon as its proxy runs. When the model
calls a backend tool in the same turn and that tool has not finished, the
SDK cancels it. On the next request the call is answered as not having run
(see resume_tool_calls), so the history stays valid, but the backend work
is not done. Disable parallel tool calls
(ModelSettings(parallel_tool_calls=False)) where that matters.
Backend tool approval
OpenAI Agents SDK tools marked needs_approval=True are different: real backend
code exists, but the OpenAI Agents SDK must pause before executing it.
from agents import function_tool
@function_tool(needs_approval=True)
def issue_refund(order_id: str) -> str:
return f"Refund issued for {order_id}"
The OpenAI Agents SDK pauses such a call before the tool runs and lists it on
result.interruptions. The integration maps that onto AG-UI interrupts and
keeps nothing on the server between the pause and the resume:
-
Pause. When
to_aguiis given theRunResultStreamingand the run paused,RUN_FINISHEDcarriesoutcome: {"type": "interrupt"}with one interrupt per paused call:idandtool_call_idare the call id,reasonis"tool_approval",metadataholdstool_nameand the parsedtool_input, andresponse_schemaasks for{"approved": boolean}. The paused call stays in theMESSAGES_SNAPSHOTwith no result, so the client keeps it in its history. -
Resume. The next request carries that history and a
resumeentry per interrupt.resume_tool_callsanswers every open call of the agent's own tools before the model runs:{"approved": true}runs the tool, any other answer gives the model the SDK's rejection text, and a cancelled interrupt givesCANCELLED. -
Undecided. A paused call with no entry (the user sent a new message instead, or a partial
resumeanswered only some calls) stays open. The run answers what the entries cover, does not call the model, and ends with that call's interrupt again, as AG-UI requires for an approval nobody decided. The new message stays in the history and reaches the model once the approval is decided. An unfinished call that needed no approval (for example one stopped by a client tool in the same turn) getsABANDONED. A call whose tool has a callableneeds_approvalpolicy stays open too: the policy's answer depended on the context of the run that paused, which a stateless resume no longer has. When several agents have tools of the call's name, the call stays open if any of them may need approval.Tools of agents reachable through
handoffs(plain agents orhandoff(...)objects) are found too. The history does not record which agent made a call, so when two of those agents have different tools with the same name, an approved call is answered withAMBIGUOUSand neither runs: give approval-gated tools unique names. (Tools of one MCP server shared by several agents count as one tool.) Tool namespaces (tool_namespace()) are not carried in AG-UI tool calls, so two approval-gated tools with the same name in different namespaces areAMBIGUOUStoo.
Only function tools that the run itself called can be resumed. Two kinds of
approval cannot, and to_agui ends such a run with RUN_ERROR
(UnresumableApprovalError) rather than an interrupt nobody can resolve:
- a tool inside an agent used as a tool (
Agent.as_tool()): it pauses within the sub-agent's own run, and the conversation only holds the outer call. Gate the agent tool itself (as_tool(..., needs_approval=True)) or reach the sub-agent by handoff; - a built-in tool with its own executor (
ShellTool,ApplyPatchTooland the like).
OpenAIAgentsAgent does all of this for you. With the translator, skip the
model run while an approval is still open:
from ag_ui_openai_agents import resume_tool_calls
resumed = await resume_tool_calls(
agent, run_input, context=context, run_config=run_config
)
if resumed.open_interrupts:
events = None # to_agui ends the run with the undecided approvals
else:
translated = translator.to_openai(resumed.run_input)
events = Runner.run_streamed(
agent, input=translated.messages, context=context, run_config=run_config
)
async for event in translator.to_agui(events, resumed.run_input, resumed=resumed):
...
Because nothing is kept on the server, one rule of the AG-UI interrupt contract is handled differently:
- Idempotency. Replaying the same resume request runs an approved tool
again; the server has no record that it already ran. Key side effects on
the call id (
ToolContext.tool_call_id) where a retried request must not repeat them. The same holds when one of several approved tools raises (failure_error_function=None): the request fails, and the calls that already ran in it are not reported.
A resumed call runs outside the SDK's run loop, so tool guardrails and run
hooks do not see it, and the agent's tool_use_behavior (StopAtTools,
stop_on_first_tool) does not end the run after it: the model is called
with the tool's result as usual. An approved agent tool whose sub-agent
then pauses for an approval of its own fails with
UnresumableApprovalError. The server trusts the history it is sent, as it does for
client tool results; authorize the decision before resuming if that matters to
you. See
human_in_the_loop_approval.py
for a complete endpoint.
Reasoning
Visible reasoning summary/text maps to AG-UI reasoning events. Replay requires encrypted reasoning content; plaintext reasoning alone cannot restore model reasoning state.
Request encrypted content through normal OpenAI Agents SDK model settings:
from agents import ModelSettings
settings = ModelSettings(
response_include=["reasoning.encrypted_content"],
)
When present, encrypted content is emitted once as
REASONING_ENCRYPTED_VALUE, kept on the reasoning message in
MESSAGES_SNAPSHOT, and returns on the next request through
ReasoningMessage.encrypted_value. Reasoning without summary text streams an
empty reasoning message, so the client has a message to attach the
encrypted value to even without the snapshot, except when the value arrives
only with the committed item after the reasoning already closed; then only
MESSAGES_SNAPSHOT carries it.
Multimodal input
| AG-UI content | OpenAI input | Notes |
|---|---|---|
| Text | input_text |
Supported. |
| Image URL or data | input_image |
Data becomes a base64 data URL; detail is auto. |
| Document URL or data | input_file |
Uses file_url or base64 file_data. |
| Audio data | input_audio |
WAV/MP3 only; requires a compatible Chat Completions model. |
| Audio URL | Not mapped | Dropped. |
| Video | Not mapped | OpenAI agent model input has no matching video part. |
Legacy BinaryInputContent |
Image, audio, or file by MIME type | Best-effort compatibility path. |
Unsupported parts are skipped. If every part of a user message is unsupported, the complete message is skipped so the OpenAI Agents SDK never receives empty content.
The integration currently emits text/tool/reasoning output only. It does not translate generated image or audio output into AG-UI events.
Custom mappings
The public translator delegates to two engine classes:
AGUIToOpenAITranslator— stateless inbound request mapping, reused.OpenAIToAGUITranslator— stateful outbound stream mapping, created per run.
Their translate_* methods are override points. Subclass only the side you
need and inject the class into AGUITranslator:
from ag_ui_openai_agents import AGUITranslator
from ag_ui_openai_agents.engine import OpenAIToAGUITranslator
class MyOutboundTranslator(OpenAIToAGUITranslator):
def translate_mcp_approval_request_item(self, item):
return [] # Application-specific behavior.
translator = AGUITranslator(outbound_cls=MyOutboundTranslator)
Avoid sharing an outbound engine instance: it tracks open text, tool,
reasoning, step, and snapshot state for one run. AGUITranslator creates the
correct fresh instance automatically.
Examples
The examples/ directory contains one runnable app per supported
Dojo feature:
| Example | Demonstrates |
|---|---|
ag_ui_docs_copilot |
Agent using the integration and AG-UI documentation. |
agentic_chat |
Basic streaming conversation. |
backend_tool_rendering |
Server function call and result rendering. |
human_in_the_loop |
Frontend-owned tool plus user interaction. |
human_in_the_loop_approval |
OpenAI Agents SDK backend approval as an AG-UI interrupt, resumed statelessly. |
tool_based_generative_ui |
Frontend renders UI from tool arguments. |
subagents |
Agents-as-tools orchestration. |
custom_lifecycle_events |
Dynamic custom events using run data. |
dynamic_system_prompt |
Instructions derived from AG-UI context. |
Run all examples:
cd examples
uv sync
cp .env.example .env
# Add OPENAI_API_KEY to .env
uv run python server.py
The server listens on http://localhost:8031 by default. PORT and HOST
override the defaults. GET /health lists the mounted demos. See the
examples guide for routes and suggested prompts.
Production responsibilities
This package translates the protocol. Your application still owns:
- authentication and authorization;
- API-key and secret management;
- session and message persistence;
- idempotency and concurrency control;
- authorizing approval decisions;
- rate limits, retries, logging, and observability;
- model choice, guardrails, tools, handoffs, and OpenAI Agents SDK
RunConfig; - HTTP/WebSocket deployment and scaling.
Testing and local development
Clone the AG-UI repository, then work from
integrations/openai-agents/python:
uv sync
uv run python -c "import ag_ui_openai_agents"
uv run pytest
uv build
uv sync installs the package and its development dependencies. The import
command is a quick installation smoke test, pytest runs the package unit
suite, and uv build verifies that the source distribution and wheel can be
created. No lint command is documented because this package does not currently
define a package-specific linter.
The test suite covers the public translator, wrapper, FastAPI endpoint, message/content mapping, event windows, snapshots, IDs, cancellation, tools, reasoning, and OpenAI Agents SDK discriminator drift. Unit tests do not require a live API key.
The runnable examples have a separate test environment and test suite:
cd examples
uv sync
uv run pytest
Package tests do not automatically collect example tests. Run both suites before opening a pull request that changes integration behavior or examples.
After changing openai-agents or openai, always run the full suite. The drift
tests compare the event and item discriminators used by this integration with
the installed OpenAI Agents SDK so renamed or newly introduced wire types are caught before
release.
Troubleshooting
| Problem | Check |
|---|---|
| Authentication fails | Confirm OPENAI_API_KEY is set in the server process, not only in another terminal or frontend environment. |
| The client receives no stream | Return StreamingResponse with EventEncoder.get_content_type() and yield every encoded event from translator.to_agui(...). |
| A frontend tool never appears | Include its definition in RunAgentInput.tools; frontend-owned tools are created from the request, not from the backend agent. |
| A backend approval cannot resume | Send the history from the paused run's MESSAGES_SNAPSHOT back with a resume entry whose interruptId is the paused call's id and whose payload is {"approved": true}; run the request through resume_tool_calls (or OpenAIAgentsAgent). |
| IDs do not match across events | Preserve the incoming AG-UI message and tool-call IDs; see ID mapping and guarantees. |
| A run fails without useful client details | Set run_error_message for a safe client-facing message and inspect the original re-raised exception in server logs. |
When reporting a bug, include the package versions, a minimal RunAgentInput,
the observed AG-UI event sequence, and a small reproducer. Remove API keys and
sensitive message content first.
Resources
Metadata
Release files for ag-ui-openai-agents 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ag_ui_openai_agents-0.1.0.tar.gz | 335.8 kB | Without provenance | Details for ag_ui_openai_agents-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ag_ui_openai_agents-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for ag_ui_openai_agents-0.1.0-py3-none-any.whl |
Total release size: 401.1 kB
Release files / ag_ui_openai_agents-0.1.0.tar.gz
| Download URL | ag_ui_openai_agents-0.1.0.tar.gz |
|---|---|
| Size | 335.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34be38e3d35f1144604dfa8ecd33474a513347294ce77098f67e3c3b6fa74400
|
|
BLAKE2b-256 checksum How to use checksums |
339c0ce50b199045bd52038b5a28669453a6feb0c3986a3293cea567d9bcbdf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ag_ui_openai_agents-0.1.0-py3-none-any.whl
| Download URL | ag_ui_openai_agents-0.1.0-py3-none-any.whl |
|---|---|
| Size | 65.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2cf3f0e5b3ffc6b35e8cd8df58a6c3bec2ac2f6c2eb258847e4f61a104bbf687
|
|
BLAKE2b-256 checksum How to use checksums |
f9d638c319be82e2d3beaa0741250606a5f57eb3a9aa8977928378fad17d461b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for ag-ui-openai-agents 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.