Monitor

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:

GateQuestionThis guide?
Permission (Security)Is this call allowed by policy?no
Arg-validationDo the args match the schema?no
Check-inDoes 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 (system rules, the task, 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 wrapping explainChoice) 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.pauseData for a check-in — use outcome.checkIn (typed) and isCheckInPause(outcome).
  • Don't use checkIn as a policy engine. Policy is permissionChecker (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 -- --live

The 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

On this page