# OpenAI Agents SDK

Source: https://immiscible.fly.dev/docs/sdks/integrations/openai-agents

# OpenAI Agents SDK

Every tool call asks Immiscible before it runs and settles after; the agent's model calls go through the gateway in the same run. TypeScript (`@openai/agents`) and Python (`openai-agents`).

Tested against `@openai/agents` 0.18 and `openai-agents` 0.23, with real agent runs on the fake server (the tests are in `packages/immiscible-js/test/frameworks.test.mjs` and `packages/immiscible-py/tests/test_frameworks.py`).

## TypeScript

```shell
npm install @immiscible/sdk @openai/agents openai zod
```

```ts
import { Agent, run, tool, setDefaultOpenAIClient, setOpenAIAPI } from '@openai/agents';
import OpenAI from 'openai';
import { z } from 'zod';
import { Immiscible } from '@immiscible/sdk';
import { guardOpenAITools } from '@immiscible/sdk/openai-agents';

const immiscible = new Immiscible().run();

// Model calls through the gateway, inside the run.
setDefaultOpenAIClient(new OpenAI(immiscible.gateway.openai()));
setOpenAIAPI('chat_completions');                      // the gateway speaks Chat Completions

const buy = tool({
  name: 'buy',
  description: 'Buy groceries from a supermarket',
  parameters: z.object({ pence: z.number().int(), domain: z.string() }),
  execute: async ({ pence, domain }) => placeOrder(pence, domain),
});

const agent = new Agent({
  name: 'Shopper',
  tools: guardOpenAITools([buy, search], {
    client: immiscible,
    mapToAction: ({ name, args }) => name === 'search' ? null
      : Immiscible.paymentAction({ amount: args.pence, currency: 'GBP', merchant: args.domain, provenance: [{ source: 'user' }] }),
    onApprovalRequired: (d) => notify(`Approve at ${d.approval.url}`),
  }),
});

const result = await run(agent, 'Buy this week\'s milk and bread from tesco.com.');
```

`guardOpenAITool(tool, opts)` guards one tool; `guardOpenAITools(list, opts)` guards every function tool in a list and passes hosted tools and handoffs through. Each returns copies; your originals are untouched.

### The guardrail alternative

If you cannot wrap the tool (it comes from somewhere else), attach a tool input guardrail. It authorises and waits for a person, but cannot settle, because a guardrail never sees the result:

```ts
import { openaiToolGuardrail } from '@immiscible/sdk/openai-agents';
tool({ name: 'buy', parameters, execute, inputGuardrails: [openaiToolGuardrail({ client: immiscible, mapToAction })] });
```

## Python

```shell
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible openai-agents
```

```python
from agents import Agent, Runner, function_tool, set_default_openai_api, set_default_openai_client
from immiscible import Immiscible
from immiscible.integrations import guard_tools

immiscible = Immiscible().run(client="openai-sdk")
set_default_openai_client(immiscible.gateway.openai_client(async_=True))   # model calls through the gateway
set_default_openai_api("chat_completions")

@function_tool
def buy(pence: int, domain: str) -> str:
    """Buy groceries from a supermarket."""
    return place_order(pence, domain)

def to_action(call):
    if call.name == "search":
        return None
    return Immiscible.payment_action(call.args["pence"], "GBP", call.args["domain"], provenance=[{"source": "user"}])

agent = Agent(name="Shopper", tools=guard_tools([buy, search], client=immiscible, map_to_action=to_action))
result = await Runner.run(agent, "Buy this week's milk and bread from tesco.com.")
```

The guarded `on_invoke_tool` makes its HTTP calls, including the wait for a person, in a worker thread, so the event loop keeps running other work.

Or guard the function itself, below the framework's decorator:

```python
from immiscible.integrations import guarded

@function_tool
@guarded(to_action, client=immiscible)
def buy(pence: int, domain: str) -> str:
    """Buy groceries from a supermarket."""
```

The signature, annotations and docstring are kept, so the tool's JSON schema is unchanged.

## What the model sees

| Immiscible says | The tool | The model reads |
|---|---|---|
| allow | runs; settled `completed` (or `failed` if it threw) | the tool's result |
| approval required | waits (default up to ten minutes), then as above | the result, or a refusal if the person says no |
| deny | never runs | "Immiscible refused this action: <reasons>. Do not proceed and do not try it another way. Tell the person what was refused and why." |

Pass `onDeny: 'throw'` (TypeScript) or `on_deny="raise"` (Python) to get the typed error instead of the message. `wait: false` refuses at once when a person would be asked. The model's tool call id becomes the idempotency key, so a retried call is the same action.

## Options

| TypeScript | Python | |
|---|---|---|
| `client` | `client` | an `Immiscible` (use a run). Default: one from the environment |
| `mapToAction(call)` | `map_to_action(call)` | `call.name`, `call.args` (parsed), `call.callId` / `call.call_id`. Return `null` / `None` to skip the check. Default: a `tool.call` |
| `wait` | `wait` | default true |
| `onApprovalRequired(d)` | `on_approval_required(d)` | show the person `d.approval.url` / `d.approval_url` |
| `timeoutMs`, `initialDelayMs`, `maxDelayMs`, `signal` | `timeout`, `initial_delay`, `max_delay`, `cancel` | the wait |
| `settle`, `settleAmount(result)` | `settle`, `settle_amount(result)` | what is recorded afterwards |
| `onDeny`, `refusal(err)` | `on_deny`, `refusal(err)` | how a refusal is returned |

Examples: [`packages/immiscible-js/examples/openai-agents.mjs`](https://github.com/efr7-7/immiscible/blob/main/packages/immiscible-js/examples/openai-agents.mjs), [`packages/immiscible-py/examples/openai_agents_example.py`](https://github.com/efr7-7/immiscible/blob/main/packages/immiscible-py/examples/openai_agents_example.py). Both run against the fake with `--demo`.
