Skip to Content
IntegrationsOpenAI Agents SDK

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 current quard.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 needsApproval stays open. Resumed from result.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 inside quard.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 execute still gets the run context and the call details.
  • A block becomes a tool guardrail rejection from the quard guardrail. The model reads Quard’s refusal as the tool result, and result.toolOutputGuardrailResults holds 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 and run() rejects with GuardBlockedError. Its cause is the SDK’s ToolCallError. A streamed run gives the same error through result.completed, its events and result.error.
  • An approval guard waits inside the call for an answer from the dashboard, as anywhere else. The SDK’s own needsApproval still 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’s Runner.prototype.run. Runners without a quardRunner() provider run as before. So do runs started with the SDK’s run(), and agent tools with their own runConfig.modelProvider.
  • An agent whose model is a Model object, 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