mockModel so they run as pasted, with no API key. Swap in a
real provider or model for your own agent; nothing else changes. Each
section links to the page that has the full detail.
1. A tool that needs approval
SetneedsApproval on the tool. The run pauses before the tool executes:
2. Give the agent a durable store
Pauses and checkpoints live in the agent’sstore. The default is in memory,
which a restart loses. Use a durable one: SqliteStore (one transactional file,
shared by several processes on one host) or fileStore(dir) (plain JSON files). See
Choosing a store.
3. Send: the run pauses
send() does not throw at a pause. It resolves with
finishReason: 'awaiting-approval' and the id to decide:
agent.session({ id }) instead of sessionId when the conversation should
keep a transcript across turns (see Sessions);
the pause and resume calls below are the same.
4. In a new process: list the pauses and decide one
Start the process again with the same store.agent.approvals.list() returns
the pending calls, oldest first, including the ones another process or an earlier
run left in the store. agent.approvals.resolve() runs the call (or rejects it),
continues the run, and resolves with the result:
- To reject, pass
approved: falseand anotethe model sees as the reason. - A session that paused is found from its pause record, so you do not need to
open it or call
resume()first. resolve()throws when the id is unknown or already decided, so two processes racing on one pause cannot both run the tool.- A pause from
send(msg, { sessionId })shows inagent.pending()withstatus: 'awaiting-approval'and itsapprovalId.
5. See the run finish
resolve() resolves with the continued run’s result. The tool ran, the model got
its output and answered, and finishReason is 'stop'. If the continued run
pauses again (another needsApproval call), you get 'awaiting-approval' and a
new id: list and decide again. To show progress to a user instead, use
agent.approvals.streamResolve(). Everything else about decisions (several calls
in one step, “don’t ask again”, approve with edited arguments, expiry) is in
Approvals.
6. When the process dies mid-run
A crash or a kill leaves the run’s checkpoint in the store with status'in-progress'. A restarted process finds it with agent.pending() and finishes
it with agent.resume(sessionId), or hands the whole job to agent.recover().
This snippet stands in for the crash with a tool that throws
PropagatingToolError the first time (the way a dying process would stop the
step), then recovers:
agent.recover() once per process at
startup: it polls pending() and resumes every 'in-progress' run, leaves
approvals for a human, and is safe to run on every replica (run leases make sure
one process resumes each run). See
Recovering automatically.
At-least-once for the step that was running. Recorded model responses and
finished tool results are never repeated: that is why charge ran once above.
But a tool that was mid-call when the process died runs again on resume, so its
side effect can happen twice. Make such tools idempotent (an idempotency key
passed to the payment API, a “have I done this” check). See
At-least-once tools.
A tool with needsApproval is not affected by this while it waits: a pause
records no work, it only waits for the decision.
7. Over HTTP
The built server (see Deployment) does this for you. A client that resends aPOST /chat after a dropped connection can send the same
Idempotency-Key header: a finished turn is replayed without running anything,
and a turn interrupted by a crash is resumed from its last checkpoint instead of
starting over. A session paused on an approval answers 409
(LOUSHO_SESSION_AWAITING_APPROVAL) until it is decided. The table of outcomes
is in Resending a turn.
Where to go next
- Approvals: permission policies, several calls in one step, expiry, deciding in code.
- Sessions: durable session turns and stores.
- Durable execution: checkpoints, run leases, the guarantees, fork and replay.