Summary / 02. Tool selection and tool calling / Document
Spec 2026-09-04
Shared copy: the operator's name and machine names are replaced by roles. Nothing else is changed.
Rendering…
# Build Spec 02 — Tool-Calling Policy (the ≈80% fraction) > Derived from `papers/02-tool-calling.md`. This document drives the build of the **tool-calling > policy layer** that sits *above* component 01's Action-Calling Interface (the six primitives) and > governs the interactive agent loop. It translates the paper's design into exact triggers, exact > validation rules, exact schemas, exact caps, and an exact build sequence — no further research > required. Where the paper says "≈80%", this spec pins *which* 80%. --- ## 1. Purpose & Scope Build a **reversible tool-calling policy** — a deterministic trigger gate, a top-5 retrieval over a 6-tool description registry, a single-shot selection-and-argument-binding step, first-class `ask_user`/`stop` terminal affordances, strict one-call-per-turn serialization, and a trust gate that captures *actual* tool output rather than the model's paraphrase — as a drop-in *replacement* for the stock "LLM → agent loop → read/write/edit/bash → repeat" surface, sitting directly above component 01's six primitives (`read_entity`, `search_symbol`, `search_code`, `edit_entity`, `run_test`, `execute`). The overall target is the paper's full 100% spec. **Round 1 lands the expected ≈80% of it**: everything the paper labels *deterministic* (D) — trigger, retrieval, and trust-gate — plus the single learned (L) selection step. The learned/probe-based triggers (Toolformer perplexity-filter training, When2Tool hidden-state probes, Self-RAG reflection tokens, ReTool RL) are **not** built in Round 1 and are **not excluded** — they are later rounds that need training/weight access and climb from that ~80% landing toward 100% (see §2). Round 1 is kept only if it wins a measured A/B test against the current loop on resolve rate, call precision, tool-selection accuracy, cost, and tokens (§9). --- ## 2. Round 1 vs later rounds (toward the 100% target) The goal is the paper's full **100%** spec. **~80%** is only the expected **first-round landing**; later rounds climb from there. Every element from the paper is classified below by *which round builds it*. - **BUILD-NOW = the Round 1 build** — the expected ≈80% deterministic landing this spec implements and A/B-tests (rows 1–8). - **DEFERRED = later rounds** — the learned/probe-based triggers and neural-scale retrieval (rows 9–13) that Round 1 intentionally does not build *yet*, because they need training data or weight / hidden-state access a frozen weak model on CPU hardware does not expose. They are **not excluded from the project** — they are the content of later rounds, the climb toward 100%, gated by the same §9 A/B protocol once the Round 1 core is in place. | # | Component (paper §) | Classification | |---|---|---| | 1 | Event-driven deterministic trigger gate (edit→lint, patch→test, symbol-token→`search_symbol`) | **BUILD-NOW** | | 2 | Validated model-requested calls (registry + arg + precondition check, rejection-with-reason) | **BUILD-NOW** | | 3 | No-op affordances — `ask_user` / `stop` as first-class terminal outcomes | **BUILD-NOW** | | 4 | Tool-call serialization (one call per turn; dependent calls wait) | **BUILD-NOW** | | 5 | Tool-description registry + top-5 retrieval (static relevance map, default-4 fallback) | **BUILD-NOW** | | 6 | Selection & argument binding (single LLM step over ≤5 candidates) | **BUILD-NOW** | | 7 | Trust gate (capture actual stdout/stderr/exit code; unverified-observation routing) | **BUILD-NOW** | | 8 | A/B runner (feature-flag router + joint-metric capture) | **BUILD-NOW** | | 9 | Toolformer perplexity-filter trigger (self-supervised τ_f training) | **DEFERRED** — needs training | | 10 | When2Tool hidden-state necessity probe (linear decode from pre-generation activations) | **DEFERRED** — needs hidden-state/weight access | | 11 | Self-RAG reflection tokens (`[Retrieve]`/`[IsRel]`/`[IsSup]`/`[IsUse]`, threshold δ) | **DEFERRED** — needs fine-tuning | | 12 | ReTool RL trigger (optimize *when/how* from terminal reward) | **DEFERRED** — needs RL training | | 13 | Sentence-BERT / neural retriever over tool descriptions (ToolLLM NDCG@5) | **DEFERRED (scale-motivated)** — a 6-tool registry makes a keyword map sufficient | **Trigger-gate decision: BUILD-NOW-DETERMINISTIC** (a Round 1 build item). The paper's *learned* trigger (model logprob/token-confidence vs threshold τ) is explicitly **deferred** — it requires log-probabilities or hidden states the frozen deployment does not expose. Round 1 substitutes the deterministic dual of the trigger: (a) **event-driven triggers** that fire on state transitions with no model involvement, and (b) **model-requested calls** that the model may initiate at any turn, gated rejectively. This preserves the paper's two-sided objective (penalize the omitted necessary call *and* the spurious one) without needing τ. It is the **first thing replaced** if a later round supplies a calibrated learned τ that §9 shows beats it. --- ## 3. The Trigger Gate (D) The trigger decides *whether* to invoke a tool this turn. In Round 1 it has three parts: (a) event-driven deterministic triggers, (b) validated model-requested calls, and (c) the no-op terminal affordances. There is **no** model-confidence threshold in Round 1 — that is deferred (§2 row 9–12). ### 3.1 (a) Event-driven deterministic triggers — always fire These fire on state transitions, **without any model request**, and their observations are appended to the observation ledger exactly like a model-requested call's. They are the only tool invocations that do not pass through the selection step (§5). | Trigger | Condition (exact) | Action (exact) | |---|---|---| | **post-edit lint** | `edit_entity` returns `committed: true` | Run `py_compile` + `ruff --select E9,F` on the affected file. Append observation `{"kind":"lint","target":selector,"result":"clean"|"<lint-errors>"}`. (Component 01 already linter-gates *pre-commit*; this trigger re-runs on the *committed* file and records the result as data the model can see.) | | **post-patch test** | a candidate patch is fully formed (all edits for one repair sample committed) | Run `run_test(repro_target)` (or the instance's `FAIL_TO_PASS` test when no repro). Append `{"kind":"test","target":...,"result":{"passed":bool,"exit_code":int}}`. | | **symbol-like token** | `issue_text` or any observation string contains a token matching the **symbol regex** (below) | For the first `k` (≤ 3) unique matches: run `search_symbol(token)` and append `{"kind":"symbol","query":token,"matches":[signature-only]}`. | **Symbol regex (exact):** `[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+` — a dotted identifier (e.g. `memstore.MemStore.remember`). This is the unambiguous signal; a bare CamelCase token (`\b[A-Z][a-zA-Z0-9_]+\b`) is a *weaker* signal and is **off by default** (flag `SYMBOL_CAMEL=1` to enable) to avoid firing on ordinary prose. Each event-driven trigger fires **at most once per distinct condition-key** (per selector, per patch, per token) so a re-entrant state cannot loop it. ### 3.2 (b) Model-requested calls — validate then execute At any turn the model may emit a structured tool request. The harness **validates** it and **rejects with a reason string** if invalid (the minefield guard of P4); it never silently executes a plausible-but-wrong action. The model's request shape (one object, no prose): ```json { "tool": "<name>", "args": { "<arg>": "<value>", ... } } ``` **Validation rules (exact, in order):** 1. **Registry check.** `tool` must be a key in the tool registry (§4) — exactly the six primitives. Unknown name → reject: `"unknown tool: <name>"`. 2. **Arg well-formedness.** For every arg in the tool's schema: required args must be present; values must be the declared type (`str`/`int`/`bool`/`list[str]`); numeric args clamped to their cap range. Any arg **not** in the schema → reject: `"unknown arg: <name>"`. Missing required arg → reject: `"missing required arg: <name>"`. Wrong type / out-of-cap → reject: `"invalid arg: <name> (<why>)"`. 3. **Precondition check** (per tool): - `read_entity` — `selector` must exist in `code_nodes`; else `"precondition failed: unknown selector <selector>"`. - `search_symbol` / `search_code` — `query` non-empty after strip; else `"precondition failed: empty query"`. - `edit_entity` — **evidence gate**: the evidence ledger must already contain a read/search record citing `selector`; else `"precondition failed: no evidence for selector <selector>"`. Also `len(search) ≤ 400`, `len(replace) ≤ 2000` (else arg-cap reject, rule 2). - `run_test` — `target` must be a parseable pytest node id or a shell string (non-empty); else `"precondition failed: empty target"`. - `execute` — `code` non-empty; else `"precondition failed: empty code"`. (Always allowed otherwise: it is sandboxed.) **Rejection format (exact):** a rejected call is returned as an observation, **not** executed: ```json { "ok": false, "rejected": true, "tool": "<name>", "reason": "<string>" } ``` The `reason` string always names the failed rule and the offending value, e.g. `"unknown tool: bash"`, `"missing required arg: selector"`, `"precondition failed: no evidence for selector repo.memstore.MemStore.remember"`, `"invalid arg: window (expected int 1..100, got 0)"`. On rejection the model is given **one** re-selection attempt on the same turn. A second rejection consumes the turn and is recorded as a `rejected` observation; the loop advances. Rejection never escalates into a prompt or a loop (§10 invariant 7). ### 3.3 (c) The no-op affordances — `ask_user` and `stop` `ask_user` and `stop` are **first-class terminal outcomes**, *not* tools. They are not in the tool registry, carry no preconditions, and halt the loop (no observation, no continuation). The model signals them through the same selection output shape (§5) using two reserved names: ```json { "tool": "ask_user", "args": { "question": "<string>" } } { "tool": "stop", "args": { "reason": "<string>" } } ``` Harness behavior: - `ask_user` → halt and emit terminal outcome `{ "outcome": "ask_user", "question": "<string>" }`. In the A/B harness this records the instance as `not_resolved` with reason `asked_user` — a *correct* abstention (ToolSandbox insufficient-information), measured separately as **correct-abstention rate** (§9), so an agentless-first policy is credited for declining rather than flailing. - `stop` → halt and emit `{ "outcome": "stop", "reason": "<string>" }` (maps to `verdict = "halt"`). A turn that produces neither a valid tool call, nor `ask_user`, nor `stop`, nor an event-driven trigger is a **no-op turn** and counts against the iteration budget (§10 invariant 4 / caps). --- ## 4. Retrieval (D) Piper's toolset is small — exactly **6 primitives** — so retrieval's job is to present **only the relevant subset, never all schemas in every prompt**. Five is the ceiling (P2), not the target; the cap exists so the mechanism generalizes when tools are added. ### 4.1 The tool registry A single Python dict, JSON-serializable, keyed by tool name. **Exact tool-description schema:** ```json { "name": "read_entity", "description": "read a code entity's signature and optional windowed body", "args": { "selector": "str (required)", "body": "bool (default false)", "window": "int 1..100 (default 100)" }, "returns": "envelope {ok, error, data:{entity, body}}", "keywords": ["read", "view", "inspect", "signature", "body", "entity", "function", "class", "method"], "category": "read" } ``` The full registry (six entries — these are the *only* executable tools): | name | description | args | category | keywords | |---|---|---|---|---| | `read_entity` | read a code entity's signature and optional windowed body | `selector` (str, req), `body` (bool, def false), `window` (int 1..100, def 100) | read | read, view, inspect, signature, body, entity, function, class, method | | `search_symbol` | signature-only AST symbol search | `query` (str, req), `keywords` (list[str], opt), `k` (int 1..10, def 10) | search | search, symbol, find, lookup, name, signature, grep, function | | `search_code` | summarized + capped embedding retrieval over entities | `query` (str, req), `cap` (int 1..8, def 8) | search | search, code, retrieve, similar, related, embedding, semantic | | `edit_entity` | exact-string replace scoped to an entity, linter-gated | `selector` (str, req), `search` (str ≤400, req), `replace` (str ≤2000, req) | edit | edit, change, fix, patch, replace, modify, update, write, correct | | `run_test` | execute a test; pass/fail + truncated traceback | `target` (str, req), `timeout` (int 1..300, def 120) | execute | test, run, verify, pytest, fail, pass, assert, regression | | `execute` | sandboxed compute (no net / no host fs / bounded time) | `code` (str, req), `timeout` (int 1..30, def 10) | execute | execute, compute, run, calculate, eval, script, code, sandbox | Two **reserved non-tool names** sit beside the registry: `ask_user`, `stop` (§3.3). They are never returned by retrieval and never pass through validation. ### 4.2 Retrieval mechanism (primary — deterministic, stdlib-only) A **static relevance map**: score each of the 6 tools by how many of its `keywords` appear (case-insensitive substring) in `issue_text + " " + latest_observation`. Keep every tool with score ≥ 1, ranked by score then by fixed registry order. **If no tool scores ≥ 1, present the default 4** — the observe-only set `{read_entity, search_symbol, search_code, run_test}`. Cap the result at **5** (`truncated` flag if more — cannot happen with 6 tools, but the field exists). This is the §2 decision to use a keyword map rather than BM25/embeddings: with a 6-tool registry the map is exact, has zero dependencies, and its errors are predictable. An **embedding fallback** (`fastembed bge-small-en-v1.5` cosine over `description + keywords`, reusing `memory/memstore.py`) is specified but **off by default** (flag `RETRIEVE=embed`), and is only enabled if a later §9 ablation shows the map measurably underperforms — at which point it is still deterministic, never a learned retriever (that is §2 row 13, deferred). Retrieval output is the candidate toolset as compact description lines (name + one-line description + args), never bodies, never the full schema dict. --- ## 5. Selection & Argument Binding (L) The single learned step. Given the candidate toolset ≤ 5 plus current state, the model selects **one** tool (or one no-op affordance) and binds its arguments. **Prompt inputs (assembled deterministically, in this exact order):** 1. **Policy preamble** (fixed text): agentless-first — act rarely, act correctly, verify what you're told; one action per turn; `ask_user` and `stop` are valid and encouraged when you lack enough to act; prefer the minimal action. 2. **Candidate toolset** — the ≤5 retrieved tools, each as `name — description — args` on one line, plus the two reserved lines `ask_user — ask the operator a question` and `stop — halt with a reason`. 3. **Current state** — the collapsed observation ledger (last 20 entries, one line each: `kind, target, result`) plus the latest observation, verbatim. 4. **Task context** — `issue_text` verbatim. **Output schema (exact — one JSON object, no prose, no list):** ```json { "tool": "<name|ask_user|stop>", "args": { "<arg>": "<value>", ... } } ``` `args` is empty (`{}`) for `ask_user`/`stop` in their exact §3.3 shapes. **One action per turn.** The model never emits multiple calls; a dependent call must wait for the prior call's observation (§6). Sampling is **greedy**: `temperature = 0`, `max_completion_tokens = 256`. Selection is discrimination, not exploration — the same logic that routes selection through ordinary next-token generation over a shortlist (ToolkenGPT's insight), with no deliberative overhead. Output of the selection step is either a valid request (→ §3.2 validation) or a no-op terminal (→ §3.3 halt). Any parse failure of the JSON is treated as a **no-op turn** (logged, advance the loop, never re-prompted). --- ## 6. Serialization & the Trust Gate (D) ### 6.1 Serialization **One tool call per turn.** No parallel dependent calls: a call whose inputs depend on a prior call's observation must wait for that observation before the next turn. The loop is strictly `observe → select → validate → execute → trust-gate → observe`. The only parallelism anywhere is *internal* to the deterministic primitives (e.g. Stage-1 embedding) and never spans model-requested calls. This eliminates the ToolSandbox shared-state race by construction. ### 6.2 The trust gate — capture actual output, never the model's summary The harness captures **actual `stdout`/`stderr`/`exit_code`** from every tool execution and passes *that* as the observation. **The model's paraphrase of a result is never trusted** — the harness's own capture is the only thing that enters the ledger. Concretely: - `run_test` → `{ "passed": exit_code == 0, "exit_code": int, "output": "<captured stdout+stderr, truncated to 50 lines / 2000 chars>" }`. `passed` is computed by the harness from the **exit code**, never from any string the model writes. - `execute` → `{ "stdout": ..., "stderr": ..., "exit_code": int, "timed_out": bool }` captured from the sandbox subprocess. - `edit_entity` → `lint_clean` is computed in the harness by `py_compile` + `ruff` (the linter gate), never from the model's claim that "the edit is clean". - `read_entity` / `search_symbol` / `search_code` → the JSON envelope is validated against its §3/§4 shape by the harness. **Trust-gate invariant checks (deterministic, on every observation):** 1. **Exit-code consistency** — `run_test`/`execute` exit code matches the reported `passed`/`timed_out`. 2. **Schema conformance** — the returned envelope parses and has the declared `data` shape. 3. **Round-trip reversibility** — a `search_symbol` match must actually contain the query token in `name + signature`; a committed `edit_entity` must have matched the `search` string exactly once (already enforced by the primitive, re-asserted here). 4. **Lint/sanitizer pass** — `edit_entity` results carry the harness's own lint verdict. On **any** check failing, the observation is **not discarded and not silently absorbed** — it is marked and routed back as evidence-with-a-caveat: ```json { "ok": true, "trust": "unverified", "caveat": "<which check failed>", "data": "<as captured>" } ``` The loop continues; the model reasons about a suspicious result rather than deferring to it blind (When-the-Tool-Decides [13]). Deterministic tool results — linter, test, AST — are **trusted as ground truth**; the model's *claim* about a result is **never** trusted without the harness's own capture. The gate keeps the Extended-Mind coupling honest: a result the harness cannot verify is unverified input, not cognition. --- ## 7. Implementation Order Six phases, each independently testable and reversible. Each lands in `build/02-tool-calling/` as a standalone importable module. Rollback = delete/flag-off that phase's module; nothing earlier breaks because phases only add modules and never mutate live harness code. | Phase | Delivers | Isolated test | Rollback | |---|---|---|---| | **1. Tool registry + description schema** | `tools/registry.py` (the 6-tool dict + 2 reserved names + keyword map) | Assert registry has exactly 6 executable tools + 2 reserved names; each entry has `name/description/args/returns/keywords/category`; JSON-serializable round-trip. | `git rm`/flag-off `registry.py`; no other module imports it yet. | | **2. Event-driven deterministic triggers** | `policy/triggers.py` (`post_edit_lint`, `post_patch_test`, `symbol_token`) | Scripted `edit_entity` commit → a `lint` observation is appended; a formed patch → a `test` observation; an issue string containing `memstore.MemStore.remember` → a `symbol` observation; each fires ≤ once per key. | Delete `triggers.py`; no live state touched (fixture repo only). | | **3. Validated model-requested calls + no-op affordances + serialization** | `policy/validate.py` + `policy/loop.py` | Unit: unknown tool → rejection `"unknown tool: bash"`; missing arg → `"missing required arg: selector"`; edit without evidence → `"precondition failed: no evidence..."`; `ask_user`/`stop` → terminal outcomes; a dependent call issued before its predecessor's observation is deferred (serialized). | Delete the two modules; loop is new, harness untouched. | | **4. Retrieval (relevance map / top-5)** | `policy/retrieve.py` | Keyword `fix`/`patch` → subset includes `edit_entity`; `test`/`pytest` → includes `run_test`; empty/irrelevant input → the default 4; result never > 5 and carries `truncated`. | Delete `retrieve.py`. | | **5. Trust gate (capture actual output)** | `policy/trust.py` | Inject a `run_test` whose reported `passed` contradicts its exit code → `trust: unverified` with a caveat; a `search_symbol` match lacking the query token → flagged; a correct captured `run_test` → trusted (no flag). | Delete `trust.py`. | | **6. A/B runner** | `policy/ab.py` (feature-flag router + joint-metric capture) | End-to-end on 3 warm-up tasks; `ab.py` runs old-vs-new on the §9 sample and emits the joint-metric report + decision. | Feature flag `off` → live harness untouched; `ab.py` is read-only. | The A/B runner (Phase 6) is the only phase that ever *invokes* the current loop — and only to measure it, never to modify it. --- ## 8. Dependencies & Supply-Chain Per Piper's rule: nothing is installed unless >72h old AND (≥10 stars OR deep-scrutinized). This build **requires zero new installs** — everything is stdlib, already-installed, or in-repo: | Dependency | Why | Status / age-stars | Install needed? | |---|---|---|---| | stdlib `re`, `json`, `subprocess`, `ast`, `py_compile`, `tempfile` | symbol regex, envelope parsing, actual-output capture, AST/schema checks | CPython stdlib. | **No** | | `ruff` | linter gate (post-edit trigger, `edit_entity` gate) | Installed in `~/venvs/memory` (used by `coding-cortex/code_check.py`). | **No** | | `pytest` | `run_test` / post-patch test trigger | Installed in venv. | **No** | | `memory/codegraph.py` (code_nodes), `memory/memstore.py` (optional embed fallback), `coding-cortex/code_check.py` | in-repo reuse: selector existence check, embedding retrieval fallback, lint gate | In-repo, already vetted. | **No** | | `fastembed` + `BAAI/bge-small-en-v1.5` | *optional* embedding retrieval fallback (`RETRIEVE=embed`), off by default | Already installed + model cached; ONNX, no torch. | **No** | | SWE-bench-Verified dataset | A/B sample (§9) | Not a pip package; a data fetch. Not installed locally. | **Flagged (data, not code)** — see §9 fallback | **Preferred path**: stdlib + in-repo components everywhere; nothing pip-installed. No new package is installed for this build. The only flagged item is the SWE-bench dataset fetch (data, not code), avoidable via the §9 fallback. --- ## 9. Acceptance Criteria — the A/B Protocol **Sample (concrete).** Reuse the **same 20 held-out SWE-bench-Verified instances as spec 01** (Python-only, single-repo, no-Docker reproduction: repro = the instance's `FAIL_TO_PASS` test run through `run_test`). Held out = never seen by any tuning, prompt, or ranking pass. Seeded RNG. **Fallback** (if the dataset cannot be fetched locally): `coding-cortex/piperbench` sampled to 20 tasks, held out identically. **Measures (joint — all five recorded, no single-number score).** 1. **Resolve rate** = `resolved / total`. 2. **Cost** = `$ / instance` (model spend only; deterministic stages are ~free). 3. **Tokens** = `tokens / instance`. 4. **Call precision** = `necessary_calls / total_calls`, where a call is *necessary* iff suppressing it (counterfactual ablation: re-run the instance with that call removed) flips the instance from resolved → unresolved. This reuses the ablation harness of the §9 ablation rule, so no separate machinery. 5. **Tool-selection accuracy** = `correct_selections / attempted_selections`, scored against a hand-written **gold-label file** committed with this spec: for each of the 20 instances, the expected tool (or `ask_user`/`stop`) at the first `m` decision points. (Also report **correct-abstention rate** = instances where `ask_user`/`stop` was the correct move and the policy declined, over instances that were genuinely under-specified.) **Decision rule (explicit thresholds).** Run candidate-vs-current on the *identical* sample with the *identical* model (`deepseek-v4-pro`) and hardware (this box). The candidate **wins iff**: ``` resolve_candidate > resolve_current (strictly; ties do NOT adopt) AND cost_candidate ≤ 1.5 × cost_current (worse cost is disqualifying) AND tokens_candidate ≤ 1.5 × tokens_current (worse token spend is disqualifying) AND call_precision_candidate ≥ call_precision_current − 0.03 (no precision regression) AND selection_accuracy_candidate ≥ selection_accuracy_current − 0.03 (no selection regression) ``` A candidate that wins resolve but fails cost/token or regresses precision/selection is **not adopted**; the tradeoff is recorded explicitly. If the candidate wins, it replaces the current loop; if it loses, the build tree is kept for the record and the live loop is unchanged. **Ablation rule (mechanical).** Each gate is removed in isolation, everything else held fixed, and the delta measured. A gate is **deleted** iff its removal changes none of the five measures beyond noise: `Δresolve = 0` AND `|Δcost| ≤ 5%` AND `|Δtokens| ≤ 5%` AND `Δcall_precision = 0` AND `Δselection_accuracy = 0`. Applied first to the two weakest a-priori cases (the retrieval relevance map's embedding-fallback branch, and the trust-gate round-trip check on `search_symbol`), then to any other gate — regardless of how sensible it looks. --- ## 10. Guardrails & Reversibility **Revocability.** - The build lives entirely in `build/02-tool-calling/` (a separate tree); it never writes into the live loop's source. - A **feature flag** (env var `TOOL_POLICY=policy|current`, default `current`) routes the A/B; the flag alone switches between old and new — no code deletion required to revert. - `policy/ab.py` is a read-only driver: it *measures* the current loop, it never modifies it. **Invariants (must hold, checked at the end of every phase).** 1. **No new `.md` files outside this folder** (`projects/self-improvement/{papers,specs,build}`). Everything else is code or DB rows. 2. **No change to the live loop** until the §9 A/B gate passes. The current tool surface stays byte-identical during the entire build. 3. **Sandbox is non-negotiable**: `execute` never gains network or host-fs access; any phase that relaxes it is rejected outright. 4. **One action per turn**: no parallel dependent calls; a dependent call waits for its predecessor's observation. 5. **Deterministic tools are the only trusted ground truth**: the model's claim about a result is never trusted without the harness's own capture of `stdout`/`stderr`/`exit_code`. 6. **Deterministic stages stay deterministic**: trigger, retrieval, and trust-gate are D; the LLM column holds exactly the selection step. No LLM call may be introduced into any D node. 7. **No escalation**: a validation rejection, a no-op turn, or a cap hit is a deterministic advance or halt — never a prompt to the model and never a new agentic loop. --- *End of spec. This document is the build contract; the next document records the A/B outcome.*