Spec §3 steps 2–5 + §8, TDD-ed offline (scripted, honesty-marked stand-in): - budget.py: BudgetMeter over TerminationContract — provider-reported usage only (missing usage fails closed), structured BudgetExceeded stop event. - loop.py: ModelClient protocol; blind parse-retry generation (never silent repair); round-capped debate with turn safety net and mandated VERDICT line; opt-in-reject checker gate (explicit REJECT overrides a validated outcome, validator rejection stands); most-recent-reason-verbatim informed refinement under max_attempts; validator_decision stamped BEFORE override, checker_decision as its own result field (§9, never conflated). - 45 new tests (121 total, no API key); four detach proofs run RED and reverted green: checker override, informed block, surfaced checker output, stamp-before-override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
53 lines
2 KiB
Python
53 lines
2 KiB
Python
"""SCRIPTED model-client stand-in for the offline test suite.
|
|
|
|
Honesty rule (method-spec §1): this is a scripted stand-in, NOT a model. It
|
|
replays canned replies (or computes them via a prompt-sensitive script
|
|
function) deterministically, records every call, and never touches a network
|
|
or an API key. The ONE genuine model run in the programme is S10.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
from portfolio_optimiser_claude.loop import ModelReply
|
|
|
|
ScriptFn = Callable[[str, str], ModelReply]
|
|
|
|
|
|
class ScriptedClient:
|
|
"""Deterministic ``ModelClient`` stand-in — canned replies, recorded calls.
|
|
|
|
Either ``replies`` (a FIFO of ``ModelReply``) or ``script`` (a function of
|
|
``(role, prompt)`` — prompt-sensitive, for load-bearing flip proofs) must
|
|
be given. Every call is recorded as ``(role, prompt)`` in ``calls``.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
replies: list[ModelReply] | None = None,
|
|
script: ScriptFn | None = None,
|
|
) -> None:
|
|
if (replies is None) == (script is None):
|
|
raise ValueError("give exactly one of 'replies' or 'script'")
|
|
self._replies = list(replies) if replies is not None else None
|
|
self._script = script
|
|
self.calls: list[tuple[str, str]] = []
|
|
|
|
def complete(self, prompt: str, *, role: str) -> ModelReply:
|
|
self.calls.append((role, prompt))
|
|
if self._script is not None:
|
|
return self._script(role, prompt)
|
|
assert self._replies is not None
|
|
if not self._replies:
|
|
raise AssertionError("scripted client exhausted: no reply left for this call")
|
|
return self._replies.pop(0)
|
|
|
|
def prompts(self, role: str) -> list[str]:
|
|
"""The recorded prompts sent to ``role``, in call order."""
|
|
return [prompt for r, prompt in self.calls if r == role]
|
|
|
|
|
|
def reply(text: str, usage_tokens: int | None = 10) -> ModelReply:
|
|
"""Shorthand for a scripted ``ModelReply`` with a small default usage."""
|
|
return ModelReply(text=text, usage_tokens=usage_tokens)
|