portfolio-optimiser-claude/src/portfolio_optimiser_claude/run_s10.py
Kjell Tore Guttormsen 7637c6feae fix(run): S10 del 2 — post-mortem: stopp-artefakt, SDK-isolasjon, raw-JSON-direktiv
Transkript-analyse av den stoppede live-kjøringen (10 kall, 162 250 tokens,
$0.331506): konfig-lekkasjen (setting_sources=None laster ALLE filsystem-
settings) injiserte operatørens Claude-konfig i hvert kall — ~10-15k uncachede
tokens, en påtvunget bekreftelses-preamble som gjorde ren-JSON-svar umulige,
og en checker kapret av lekkede instrukser (debatt konvergerte aldri).

- persist_stop_artifacts: stopp-event verbatim + usage/kost persisteres ALLTID
  ved BudgetExceeded (delt usage-shape med fullført-run-stien)
- build_call_options: setting_sources=[] (SDK isolation mode, verifisert mot
  installert 0.2.110-kilde), system_prompt=None → tom system-prompt; detach-
  bevis via monkeypatchet query
- _generation_prompt: krever ONLY the raw JSON object (fence-innpakning ga
  4 fullpris parse-retries)

187/187 uten nøkkel · ruff + mypy --strict rene · tre detach-bevis RØDE → grønn

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 10:49:51 +02:00

148 lines
6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""S10 — the ONE live model run of the programme (D6). RUN-PATH ONLY.
Wires the offline-proven pipeline (S6S9) to the real Claude Agent SDK client
for a single controlled run on the micro-bundle: startup contracts validated
fail-fast BEFORE the client is built (§10), citations proven citable BEFORE any
spend (§9), the read-context navigated + experience-folded exactly as the
offline suite proves (§3 Step 1), the loop bounded by the §8 meter AND the
per-call USD cap, and the captured artifacts persisted for S11 with the cost
logged. Never imported by the test suite.
Run: uv run python -m portfolio_optimiser_claude.run_s10
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from portfolio_optimiser_claude.artifacts import (
build_citations,
persist_run_artifacts,
persist_stop_artifacts,
)
from portfolio_optimiser_claude.budget import BudgetExceeded, BudgetMeter
from portfolio_optimiser_claude.contracts import load_contracts
from portfolio_optimiser_claude.experience import (
CandidateFeatures,
VerdictStore,
fold_experience,
seed_store_from_bundle,
)
from portfolio_optimiser_claude.ir import load_validator_input
from portfolio_optimiser_claude.loop import run_project
from portfolio_optimiser_claude.okf import bundle_context, navigate_bundle
from portfolio_optimiser_claude.provenance import Provenance
from portfolio_optimiser_claude.sdk_client import SdkModelClient
from portfolio_optimiser_claude.validator import Rejection
_DEFAULT_BUNDLE = Path(__file__).resolve().parents[2] / "shared" / "examples" / "bygg-energi-mikro"
_PROPOSER_ROLE = "proposer"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="S10: the single live run (hard caps, D6).")
parser.add_argument("--bundle", type=Path, default=_DEFAULT_BUNDLE)
parser.add_argument("--out", type=Path, default=Path("runs") / "s10")
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)
parser.add_argument("--max-debate-rounds", type=int, default=3)
parser.add_argument("--max-attempts", type=int, default=3)
parser.add_argument("--top-k", type=int, default=3)
args = parser.parse_args(argv)
if not os.environ.get("ANTHROPIC_API_KEY"):
# The bundled CLI resolves its own credentials (Claude Code login) when
# no key is exported; an unauthenticated run fails on the FIRST call.
print("note: ANTHROPIC_API_KEY not set — relying on the CLI's own credentials.")
# §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},
termination={"max_rounds": args.max_rounds, "max_tokens": args.max_tokens},
feedback={"decision": "approved", "rationale": "startup shape check (§10)"},
)
# §9: prove the context citable BEFORE any spend; §3 Step 1: navigate + fold.
concepts = navigate_bundle(args.bundle)
citations = build_citations(concepts)
ir_projection = load_validator_input(args.bundle)
store = VerdictStore()
seeded = seed_store_from_bundle(store, args.bundle)
context = fold_experience(
store,
CandidateFeatures.from_proposal(ir_projection),
bundle_context(args.bundle),
contracts.data_source.top_k,
)
meter = BudgetMeter(contracts.termination)
client = SdkModelClient(
contracts.model_map, max_budget_usd_per_call=args.max_budget_usd_per_call
)
print(
f"S10 live run: bundle={args.bundle.name} seeded_verdicts={seeded} "
f"caps: max_rounds={args.max_rounds} max_tokens={args.max_tokens} "
f"max_budget_usd_per_call={args.max_budget_usd_per_call}"
)
try:
result = run_project(
client,
context,
meter=meter,
max_debate_rounds=args.max_debate_rounds,
max_attempts=args.max_attempts,
default_project_id=ir_projection.project_id,
)
except BudgetExceeded as stop:
# §8: the structured stop event — report it, never a silent hang, and
# persist the spend (the first live run stopped here with NO record).
print(f"STOPPED by budget: {stop.kind} observed {stop.observed} > limit {stop.limit}")
print(f"cost so far: {client.total_cost_usd:.6f} USD")
stop_paths = persist_stop_artifacts(
args.out,
stop=stop,
termination=contracts.termination,
tokens_used=meter.tokens_used,
rounds_used=meter.rounds_used,
cost_usd=round(client.total_cost_usd, 6),
)
for name, path in sorted(stop_paths.items()):
print(f"artifact: {name} -> {path}")
return 3
provenance = Provenance(
citations=citations,
model=client.last_model or "unknown", # §9: the REAL id, neutral fallback
role=_PROPOSER_ROLE,
validator_decision=result.validator_decision,
tokens_used=meter.tokens_used,
)
paths = persist_run_artifacts(
args.out,
run=result,
provenance=provenance,
termination=contracts.termination,
tokens_used=meter.tokens_used,
rounds_used=meter.rounds_used,
cost_usd=round(client.total_cost_usd, 6),
)
outcome_kind = "rejected" if isinstance(result.outcome, Rejection) else "validated"
print(
f"result: validator={result.validator_decision} checker={result.checker_decision} "
f"attempts={result.attempts} outcome={outcome_kind}"
)
print(
f"usage: tokens={meter.tokens_used}/{contracts.termination.max_tokens} "
f"rounds={meter.rounds_used}/{contracts.termination.max_rounds}"
)
print(f"model: {client.last_model} cost: {client.total_cost_usd:.6f} USD")
for name, path in sorted(paths.items()):
print(f"artifact: {name} -> {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())