portfolio-optimiser-claude/src/portfolio_optimiser_claude/run_s10.py
Kjell Tore Guttormsen 0238507df4 feat(run): S10 del 1 — run-lag: §9-citations, artefakt-persistens, SDK-klient
TDD offline (RØD bekreftet før implementasjon): resolve_model (rolle->modell-id,
ukjent profil feiler fail-fast), build_citations (eksakte char-spans, verdict-
ekskludering, uncitable kontekst -> raise FØR spend), persist_run_artifacts
(deterministiske bytes; validator/checker-avgjørelser speilet VERBATIM fra
RunResult — §9 non-konflatering). Run-path-only, aldri importert av tester:
SdkModelClient (claude-agent-sdk 0.2.110 verifisert mot installert pakke;
max_turns=1, tools=[], max_budget_usd per kall; manglende usage -> None så
§8-meteret feiler lukket) + run_s10 (kontrakter FØR klient §10, BudgetExceeded
som strukturert stopp). 178/178 uten nøkkel; ruff+mypy --strict rene; fire
detach-bevis røde -> revertert grønne. Live-kjøringen gjenstår (credential-gated).

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

133 lines
5.5 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
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.
print(f"STOPPED by budget: {stop.kind} observed {stop.observed} > limit {stop.limit}")
print(f"cost so far: {client.total_cost_usd:.6f} USD")
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())