The CLI is published on npm (npm i -g @aex-lang/cli). APIs are experimental and may change before v1.0. Roadmap →
Skip to content

OpenAI Agents SDK

The @aex-lang/openai-agents package wraps an OpenAI Agents SDK workflow with AEX enforcement. Your agent runs normally — but every tool call is gated by an AEX contract at runtime.

Installation

bash
npm install @aex-lang/openai-agents

This also installs @aex-lang/parser and @aex-lang/runtime as dependencies.

Quick Start

ts
import { AEXGuardedAgent } from "@aex-lang/openai-agents";

const agent = new AEXGuardedAgent({
  taskPath: "tasks/support-ticket.aex",
  tools: {
    "crm.lookup": {
      sideEffect: "none",
      handler: async (args) => {
        return { name: "Alice", plan: "pro", email: "alice@example.com" };
      },
    },
    "ticket.read": {
      sideEffect: "none",
      handler: async (args) => {
        return { subject: "Login issue", body: "Can't sign in since Tuesday." };
      },
    },
    "email.draft": {
      sideEffect: "write",
      handler: async (args) => {
        return { drafted: true, to: args.to, body: args.body };
      },
    },
  },
});

const result = await agent.run({
  customer_id: "cust_123",
  ticket_id: "tkt_456",
});

if (result.status === "success") {
  console.log("Output:", result.output);
} else {
  console.log("Blocked:", result.issues);
}

Contract

The agent's behavior is governed by an AEX contract:

aex
task support_ticket v0

goal "Draft a reply to a support ticket."

use crm.lookup, ticket.read, email.draft
deny email.send, payment.*, admin.*, secrets.read

need customer_id: str
need ticket_id: str

do crm.lookup(id=customer_id) -> customer
do ticket.read(id=ticket_id) -> ticket

make reply: str from customer, ticket with:
  - address the customer by name
  - reference the ticket subject
  - do not include internal notes

check reply does not include customer.internal_notes
confirm before email.draft

do email.draft(to=customer.email, body=reply) -> result

return {
  status: "drafted",
  reply: reply
}

API Reference

new AEXGuardedAgent(options)

OptionTypeRequiredDescription
taskPathstringyesPath to .aex contract file
toolsToolRegistryyesTool implementations keyed by AEX tool name
modelModelHandlernoHandler for make steps (generates artifacts)
policyRuntimePolicynoAdditional runtime policy on top of the contract
confirm(tool: string) => Promise<boolean>noConfirmation handler for confirm before gates
logger(event: RuntimeEvent) => voidnoAudit event stream

agent.run(inputs)

Executes the contract with the given inputs. Returns a RunResult:

ts
interface RunResult {
  status: "success" | "blocked";
  output?: unknown;
  issues: string[];
}

Tool Registry

Each tool has a sideEffect declaration and a handler:

ts
interface ToolDefinition {
  sideEffect: "none" | "read" | "write";
  handler: (
    args: Record<string, unknown>,
    ctx: ToolContext,
  ) => Promise<unknown>;
}

type ToolRegistry = Record<string, ToolDefinition>;

The sideEffect field is informational. The AEX contract's use/deny/confirm rules are what's enforced at runtime.

Adding a Model Handler

If your contract uses make steps, provide a model handler:

ts
const agent = new AEXGuardedAgent({
  taskPath: "tasks/support-ticket.aex",
  tools: { /* ... */ },
  model: async (step) => {
    // step.type — artifact type ("str", "diff", "markdown")
    // step.inputs — named inputs available to the step
    // step.instructions — bullet points from the contract
    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [
        {
          role: "system",
          content: `Generate a ${step.type}. ${step.instructions.join(". ")}`,
        },
        { role: "user", content: JSON.stringify(step.inputs) },
      ],
    });
    return response.choices[0].message.content;
  },
});

Without a model handler, execution blocks at the first make step.

Runtime Policy Overlay

Layer a policy on top of the contract for additional constraints:

ts
const agent = new AEXGuardedAgent({
  taskPath: "tasks/support-ticket.aex",
  tools: { /* ... */ },
  policy: {
    deny: ["email.send"],
    budget: { calls: 10 },
  },
});

Effective permissions are the most restrictive combination: allow is intersected, deny is unioned, budget takes the minimum.

Logging and Audit

Stream structured audit events:

ts
const agent = new AEXGuardedAgent({
  taskPath: "tasks/support-ticket.aex",
  tools: { /* ... */ },
  logger: (event) => console.log(JSON.stringify(event)),
});

await agent.run(inputs);
json
{"event": "run.started", "agent": "support_ticket", "version": "v0"}
{"event": "tool.allowed", "tool": "crm.lookup", "step": 1}
{"event": "tool.result", "tool": "crm.lookup", "bind": "customer"}
{"event": "tool.allowed", "tool": "ticket.read", "step": 2}
{"event": "check.passed", "condition": "reply does not include customer.internal_notes"}
{"event": "confirm.required", "tool": "email.draft"}
{"event": "run.finished", "status": "success"}

Error Handling

When a contract blocks execution, result.issues contains the reasons:

ts
const result = await agent.run({ customer_id: "cust_123" });

if (result.status === "blocked") {
  console.log(result.issues);
  // ["AEX030: Missing required input: ticket_id (expected: str)"]
}

Common block reasons:

CodeReason
AEX030Missing required input
AEX031Input type mismatch
Tool not declared in use list
Tool matches a deny pattern
Budget exhausted
Confirmation required (no handler provided)
check condition failed

See Also

Prompts are not permissions. Plans are not contracts.