feat(prepass,explore): padding dies at the prompt; a refusal the model can act on is a return value [skip-docs]
Order 20260908T195801Z. Findings 4 and 5 from the S7 acid test, then the two things finding 99 measured and deliberately did not fix (D3, D2). No user-facing surface changes: no new flag, no new command, no changed output contract. Both seams are internal (the pre-pass rendering, and the shape a tool answers a model with), so [skip-docs] rather than a README edit that would describe nothing an operator can do differently. FINDING 4 -- MEASURED, NOTHING BUILT. K2's price schedule IS readable without guessing (8 column spans, 71 of 91 non-blank rows give >= 2 cells, the split stable for K = 2..64). But 0 of 92 rows name all three of code/quantity/unit_cost -- also under a looser substring match -- and 0 of 91 data rows carry code + quantity + amount. The triple is not formatted away; it is not in the document. It is a price SUMMARY plus nine rate cards whose unit-price columns are empty (pre-award). The order's binding decision rule therefore falls against building: --derive-cost-baseline keeps refusing, and MAJOR-4's own honesty limit holds. FINDING 5 -- BUILT. Measured on the actual rendering path (concept_text, not the raw file): the delivered excerpt is 104 lines / 67 245 chars, carrying 208 interior whitespace runs, 117 of them >= 100 and the longest 887 -- 56 806 of 67 245 characters = 84.5 %, over 72 of 104 lines. collapse_padding, called from _data_blocks (the one renderer both arms share, and therefore AFTER verify_against_bundle -- collapsing in concept_text would break every payload's own digest), gives -72.4 %: line count invariant, non-whitespace byte-identical, leading indentation untouched, no number changed. F99-D3 -- read_file / read_dir / read_bundle now RETURN their refusal. MAF turns a tool raise into "Error: Function failed." (_tools.py:1410-1432, :1427) and counts it against DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST = 3, so everything the refusing arm knows is destroyed on the way out. The gates are unchanged; the property they exist for -- the reason travels, the bytes never do -- is now asserted explicitly on the returned value. The arm is keyed on named classes, never bare Exception, because ExplorationError is itself a RuntimeError subclass. F99-D2 -- the invariant row, plus one for finding 5 (a stated deviation from "one row only": finding 5 is a separately built seam and the ledger's standing rule requires its own row). 19 existing arms rewritten, never deleted and never weakened: where the class carried a distinction, the refusal KIND carries it now. 13 mutations, all red against the whole suite (W1-W5, M1-M8), each restored from scratchpad with shasum -c. Control 1543 passed / 5 skipped (from 1529/5, a strict superset, 0 removed). Golden demo-transcript.stdout unchanged (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). Measurement: docs/2026-09-08-funn-4-5-og-read-nekt.md Co-Authored-By: Claude <Opus 5>
This commit is contained in:
parent
078a099898
commit
eb4137415c
13 changed files with 975 additions and 69 deletions
|
|
@ -927,6 +927,58 @@ def _index_excerpt(body: str) -> tuple[str, bool]:
|
|||
return (body[:cut] if cut > 0 else body[:_CATALOGUE_EXCERPT_CHARS]), True
|
||||
|
||||
|
||||
#: The refusal classes the navigator's read tools turn into an ANSWER instead of an exception,
|
||||
#: named ONE BY ONE and never as a base class. ``ExplorationError`` is itself a ``RuntimeError``
|
||||
#: subclass, so an arm written as ``except RuntimeError`` would also swallow a failure nobody
|
||||
#: named -- turning an unknown fault into a confident-looking answer, which is worse than the
|
||||
#: opaque string this conversion removes. MEASURED: inside these three tool bodies the only
|
||||
#: reachable ``ExplorationError`` is ``_resolve_bundle``'s unknown-base refusal.
|
||||
_RETURNABLE_REFUSALS: Final = (
|
||||
ExplorationError,
|
||||
okf.BundleIdMismatch,
|
||||
okf.BundlePathNotFound,
|
||||
okf.DocumentPathRefused,
|
||||
DirectoryPathRefused,
|
||||
VerdictLayerRefused,
|
||||
DimensionScopeRefused,
|
||||
)
|
||||
|
||||
|
||||
def _refusal_kind(exc: Exception) -> str:
|
||||
"""The refusal's KIND, from the exception's own class name -- one source, never a second table.
|
||||
|
||||
F3 measured that ``read_dir`` must tell "this is a document, use the other rung" apart from
|
||||
"this does not exist", and before this change a caller could tell them apart by CLASS. A
|
||||
returned refusal would lose that distinction unless it carries it, so it does.
|
||||
"""
|
||||
return type(exc).__name__
|
||||
|
||||
|
||||
def _refused_mapping(exc: Exception) -> dict[str, Any]:
|
||||
"""A listing tool's refusal: a mapping with no key a successful listing has.
|
||||
|
||||
``directory_listing`` answers with ``path`` / ``directories`` / ``documents``, so ``refused``
|
||||
and ``refusal`` cannot be mistaken for a result -- the property that makes a returned refusal
|
||||
worth anything at all.
|
||||
"""
|
||||
return {"refused": str(exc), "refusal": _refusal_kind(exc)}
|
||||
|
||||
|
||||
def _refused_text(exc: Exception) -> str:
|
||||
"""``read_file``'s refusal, as text, because that tool answers with text.
|
||||
|
||||
**Why not a mapping here.** Making ``read_file`` return a dict would change the shape of every
|
||||
SUCCESSFUL read, and therefore every prompt byte S2c measured when it gave the debate these
|
||||
tools. The refusal is a string with a fixed leading sentinel instead.
|
||||
|
||||
**Honesty limit, stated.** A document whose first characters were exactly this sentinel would
|
||||
be indistinguishable from a refusal. That is a real gap and it is recorded rather than papered
|
||||
over; what IS guaranteed is the direction that matters -- a refusal never carries the bytes of
|
||||
the document it refused, which is the property the verdict and dimension gates exist for.
|
||||
"""
|
||||
return f"REFUSED ({_refusal_kind(exc)}): {exc}"
|
||||
|
||||
|
||||
def navigator_tools(
|
||||
bundle_dirs: Sequence[str], *, dimension: str | None = None
|
||||
) -> list[FunctionTool]:
|
||||
|
|
@ -1022,6 +1074,12 @@ def navigator_tools(
|
|||
),
|
||||
)
|
||||
def read_bundle(bundle_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return _read_bundle(bundle_id)
|
||||
except _RETURNABLE_REFUSALS as exc:
|
||||
return _refused_mapping(exc)
|
||||
|
||||
def _read_bundle(bundle_id: str) -> dict[str, Any]:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
# The base is OPENED here, so this is where it must be able to say what it IS (S7a-3
|
||||
|
|
@ -1044,16 +1102,25 @@ def navigator_tools(
|
|||
),
|
||||
)
|
||||
def read_dir(bundle_id: str, path: str) -> dict[str, Any]:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
okf.assert_declared_ids_agree(bundle)
|
||||
return okf.directory_listing(bundle, path, dimension=dimension)
|
||||
try:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
okf.assert_declared_ids_agree(bundle)
|
||||
return okf.directory_listing(bundle, path, dimension=dimension)
|
||||
except _RETURNABLE_REFUSALS as exc:
|
||||
return _refused_mapping(exc)
|
||||
|
||||
@tool(
|
||||
name="read_file",
|
||||
description="Read ONE document inside a knowledge base, by base id and relative path.",
|
||||
)
|
||||
def read_file(bundle_id: str, path: str) -> str:
|
||||
try:
|
||||
return _read_file(bundle_id, path)
|
||||
except _RETURNABLE_REFUSALS as exc:
|
||||
return _refused_text(exc)
|
||||
|
||||
def _read_file(bundle_id: str, path: str) -> str:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
# safe_resolve is the ONE in-/out-of-bundle test in this repo, and it is fail-closed. A
|
||||
# model-chosen path is untrusted input by definition, so it goes through the same gate the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue