A run that waits
A run that is waiting for a person can be saved as JSON, and resumed from it later. This file does both halves in one process; in an app they are two requests, or two servers.
pnpm add @niscorp/cortex @niscorp/signal zod openaiThe 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.
import { z } from 'zod';
import { createSignal } from '@niscorp/signal';
import { defineAgent, defineTool, resumeRun } from '@niscorp/cortex';
import type { RunSnapshot } from '@niscorp/cortex';
const llm = createSignal('groq');An agent that always asks
policy is the short way to write the common gates. This one says the refund tool needs a person’s approval, by the tool’s id.
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 }),
});
const desk = defineAgent({
id: 'desk',
instructions: 'Refund a booking when the customer asks for it.',
tools: [refund],
policy: { tools: { requireApproval: ['refund'] } },
});Save it where it stops
run.snapshot() is the whole run as plain JSON: the conversation so far, the call that is waiting, and what has been used. It can go in a database row.
The run is then aborted here. Nothing has to stay in memory while a person decides.
const first = desk.run('Please refund booking B-2041, 58 euros.', { llm });
const saved = await new Promise<string>((resolve) => {
first.onEvent((event) => {
if (event.type === 'approval-required') resolve(JSON.stringify(first.snapshot()));
});
});
first.abort('saved');
console.log('saved', saved.length, 'characters');Resume it
resumeRun takes the agent and the document. The resumed run asks its gates again for the waiting call, so approval-required is sent again, with a new id.
Answer with the id from that event. The id inside the saved document belongs to the run that was saved.
const snapshot = JSON.parse(saved) as RunSnapshot;
const second = resumeRun(desk, snapshot, { llm });
second.onEvent((event) => {
if (event.type === 'approval-required') second.approve(event.approval.id);
});
const result = await second.result;
console.log(result.ok ? result.output.response : result.error.message);Run it.
This file asks a model, so it is not run on this site. When the model asks for the refund, it prints how long the saved JSON is, and then the model’s sentence saying the refund was made: the function ran in the second run, after the approval.
GROQ_API_KEY=your-key npx tsx waits.mts