feat(s2c): debatten navigerer basen i stedet for aa faa den utlevert [skip-docs]
MAJOR-3/S7a-3 gjorde utforskningen billig og lot pipelinen staa. Maalt paa K2
(630 konsepter, S7bs eget instrument, kjent-positiv-kontrollen reprodusert
eksakt FOER bruk): okf.bundle_context er 648 962 o200k-tokens og rir i TRE
kopier = 1 947 342 = 99,1 % av en kjoerings prompt-tokens.
Et premiss i maaledokumentet ble presisert foerst: de tre kopiene er tre
DEBATT-turer (proposer x2, checker x1), mens genererings-prompten er 156
tokens, fordi gen_context = debate_output or context. Det avgjorde formen -
generering trengte ingen egen soem, for aa binde `context` binder
siste-utvei-fallbacken ved konstruksjon.
run_project sender naa en PEKER (fast tekst + erklaert bundle_id + antall
konseptdokumenter i scope + stigen, O(1) i korpuset) og gir debatten de SAMME
fire verktoeyene utforskningen bruker - explore.navigator_tools gjenbrukt,
aldri en andre kopi av policyen.
Etter: 753 tokens like-for-like (samme manus, samme fire prompter, -99,96 %)
og 8 942 med en debatt som faktisk gaar stigen (-99,5 %), mot operatoerens
terskel 195 000 = 4,6 % av taket. Validert besparelse og validatorens dom er
UENDRET (850 000 NOK av 3 852 500, 2 av 5 felt paa stage 4 og 5, samme
dom-noekkel), og utforskningens 18 355 er uendret til tokenet.
§4.1a maatte flytte, ikke forsvinne: dimensjonsfilteret bodde i renderingen og
bor naa i VERKTOEYENE, paa begge trinn - en listing som skjuler et fremmed
dokument mens read_file serverer det paa sti er et filter i navnet alene.
okf.in_dimension er eneste predikat.
Sporet er kaller-eid (ExplorationToolRecorder -> RunResult.debate_tool_calls ->
{run_id}-debate.json fra en finally) og skrives ogsaa TOMT: en debatt som
navigerer ingenting ER S2c-regresjonen, saa den maa kunne leses.
Load-bearing MAALT: aatte mutasjoner roede mot HELE suiten, groenn kontroll
1306/5 (fra 1295/5), golden demo-transcript.stdout BYTE-UENDRET
(shasum -a 1 av innholdet = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
M7 falsifiserte seg selv, ikke gaten - staar som maalt.
Maaling: docs/2026-09-04-s2c-debatt-k2.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5d8844fef5
commit
da5f10f140
13 changed files with 1044 additions and 98 deletions
|
|
@ -214,6 +214,17 @@ class ExplorationError(RuntimeError):
|
|||
"""The exploration cannot be honoured as configured, or produced something unreadable."""
|
||||
|
||||
|
||||
class DimensionScopeRefused(ValueError):
|
||||
"""A navigator asked for a document belonging to ANOTHER dimension than the run is scoped to.
|
||||
|
||||
A ``ValueError``, the ``BundlePathNotFound``/``BundleIdMismatch`` precedent: the caller is a
|
||||
model choosing a path, so the refusal must land on the CLI's refusal tuple and hosting's 400
|
||||
arm rather than the crash channel. It is deliberately NOT an ``ExplorationError``
|
||||
(a ``RuntimeError``): this is a refused read inside a run that is otherwise fine, not an
|
||||
exploration that cannot be honoured as configured.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LedgerEntry:
|
||||
"""One progress-ledger round, reduced to the five fields the manager steers on (C.2).
|
||||
|
|
@ -375,6 +386,20 @@ class ExplorationTrace:
|
|||
tokens_spent: int = 0
|
||||
|
||||
|
||||
def tool_call_payload(calls: Sequence[ToolCall]) -> list[dict[str, Any]]:
|
||||
"""The ONE rendering of a tool trace into plain data, in CALL ORDER.
|
||||
|
||||
Two surfaces now write one: ``trace_payload`` for ``{run_id}-exploration.json`` and
|
||||
``run_project`` for ``{run_id}-debate.json`` (S2c). Two copies of "what a recorded call looks
|
||||
like" would drift into two answers about the same fact, which is the kø-(p) defect — and here
|
||||
the drift would land in the artefacts an operator reads to find out what a paid run opened.
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def trace_payload(
|
||||
trace: ExplorationTrace, *, stop: str | None, completed: bool, mandate: Mandate | None
|
||||
) -> dict[str, Any]:
|
||||
|
|
@ -442,10 +467,7 @@ def trace_payload(
|
|||
}
|
||||
for call in trace.quick_validations
|
||||
],
|
||||
"tool_calls": [
|
||||
{"name": call.name, "bundle_id": call.bundle_id, "path": call.path}
|
||||
for call in trace.tool_calls
|
||||
],
|
||||
"tool_calls": tool_call_payload(trace.tool_calls),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -859,7 +881,9 @@ def _index_excerpt(body: str) -> tuple[str, bool]:
|
|||
return (body[:cut] if cut > 0 else body[:_CATALOGUE_EXCERPT_CHARS]), True
|
||||
|
||||
|
||||
def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
||||
def navigator_tools(
|
||||
bundle_dirs: Sequence[str], *, dimension: str | None = None
|
||||
) -> list[FunctionTool]:
|
||||
"""The navigator's three tools: survey the catalogue, open one base, read one document.
|
||||
|
||||
Progressive disclosure, not stuffing (målbilde §2/§4): ``list_bundles`` never returns content,
|
||||
|
|
@ -898,6 +922,13 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
The listing is built from ``Bundle.context_files``, which EXCLUDES the ``type: verdict`` layer
|
||||
by construction — prior verdicts reach a hypothesis only through the gated ExpeL fold inside
|
||||
``run_project``, never by being read as context here.
|
||||
|
||||
**``dimension`` scopes BOTH rungs, and both halves are the promise** (§4.1a, carried over in
|
||||
S2c when the DEBATE started navigating instead of being handed ``bundle_context``). The filter
|
||||
used to live in the rendering; with navigation it has to live in the tools, and a listing that
|
||||
hides a foreign-dimension document while ``read_file`` still serves it by path is a filter in
|
||||
name only — a model-chosen path is untrusted input, so the gate belongs where the bytes leave.
|
||||
``None`` (the exploration's own call) admits everything, byte-identical to before.
|
||||
"""
|
||||
index = _bundle_index(bundle_dirs)
|
||||
|
||||
|
|
@ -921,7 +952,9 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
"id": bundle_id,
|
||||
"index_excerpt": excerpt,
|
||||
"index_truncated": truncated,
|
||||
"documents": len(bundle.context_files),
|
||||
"documents": sum(
|
||||
1 for f in bundle.context_files if okf.in_dimension(f, dimension)
|
||||
),
|
||||
"verdict_count": len(bundle.verdicts),
|
||||
# Tolerant on CONTENT, fail-fast on the PATH: an operator's bad directory is
|
||||
# refused by navigate_bundle above, while a navigable base that simply has no
|
||||
|
|
@ -953,7 +986,7 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
# ONE renderer for both rungs (kø-(p)): this tool and ``read_dir`` differ only in WHICH
|
||||
# level they ask for, and two copies of a listing rule would drift into two answers about
|
||||
# one bundle. ``okf`` owns it, so the context seam stays framework-neutral.
|
||||
return okf.directory_listing(bundle)
|
||||
return okf.directory_listing(bundle, dimension=dimension)
|
||||
|
||||
@tool(
|
||||
name="read_dir",
|
||||
|
|
@ -968,7 +1001,7 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
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)
|
||||
return okf.directory_listing(bundle, path, dimension=dimension)
|
||||
|
||||
@tool(
|
||||
name="read_file",
|
||||
|
|
@ -979,7 +1012,30 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
# 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
|
||||
# navigation walk uses rather than a second, laxer check.
|
||||
return Path(safe_resolve(bundle_dir, path)).read_text(encoding="utf-8")
|
||||
resolved = Path(safe_resolve(bundle_dir, path))
|
||||
if dimension is not None:
|
||||
# The SECOND half of the scope (§4.1a). A listing that hides a document while this rung
|
||||
# still serves it by path is a filter in name only, and the caller here is a model that
|
||||
# can name a path no listing gave it. Only a NAVIGATED concept file is judged: the walk
|
||||
# is what knows a file's declared dimension, and a path outside it is already refused —
|
||||
# or, for ``index.md``, is navigation rather than scoped knowledge.
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
foreign = next(
|
||||
(
|
||||
f
|
||||
for f in bundle.context_files
|
||||
if Path(safe_resolve(bundle_dir, f.name)) == resolved
|
||||
and not okf.in_dimension(f, dimension)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if foreign is not None:
|
||||
raise DimensionScopeRefused(
|
||||
f"document {path!r} in knowledge base {bundle_id!r} declares dimension "
|
||||
f"{foreign.frontmatter.get('dimension')!r}; this run is scoped to "
|
||||
f"{dimension!r} and reads only knowledge in scope"
|
||||
)
|
||||
return resolved.read_text(encoding="utf-8")
|
||||
|
||||
return [list_bundles, read_bundle, read_dir, read_file]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue