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
npm install @immiscible/sdk @openai/agents openai zodimport { 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:
import { openaiToolGuardrail } from '@immiscible/sdk/openai-agents';
tool({ name: 'buy', parameters, execute, inputGuardrails: [openaiToolGuardrail({ client: immiscible, mapToAction })] });#Python
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible openai-agentsfrom 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:
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, packages/immiscible-py/examples/openai_agents_example.py. Both run against the fake with --demo.