# Observe quality and budget a hank The pipeline from the previous chapters normalizes three supplier quotes and reconciles them into unified records, but so far nothing watches it run and nothing limits what it spends. In this chapter we add both: a sentinel that observes the validation step's completion event and reports what it can see, and a `budget` block on every codon that caps dollars, time, and tokens. Along the way we will validate the resolved plan before spending anything, run the chapter against prepared inputs, and check both the sentinel's report and the reconciled artifacts afterward. By the end you will know when a sentinel is the right tool and when a review belongs in a codon, how codon-level caps and the operator's run envelope interact, and how to read the evidence a run leaves behind. ## What changes after chapter 3? We begin in the shipped `chapters/ch4/` directory. A checkpoint is a persisted run record, not the starting files for this chapter, so the chapter directory itself is the starting point. The shipped `chapters/ch4/` hank adds two independent concerns: a quality-observer sentinel (a parallel event observer) watching the `validate-and-repair` completion event, and a `budget` block on every codon. A hank is the JSON run definition, and a codon is one agent task in its sequence. We now have five codons: three normalizers, `validate-and-repair`, and `reconcile`. We still work with three suppliers – AST, BCN, and CDR – across five buyer part IDs, or 15 supplier-part data rows. Before running anything, get the fixtures and credentials in place. Tutorial chapters are shipped directories with their own `hank.json`; they are not checkpoint tags to check out or roll back. Use the ch4 directory against a prepared task-input directory. The credentials ladder is also part of the setup: chapters 1–2 use `ANTHROPIC_API_KEY`, while chapters 3–5 and the full anchor additionally need `BASETEN_API_KEY` for `pi/baseten/deepseek-ai/DeepSeek-V4-Pro`. Ch4's sentinel uses the full registry ID `anthropic/claude-haiku-4-5`, not a codon shortcut or a `pi/` spelling. The fixture bundle's publication path is [hankweave-fixtures-0.10.0.tar.gz](/content-assets/cf45dff5691c48c0/fixtures/0.10.0/hankweave-fixtures-0.10.0.tar.gz); individual artifacts use `/fixtures/0.10.0/files/`. That archive already contains one top-level `hankweave-fixtures-0.10.0/` directory. From its parent, run `tar -xzf hankweave-fixtures-0.10.0.tar.gz`, then enter `hankweave-fixtures-0.10.0/chapters/ch4`. Do not create another directory with that name before extracting. For the supplied `bunx`/`npx` recipe, use Node `>=22.19.0`; Bun supplies the chosen `bunx` package launcher. An npm user can substitute `npx hankweave@0.10.0` once for `bunx hankweave@0.10.0`. Run as a non-root user with Bun, Git, `ANTHROPIC_API_KEY`, and `BASETEN_API_KEY` available. A passing preflight is not evidence that a Baseten key is valid; Baseten is absent from Hankweave's explicit Pi credential-enforcement map. ## How can a sentinel watch without editing? A sentinel is a parallel observation agent with its own model, trigger, execution strategy, and output. It observes matching events and fires a report; it does not edit codon outputs, run tools, or block the main agent. Use a codon instead when the review must read the workspace or change an artifact. That read-only boundary shapes everything about how the ch4 observer is configured, so we will build it up piece by piece. ### Attach the quality observer The ch4 codon attaches a reusable configuration with a wrapper object. The `sentinels` array on the codon points at a standalone sentinel file rather than inlining the whole definition: ```json "sentinels": [ { "sentinelConfig": "sentinels/quality-observer.json" } ], ``` The referenced file defines when the observer fires. The quality observer's event trigger is `type: "event"`, listens for `codon.completed`, and applies an `equals` condition to the `codonId` field with value `validate-and-repair`. Its `immediate` execution strategy fires for every matching event. Other supported strategies are `debounce` (quiet-period `milliseconds` 1–300000), `count` (a `threshold` of 1–1000), and `timeWindow` (`milliseconds` 1–3600000). Conditions can use `equals`, `notEquals`, `in`, `notIn`, `contains`, `matches`, `greaterThan`, or `lessThan` on event paths. The schema strip below summarizes the trigger and execution fields: | field | type | default | required | constraints | description | | ---------------- | ---------------------------------------------------------- | ------- | -------- | ------------------------- | ----------- | | `type` | `string` | | yes | = `event` | | | `on` | `array` | | yes | minItems 1 | | | `conditions` | `array` | | no | | | | `immediate` | `object` | | | `strategy` = `immediate` | | | `debounce` | `object` | | | `strategy` = `debounce` | | | ↳ `milliseconds` | `integer` | | yes | > 0; max 300000 | | | `count` | `object` | | | `strategy` = `count` | | | ↳ `threshold` | `integer` | | yes | > 0; max 1000 | | | `timeWindow` | `object` | | | `strategy` = `timeWindow` | | | ↳ `milliseconds` | `integer` | | yes | > 0; max 3600000 | | A sentinel also needs a prompt and an output destination. At least one of `userPromptText` (inline) or `userPromptFile` (a relative path with `/` separators) is required; when both are present, Hankweave loads the files first and combines them with the inline text. A system prompt can likewise be inline or a relative file, and `joinString` joins multiple events for a text output. We will use the fixture's output settings here. To choose another output format or path, configure `lastValueFile`, or change attachment settings, see [sentinel configuration](/reference/sentinel-config#write-output-files). Here is the complete shipped observer configuration. Note the `userPromptText`: it interpolates event data with Eta, a template language, and it instructs the model to report only what the events contain. The template does not grant the sentinel filesystem tools: ```json { "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" } } ``` The template context contains up to 1,000 events, the codon identity and metadata, and the current time. Event data reaches the model only where the prompt interpolates it. A separate two-codon `sentinel-sweep` fixture demonstrates both sides of that boundary: a prompt without the events expression receives no event data, while the templated prompt produces a report. The capture below shows the two together: the report is for the `step-two` trigger and receives one matching event, while the companion event log records both codon completions from the run. Notice how the untemplated variant asks for the events it never received. ```text # Sweep Report 1. Received 1 event of type `codon.completed`. 2. Codon `step-two` completed successfully with a cost of $ and duration of ms. 3. Sweep: clean ``` ```text {"id":"","timestamp":"","type":"codon.completed","data":{"codonId":"step-one","success":true,"cost":"","duration":"","exitStatus":{"type":"success"}}} {"id":"","timestamp":"","type":"codon.completed","data":{"codonId":"step-two","success":true,"cost":"","duration":"","exitStatus":{"type":"success"}}} {"id":"","timestamp":"","type":"sentinel.triggered","data":{"sentinelId":"sweep-observer","codonId":"step-two","triggerNumber":1,"strategy":"immediate","eventCount":1,"queueSize":0}} {"id":"","timestamp":"","type":"sentinel.output","data":{"sentinelId":"sweep-observer","codonId":"step-two","triggerNumber":1,"outputType":"text","content":"I don't see any events in your message. You've described a scenario involving codon completion events and asked me to write a sweep report, but no actual event data has been provided for me to analyze.\n\nTo write the three-line report you've requested, I would need to see the events themselves. Could you please share:\n\n- The event log or event stream from the two-codon run\n- Details about each `codon.completed` event (success/failure status, costs if applicable)\n\nOnce you provide the events, I can generate:\n1. Count of codon.completed events and their success status\n2. Total cost calculation\n3. Sweep status assessment\n\nPlease paste the events and I'll write your report.","cost":"","tokens":{"input":102,"output":158},"eventCount":1}} ``` One spelling detail matters here. Use the current ch4 spelling, `anthropic/claude-haiku-4-5`. A historical bare `haiku` spelling in a sentinel configuration produced `No LLM provider available`. The `haiku` entries in the captured plan refer to normalizer codons, a separate model-resolution path. For provider setup, see [authentication and models](/operate/authentication-and-models). The bare output filename has a managed location. With `output.file: "quality-observer.log"`, the report is written to: `/.hankweave/sentinels/outputs/quality-observer/quality-observer.log` A path containing a slash is a different case: it resolves relative to the run's working directory (`agentRoot`) when present, otherwise the execution directory. In particular, the bare filename is not placed in the external `-o out` directory. **Check-it:** after `validate-and-repair`, inspect the managed path and confirm that the captured report contains completion status, duration, and the output-token alert while explicitly declining to verify artifacts. ## Why do budgets have two owners? The sentinel watches spending; the budget blocks limit it. A budget has two owners. The author uses codon-level caps to express what each step may spend or how long it may run; the operator supplies the run envelope with `--max-cost ` and `--max-time `. At codon level, `budget` supports `maxDollars`, `maxTimeSeconds`, `maxOutputTokens`, `maxContextTokens` (the input-plus-output high-water mark per turn), and `onExceeded: "complete"` or `"fail"`. At hank level, `overrides.budget` adds `allocation` and `shares`; its `onExceeded` is the default for codons. The four currencies are dollars, wall-clock time, model output tokens, and per-turn context tokens. The pool that `allocation` and `shares` distribute is the dollar pool. Output-token and per-turn context-token caps exist only at codon level; wall-clock time is a codon cap in ch4, and a hank-level budget or `--max-time` can also carry a run-level time ceiling that codons draw against (below). The precedence is important. First, runtime configuration and hank-level ceilings combine as their tightest shared ceiling. If a CLI `--max-cost` or `--max-time` is present, that CLI value is the highest-priority override: it can lower or raise the hank-level ceiling. A codon's own dollar cap is still applied separately as a ceiling against its resolved allocation. Thus `overrides.budget` of `$12` with `--max-cost $5` resolves to a `$5` global ceiling, while `--max-cost $20` can raise that run-level pool without loosening a codon's own cap. The `--max-cost` and `--max-time` flags parse in 0.10.0 but are omitted from `--help`. The schema strip below covers the hank-level allocation fields: | field | type | default | required | constraints | description | | ------------ | ---------------- | -------- | -------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allocation` | `enum` | `shared` | no | `shared` \| `proportional` \| `proportional-strict` | How the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates. | | `shares` | `object` | | no | | Map of child codon/loop ID to fraction (0-1) of the budget. | | `onExceeded` | `enum` | | no | `complete` \| `fail` | What happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy. | **Check-it:** find `shared`, `proportional`, and `proportional-strict` in the generated budget strip; use the [CLI reference](/reference/cli#timeouts-budgets-and-limits) for the flags and [Hank JSON reference](/reference/hank-json#configure-codon-fields) for the codon budget shape. ## How does Hankweave resolve competing caps? With two owners setting limits, the runtime needs a deterministic way to combine them. The default `allocation: "shared"` is first-past-the-post: codons draw from one pool in execution order, so early thrift leaves more for later codons. `proportional` pre-allocates shares and returns unspent money to the pool. `proportional-strict` pre-allocates shares and lets unspent money evaporate; it is useful for benchmarking or strict partitioning. For each codon, the dollar resolution chain is: 1. Set the global ceiling to the CLI `--max-cost` value when present; otherwise use the tightest runtime/hank ceiling. 2. Subtract spend to find the remaining pool. 3. Allocate from that pool according to `shared`, `proportional`, or `proportional-strict`. 4. Apply the codon cap as `min(allocated, codon.maxDollars)`. 5. If the codon dollar cap is undefined, it is uncapped. Time and token caps are not shares of the dollar pool: ch4 sets them at codon level, while a hank-level budget or `--max-time` can also impose a run-level time ceiling. `onExceeded: "complete"` marks the codon completed and allows downstream codons to run. `onExceeded: "fail"` marks it failed and invokes that codon's `onFailure` policy; see [codon failure handling](/concepts/codons#what-happens-when-a-codon-fails). The interrupt is separate from `onExceeded`. Metered assistant messages update the budget tracker; when accumulated cost or output tokens reach a cap, the tracker emits `exceeded` and the codon runner requests `SIGTERM`. A cap can therefore interrupt after an already-billable message; it is not a prepaid-spending guarantee. A completion event can carry a `budgetExceeded` object such as `{ "currency": "outputTokens", "limit": 8000, "used": 10687 }`. Loop budget exhaustion uses the active watchdog to kill the running codon. It does not add a `budget` value to `loop.iteration.completed.terminationReason`: that enum contains only `iteration_limit`, `context_exceeded`, `sentinel_skip`, and `failure`. **Check-it:** compare a plan's `onExceeded` values with the resolution rule: `complete` continues downstream work, while `fail` invokes the codon's failure policy. ## Why do our codons need different caps? The resolution rules are uniform, but the ch4 caps are not, because the five codons do different kinds of work. Every ch4 codon carries a budget block. Each `normalize-*` codon has `maxDollars: 0.50`, `maxTimeSeconds: 240`, and `onExceeded: "fail"`. `validate-and-repair` has a `$1.00` cap, a 600-second cap, `maxOutputTokens: 8000`, and `onExceeded: "complete"`. `reconcile` has a `$0.50` cap, a 420-second cap, and `onExceeded: "fail"`. The normalize steps are mechanical extraction and fail loudly if they cannot complete. Validation is judgment work: a soft completion cap can preserve useful partial output. Reconciliation is an all-or-nothing join, so a partial result is worse than no result. The five per-codon dollar caps sum to `$3.00`. The two blocks below show the contrast between the soft validation policy and the hard reconciliation policy: ```json "budget": { "maxTimeSeconds": 600, "maxDollars": 1, "maxOutputTokens": 8000, "onExceeded": "complete" } ``` ```json "budget": { "maxTimeSeconds": 420, "maxDollars": 0.5, "onExceeded": "fail" } ``` The normalize caps were raised from `$0.20` after haiku price drift reached `$0.204` and forfeited a hard-cap attempt in the re-proven anchor run. The lesson is not that these captured amounts are universal; they are the shipped ch4 arrangement for this workload. **Check-it:** compare the two budget blocks: `validate-and-repair` uses the soft `complete` policy, while `reconcile` uses the hard `fail` policy. > **Pitfall:** Do not set `onExceeded: "fail"` everywhere by habit. A hard cap is appropriate for an all-or-nothing reconciliation, while an expensive judgment codon may be more useful with `onExceeded: "complete"`. ## What did the anchor build teach us? The shipped caps reflect experience from building the full anchor, a larger workload than ch4. These are captured lessons, not predictions for every ch4 run. * **Leave room above an estimate.** Three-supplier estimates tripped on an eight-supplier workload: `validate-and-repair` reached `$1.05` against `$1.00`, and `reconcile` reached `$0.51` against `$0.50`. When workload is uncertain, the cap-2x rule says to leave at least twice the estimate. * **Hard cost aborts forfeit spend.** A failing cost check can kill a codon at a turn boundary; the spend up to that point can be lost with zero output written. A soft `complete` cap was the working escape hatch in the anchor record. One later hard-cap abort fired after `codon.completed` and sealed an `error:` checkpoint with none of that codon's outputs, so do not assume checkpoint-write order. * **Time enforcement is active, not a teardown guarantee.** Elapsed time is checked on cost updates and by a 1,000 ms watchdog; on exceed, the codon runner requests `SIGTERM`. That request does not prove prompt harness (the process managing the run) teardown. An older captured run observed a codon surviving a 2,400-second cap for roughly 24,923 seconds; its source/runtime cause remains unresolved. For unattended work, add an external process deadline rather than claiming the watchdog recovers a checkpoint. * **Zai costs have a special caveat.** At 0.10.0, the `pi/zai/*` provider route reports codon costs as `$0` on `codon.completed`, so dollar budgets do not bind for that route; `maxTimeSeconds` is the effective guard described by the capture. This page does not prescribe how to select that route. * **Read budget events carefully.** A budget-exceeded abort was logged as `aborted by user` in the event stream. That diagnosis is misleading; it is still a budget breach. ## How do we read the resolved plan? Before spending tokens on a run, run `--validate`. It validates the configuration and displays the resolved per-codon caps, their source, allocation mode, `onExceeded` policy, and sentinel count; it does not run codons or provider health checks. The captured ch4 plan has five codons: the three normalizers use `haiku`, while `validate-and-repair` and `reconcile` use `pi/baseten/deepseek-ai/DeepSeek-V4-Pro`. It shows one sentinel on `validate-and-repair`, `onExceeded: complete` there, and `onExceeded: fail` on `reconcile`. The full capture appears below; read the budget table to confirm each codon's cap and policy before trusting the `GOOD TO RUN!` verdict. ```text ╭────────────────────────────────────────────────────────────────────╮ │ Hankweave v0.10.0 │ │ darwin arm64 • node v23.8.0 │ ╰────────────────────────────────────────────────────────────────────╯ Calculating data signature for validation... > Validating configuration: /fixtures/chapters/ch4/hank.json Data source: /fixtures/quote-template-unification Execution path: ~/.hankweave-executions/validation- ✓ Configuration is valid! ╭──────────────────────────────────────────────────────────────────────────────╮ │ quote-template-unification v1.0.0 │ │ 5 codons • 0 loops │ ╰──────────────────────────────────────────────────────────────────────────────╯ ├─ [1] normalize-aster (Normalize Aster (Datalab dialect)) │ model: haiku │ mode: fresh │ prompts: 1 (72 lines) │ checkpointedGlobs: 1 │ rigs: cmd: mkdir -p pipeline │ ↓ ├─ [2] normalize-beacon (Normalize Beacon (Reducto dialect)) │ model: haiku │ mode: fresh │ prompts: 1 (71 lines) │ checkpointedGlobs: 1 │ ↓ ├─ [3] normalize-cedar (Normalize Cedar (generic-OCR dialect)) │ model: haiku │ mode: fresh │ prompts: 1 (71 lines) │ checkpointedGlobs: 1 │ ↓ ├─ [4] validate-and-repair (Validate & Repair Envelopes) │ model: Pro │ mode: fresh │ prompts: 1 (67 lines) │ checkpointedGlobs: 2 │ sentinels: 1 │ ↓ └─ [5] reconcile (Reconcile to Unified Records) model: Pro │ mode: fresh │ prompts: 1 (41 lines) │ checkpointedGlobs: 2 Budget ───────────────────────────────────────────────────────────────── No global budget. Per-codon limits only. Codon Model Max Dollars Max Time Max Tokens On exceeded ───── ───── ─────────── ──────── ──────── ─────────── normalize-aster Claude Ha… $0.50 (codon cap) 240s (cap) — ⚠ fails run normalize-beacon Claude Ha… $0.50 (codon cap) 240s (cap) — ⚠ fails run normalize-cedar Claude Ha… $0.50 (codon cap) 240s (cap) — ⚠ fails run validate-and-repair Deepseek … $1.00 (codon cap) 600s (cap) 8000 (output cap) completes reconcile Deepseek … $0.50 (codon cap) 420s (cap) — ⚠ fails run ╭─ GOOD TO RUN! ─────────────────────────────────────────────────────╮ │ 5 codons • 5 prompts • 0 system prompts • 3 rigs • 5 checkpoints │ ╰────────────────────────────────────────────────────────────────────╯ Run it: hankweave hank.json Environment Variables: From System (HANKWEAVE_ prefixed): - CAPTURE_VERSION: 0.10.0 exit=0 ``` The same preflight warns about a codon cap larger than its allocation, shares below 1.0 with no absorber, missing model pricing, and `onExceeded: "fail"` paired with `onFailure: "retry"`. These are preflight warnings, not runtime surprises. The validation capture is a plan snapshot; use the current ch4 hank as the source of model and cap values if the two differ. **Check-it:** the captured plan ends with `GOOD TO RUN!` and shows one sentinel on `validate-and-repair`, `complete` for that codon, and `fail` for `reconcile`. ## How do we check ch4's output? With the plan validated, we can run the chapter and examine what it produces. From `hankweave-fixtures-0.10.0/chapters/ch4`, prepare the task-only input once, validate, run, and let the held-out checker verify the artifacts. The destination must not already exist; preparation refuses to overwrite it. It copies only the task inputs, while truth, generator data, and planted failure specimens stay outside the agent's input. ```sh python3 ../../verify.py prepare-data ../../quote-template-unification task-data bunx hankweave@0.10.0 hank.json task-data --validate bunx hankweave@0.10.0 hank.json task-data --headless --start-new --execution exec --max-cost 9 --shim-idle-timeout 1800 --overwrite-output -o out python3 ../../verify.py chapter 4 out ``` Use both keys before the paid run. `--validate` does not run codons or provider health checks. Reuse `task-data` for repeats, but use `--start-new` and distinct `--execution` and `-o` paths for an independent attempt. A fresh directory is the next chapter handoff; there is no ch4 checkpoint tag to roll back to. The output contract includes the three supplier envelopes, `validated-records.json`, `exception-ledger.json`, and `unified-records.csv`. The sentinel observes completion-event fields only; the deterministic (repeatable for the same inputs) Python checker verifies artifact existence, row coverage, and correctness. The captured per-codon receipt is the source for the dated cost and model record: ```json { "runtime": "0.10.0", "captured_at": "2026-09-06T13:23:55.262824+00:00", "scope": "one complete live execution", "tracked_codons_cost": 0.40847623499999997, "models": { "normalize-aster": "haiku", "normalize-beacon": "haiku", "normalize-cedar": "haiku", "validate-and-repair": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro", "reconcile": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro" }, "codons": [ { "codonId": "normalize-aster", "success": true, "cost": 0.13005475, "duration": 158255, "exitStatus": { "type": "success" } }, { "codonId": "normalize-beacon", "success": true, "cost": 0.03986045, "duration": 40188, "exitStatus": { "type": "success" } }, { "codonId": "normalize-cedar", "success": true, "cost": 0.04191160000000001, "duration": 48792, "exitStatus": { "type": "success" } }, { "codonId": "validate-and-repair", "success": true, "cost": 0.14996799, "duration": 82895, "exitStatus": { "type": "success" }, "budgetExceeded": { "currency": "outputTokens", "limit": 8000, "used": 10687 } }, { "codonId": "reconcile", "success": true, "cost": 0.046681445, "duration": 35308, "exitStatus": { "type": "success" } } ], "task_data_scope": "task inputs only; oracle/generator/failure fixtures excluded" } ``` That capture records 5/5 codons completed at tracked codon cost `$0.40847623`. `validate-and-repair` exceeded its output-token cap, with `budgetExceeded` at 8,000/10,687, but completed under its soft `onExceeded: "complete"` policy. These are values from one 0.10.0 execution, not a guarantee for another workload. The reconciled CSV has exactly 15 data rows plus its header. All five CDR rows have `ranking_eligible: false`: EX-03 marks the whole expired-at-award Cedar quote regardless of per-line status, and `NC-1004-A` is also `NO_BID` under EX-02. The CSV and the exception ledger appear together below so you can trace each ledger entry to the rows it affects. ```csv supplier_code,buyer_part_id,rfq_rev,qty,unit_price_minor,price_basis,line_extension_minor,currency,status,ranking_eligible,source_document_id,source_quote_id,source_line_id,source_block_id AST,NC-1001-A,B,500,410,PER_EACH,205000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L1,doc-aster-qb-1047-rev2-b1 AST,NC-1002-A,B,2000,79,PER_EACH,158000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L2,doc-aster-qb-1047-rev2-b2 AST,NC-1003-B,B,1000,115,PER_EACH,115000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L3,doc-aster-qb-1047-rev2-b3 AST,NC-1004-A,B,25,640,PER_EACH,16000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L4,doc-aster-qb-1047-rev2-b4 AST,NC-1005-A,B,5000,22,PER_EACH,110000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L5,doc-aster-qb-1047-rev2-b5 BCN,NC-1001-A,B,500,395,PER_EACH,197500,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L1,doc-beacon-8821-b1 BCN,NC-1002-A,B,2000,81,PER_EACH,162000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L2,doc-beacon-8821-b2 BCN,NC-1003-B,B,1000,115,PACK,115000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L3,doc-beacon-8821-b3 BCN,NC-1004-A,B,25,675,PER_EACH,16875,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L4,doc-beacon-8821-b4 BCN,NC-1005-A,B,5000,19,PER_EACH,95000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L5,doc-beacon-8821-b5 CDR,NC-1001-A,B,500,425,PER_EACH,212500,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L1,doc-cedar-cw-77-b1 CDR,NC-1002-A,B,2000,88,PER_EACH,176000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L2,doc-cedar-cw-77-b2 CDR,NC-1003-B,B,1000,110,BOX,110000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L3,doc-cedar-cw-77-b3 CDR,NC-1004-A,B,25,,PER_EACH,,USD,NO_BID,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L4,doc-cedar-cw-77-b4 CDR,NC-1005-A,B,5000,24,PER_EACH,120000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L5,doc-cedar-cw-77-b5 ``` ```json [ { "exception_id": "EX-01", "type": "DEDUP_CONFLICT", "supplier_code": "AST", "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A", "detail": "Two submissions share dedup_key AST|NC-RFQ-0042|B. Original doc-aster-qb-1047 (issued 2026-01-10) stated_total_minor 604000; rev2 doc-aster-qb-1047-rev2 (issued 2026-01-14, supersedes original) stated_total_minor 604001. Rev2 wins on later issued_at. Superseded document lines dropped.", "resolution": "Rev2 (doc-aster-qb-1047-rev2) carried forward; original discarded.", "source_ref": "doc-aster-qb-1047|doc-aster-qb-1047-rev2" }, { "exception_id": "EX-02", "type": "NO_BID", "supplier_code": "CDR", "buyer_part_id": "NC-1004-A", "detail": "Cedar declined to bid on NC-1004-A (gasket sheet). Line marked NO_BID with null pricing.", "resolution": "Line excluded from ranking; unit_price_minor and line_extension_minor set to null.", "source_ref": "doc-cedar-cw-77-b4" }, { "exception_id": "EX-03", "type": "EXPIRED_VALIDITY", "supplier_code": "CDR", "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A", "detail": "Cedar quote valid_until 2026-01-20 is before RFQ award_decision_date 2026-02-01. Quote is expired at moment of award. All lines marked ranking_eligible: false regardless of per-line status.", "resolution": "Quote kept visible with ranking_eligible=false pending human review.", "source_ref": "doc-cedar-cw-77", "judgment_call": true, "options": ["exclude", "show-flagged"], "tentative_resolution": "show-flagged", "confidence": 0.5 } ] ``` The ledger carries three typed entries: EX-01 `DEDUP_CONFLICT`, EX-02 `NO_BID`, and EX-03 `EXPIRED_VALIDITY`. The checker, not the observer, establishes that contract. **Check-it:** `python3 ../../verify.py chapter 4 out` is the executable assertion for the 15-row CSV, the three exception types, and the rest of the output contract. Finally, inspect the managed observer output, not the external `out` directory: `exec/.hankweave/sentinels/outputs/quality-observer/quality-observer.log` The fired report contains the completion status and duration, the output-token alert at 8,000/10,687, and the unresolved `$` placeholder. It explicitly says that artifact existence, row coverage, and correctness cannot be verified from completion-event input. The receipt above carries the numeric per-codon dollar costs. ```text --- # Completion Summary: `validate-and-repair` **Status:** Success ✓ **Duration:** 82,895 ms (~83 seconds) **Cost:** $ **Budget Alert:** - Currency: Output Tokens - Limit: 8,000 - Used: 10,687 - **Status: EXCEEDED** (2,687 tokens over limit) --- **Limitations of this report:** - No file contents provided - No file.updated events present - **Artifact existence cannot be verified** - **Row coverage cannot be verified** - **Correctness cannot be verified** Only completion status and budget metrics are observable from this input. ``` ## When should we use a sentinel or codon? The ch4 run illustrates the division of labor. Use a sentinel for a read-only observation of named events and a sentinel-owned report. It sees only the event data that the template passes to its LLM; it cannot inspect files, run tools, edit outputs, or block the main agent. Use a separate codon for a review that needs architecture, broad workspace context, artifact inspection, or a change. The observing model can differ from the observed codon's model. Ch4 uses the registry-spelled `anthropic/claude-haiku-4-5` for the quality observer and `pi/baseten/deepseek-ai/DeepSeek-V4-Pro` for the two judgment/reconciliation codons. The anchor's captured plan likewise uses haiku for normalization and DeepSeek for validation and reconciliation. A cheaper one-shot observer does not become an artifact verifier merely because its prompt names output files. **Check-it:** the observer report is evidence of completion-event observation; use the Python checker and the files it checks for artifact correctness. ## Where can we look up details? * For the complete two-party contract, currencies, allocation modes, preflight checks, and resume semantics, see [Budgets](/concepts/budgets). * For the sentinel concept, see [Sentinels](/concepts/sentinels). Look up triggers, execution, output, template context, conversational mode, and structured output in [Sentinel configuration](/reference/sentinel-config). * Look up `--max-cost` and `--max-time` in the [CLI reference](/reference/cli#timeouts-budgets-and-limits), and `--validate` in its [validation entry](/reference/cli#-v---validate). * For the hank and codon budget schema, use the [Hank JSON reference](/reference/hank-json#configure-codon-fields). * To choose `onFailure` policies (`abort`, `retry`, and `ignore`) and `retryConfig`, see [Codons](/concepts/codons#what-happens-when-a-codon-fails), including the warning for `onExceeded: "fail"` paired with `onFailure: "retry"`. For a fresh repetition, run the same ch4 hank with `--start-new`, a distinct `--execution`/`-o` pair, and the same prepared `task-data`. This compares independent executions under a chosen envelope without pretending that a resumed execution re-plans the model or proving an unrecorded model-tier experiment.