Guardrails and approvals
Check input and output, and pause for a human before risky tools.
Approvals
Mark a tool needsApproval: true and the run stops before calling it. Nothing runs until someone decides.
const issueRefund = tool({
name: 'issue_refund',
description: 'Refund an order.',
parameters: z.object({ id: z.string(), amount: z.number() }),
needsApproval: true,
execute: async ({ id, amount }) => payments.refund(id, amount),
});
const result = await run(billing, 'Refund order 1042');
if (result.interruptions.length > 0) {
// [{ type: 'tool_approval', callId, toolName: 'issue_refund', arguments: '{"id":"1042","amount":40}', agentName }]
await db.reviews.insert({ state: JSON.stringify(result.state), items: result.interruptions });
}Later, maybe in another process, after a person decides:
import { RunState, run } from 'cencori/agents';
const review = await db.reviews.find(reviewId);
const state = RunState.fromJSON(review.state);
state.approve(review.items[0].callId);
// or: state.reject(review.items[0].callId, { message: 'Over the limit without a manager.' });
const result = await run(billing, state);
result.finalOutput; // "Done: I've refunded $40 for order 1042."RunStateis plain JSON: keep it in your database, a queue, anywhere.- Cencori holds a paused session for 24 hours.
- A rejected call isn't run; the model is told it was rejected (with your message) and continues.
- With
needsApproval: true, the request and the decision are recorded in your project's governance ledger.
Deciding per call
Pass a function to decide for each call, in your process:
needsApproval: async ({ context }, { amount }) => amount > 100 || !context.isManager,In a chat
Agent UI shows an approval card and continues the run when the user decides. The paused state is signed so the browser can't change it.
Input guardrails
Run before the first model call. Return tripwireTriggered: true to stop the run.
const noSecrets = {
name: 'no_secrets',
execute: async ({ input }) => ({ tripwireTriggered: /sk_live_|-----BEGIN/.test(input) }),
};
const agent = new Agent({ name: 'Support', inputGuardrails: [noSecrets] });Output guardrails
Run on the final answer before run() returns it.
const onTopic = {
name: 'on_topic',
execute: async ({ output }) => {
const verdict = await run(classifier, String(output));
return { tripwireTriggered: verdict.finalOutput.offTopic, outputInfo: verdict.finalOutput };
},
};A tripped guardrail throws GuardrailTripwireTriggered with kind ('input' or 'output'), the guardrail's name and your outputInfo.
Gateway protection still applies
Every model call in a run goes through the Cencori gateway, so your project's PII rules, security policies, budgets and rate limits apply to agents too, with no extra code.

