Human In The Loop (HITL) โ
HITL lets an agent pause before performing a high-risk action (send an email, charge a card, delete a record) and wait for a human decision. The approval request is persisted durably so the agent can resume even after a restart.
ts
import {
InMemoryApprovalStore,
SqliteApprovalStore,
createSqliteApprovalStore,
waitForApproval,
ApprovalRejectedError,
} from 'confused-ai';Quick start โ
ts
import { createAgent, tool } from 'confused-ai';
import { createSqliteApprovalStore, waitForApproval, ApprovalRejectedError } from 'confused-ai';
import { z } from 'zod';
const approvalStore = createSqliteApprovalStore('./agent.db');
// Wrap a risky tool with an approval gate
const sendInvoice = tool({
name: 'send_invoice',
description: 'Send an invoice email to a customer.',
schema: z.object({ customerId: z.string(), amount: z.number() }),
execute: async ({ customerId, amount }, ctx) => {
// Gate: pause here until a human approves
await waitForApproval(approvalStore, {
runId: ctx.runId!,
agentName: 'billing-agent',
toolName: 'send_invoice',
toolArguments: { customerId, amount },
riskLevel: 'high',
description: `Send a $${amount} invoice to customer ${customerId}`,
timeoutMs: 24 * 60 * 60 * 1000, // 24 hours
});
// Only runs after approval
await emailService.sendInvoice(customerId, amount);
return { sent: true };
},
});
const agent = createAgent({
name: 'billing-agent',
instructions: 'Handle billing and invoicing tasks.',
model: 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY!,
tools: [sendInvoice],
});
try {
const result = await agent.run('Send a $500 invoice to customer cust-123.');
} catch (err) {
if (err instanceof ApprovalRejectedError) {
console.log('Human rejected the action:', err.comment);
}
}Approval stores โ
InMemoryApprovalStore (testing) โ
ts
import { InMemoryApprovalStore } from 'confused-ai';
const store = new InMemoryApprovalStore();SqliteApprovalStore (production) โ
ts
import { createSqliteApprovalStore } from 'confused-ai';
const store = createSqliteApprovalStore('./agent.db');ApprovalStore interface โ
ts
interface ApprovalStore {
create(request: Omit<HitlRequest, 'id' | 'status' | 'createdAt' | 'expiresAt'>): Promise<HitlRequest>;
get(id: string): Promise<HitlRequest | null>;
getByRunId(runId: string): Promise<HitlRequest[]>;
decide(id: string, decision: ApprovalDecision): Promise<HitlRequest>;
listPending(): Promise<HitlRequest[]>;
cleanup(olderThanMs: number): Promise<number>;
}HitlRequest shape โ
ts
interface HitlRequest {
readonly id: string;
readonly runId: string;
readonly agentName: string;
readonly toolName: string;
readonly toolArguments: Record<string, unknown>;
readonly riskLevel: 'low' | 'medium' | 'high' | 'critical';
readonly description?: string;
readonly status: 'pending' | 'approved' | 'rejected' | 'expired';
readonly comment?: string; // reviewer comment
readonly createdAt: string;
readonly expiresAt: string;
readonly decidedAt?: string;
}HTTP approval endpoint โ
When you serve your agent with createHttpService(), pass an approvalStore to expose a built-in REST endpoint:
ts
import { createHttpService } from 'confused-ai';
import { createSqliteApprovalStore } from 'confused-ai';
const approvalStore = createSqliteApprovalStore('./agent.db');
const app = createHttpService({ agent, approvalStore });
app.listen(3000);List pending approvals:
GET /v1/approvals?status=pendingSubmit a decision:
POST /v1/approvals/:id
Content-Type: application/json
{ "decision": "approved", "comment": "Looks good to me" }Manual approval decision (testing) โ
ts
// Simulate an approver submitting a decision
const pending = await approvalStore.listPending();
for (const req of pending) {
await approvalStore.decide(req.id, {
decision: 'approved',
comment: 'Reviewed and approved.',
});
}Rejection handling โ
When a human rejects a request, waitForApproval throws ApprovalRejectedError:
ts
import { ApprovalRejectedError } from 'confused-ai';
try {
const result = await agent.run(prompt, { runId: 'run-abc' });
} catch (err) {
if (err instanceof ApprovalRejectedError) {
console.log('Rejected because:', err.comment);
// Notify user, log, update UI, etc.
}
}Where to go next โ
- Guardrails โ automatic policy-based blocking without human review.
- Production โ circuit breakers, audit logs, idempotency.
- Example 03: Approval tool โ full HITL example.