feat(portfolio): K5 — outbox persistence, run_id-named pairs (parity row 7) [skip-docs]
S2.1-analog: each completed run persists a run_id-named proposal/outcome pair to the outbox — the system's OWN output layer (the role split §3 Step 7 governs the inbox and wiki, not this). The outcome carries outcome type + figures, the two §9 falsifiers mirrored verbatim from the RunResult, the provenance stamp, and verdict_id — minted the SAME way inbox.py mints an expert verdict's id (mint_verdict_id over the proposal's candidate features), so a later inbox verdict about the same candidate joins by id (the K9 key assumption, pinned here in test and reused there). New outbox.py reuses artifacts' deterministic house JSON writer (sorted keys / indent 2 / LF) — same input + same run_id => byte-identical files — and never touches the S10 artifacts.py fasit formats. run.py grows optional --outbox/--run-id; run_id is REQUIRED when the outbox is set (no wall-clock default — a timestamp would break determinism) and is fail-fasted at the CLI BEFORE any client/spend. A budget stop has no proposal, so it writes no pair. New test_outbox_loadbearing.py (14): unit (pair, verdict_id join key on both outcome types, percentiles-vs-reason, verbatim falsifiers, provenance, round-trip, byte-determinism, run_id fail-fast) + wiring (entrance writes the pair; no-outbox control; --outbox-without-run-id fails fast before spend). Detach-proved: drop the persist_outbox call in execute_run -> wiring test RED. 443->457 green, golden byte-exact (13/13), run_s10.py/runs/ untouched, full gate clean (ruff+format+mypy strict). README synced (count + module + seam). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
This commit is contained in:
parent
613b00f882
commit
a926e4ad46
4 changed files with 378 additions and 3 deletions
|
|
@ -28,6 +28,7 @@ from portfolio_optimiser_claude.artifacts import (
|
|||
persist_run_artifacts,
|
||||
persist_stop_artifacts,
|
||||
)
|
||||
from portfolio_optimiser_claude.outbox import persist_outbox
|
||||
from portfolio_optimiser_claude.budget import BudgetExceeded, BudgetMeter
|
||||
from portfolio_optimiser_claude.contracts import Contracts, load_contracts
|
||||
from portfolio_optimiser_claude.experience import (
|
||||
|
|
@ -124,12 +125,19 @@ def execute_run(
|
|||
out_dir: Path,
|
||||
max_debate_rounds: int,
|
||||
max_attempts: int,
|
||||
outbox_dir: Path | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> int:
|
||||
"""Drive the loop under the §8 meter; persist artifacts on BOTH outcomes.
|
||||
|
||||
Exit 0: the run completed (validated or typed rejection) and its artifacts
|
||||
are on disk. Exit 3: a structured budget stop (§8) — the stop event and
|
||||
the usage-vs-caps artifact are persisted; a stop is never a silent hang.
|
||||
|
||||
When ``outbox_dir`` is set, the completed run also persists a ``run_id``-named
|
||||
proposal/outcome pair to the outbox (S2.1) — the system's own output layer,
|
||||
read by K8 (live capture) and K9 (pending tracking). A budget stop has no
|
||||
proposal, so it writes no outbox pair.
|
||||
"""
|
||||
meter = BudgetMeter(contracts.termination)
|
||||
try:
|
||||
|
|
@ -178,6 +186,12 @@ def execute_run(
|
|||
)
|
||||
for name, path in sorted(paths.items()):
|
||||
print(f"artifact: {name} -> {path}")
|
||||
if outbox_dir is not None:
|
||||
outbox_paths = persist_outbox(
|
||||
outbox_dir, run=result, provenance=provenance, run_id=run_id or ""
|
||||
)
|
||||
for name, path in sorted(outbox_paths.items()):
|
||||
print(f"outbox: {name} -> {path}")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -189,6 +203,8 @@ def main(argv: list[str] | None = None, *, client_factory: ClientFactory | None
|
|||
parser.add_argument("--bundle", type=Path, required=True)
|
||||
parser.add_argument("--inbox", type=Path, default=None)
|
||||
parser.add_argument("--out", type=Path, default=Path("runs") / "run")
|
||||
parser.add_argument("--outbox", type=Path, default=None)
|
||||
parser.add_argument("--run-id", type=str, default=None)
|
||||
parser.add_argument("--max-rounds", type=int, default=12)
|
||||
parser.add_argument("--max-tokens", type=int, default=150_000)
|
||||
parser.add_argument("--max-budget-usd-per-call", type=float, default=0.25)
|
||||
|
|
@ -197,6 +213,12 @@ def main(argv: list[str] | None = None, *, client_factory: ClientFactory | None
|
|||
parser.add_argument("--top-k", type=int, default=3)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# fail-fast (§10 spirit): a run persisted to the outbox MUST carry an explicit
|
||||
# run_id — reject BEFORE composing or constructing a client, so no spend rides
|
||||
# on a run that cannot be filed (no wall-clock default fills the gap).
|
||||
if args.outbox is not None and not (args.run_id or "").strip():
|
||||
parser.error("--outbox requires --run-id (no wall-clock default)")
|
||||
|
||||
# §10: ALL startup contracts schema-validated BEFORE any model client exists.
|
||||
contracts = load_contracts(
|
||||
data_source={"docs_dir": str(args.bundle), "top_k": args.top_k},
|
||||
|
|
@ -222,6 +244,8 @@ def main(argv: list[str] | None = None, *, client_factory: ClientFactory | None
|
|||
out_dir=args.out,
|
||||
max_debate_rounds=args.max_debate_rounds,
|
||||
max_attempts=args.max_attempts,
|
||||
outbox_dir=args.outbox,
|
||||
run_id=args.run_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue