A codon that finishes is not always a codon that finished the work. An agent can stop early, write less than you expected, or leave an open-ended task half-explored, and the run will still report success. This page covers the three completion patterns hankweave gives us – extending one codon with exhaustWithPrompt, nudging it with an "Any more?" follow-up, and wiring a Sentinel Sweep so an observer's notes feed the agent's next pass – plus the loop form of "keep going", which borrows the loop machinery from concepts/loops. Each shape costs something: tokens, complexity, or both. We start with how to choose, walk through each pattern, and end with how to run and verify the result.
Choosing how hard to push for completeness#
A codon is one agent task in a hank. When we want it to take another pass, we have three patterns to choose from, and the choice follows from the shape of the work:
- Extension (
exhaustWithPrompt) for open-ended, single-agent exploration where each pass needs no different structure. - Loop for work where each iteration needs multiple distinct steps.
- Sentinel Sweep when we want review-loop behavior without adding codons.
A nudge – the Any More? follow-up codon – is the lightest option when one more prompt may be enough.
For the field contracts, see codon concepts, loop termination, and sentinel configuration and output paths. Before wiring a sweep, also read the one-way sentinel flow: the observer writes its own report, and the working agent reads it.
Three terms come up throughout the page. A session is one agent conversation inside a codon, identified by a SessionId; continue-previous reuses the previous codon's session, while fresh starts a new one – both are values we write explicitly in continuationMode. A run spans RunStarted through RunCompleted, and the execution directory is the <exec>/ folder holding agentRoot/ and .hankweave/ state. The distinction matters because each pattern operates at a different level: exhaustWithPrompt extends the current codon's session to its context boundary; it does not extend the whole run. The setting is one line of JSON, but the work is an extension prompt that knows what "more" looks like.
Check-it: For each codon, decide whether you need per-iteration structure, a second perspective, or an open-ended continuation. Pick a loop, sweep, extension, or nudge from those answers before adding continuation work.
Making one codon work to the context boundary#
The simplest pressure shape is exhaustWithPrompt. Set it when one codon should continue in the same session. After each successful completion, the harness re-sends that prompt unchanged, in the same session, until the context is exhausted or maxExtensions forces completion. Each extension is one additional completion round.
Three fields control the behavior:
| field | type | default | required | constraints | description |
|---|---|---|---|---|---|
exhaustWithPrompt | string | no | Prompt to send when extending codon until context exhaustion. When set, the codon will automatically continue with this prompt after each successful completion until context is exhausted. | ||
maxExtensions | integer | 100 | no | > 0 | Maximum number of extensions before forcing completion. Default: 100. Safety valve to prevent infinite extension loops. |
autoCompact | boolean | no | Whether the harness may auto-compact (summarize/trim) the session when the context window fills. Default: false — compaction is disabled, the provider's context-overflow error surfaces instead, plain codons fail at the… |
The defaults change what "done" means. With autoCompact off (its default), a plain codon fails at the provider's context-overflow error. A top-level exhaustWithPrompt codon treats reaching that same boundary as successful completion and keeps extending to it; the same provider signal is a failure only in the plain-codon case. The runtime can also stop at maxExtensions, whose default is 100, so set a deliberately small limit when you are testing or containing spend.
An extension opens only when all of these conditions hold: the configuration is present; the codon is not interrupted; it is under maxExtensions; its exit code is 0; a result message arrived; there is no error result or classified failure; the budget is still available; and the context has not yet been exceeded.
Each extension emits one codon.extended event. It is journaled, routed to sentinels, and carries the codon id and name, the extension number, the prompt, cumulative tokens, and cumulative cost. While the codon runs, the state file (state.json) tracks extensionCount on it.
| id | category | journaled | sentinelRouted | payloadFields | receipts |
|---|---|---|---|---|---|
| codon.extended | server-state | true | true | codonId, codonName, extensionNumber, exhaustWithPrompt, cumulativeTokens, cumulativeCost | schemas/event-schemas.ts:677, schemas/event-schemas.ts:1076, schemas/event-schemas.ts:1350, hankweave-runtime.ts:3378 |
Extensions change how often the agent runs, not how often the run seals state. An extended codon seals exactly one checkpoint at its end, of type completed or error; extensions do not seal per-iteration checkpoints, because the extension loop is internal to the codon runner. An extended codon emits one codon.completed event (one per terminal attempt, so a retried codon emits one for each failed attempt plus the final one) and runs outputFiles once, after the final exit.
One load-time constraint follows from the mechanics: a codon placed after an exhaustWithPrompt codon cannot use continuationMode: "continue-previous" – the previous context is exhausted and there is nothing to continue from, so hankweave rejects the arrangement at load. Use fresh for the next codon.
Check-it: While the run is live, its journal carries codon.extended events with an increasing extensionNumber, and state.json carries extensionCount. After the run, confirm the codon sealed one final checkpoint rather than one per extension. (The finalized journal forms are owned by the event journal.)
When an extension fails#
The authoritative lifecycle: the codon runs, with each extension round re-sending the prompt in the same session and emitting codon.extended while the codon is running. After the final exit, one CheckpointCreated transition seals the codon – completed for success, error when the last round failed – then codon.completed is emitted, and outputFiles.copy runs when an output directory is set by -o or outputDirectory in hankweave.json. Without an output directory, outputs stay in agentRoot/. No checkpoint is written between extension rounds.
So a failed final round does not leave a completed checkpoint behind: the codon is recorded failed with an error checkpoint, and its onFailure policy decides whether the run continues (ignore) or ends (abort). Files the codon already wrote may still be in the workspace. The failure taxonomy and exit codes are owned by errors and exit codes.
[changed 0.11.0] On the published 0.10.0 artifact, a failure during an extension was observed to fail the whole run after the codon's checkpoint had already sealed with its artifact complete, so resume re-ran the sealed codon; the runtime now records the codon failed with an error checkpoint.
That failure pattern is why the documentation project stopped using exhaustWithPrompt and moved completeness pressure into prompt-tail self-audits instead. The forge's own recovery was to resume the same execution with a --model override for the remaining codons; that is a dated plan the runtime does not carry out – a model override applies only to a plan built by that invocation. Recovery mechanics are owned by resume, rollback, and retry.
If you keep extensions, pair them with a small maxExtensions and a codon budget. Every extension re-sends the accumulated context, so the cost grows with the continued session.
Check-it: When a run fails on an extension, expect the codon recorded failed with an error checkpoint and onFailure deciding the run – not a sealed completed: checkpoint beside a failed run.
Nudging instead of extending#
If an internal extension loop is more machinery than the task needs, use the Any More? pattern for a light follow-up. Add an ordinary follow-up codon with continuationMode: "continue-previous" and the whole prompt "Any more left to do?". We are asking the agent for another judgment about completeness, not independently checking that judgment.
{
"continuationMode": "continue-previous",
"promptText": "Any more left to do?"
}
continue-previous needs a predecessor that completed, or was skipped after writing at least one assistant message, and it must use the same model – different models cannot share a session. A failed predecessor does not qualify. Continuation mechanics are owned by codon concepts. Do not put the nudge after an exhaustWithPrompt codon, whose context is exhausted.
A nudge asks the agent to look again, but it cannot make the agent look carefully. If a codon writes files and could finish silently, put a self-audit in its prompt: write a skeleton first, make at least four incremental write steps, verify the result, and run wc -l before finishing. Without that discipline, a one-codon hank can report success while writing nothing and sealing an empty checkpoint – a failure the documentation project hit and fixed with exactly this prompt-tail protocol.
Unlike an extension, the nudge is an ordinary codon: it seals its own checkpoint and rolls back independently.
Check-it: Confirm the nudge follows a codon that completed, or was skipped after at least one assistant message, and uses the same model. If the prior codon exhausted its context, replace the nudge with a fresh codon or another completion shape.
Looping until the context is full#
Choose a loop when "keep going" needs a runtime termination rule rather than a fixed count. Set terminateOn to { "type": "contextExceeded" }; the loop iterates until the runtime signals context exhaustion, and exhaustion is success, not failure.
{
"terminateOn": {
"type": "contextExceeded"
}
}
Set every codon in that loop to continuationMode: "continue-previous". A fresh codon resets context on every iteration, so the loop would continue forever; hankweave rejects that configuration at load time. The loop stops on the provider's real context boundary: the same signal that fails a plain codon is the loop's success condition. Full termination semantics are owned by loops.
Budget for accumulation: a continue-previous loop re-pays for all prior context on every iteration. By iteration five, output from iteration one is being paid for four more times.
Check-it: Validate the hank after setting the loop. Every codon in a contextExceeded loop must continue the previous session, and the loop must stop on the real context boundary rather than resetting context.
Sweeping: sentinels that feed the agent's next pass#
The patterns so far all ask the same agent to continue its own work. A Sentinel Sweep adds a second perspective without adding codons. A sentinel observes the working codon and writes findings through an output path containing /, such as ./sentinel-notes/issues.md, so the agent can read them on its next pass. Pair that path with an exhaustWithPrompt that tells the agent to read the notes and fix what was caught: the sentinels are the eyes, the exhaust prompt is the nudge. Keep the flow one-way – the sentinel writes its own file, and the agent reads but never edits it, or concurrent-edit hash protection forces re-reads and pollutes the context. The consumer rule is owned by sentinel concepts.
Where the notes land depends on the output path. A bare filename lands in the managed sentinel-outputs directory, where the codon's agent cannot read it; a path containing /, including ./x.md, resolves against agentRoot. The full path and sandbox rules belong to sentinel configuration.
| field | type | default | required | constraints | description |
|---|---|---|---|---|---|
format | enum | no | text | jsonl |
Use output.format: "text" or output.format: "jsonl"; "json" is not a valid value.
Sentinel prompts are Eta templates. Their it value contains up to 1000 events, world.currentTime, and a codon object in which only id is the observed codon's runtime id – name and description come from the sentinel's own config, and startTime is when the sentinels were loaded for that codon. Include <%= it.events %> when the sentinel needs event data; without that interpolation, the model receives no event data. The checked observer uses <%= JSON.stringify(it.events, null, 1) %>.
[removed 0.5.5] Sentinel
output.format: "json"left the enum; the supported values aretextandjsonl.
Model ids behave differently for sentinels than for codons, and the difference bites at runtime. Use a full provider-qualified model id for a sweep sentinel. A model string without / is not checked against the registry at load and fails at its first model call with No LLM provider available. A full model id is checked at load and, when unavailable, is skipped with an info-level log. In the checked failure capture, the short sentinel model string haiku passes validation with GOOD TO RUN! and sentinels: 1, then throws No LLM provider available at every trigger. The anchor therefore spells its model anthropic/claude-haiku-4-5.
The anchor's quality-observer demonstrates the observer half. It triggers on codon.completed for validate-and-repair; its prompt receives completion-event fields and explicitly says that artifact existence, row coverage, and correctness cannot be verified from that input. Its bare quality-observer.log output therefore goes to the managed sentinel directory, not beside the codon's files – it observes; it does not close the loop.
{
"id": "quality-observer",
"name": "Quality Observer",
"description": "Observes the validate-and-repair completion event. Reports only status and budget fields present in that event; cannot read or verify output artifacts.",
"trigger": {
"type": "event",
"on": [
"codon.completed"
],
"conditions": [
{
"operator": "equals",
"path": "codonId",
"value": "validate-and-repair"
}
]
},
"execution": {
"strategy": "immediate"
},
"model": "anthropic/claude-haiku-4-5",
"userPromptText": "The codon `validate-and-repair` completed. Here are the triggering completion events:\n\n<%= JSON.stringify(it.events, null, 1) %>\n\nSummarize only status, failure reasons and budget fields actually present. These completion events do not provide file contents or file.updated events. State explicitly that artifact existence, row coverage and correctness cannot be verified from this input. Never treat missing file-update events as proof of missing files. Observe and report only.",
"output": {
"format": "text",
"file": "quality-observer.log"
}
}
"sentinels": [
{
"sentinelConfig": "sentinels/quality-observer.json"
}
],
The checked sentinel-sweep fixture wires an event-conditioned observer per codon. Captured 2026-09-04 against hankweave@0.10.0, it demonstrates the observer half only: no exhaustWithPrompt, no slash-containing agentRoot output, and no codon.extended events. Its two expected artifacts come from two different captured runs of the fixture – the templated report from the later run, the event excerpt from the first, untemplated one – so neither is a whole-run inspection.
{
"$schema": "https://unpkg.com/hankweave@0.11.0/schemas/hank.schema.json",
"meta": {
"name": "sentinel-sweep demo",
"version": "1.0.0"
},
"hank": [
{
"id": "step-one",
"name": "write part one",
"model": "haiku",
"continuationMode": "fresh",
"promptText": "Write the single word 'alpha' to part-one.txt. Nothing else.",
"checkpointedFiles": [
"part-one.txt"
]
},
{
"id": "step-two",
"name": "write part two",
"model": "haiku",
"continuationMode": "fresh",
"promptText": "Write the single word 'beta' to part-two.txt. Nothing else.",
"checkpointedFiles": [
"part-two.txt"
],
"sentinels": [
{
"sentinelConfig": "sentinels/sweep-observer.json"
}
]
}
]
}
To close the loop, the sentinel-sweep-exhaust fixture (also captured 2026-09-04 against hankweave@0.10.0) pairs one digest codon – exhaustWithPrompt with maxExtensions: 2 – with an observer sentinel triggered by codon.started and codon.extended, writing sentinel-notes/observer.log under agentRoot/. The prompt reads ./sentinel-notes/, and the capture shows the digest growing across passes.
Read the diagram as text
+-- agentRoot/ -----------------------------------------------------+
| |
| +-----------------------------------------+ folds the notes |
| | digest codon, extended with |-------------------+ |
| | exhaustWithPrompt | v |
| +--------+------------------------^-------+ +-----------------+|
| | | | notes-digest.md ||
| | emits codon.extended | read on +-----------------+|
| | | the next pass |
| | +-----------+------------------+ |
| | | sentinel-notes/observer.log | |
| | +--------------^---------------+ |
+-----------|---------------------------|---------------------------+
v | writes findings
+--------------------+ |
| observer sentinel |---------------+
+--------------------+
{
"$schema": "https://unpkg.com/hankweave@0.11.0/schemas/hank.schema.json",
"meta": { "name": "sentinel-sweep-exhaust", "version": "1.0.0", "description": "One codon that extends itself with exhaustWithPrompt and folds in the notes a sentinel writes into its workspace." },
"hank": [
{
"id": "digest",
"name": "Digest the observer's notes",
"model": "deepseek-v4-flash",
"continuationMode": "fresh",
"promptFile": "./prompts/digest.md",
"exhaustWithPrompt": "Look in ./sentinel-notes/ again. Fold any note you have not digested yet into notes-digest.md as a new bullet (quote the note's line). If there is nothing new, append the single line 'no new notes' and stop.",
"maxExtensions": 2,
"checkpointedFiles": ["notes-digest.md", "sentinel-notes/**"],
"sentinels": [ { "sentinelConfig": "./sentinels/observer.json" } ]
}
]
}
Check-it: For an event observer, verify the completion log only for the status and budget fields the event contains; use an artifact checker or a codon to verify files. For the loop-closing shape, look for sentinel-notes/observer.log under agentRoot/ and have the agent read it without editing it.
Running and verifying a completion pattern#
Whichever shape you chose, the verification steps are the same. Work from the directory containing the hank. Validate the configuration first:
bunx hankweave@0.11.0 hank.json data/ --validate
--validate is a configuration preflight, not a live run: it hashes the data source, validates the hank, resolves the execution path, and runs per-model shim self-tests that check SDK availability, CLI presence, credentials, and the model catalog. It does not execute codons and makes no model-generation self-test – its registry initialization disables provider health checks, so no billable model call happens. It may add $schema to the hank and write a temporary log. Runtime startup is separate: it can run provider health checks such as generateText("Hi", maxOutputTokens: 16) for available registry providers, and those calls may be billable outside tracked codon cost.
A successful validation looks like this:
╭────────────────────────────────────────────────────────────────────╮
│ Hankweave v0.11.0 │
│ linux x64 • node v22.23.2 │
╰────────────────────────────────────────────────────────────────────╯
Calculating data signature for validation...
> Validating configuration: <workspace>/fixtures/minimal-single-provider/hank.json
Data source: <workspace>/fixtures/minimal-single-provider/data
Execution path: ~/.hankweave-executions/validation-<id>
✓ Configuration is valid!
╭──────────────────────────────────────────────────────────────────────────────╮
│ Minimal single provider v1.0.0 │
│ 1 codon • 0 loops │
╰──────────────────────────────────────────────────────────────────────────────╯
└─ [1] summarize-notes (Summarize the notes)
model: haiku │ mode: fresh │ prompts: 1 (13 lines)
checkpointedGlobs: 1
╭─ GOOD TO RUN! ─────────────────────────────────────────────────────╮
│ 1 codons • 1 prompts • 0 system prompts • 0 rigs • 1 checkpoints │
╰────────────────────────────────────────────────────────────────────╯
Run it: hankweave hank.json <data_path>
Environment Variables:
From System (HANKWEAVE_ prefixed):
- CAPTURE_VERSION: 0.11.0
- TELEMETRY: 0
exit=0
Run the hank explicitly; a bare hankweave invocation launches the welcome wizard instead of this hank:
bunx hankweave@0.11.0 hank.json data/ --headless --start-new -o out
The copied outputs are in out/; inspect ~/.hankweave-executions/<id>/agentRoot/ for the run's agent files. A slash-containing sentinel output appears below agentRoot/; a bare-named sentinel log appears under .hankweave/sentinels/outputs/<sentinelId>/; while the run is live, its journal carries codon.extended; and state.json carries extensionCount. The observer log and the execution tree below show what a healthy sweep leaves behind: the sentinel's stated limits, and both the digest and the notes file under agentRoot/.
---
# Codon Completion Summary
**Status:** Success
**Cost:** $<cost>
**Duration:** 117,863 ms (~1m 58s)
---
## Verification Limitations
Artifact existence, row coverage, and correctness **cannot be verified** from this input. No file-update events were provided. The absence of file-update events does not indicate missing files.
./notes-digest.md
./sentinel-notes/observer.log
Check-it: The captured extension records show incrementing extensionNumber values 1 and 2; the execution tree contains both notes-digest.md and sentinel-notes/observer.log.
Recognize two failure signatures. A failed extension round records the codon failed with an error checkpoint, and onFailure decides whether the run continues or ends – there is no sealed completed: checkpoint to mistake for success. Separately, a short, unqualified sentinel model can fail at trigger time with No LLM provider available while codons run normally; use a full provider-qualified sentinel model. Setting failCodonIfNotLoaded: true makes only sentinel config-loading failures fatal – a sentinel skipped for a missing registry entry, an unconfigured provider, or a pending health check still leaves the codon running. An outputFiles beforeCopy or copy failure forces RunFailed and shutdown with exit 1.
Check-it: Name both failure signatures: a codon recorded failed with an error checkpoint and onFailure deciding the run, and the sentinel-side No LLM provider available error.
Choose the pressure shape#
For each codon, decide whether another pass needs its own structure, another perspective, and enough value to justify the extra spend. Applied to the anchor: reconcile is bounded, structured work, so it takes no completion pattern; survey-and-extracts is an open-ended survey, so a nudge or extension fits; validate-and-repair is a review loop, so a sweep with quality-observer fits – though the anchor's observer demonstrates observation only, and sentinel-sweep-exhaust is the fixture that closes the feedback loop.