Skip to content

SDKs

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
TypeScript
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:

TypeScript
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 saysThe toolThe model reads
allowruns; settled completed (or failed if it threw)the tool’s result
approval requiredwaits (default up to ten minutes), then as abovethe result, or a refusal if the person says no
denynever 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

TypeScriptPython
clientclientan 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
waitwaitdefault true
onApprovalRequired(d)on_approval_required(d)show the person d.approval.url / d.approval_url
timeoutMs, initialDelayMs, maxDelayMs, signaltimeout, initial_delay, max_delay, cancelthe 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, packages/immiscible-py/examples/openai_agents_example.py. Both run against the fake with --demo.