> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsonic.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI Agents SDK

> Run an OpenAI Agents SDK agent on AgentOS: models through the gateway, and a tool with needs_approval=True paused for a person.

This example is a support agent built with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/). It looks orders up with one tool, and its `refund` tool is declared with `needs_approval=True`. When the model calls it, the run stops, AgentOS asks a person, and the run continues where it stopped with their decision, in whichever container answers the resume.

The SDK's `openai_agents` adapter does the work: it keeps the agent's `RunState` in run state, asks one hold for every call waiting for approval, and applies the decisions on the resume. See [Agent frameworks](https://docs.upsonic.ai/agentos/build/frameworks.md).

> [!NOTE]
>
> Tested with `openai-agents` 0.23.1 and Python 3.13, using the [conformance kit](https://docs.upsonic.ai/agentos/build/conformance.md): 17 checks passed, including an approved and a rejected refund.

## The bundle

```python app.py theme={null}
from agents import Agent, function_tool

import agentos
from agentos.adapters import openai_agents

ORDERS = {"A-1001": {"status": "delivered", "total": "49.90", "currency": "EUR"}}


@function_tool
def lookup_order(order_id: str) -> dict:
    """Look an order up by its id, e.g. A-1001."""
    return ORDERS.get(order_id, {"error": f"no order {order_id}"})


@function_tool(needs_approval=True)
def refund(order_id: str, amount: str) -> str:
    """Refund an order. The amount is text, e.g. "49.90". A person approves every refund first."""
    return f"refunded {amount} for order {order_id}"


async def support(inv: agentos.Invocation):
    agent = Agent(
        name="support",
        instructions=inv.config["prompts"]["system"],
        tools=[lookup_order, refund],
        model=openai_agents.model(inv),  # config.models.main, through the gateway with the run token
    )
    try:
        result = await openai_agents.run(inv, agent, inv.input["message"], intent="Approve a refund")
    except Exception as error:
        if block := agentos.guardrail_block(error):  # a guardrail refused a model call
            raise block from error
        raise
    return {"answer": str(result.final_output)}


if __name__ == "__main__":
    agentos.serve({"support": support}, port=8080)
```

```yaml agentos.yaml theme={null}
name: support-openai-agents
title: Support agent (OpenAI Agents SDK)
description: Answers order questions with a lookup tool; refunds go through a person first.
runtime: stateless
port: 8080
capabilities:
  support:
    description: Handles a customer's message. A refund the model wants to issue waits for a person's approval.
    input:
      type: object
      properties: {message: {type: string, minLength: 1}}
      required: [message]
    output:
      type: object
      properties: {answer: {type: string}}
      required: [answer]
    timeout: 120s
config:
  models:
    main: anthropic/claude-sonnet-4-5    # a model from your Models page: <provider slug>/<model ID>
  prompts:
    system: >-
      You are a support agent for an online shop. Look orders up with lookup_order before you
      answer anything about an order. Refund only with the refund tool. Never refund more than the order's total.
```

```dockerfile Dockerfile theme={null}
FROM python:3.13-slim
WORKDIR /app
COPY vendor/ vendor/
COPY requirements.txt .
RUN pip install --no-cache-dir "$(ls vendor/*.whl)[openai-agents]" -r requirements.txt
COPY app.py .
USER nobody
EXPOSE 8080
CMD ["python", "-u", "app.py"]
```

```text requirements.txt theme={null}
openai-agents==0.23.1
```

The SDK wheel goes in the bundle's `vendor/` folder and is installed with its `openai-agents` extra; see [SDKs](https://docs.upsonic.ai/agentos/build/sdks.md).

## How the model is wired

`openai_agents.model(inv)` is an `OpenAIChatCompletionsModel` for `config.models.main`, on an `AsyncOpenAI` client 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 reaches the gateway exactly as written in `agentos.yaml` (`anthropic/claude-sonnet-4-5`). Pass a role for another model: `openai_agents.model(inv, "judge")`.

The agent is built inside the handler, on every invocation, because the token belongs to one invocation. The adapter turns the SDK's tracing off, since it would send traces to OpenAI.

When a guardrail blocks a model call, the OpenAI client raises `BadRequestError`. `agentos.guardrail_block(error)` finds the block in it, and raising that block fails the run with `blocked_by_guardrail` and the guardrail's message. See [Guardrails](https://docs.upsonic.ai/agentos/platform/guardrails.md).

## How the approval works

**1. The run stops for approval**

The model calls `refund`. Because the tool has `needs_approval=True`, the SDK stops the run with an interruption instead of running it. `openai_agents.run` keeps the run's `RunState` and asks one hold: one item per waiting call, its id the tool call id, its data the call's arguments, with **approve** and **reject**.

**2. A person decides**

The run is held until someone decides in the **Review queue** or through the [Holds API](https://docs.upsonic.ai/agentos/api/holds.md).

**3. The run continues**

On the resume the adapter restores the `RunState`, approves or rejects each call, and continues the run. An approved refund runs; a rejected one returns the reviewer's comment (or "A person rejected this tool call.") to the model as the tool's result, and the model answers the customer.

> [!NOTE]
>
> The OpenAI Agents SDK can't run a call with changed arguments, so this adapter offers **approve** and **reject**, not **edit**. Use [LangGraph](https://docs.upsonic.ai/agentos/frameworks/langgraph.md) or [Agno](https://docs.upsonic.ai/agentos/frameworks/agno.md) when reviewers should change the amount.

## Run it

**1. Zip and deploy**

Put the SDK wheel in `vendor/` (see [SDKs](https://docs.upsonic.ai/agentos/build/sdks.md)), set `config.models.main` to a model from your **Models** page, then zip the folder's contents and upload them:

```bash theme={null}
cd support-openai-agents && zip -r ../support-openai-agents.zip . && cd ..
curl -sS -H "Authorization: Bearer $AGENTOS_API_KEY" -F bundle=@support-openai-agents.zip \
  $AGENTOS/v0/envs/default/agents/support-openai-agents/versions
```

**2. Ask for a refund**

```bash theme={null}
curl -sS -X POST "$AGENTOS/v0/envs/default/agents/support-openai-agents/capabilities/support/runs?wait=true" \
  -H "Authorization: Bearer $AGENTOS_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"input": {"message": "Order A-1001 arrived broken. Can I get my 49.90 back?"}}'
# 200 {"run_id": "r_41", "status": "held", "hold": {"hold_id": "h_12", ...}, ...}
```

The run is **held**: the refund waits in the **Review queue**. Decide it there, or through the [Holds API](https://docs.upsonic.ai/agentos/api/holds.md). The item's id is the model's tool call id: read it from the hold (`GET …/holds/h_12`).

```bash theme={null}
curl -sS -X POST -H "Authorization: Bearer $AGENTOS_API_KEY" -H "Content-Type: application/json" \
  $AGENTOS/v0/envs/default/holds/h_12/decision \
  -d '{"items": [{"id": "call_…", "action": "approve"}]}'
curl -sS -H "Authorization: Bearer $AGENTOS_API_KEY" $AGENTOS/v0/envs/default/runs/r_41
# {"status": "completed", "output": {"answer": "Your refund of 49.90 for order A-1001 is on its way."}, ...}
```

## Test it before you deploy

The [conformance kit](https://docs.upsonic.ai/agentos/build/conformance.md) builds the bundle and runs it against a mock platform whose model follows `call:<tool> {arguments}` in a message, so these cases drive the tools and both decisions without a real model:

```yaml conformance.yaml theme={null}
# The mock model answers `call:<tool> {arguments}` in a user message with that tool call (its id is call_1).
cases:
  - capability: support
    name: lookup
    input: {message: 'call:lookup_order {"order_id": "A-1001"}'}
  - capability: support
    name: refund-approved
    input: {message: 'call:refund {"order_id": "A-1001", "amount": "49.90"}'}
    expect: suspended
  - capability: support
    name: refund-rejected
    input: {message: 'call:refund {"order_id": "A-1001", "amount": "49.90"}'}
    expect: suspended
    decision: [{id: call_1, action: reject}]
```

```bash theme={null}
agentos-conformance check support-openai-agents/
```

## Things to know

* **Chat Completions, not the Responses API.** The adapter's model uses `/v1/chat/completions`, which every provider behind the gateway supports.
* **The `RunState` travels in run state.** A value there can be at most 1 MiB, and the `RunState` grows with the conversation; keep very long conversations short.
* **A finished run isn't run again.** If the same AgentOS run invokes the handler after the agent finished, `openai_agents.run` returns what it kept (`result.final_output`) instead of calling the model and the tools again.

## Next steps

> **[Human approvals](https://docs.upsonic.ai/agentos/build/human-approvals.md)**
>
> Everything an ask can carry: several items, details, files, an expiry.

> **[More frameworks](https://docs.upsonic.ai/agentos/frameworks/overview.md)**
>
> The same agent in other frameworks and languages.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.