Agent Approval
personaforge/approval provides the signals and stores for human-in-the-loop agent control. Two suspension mechanisms are supported:
- Before execution — a tool call is paused before
executeruns whenrequireApproval/needsApprovalis set on the tool (orrequireToolApprovalon the run). - Mid-execution — a tool self-pauses by calling
context.agent.suspend(payload)to request more input.
import {
isApprovalRequiredError,
isToolSuspendedError,
InMemorySuspendedRunStore,
createSqliteSuspendedRunStore,
} from 'personaforge/approval';Signals
ApprovalRequiredError
Raised before a tool executes when approval is required. The toolCall and step are attached for inspection:
import { isApprovalRequiredError } from 'personaforge/approval';
try {
await agent.run('Send an email to bob@example.com');
} catch (err) {
if (isApprovalRequiredError(err)) {
console.log(`Tool ${err.toolName} needs approval (args:`, err.args, ')');
// answer via the durable agent / approval store, or surface to a human UI.
}
}ToolSuspendedError
Raised inside a tool's execute when it calls context.agent.suspend(payload):
import { tool } from 'personaforge';
const collectAddress = tool({
name: 'collect_address',
description: 'Collect a shipping address.',
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }, ctx) => {
// Pause and ask for more input instead of failing:
ctx.agent.suspend({ orderId, question: 'Please provide the shipping address.' });
// Unreachable — suspend() never returns.
},
});Suspended-run store
Pending approvals / suspensions are persisted as SuspendedRun records so a later request (after a restart, or from a different server) can rediscover and answer them.
In-memory (development)
import { InMemorySuspendedRunStore } from 'personaforge/approval';
const store = new InMemorySuspendedRunStore();SQLite (production)
import { createSqliteSuspendedRunStore } from 'personaforge/approval';
const store = createSqliteSuspendedRunStore('./agent.db');
await store.save({
runId: 'run_123',
agentId: 'support-bot',
threadId: 't1',
resourceId: 'user-7',
status: 'approval',
toolCalls: [{
toolCallId: 'call_1',
toolName: 'send_invoice',
args: { customerId: 'c1', amount: 500 },
requiresApproval: true,
}],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const pending = await store.list({ threadId: 't1' });
await store.markResolved('run_123');Run-scoped approval
For run-wide approval policy, use requireToolApproval in the agent run options — boolean for every tool, or a function for per-call decisions (fails closed):
await agent.run('Deploy to prod', {
requireToolApproval: ({ toolName, args }) =>
toolName === 'deploy' && args.environment === 'production',
});Use approvedToolCalls to carry already-approved call ids into a resumed run:
await agent.run('Deploy to prod', {
approvedToolCalls: ['call_789'],
});Integration with durable agents
Durable runs expose approveToolCall, declineToolCall, resumeStream, and listSuspendedRuns directly. See Durable Agents for the full flow.
Related pages
- Durable Agents — resumable, replayable runs with approval wiring.
- Human-in-the-Loop (HITL) — the production approval store + HTTP endpoints.
- Tools —
needsApproval/requireApprovalontool().