Check in with the receipts
Evidence-carrying human consent for consequential tool actions. A tool declares checkIn; the ask rides an evidence pack (willDo / read / drivers / trail); the decision lands as a typed record.
An agent is about to issue a $5,000 refund. You don't want a policy to silently allow or block it — you want a person to say yes, and you want them to see WHY the agent is asking: what it will do, what context it read, which context drove the choice. That's a check-in. OpenWorker-class agents check in; agentfootprint checks in with the receipts.
Check-in vs. the other gates
Three independent gates run before a tool executes, in this order:
| Gate | Question | This guide? |
|---|---|---|
| Permission (Security) | Is this call allowed by policy? | no |
| Arg-validation | Do the args match the schema? | no |
| Check-in | Does a human consent to this consequential action? | yes |
Permission is a policy engine (allow/deny/halt, no human). Check-in is consent — a human decides, with an evidence pack in front of them. A call the policy denies never reaches the check-in gate; a call awaiting consent never resolves credentials or executes.
Declare the demand
A tool declares checkIn — 'always', or a (args, ctx) => boolean predicate for the "only high-effect" case. Configure the evidence pack on the builder with .checkIn({ evidence }) (optional — 'standard' is the default).
return Agent.create({ provider: provider ?? defaultProvider(), model: 'mock' }) .system('You are a refunds assistant. Refund only verified orders; confirm the amount first.') .tool( defineTool<{ amount: number }, string>({ name: 'issue_refund', description: 'Issue a refund to the customer', inputSchema: { type: 'object', properties: { amount: { type: 'number' } }, required: ['amount'], }, // Demand a human check-in ONLY for big refunds (a predicate; use // 'always' to gate every call). A refund under the line runs untouched. checkIn: (args) => args.amount > 1000, execute: ({ amount }) => `refunded ${amount}`, }), ) // Configure the evidence pack that rides the ask (optional — 'standard' is // the default; 'minimal' ships only `willDo`; or pass your own assembler). .checkIn({ evidence: 'standard' }) .build();A tool WITHOUT checkIn is byte-identical to before — no gate, no events, no pause.
The ask carries the receipts
When the demand trips, agent.run() pauses BEFORE the tool executes and returns a RunnerPauseOutcome whose checkIn field is a typed CheckInRequest. isCheckInPause(outcome) distinguishes it from a plain askHuman pause.
const outcome = await agent.run({ message: input });if (isCheckInPause(outcome)) { // The receipts — render these for the human who decides. const { tool, args, intent, evidence } = outcome.checkIn; console.log(`Approve "${tool}"? args=${JSON.stringify(args)}`); console.log(` will do : ${evidence.willDo}`); console.log(` because : ${intent ?? '(no stated reason)'}`); console.log(` drivers : ${(evidence.drivers ?? []).slice(0, 3).map((d) => d.id).join(', ')}`); // Persist `outcome.checkpoint` (JSON) until the human answers.}return outcome;The evidence pack ('standard') has four plain-named fields:
willDo— a plain-words claim: the tool description + the rendered args.read— what context this run consumed, one frame per piece (systemrules, thetask,results from earlier tools).drivers— which context drove THIS choice, ranked. The default scorer is deterministic and makes zero LLM calls; pass your own (e.g. an embedding-backed one wrappingexplainChoice) via.checkIn({ scorer }).trail— a compact grouped run-so-far summary.
evidence: 'minimal' ships only willDo (zero cost). The whole pack survives structuredClone + JSON, so it can ride the checkpoint to another process.
The decision is a record
The human answers with checkInApproved({ by, note? }) or checkInDeclined({ by, note? }), and you agent.resume(checkpoint, decision). On approve the tool executes normally (same iteration semantics as any resume); on decline the model receives "declined by human: <note>" as the tool result and adapts in-loop.
// Attach the built-in store to capture the ask + decision as an audit record.const audit = new CheckInRecorder();agent.attach(audit);const final = await agent.resume(checkpoint, checkInApproved({ by: 'alice@ops', note: 'verified' }));// audit.getDecisions() → [{ toolName: 'issue_refund', approved: true, by: 'alice@ops', ... }]return final;Both the ask and the decision fire typed events — agentfootprint.checkin.request and agentfootprint.checkin.decision — that any recorder can observe via the emit channel, and the built-in CheckInRecorder captures them as a queryable audit trail (getRequests() / getDecisions() / getStats()).
Process A → checkpoint → Process B
Check-in rides the same JSON checkpoint machinery as pause / resume: the ask happens in one process, the human decides hours later, and a fresh agent resumes from the stored checkpoint. Build the agent fresh in Process B — only the checkpoint (and the decision) cross the boundary.
Anti-patterns
- Don't reach into
outcome.pauseDatafor a check-in — useoutcome.checkIn(typed) andisCheckInPause(outcome). - Don't use
checkInas a policy engine. Policy ispermissionChecker(deny/halt, no human); check-in is consent (a person decides). They compose — permission runs first. - Don't resume a check-in pause with a bare string — pass a
CheckInDecision(checkInApproved/checkInDeclined). A malformed resume declines by default, so a consequential tool never runs by accident.
Run the demo
The flagship demo — the coworker with the receipts — is a runnable "AI coworker" that drafts a weekly status doc, saves it (a save_draft tool with no check-in — real work, no interruption), then reaches for the one consequential action: posting to the team channel. That post_to_channel tool declares checkIn: 'always', so the run pauses and renders the receipts (willDo / read / drivers / trail) in your terminal.
# non-interactive approve (the tool runs)
npm run example examples/features/34-checkin-coworker.ts -- --approve
# non-interactive decline (watch the model adapt: it keeps the draft, posts nothing)
npm run example examples/features/34-checkin-coworker.ts -- --decline --note "not this week"
# interactive — a TTY prompts you to approve or decline
npm run example examples/features/34-checkin-coworker.ts
# real Claude instead of the deterministic $0 mock (only if ANTHROPIC_API_KEY is set)
npm run example examples/features/34-checkin-coworker.ts -- --liveThe default provider is a deterministic mock, so it runs for anyone with zero setup; the end of the run prints the CheckInRecorder audit trail (asks + decisions with by / note).
Next steps
- Pause / Resume — the checkpoint machinery check-in rides
- Security guide — the permission gate that runs before check-in
- Observability guide — the full typed-event taxonomy
Pause / Resume
Human-in-the-loop with JSON-checkpointed state. Pause hours mid-run via askHuman or pauseHere; resume on a different process, day, or server.
Resilience
withRetry + withFallback + fallbackProvider + withCircuitBreaker — composable decorators that wrap any LLMProvider with retry-on-transient, cross-provider failover, and fail-fast breaking. Compose freely.
