Design spec · 2026-08-22 · live verified

Looking Is the Price of Claiming

Three read-only tools, a receipt for every use, and one rule enforced in the writer: a settlement made in a turn with no receipt is refused. This is the design, the reasoning for departing from both our own "no tools" contract and Kairo's tool layer, the controls that were fired before it was trusted, and what the first live look did when it guessed wrong.

What would count as this design failing

Why "no tools" was the right fence around the wrong risk

Our model contract (docs/PULSE.md, "The model contract") says no tools. It was written against one risk: a model that can call believe() manufactures the evidence that unlocks its own convictions. That fence is correct and it stands — the only write path from the model remains the two typed lines EXPECT: and SETTLE:, validated in the store.

But "tools that write self-state" and "tools that read the world" are different risks, and the ban covered both. The cost showed up as a number: 23 open expectations, one ever settled, that one self-graded. The model was being asked "has it been observed yet?" about observables it had named — a SQL count, a file — with no way to look. Reads never touched the fence.

The three looks

typed linereadscaps
LOOK: store.query <SELECT …>the store, read-only; columns named in the menu20 rows, 1200 chars
LOOK: canon.read <message_id>one Discord canon post, by id; digits only, no path components1200 chars
LOOK: frame.count [dimension]live self-state rows per dimension; an empty dimension returns 0, not absence

Same regex family as EXPECT:/SETTLE:: anchored to line start, leading markdown tolerated. One source for the syntax and the parser (tools.syntax_help), so the instruction cannot drift from the regex it describes.

Receipts

Every look becomes an observation event — look: <tool> <args> — whose meta carries the full receipt: tool, args, result, a 16-hex SHA-256 of the result, wall time, and the cause_event of the human or dream turn that asked. The turn's telemetry carries a summary (fields.receipts), the order of the menu it was shown (fields.tool_order), and fields.look_rounds.

An accepted settlement's evidence string is stamped with the receipt hashes: [receipts: store.query#1551:a956d404a77d51ad]. A settlement is now a claim plus a pointer to the observation it was made against, and both are in the same store.

The one round

The first reply's LOOK lines are run; the receipts are sent back as a single numbered message with the first exchange as history; the second reply is the one predict.extract reads. Bounded on purpose. The second round uses the same voice (sampler options) as the first.

Ordering — the voice's second hand

The menu the model sees is ordered by what is owed: the curiosity engine names the largest measured gap, and the tool that closes that kind of gap goes first (open/overdue/unverifiedstore.query; unreadcanon.read; thinframe.count). The order used is recorded per turn.

Departure from Kairo, cited. Kairo's pulse report discloses that "tool ordering currently reads state tokens without gating on decayed confidence — a zero-confidence field moved alpha to the first position." Ours orders from measured quantities (row counts, ages, settlement records) and never from a field's self-reported confidence, so that class of bug has no input to arrive through. This is an improvement only if the measured quantities are the right ones to order by; that is a design claim, not a measured one.

Controls fired before trusting it

guardknown-bad inputobserved
SQL write refusalDELETE FROM frame, select 1; DROP TABLE frame, PRAGMA journal_mode=off, UPDATE …each returns an error; frame_live() unchanged after all four
settle needs receiptSETTLE: <id> confirmed with receipts=[]refused, counted, reason names the rule; expectation still open
legacy exemptionsame line with receipts=None (caller predates tools)not refused for that reason — offline extract still works
look capfour LOOK lines in one replythree run, looks_dropped: 1 in telemetry
path gamescanon.read ../xerror; no filesystem access
server round-tripa scripted responder that looks, then settles on round twotwo rounds, receipt event id in telemetry, settlement accepted

29 checks in src/test_tools.py, plus the server-level case in src/test_foreground_run.py. The SQL-count assertion went red once before green because store.expect() writes a frame row too; the test, not the code, was wrong, and it is recorded here because a test that passes first time has not proved it can fail.

Live: the first look

Asked for the most recent observation event "word for word — you cannot know this without looking," the model emitted LOOK: store.query SELECT content FROM events ORDER BY timestamp DESC LIMIT 1. Both column names were guesses. The receipt came back no such column: content, and the model's second reply said so — it reported the error it was handed rather than inventing a row. That is the behaviour the receipt rule exists to produce.

Defect found live, in this runtime, not the model: the server's receipt summary line raised a TypeError on the first real receipt (a tuple-or-set expression that no unit test reached, because the tests exercised tools.loop and not the server's summary of it). Fixed; the server-level test above now covers it. Two turns later, with the columns named in the menu, the model ran SELECT body FROM events ORDER BY ts DESC LIMIT 1 and answered from the receipt.

Boundaries

Implementation map

src/tools.py            the three reads, run(), loop(), order(), syntax_help()
src/predict.py          SETTLE refused without receipts; evidence stamped with hashes
src/server.py           tool order from curiosity → prompt; look round between respond and extract
src/identity.py         renders the menu in the order supplied
src/test_tools.py       29 checks · src/test_foreground_run.py  server round-trip