OpenAI Agents SDK
Quard works with the OpenAI Agents SDK for JavaScript. It follows handoffs and agents run as tools, so every agent’s steps land in one run under the right name. Import it from quard/openai-agents. It needs @openai/agents 0.18.
Set it up
Set up Quard with quard.configure() as usual. Then build your tools with guardedTool() and run your agents with quardRunner():
import OpenAI from "openai";
import { Agent } from "@openai/agents";
import { z } from "zod";
import { guardedTool, quardRunner } from "quard/openai-agents";
const fetchPage = guardedTool({
name: "fetchPage",
description: "Fetch a web page",
parameters: z.object({ url: z.string() }),
execute: async ({ url }) => (await fetch(url)).text(),
guard: { type: "source", origin: "web" },
});
const payInvoice = guardedTool({
name: "payInvoice",
description: "Pay a supplier invoice",
parameters: z.object({ iban: z.string(), amount: z.number() }),
execute: async ({ iban, amount }) => pay(iban, amount),
guard: { type: "action", rules: [{ field: "iban", from: ["tool:getSupplier"] }] },
});
const billing = new Agent({ name: "billing", instructions: "Pay invoices.", tools: [payInvoice] });
const researcher = new Agent({ name: "researcher", instructions: "Read pages.", tools: [fetchPage] });
const orchestrator = new Agent({
name: "orchestrator",
instructions: "Plan the work.",
tools: [researcher.asTool({ toolName: "research", toolDescription: "Read a web page" })],
handoffs: [billing],
});
const runner = quardRunner({ client: new OpenAI() });
const result = await runner.run(orchestrator, "Pay invoice 114 from Acme Supplies.");quardRunner()
quardRunner() returns the SDK’s Runner. It takes the SDK’s run config, plus client: your OpenAI client. Quard wraps the client with quard.wrap() unless it already is.
- One run. Each
run()lands in one Quard run: the run of the currentquard.run(), or a new run named after the first agent. - The right agent. The agent comes from the framework. A handoff switches it, and an agent run as a tool, with
agent.asTool(), runs under its own name. Model calls and guarded tools carry that agent. - The run graph. Each switch is recorded as a handoff, from one agent to the other, through a handoff or a tool. The new agent’s parent step is the step that handed over. A handoff keeps the delegation depth. An agent run as a tool is one level deeper, below the model call that asked for it.
- Model calls. They use the Responses API over HTTP, so Quard sees each one and checks the run limits on steps and cost.
- Resumed runs. A run stopped by the SDK’s own
needsApprovalstays open. Resumed fromresult.state, or from a state rebuilt in the same process, it goes on in the same Quard run with the agent it stopped at. So the approved call’s guards still see everything the run read. A run that stopped insidequard.run()ends with that scope. A state resumed in another run, or a second time, brings the labels of the run it stopped in.
guardedTool()
guardedTool() takes the SDK’s tool() options, plus guard: the options of one guard or a list of them. Each guard takes the tool’s name.
- Every call runs through Quard’s checks. Only the input is checked, and
executestill gets the run context and the call details. - A block becomes a tool guardrail rejection from the
quardguardrail. The model reads Quard’s refusal as the tool result, andresult.toolOutputGuardrailResultsholds it. - With
outputSchema, real results are checked against it, as the SDK does. A refusal is not, so the model still reads it. - With
onBlock: "throw", the run stops andrun()rejects withGuardBlockedError. Itscauseis the SDK’sToolCallError. A streamed run gives the same error throughresult.completed, its events andresult.error. - An approval guard waits inside the call for an answer from the dashboard, as anywhere else. The SDK’s own
needsApprovalstill works beside it. - When the SDK gives up on a call, on the tool’s
timeoutMs, an aborted run or a sibling call that failed, the wait stops. The request is dropped and the tool never runs.
Known gaps
- To follow agents run as tools, the first
quardRunner()call wraps the SDK’sRunner.prototype.run. Runners without aquardRunner()provider run as before. So do runs started with the SDK’srun(), and agent tools with their ownrunConfig.modelProvider. - An agent whose
modelis aModelobject, not a name, skips the wrapped client. Its model calls are not recorded, but its guarded tools still carry the right agent. - Inside an agent run as a tool,
onBlock: "throw"stops only that agent’s run. The calling agent reads the error as the tool result.
Last updated on