feat(p19): the trace says HOW, and a run says what it spent and why it stopped

DEL C. P18 gave read_dir a window (filter/offset/limit) and then measured its
own paid round without being able to see it used: five of 31 documents read
lay outside the default window, so the window HAD been widened and the trace
could not say with which knob. ToolCall now carries the three arguments,
always present and empty/zero when not passed -- an absent key and "not
narrowed" must not read the same -- and the judge counts filter_calls and
paged_calls. _number_argument is a SIBLING of _string_argument, not a widening
of it: a model may send limit as 10 or as "10", and a reader that knew one
shape would report a paged call as unpaged.

DEL D. P18's finding 4 was WRONG AS WRITTEN. provenance.token_usage has been
stamped on every proposal artefact since S3.4 and stands in every one of round
2's; what was missing is a READER. The judge reads it now (round 2 measured:
289 054 tokens against round 1's 2 679 305, -89 %), and the P18 report gets a
dated correction UNDER its original paragraph rather than instead of it.

What was genuinely absent is {run_id}-coverage.json. settle prints the
coverage report and ApproachOutcome has carried not_evaluated since Trekk A3,
but neither ever reached a file, so a judge could see an approach had no
artefact and could not tell a budget stop from an approach nobody ordered.
Written from the finally IFF a mandate was given. stop_reason comes from a
CALLER-OWNED sink rather than from in_flight, and that is a measurement:
_evaluate_mandate SWALLOWS BudgetExceeded once something has been produced, so
run_project's own in_flight never sees it.

Load-bearing measured (10 arms), four mutations all red against the whole
suite, green control 1744/5 and the golden byte-unchanged. D-i stood GREEN
first -- the vacuous-gate class, 25th time: the arm called write_coverage
itself and therefore chose the reason it then asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-15 03:05:52 +02:00
commit 4c6084e5df
12 changed files with 684 additions and 11 deletions

View file

@ -343,6 +343,15 @@ class ToolCall:
name: str
bundle_id: str
path: str
#: P19 DEL C — HOW the listing was asked for, not just WHICH one. P18 gave ``read_dir`` a
#: window (``filter``/``offset``/``limit``) and the trace could not say whether a model used
#: it: five of 31 documents read in round 2 lay outside the default window, so the window HAD
#: been widened, and nothing said with which knob. Empty/zero mean "not passed" — the
#: ``bundle_id``/``path`` rule one field over, and unambiguous here because ``limit`` is
#: clamped to at least one wherever it is given.
filter: str = ""
offset: int = 0
limit: int = 0
@dataclass(frozen=True)
@ -393,6 +402,28 @@ def _string_argument(arguments: Any, key: str) -> str:
return value if isinstance(value, str) else ""
def _number_argument(arguments: Any, key: str) -> int:
"""One NUMERIC argument of a call — ``_string_argument``'s sibling, and deliberately separate.
A model may send ``limit`` as ``10`` or as ``"10"`` (both reach a tool through the same wire),
so a recorder that read only the first shape would say a paging call was unpaged. Anything that
is neither is ``0``: the recorder describes the call, and inventing a number for an argument
nobody passed would be the false attribution ``_string_argument`` refuses for its own field.
``bool`` is excluded explicitly because it is an ``int`` in Python and a flag is not a window.
"""
if isinstance(arguments, Mapping):
value: Any = arguments.get(key)
else:
value = getattr(arguments, key, None)
if isinstance(value, bool):
return 0
if isinstance(value, int):
return value
if isinstance(value, str) and value.strip().lstrip("-").isdigit():
return int(value.strip())
return 0
class ExplorationToolRecorder(FunctionMiddleware):
"""Records WHICH exploration tool an agent actually called, in the order it called them.
@ -424,6 +455,9 @@ class ExplorationToolRecorder(FunctionMiddleware):
name=name,
bundle_id=_string_argument(arguments, "bundle_id"),
path=_string_argument(arguments, "path"),
filter=_string_argument(arguments, "filter"),
offset=_number_argument(arguments, "offset"),
limit=_number_argument(arguments, "limit"),
)
)
await call_next()
@ -479,7 +513,20 @@ def tool_call_payload(calls: Sequence[ToolCall]) -> list[dict[str, Any]]:
Plain mappings only, so the RAW output layer stays MAF-free (``outbox.py`` may not import
this module).
"""
return [{"name": call.name, "bundle_id": call.bundle_id, "path": call.path} for call in calls]
return [
{
"name": call.name,
"bundle_id": call.bundle_id,
"path": call.path,
# P19 DEL C: HOW the level was asked for. Always present, zero/empty when not passed —
# the ``write_debate_tools`` rule one field down: an absent key and "not narrowed" must
# not be the same reading.
"filter": call.filter,
"offset": call.offset,
"limit": call.limit,
}
for call in calls
]
def trace_payload(