``run_mandate_across_bundles`` has existed since session 58, reachable from FIVE
test files and from NO command line (measured: ``grep -n across-bundle run.py``
= 0 hits). ``--across-bundle <dir>``, repeated once per base, is that door.
The engine takes a CALLBACK rather than an outbox directory. Its own docstring
has always said N runs need N ``run_id``s and that minting them there would
default a key this repo requires a caller to supply -- so ``outbox_for`` is that
contract KEPT, not relaxed, and the operator-chosen ``<run-id>-<bundle_id>``
rule lives in ``main()`` where the decision was made. The order's alternative (a
caller running ``run_project`` itself over ``route_by_bundle``'s sub-mandates)
would be a second copy of the loop's id reconciliation, shared store, per-base
project resolution, collision accounting and both budget teeth.
``resolve_bundle_routing`` is ONE resolution shared by the engine and the
dry-run arm: a free trip answering with a different project id, or tolerating a
duplicate id the paid dispatch refuses, would rehearse a different run.
``{run-id}-multibase.json`` is written from a ``finally`` and every row is built
from the resolution plus disk, so the pass a cap cut short still leaves the
record. ``completed`` is a required field for ``ExplorationTrace.completed``'s
reason. ``stop_reason`` is read BACK from each base's own coverage artefact.
Load-bearing MEASURED (17 arms), four mutations all red against the WHOLE suite,
green control 1761/5 (from 1744/5, superset, 0 removed), golden byte-unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
433 lines
20 KiB
Python
433 lines
20 KiB
Python
"""RAW output layer (Fase 2a, S2.1 · målbilde §3, R2): byte-deterministic outbox writers.
|
|
|
|
Two writers live here. ``write_run_config`` (S4.2, comparison protocol §4 pkt 3) persists the
|
|
run-config artefact ``{run_id}-runconfig.json`` — the resolved model-id per built role, profile,
|
|
params and token cap — describing a run WITHOUT a model call (the S4.2 dry-run drill + the M2 live
|
|
run). ``write_outbox`` persists the post-run proposal/outcome artefacts.
|
|
|
|
After a run, ``write_outbox`` persists two JSON artefacts — ``{run_id}-proposal.json`` (the candidate
|
|
IR + its provenance stamp) and ``{run_id}-outcome.json`` (the validated Monte-Carlo percentiles OR
|
|
the rejection reason, plus the checker verdict + the captured verdict id). This is the traceable
|
|
output layer Fase 5 (S5.1/S5.4) consumes; it is the OUTBOX, distinct from the async verdict INBOX
|
|
(``verdict_dir``) — a run must never write its outbox into a folder it also reads as an inbox
|
|
(that would bypass the Step-8 promotion gate; see ``run_project``'s docstring).
|
|
|
|
**MAF-free** (D7-portable): pure stdlib plus the MAF-free ``validator`` leaf for the outcome
|
|
``isinstance`` branch. ``ProvenanceStamp`` is imported ONLY under ``TYPE_CHECKING`` and serialized
|
|
duck-typed via ``.model_dump()`` — so importing this module never pulls in ``agent_framework``.
|
|
Registered in ``tests/test_okf.py``'s ``_MAF_FREE_MODULES`` and enforced by ``test_okf_is_maf_free``
|
|
(which proves no DIRECT ``agent_framework``/``mcp`` import — MAF-freedom otherwise holds by
|
|
construction, since only the MAF-free ``validator`` is imported at runtime).
|
|
|
|
Byte-determinism: ``json.dumps(payload, sort_keys=True, indent=2)`` + explicit trailing ``\\n`` +
|
|
UTF-8, and the caller supplies ``run_id`` (no wall-clock / uuid default) — so two runs with identical
|
|
input produce byte-identical files (diff-stable, mirrors ``verdicts.write_verdict`` /
|
|
``ledger.SavingsLedger.save``).
|
|
|
|
Deliberately NOT exported in ``portfolio_optimiser.__all__``: this is an internal wiring primitive
|
|
called by ``run_project``, not a public authoring API (contrast ``verdicts.write_verdict``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Mapping, Sequence
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from portfolio_optimiser.validator import Rejection, ValidatedProposal
|
|
|
|
if TYPE_CHECKING: # provenance imports agent_framework — keep it out of the runtime import graph
|
|
from portfolio_optimiser.provenance import ProvenanceStamp
|
|
|
|
|
|
def _dump(payload: dict[str, Any]) -> str:
|
|
"""Byte-deterministic on-disk form: sorted keys, 2-space indent, explicit trailing newline."""
|
|
return json.dumps(payload, sort_keys=True, indent=2) + "\n"
|
|
|
|
|
|
def write_outbox(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
outcome: ValidatedProposal | Rejection,
|
|
provenance: ProvenanceStamp,
|
|
checker_verdict: str | None,
|
|
verdict_id: str,
|
|
approach_id: str | None = None,
|
|
) -> tuple[Path, Path]:
|
|
"""Write ``{run_id}-proposal.json`` + ``{run_id}-outcome.json`` into ``outbox_dir`` (created if
|
|
needed) and return their paths. The proposal file carries the candidate IR + provenance; the
|
|
outcome file branches on the outcome type — a ``ValidatedProposal`` writes its percentiles, a
|
|
``Rejection`` writes its reason (and NO percentiles, mirroring the type distinction).
|
|
|
|
``approach_id`` (A5) names WHICH commissioned approach an artefact belongs to, so a run that
|
|
evaluated several can have each of them judged rather than only the one it selected. It widens
|
|
the file key to ``{run_id}-{approach_id}-*.json`` AND is written into the payload, because the
|
|
reader (``hitl._read_outbox_proposals``) joins proposal to outcome on file CONTENT, never on the
|
|
filename — a widened filename alone would still collapse three approaches onto one ``run_id``
|
|
key. When it is ``None`` the key and the payload are exactly as before: the field is OMITTED
|
|
rather than written as null, since these artefacts are byte-deterministic by contract and a run
|
|
nobody commissioned has no approach to name."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
stem = run_id if approach_id is None else f"{run_id}-{approach_id}"
|
|
keys: dict[str, Any] = {"run_id": run_id}
|
|
if approach_id is not None:
|
|
keys["approach_id"] = approach_id
|
|
|
|
proposal_path = directory / f"{stem}-proposal.json"
|
|
proposal_path.write_text(
|
|
_dump(
|
|
{
|
|
**keys,
|
|
"proposal": outcome.proposal.model_dump(),
|
|
"provenance": provenance.model_dump(),
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
outcome_path = directory / f"{stem}-outcome.json"
|
|
outcome_path.write_text(
|
|
_dump(
|
|
{
|
|
**keys,
|
|
**outcome_payload(outcome, checker_verdict=checker_verdict, verdict_id=verdict_id),
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return proposal_path, outcome_path
|
|
|
|
|
|
def outcome_payload(
|
|
outcome: ValidatedProposal | Rejection,
|
|
*,
|
|
checker_verdict: str | None,
|
|
verdict_id: str,
|
|
) -> dict[str, Any]:
|
|
"""The outcome artefact's payload minus the file keys — the ONE copy of the
|
|
validated/rejected branching, shared by ``write_outbox`` and the hosted invocations
|
|
response (``hosting._response_payload``). Two copies of the branch would drift, and a
|
|
drifted copy would let the HTTP surface describe an outcome the outbox never wrote —
|
|
the ``to_ore`` single-source rule (kø-(p)) applied to a payload shape."""
|
|
if isinstance(outcome, ValidatedProposal):
|
|
return {
|
|
"outcome_type": "validated",
|
|
"p10": outcome.p10,
|
|
"p50": outcome.p50,
|
|
"p90": outcome.p90,
|
|
"nominal_feasible": outcome.nominal_feasible,
|
|
"checker_verdict": checker_verdict,
|
|
"verdict_id": verdict_id,
|
|
}
|
|
return {
|
|
"outcome_type": "rejected",
|
|
"reason": outcome.reason,
|
|
"checker_verdict": checker_verdict,
|
|
"verdict_id": verdict_id,
|
|
}
|
|
|
|
|
|
def write_parse_failures(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
failures: Sequence[Mapping[str, str]],
|
|
) -> Path:
|
|
"""Write ``{run_id}-parse-failures.json`` — the raw model replies that did NOT parse into the
|
|
typed IR (Fase 1b, funn 1) — and return its path.
|
|
|
|
**This is the only outbox artefact written from a ``finally``**, because it is the only one whose
|
|
subject is a run that may never finish: the measured 1b failure exhausted the round ledger inside
|
|
the generation loop and left ``run_project`` as a ``BudgetExceeded``, so the proposal/outcome
|
|
writers below were never reached. An artefact that recorded parse failures only for runs that
|
|
survived them would be silent for exactly the runs that need it.
|
|
|
|
Takes plain mappings (the caller flattens ``generate.ParseFailure``), so this module stays
|
|
MAF-free — ``generate`` imports ``agent_framework``, and importing it here would drag MAF into
|
|
the RAW output layer.
|
|
|
|
Byte-determinism is NOT claimed for this file, unlike its two neighbours: its content is a live
|
|
model's prose, which is not reproducible by construction. It uses the same ``_dump`` form for
|
|
consistency of reading, not to pin bytes. The caller writes it only when there is at least one
|
|
failure, so the file's PRESENCE is itself the signal that something did not parse."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-parse-failures.json"
|
|
path.write_text(
|
|
_dump({"run_id": run_id, "parse_failures": [dict(f) for f in failures]}),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def write_exploration(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
payload: Mapping[str, Any],
|
|
) -> Path:
|
|
"""Write ``{run_id}-exploration.json`` — what the U4 exploration did before the pipeline ran
|
|
(§ C.2) — and return its path.
|
|
|
|
Takes an already-rendered plain mapping (``explore.trace_payload``) for the reason
|
|
``write_parse_failures`` takes plain mappings: ``explore`` imports ``agent_framework``, and
|
|
importing it here would drag MAF into the RAW output layer. The ONE renderer lives beside the
|
|
dataclasses it renders; this writer only decides bytes and a filename.
|
|
|
|
Byte-deterministic like its neighbours (the caller supplies ``run_id``; no wall-clock), and
|
|
written even when the exploration RAISED — the caller writes it from a ``finally``, because a
|
|
capped exploration is precisely the one whose per-round ledger a reader needs."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-exploration.json"
|
|
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_debate_tools(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
tool_calls: Sequence[Mapping[str, Any]],
|
|
requirements: Sequence[Mapping[str, Any]] = (),
|
|
) -> Path:
|
|
"""Write ``{run_id}-debate.json`` — WHICH documents the debate opened, in call order (S2c), and
|
|
WHICH requirement it declared as binding (P19 DEL A).
|
|
|
|
The sibling of ``write_exploration`` one phase over. Since S2c the debate navigates the
|
|
knowledge base instead of being handed it whole, so "what did this run actually read" is a
|
|
question about the debate too, and over a 630-concept corpus it is not answerable from a
|
|
prompt log: the whole point is that the prompts no longer carry the base.
|
|
|
|
Written on EVERY run that has an outbox, including one whose agents opened nothing — unlike
|
|
``write_parse_failures``, whose presence IS its signal. Here the empty case is the S2c
|
|
regression itself (a debate that navigates nothing looks exactly like a cheap one), so it must
|
|
be readable off the artefact rather than inferred from a file that is not there.
|
|
|
|
Takes already-rendered plain mappings (``explore.tool_call_payload``) so the RAW output layer
|
|
stays MAF-free — ``write_exploration``'s own rule, same reason."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-debate.json"
|
|
path.write_text(
|
|
_dump(
|
|
{
|
|
"run_id": run_id,
|
|
"tool_calls": [dict(call) for call in tool_calls],
|
|
# DEFAULTS to empty for the same reason ``Bundle.skipped`` does: "this debate
|
|
# declared nothing" is an honest positive statement, and it is the one every run
|
|
# written before today makes.
|
|
"requirements": [dict(r) for r in requirements],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def write_multibase(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
runs: Sequence[Mapping[str, Any]],
|
|
completed: bool,
|
|
unreached: Sequence[Mapping[str, Any]],
|
|
collisions: Sequence[Mapping[str, Any]],
|
|
stopped_early: bool,
|
|
budget_stop: Mapping[str, Any] | None,
|
|
) -> Path:
|
|
"""Write ``{run_id}-multibase.json`` — what ONE commission did across SEVERAL bases (P17b).
|
|
|
|
The question no per-base artefact can answer. Each base writes its own full set under its own
|
|
minted ``run_id``, but nothing in that set says in which ORDER the bases were spent, which id
|
|
each one was given, which approaches were never reached, or which candidates two bases both
|
|
described — and a reader who has to reconstruct the ``<run-id>-<bundle_id>`` convention to
|
|
pair the files back to the pass has been handed a naming rule instead of a record.
|
|
|
|
Written IFF the pass was given an outbox, exactly like its neighbours, and the per-base
|
|
``stop_reason`` rows are READ BACK from each base's own ``{run_id}-coverage.json`` by the
|
|
caller rather than recomputed here: P19 D2 put that fact in that file, and a second derivation
|
|
of it would be free to disagree with the one the judge reads.
|
|
|
|
Plain data only, so the RAW output layer stays MAF-free (``write_debate_tools``' own rule)."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-multibase.json"
|
|
path.write_text(
|
|
_dump(
|
|
{
|
|
"run_id": run_id,
|
|
# REQUIRED, never inferred from an empty ``unreached``: a pass a cap or a provider
|
|
# cut short never got to say what it did not reach, and "nothing was left
|
|
# unreached" must not be the value that means "we never found out"
|
|
# (``ExplorationTrace.completed``'s own reason).
|
|
"completed": completed,
|
|
"runs": [dict(row) for row in runs],
|
|
"unreached": [dict(row) for row in unreached],
|
|
"collisions": [dict(row) for row in collisions],
|
|
"stopped_early": stopped_early,
|
|
"budget_stop": dict(budget_stop) if budget_stop is not None else None,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def write_prepass(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
declaration: Mapping[str, Any],
|
|
) -> Path:
|
|
"""Write ``{run_id}-prepass.json`` — the CUT this run was given (order 20260907T080223Z).
|
|
|
|
Written IFF a payload was supplied, including when the cut withheld nothing —
|
|
``write_proposal_reviews``' rule, and here it does a second job. Since a payload WITHDRAWS the
|
|
navigator tools, ``{run_id}-debate.json``'s ``tool_calls`` is empty by construction on this
|
|
path; and that file is written unconditionally precisely because an empty trace IS the S2c
|
|
regression (a debate that navigated nothing looks exactly like a cheap one). Without this
|
|
artefact beside it, "withdrawn by design" and "regressed" would read identically. Its PRESENCE
|
|
is what tells them apart.
|
|
|
|
Takes an already-rendered plain mapping (``prepass.declaration_payload``) so the RAW output
|
|
layer stays framework-free — ``write_exploration``'s rule, same reason."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-prepass.json"
|
|
path.write_text(_dump({"run_id": run_id, "prepass": dict(declaration)}), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_plan_review(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
payload: Mapping[str, Any],
|
|
) -> Path:
|
|
"""Write ``{run_id}-plan-review.json`` — the open question of a PARKED exploration (U12) — and
|
|
return its path.
|
|
|
|
This is the outbox half of the asynchronous HITL door: the run writes the question, the expert
|
|
writes the answer into a separate review INBOX, days later. The two folders are never the same
|
|
one, for the reason the verdict inbox is never the outbox — a run that read its own output as
|
|
input would be answering itself.
|
|
|
|
Takes an already-rendered plain mapping (``explore.parked_payload``) for the reason
|
|
``write_exploration`` does: ``explore`` imports ``agent_framework`` and this layer stays
|
|
MAF-free. Byte-deterministic like its neighbours.
|
|
|
|
**Last write wins**, exactly one open question per run: a revision produces a NEW review of a
|
|
REPLANNED plan, and leaving the superseded one on disk would let an expert answer a question
|
|
the loop has already moved past. Staleness is caught anyway — the answer names the
|
|
``request_id`` it answers — but the file should not invite it."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-plan-review.json"
|
|
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_proposal_reviews(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
payload: Mapping[str, Any],
|
|
) -> Path:
|
|
"""Write ``{run_id}-proposal-reviews.json`` — what a human answered about the proposals this
|
|
run put on the table (MAJOR-2) — and return its path.
|
|
|
|
Takes an already-rendered plain mapping (``proposal_review.proposal_reviews_payload``) for the
|
|
reason ``write_plan_review`` does: the renderer lives in the module that owns the type, and
|
|
this layer stays MAF-free. The writer prepends ``run_id`` exactly as its sibling does, so the
|
|
on-disk object is ``{"reviews": [...], "run_id": ...}`` under ``_dump``'s sorted keys.
|
|
|
|
**The write rule is: iff a reviewer was given, INCLUDING when the list is empty** (D4).
|
|
Both halves are load-bearing and they pull in opposite directions. ``write_debate_tools``
|
|
writes unconditionally because THERE the empty case is the regression; here a reviewer-LESS
|
|
run must leave the outbox byte-identical, and two existing tests pin an exact four-name
|
|
listing on such a run. But a reviewer that was offered and never consulted — no candidate ever
|
|
validated — is a fact this artefact must be able to STATE, not something an operator has to
|
|
infer from a missing file. "Iff a reviewer was given" is the only rule that keeps both."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-proposal-reviews.json"
|
|
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def write_coverage(
|
|
outbox_dir: str,
|
|
run_id: str,
|
|
*,
|
|
rows: Sequence[Mapping[str, Any]],
|
|
stop_reason: str,
|
|
) -> Path:
|
|
"""Write ``{run_id}-coverage.json`` — WHY each commissioned approach ended as it did (P19 D2).
|
|
|
|
``settle`` prints the coverage report and ``ApproachOutcome`` has carried ``not_evaluated``
|
|
since Trekk A3, but neither ever reached a FILE: measured 14.09, a judge reading an outbox could
|
|
see that an approach had no artefact and could not tell a budget stop from an approach nobody
|
|
ordered. That is the very silence ``ApproachOutcome`` exists to remove, one layer out.
|
|
|
|
``stop_reason`` is ``BudgetExceeded.kind`` when a cap cut the run short (``tokens`` /
|
|
``rounds`` / the portfolio's own kinds) and ``""`` when nothing did. A REQUIRED argument rather
|
|
than an inferred one, for ``cost_baseline_anchored``'s reason: "the run finished" and "we never
|
|
found out" must not be the same value.
|
|
|
|
Byte-deterministic and wall-clock-free, mirroring ``write_run_config``; plain data only, so the
|
|
RAW output layer stays MAF-free."""
|
|
directory = Path(outbox_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-coverage.json"
|
|
path.write_text(
|
|
_dump(
|
|
{
|
|
"run_id": run_id,
|
|
"stop_reason": stop_reason,
|
|
"rows": [dict(row) for row in rows],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def write_run_config(
|
|
config_dir: str,
|
|
run_id: str,
|
|
*,
|
|
profile: str,
|
|
resolved_models: dict[str, str],
|
|
max_rounds: int,
|
|
max_tokens: int,
|
|
top_k: int,
|
|
) -> Path:
|
|
"""Write the byte-deterministic run-config artefact ``{run_id}-runconfig.json`` (S4.2, comparison
|
|
protocol §4 pkt 3): the resolved model-id per *built* role, the profile, the round/token
|
|
parameters and the token cap — everything that describes a run WITHOUT a model call. Takes plain
|
|
data only (the caller resolves the models via ``resolve_model``), so this module stays MAF-free.
|
|
NO wall-clock / date (that lives in the S11 report envelope, not the deterministic artefact), so
|
|
two runs with identical config produce byte-identical files (mirrors ``write_outbox``)."""
|
|
directory = Path(config_dir)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = directory / f"{run_id}-runconfig.json"
|
|
path.write_text(
|
|
_dump(
|
|
{
|
|
"run_id": run_id,
|
|
"profile": profile,
|
|
"resolved_models": resolved_models,
|
|
"max_rounds": max_rounds,
|
|
"max_tokens": max_tokens,
|
|
"top_k": top_k,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
return path
|