Cortex · guide

A gate before a tool

A gate is a function Cortex calls before every tool call. It lets the call through, refuses it, or stops the run until a person answers.

Install it
pnpm add @niscorp/cortex @niscorp/signal zod openai
Step 1 of 5

The model

createSignal makes the client that calls the model. Cortex is handed this client, here for Groq, and Signal reads the key from GROQ_API_KEY.

The model
import { z } from 'zod';
import { createSignal } from '@niscorp/signal';
import { defineAgent, defineTool } from '@niscorp/cortex';
import type { ToolGate } from '@niscorp/cortex';

const llm = createSignal('groq');
Step 2 of 5

A tool that moves money

The function gives a refund. Nothing in the tool says who may call it: that is the gate’s.

A tool that moves money
const refund = defineTool({
  id: 'refund',
  name: 'refund',
  description: 'Refund a booking to the card it was paid with',
  input: z.object({ booking: z.string(), amount_cents: z.number() }),
  execute: ({ booking, amount_cents }) => ({ booking, refunded_cents: amount_cents }),
});
Step 3 of 5

The gate

A gate is given the call, by the tool’s id and its arguments, and answers one of three things.

deny refuses: the function does not run, the model is given the reason as the result of its call, and the run goes on. ask stops the run for a person. allow lets it through, and may hand back changed arguments.

The gate
const LIMIT_CENTS = 10_000;

const gate: ToolGate = (call) => {
  if (call.toolId !== 'refund') return { allow: true };
  const asked = z.object({ amount_cents: z.number() }).safeParse(call.args);
  if (asked.success && asked.data.amount_cents > LIMIT_CENTS) {
    return { deny: 'A refund over 100 euros is made at the desk.' };
  }
  return { ask: { reason: 'A refund moves money, so a person says yes or no.' } };
};
Step 4 of 5

The agent

The gate is listed on the agent. It is asked before every call to any of its tools.

The agent
const desk = defineAgent({
  id: 'desk',
  instructions: 'Refund a booking when the customer asks for it.',
  tools: [refund],
  toolGates: [gate],
});
Step 5 of 5

A person answers

When a gate asks, the run sends an approval-required event and waits. run.approve lets the call through, and run.deny refuses it with a reason the model is told.

Here the answer is given at once. In an app it is a dialog: the event carries the tool, its arguments and the gate’s reason, which is everything a person needs to decide.

A person answers
const run = desk.run('Please refund booking B-2041, 58 euros.', { llm });
run.onEvent((event) => {
  if (event.type === 'approval-required') {
    console.log('asked:', event.approval.toolId, event.approval.args, event.approval.reason);
    run.approve(event.approval.id);
  }
  if (event.type === 'tool-end') console.log('tool ended:', event.observation.kind);
});

const result = await run.result;
console.log(result.ok ? result.output.response : result.error.message);
Then

Run it.

This file asks a model, so it is not run on this site. When the model asks for the refund, it prints the refund and the gate’s reason, then tool ended: result, and the model’s sentence saying the refund was made. Change the question to a refund over 100 euros and it prints tool ended: denied, and the model’s answer gives the gate’s reason.

Run it
GROQ_API_KEY=your-key npx tsx gate.mts