feat(persona): build the shared expert-reviewer persona as a framework-neutral Agent Skill
The expert reviewer was only a hardcoded verdict_input dict inside the offline simulation. Build it as the real, shared artifact target picture §8 calls for: shared/skills/expert-reviewer/ — a SKILL.md persona prompt (energy-advisor / M&V role + the realization-gap methodology the validator cannot compute) plus a canonical references/example-verdict.json. shared/ stays pure data; the MAF side reads it via portfolio_optimiser.persona.load_persona_example (call-time, fail-fast) and the Claude-SDK sibling reads the same JSON with its own loader. This de-stubs the simulation: its persona judgement (decision + rationale + traced marker) is now sourced from the artifact at call time, not an inline literal — so the shared persona is genuinely consumed and cannot rot silently. decision is binary (approved/rejected, the FeedbackContract the run path accepts); approved_with_adjustment is rejected there and lives only in the bundle seed frontmatter + the promotion gate, so the realization correction is carried in the rationale prose. Load-bearing trio (tests/test_persona_skill_loadbearing.py), each proven RED on its own detach: structure + framework-neutrality, the example is valid pipeline input (incl. FeedbackContract, on a throwaway copy), and the simulation's marker follows the artifact file. Suite 149->152. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
This commit is contained in:
parent
a9144cb9bb
commit
6f861a0078
8 changed files with 281 additions and 13 deletions
|
|
@ -36,6 +36,7 @@ from agent_framework import (
|
|||
)
|
||||
from agent_framework_openai import OpenAIChatCompletionClient
|
||||
|
||||
from portfolio_optimiser.persona import load_persona_example
|
||||
from portfolio_optimiser.run import RunResult, run_project
|
||||
from portfolio_optimiser.validator import ValidatedProposal
|
||||
from portfolio_optimiser.verdicts import VerdictStore, promote_verdict, seed_store_from_bundle
|
||||
|
|
@ -52,14 +53,10 @@ _VALID_PROPOSAL = (
|
|||
# The checker's debate turn ends with the gate marker the run parses (run._checker_verdict).
|
||||
_CHECKER_APPROVE = "Tallene er innenfor feasibelt område og resonnementet holder. VERDICT: APPROVE"
|
||||
|
||||
# The persona's verdict: an APPROVE that carries NEW realization knowledge the validator cannot
|
||||
# compute. The marker (a realization rate ABSENT from the bundle — the seed is 0.82) is the payload
|
||||
# we trace from Run A's persona judgement, through promotion, into Run B's hypothesis prompt.
|
||||
_DEFAULT_MARKER = "realiseringsgrad=0.79"
|
||||
_DEFAULT_PERSONA_RATIONALE = (
|
||||
"Godkjent. I drift realiseres erfaringsvis ~79% av en timeplan-stipulert LED-besparelse i "
|
||||
f"kontorbygg ({_DEFAULT_MARKER}) pga overestimerte driftstimer; forventet faktisk ca 23700 NOK/aar."
|
||||
)
|
||||
# The persona's verdict is sourced from the shared expert-reviewer skill (``load_persona_example``),
|
||||
# NOT inlined here — that de-stubs the persona and makes the shared artifact genuinely consumed. Its
|
||||
# marker (a realization rate ABSENT from the bundle — the seed is 0.82) is the payload we trace from
|
||||
# Run A's persona judgement, through promotion, into Run B's hypothesis prompt.
|
||||
|
||||
|
||||
class ScriptedChatClient(OpenAIChatCompletionClient):
|
||||
|
|
@ -148,19 +145,29 @@ async def simulate_learning_loop(
|
|||
bundle_dir: str,
|
||||
work_dir: str,
|
||||
*,
|
||||
persona_rationale: str = _DEFAULT_PERSONA_RATIONALE,
|
||||
marker: str = _DEFAULT_MARKER,
|
||||
persona_rationale: str | None = None,
|
||||
marker: str | None = None,
|
||||
timestamp: str = "2026-06-30",
|
||||
max_rounds: int = 3,
|
||||
) -> LearningSimulationResult:
|
||||
"""Run the loop twice on a throwaway COPY of the bundle (the shared fixture is never mutated),
|
||||
with a promotion in between, and trace whether the persona's approved knowledge crosses runs.
|
||||
|
||||
The persona's verdict (decision + rationale + traced ``marker``) defaults to the shared
|
||||
expert-reviewer skill's canonical example (``load_persona_example``), read at CALL time — so the
|
||||
simulation is genuinely artifact-driven, not inlined. Callers may override ``marker`` /
|
||||
``persona_rationale`` for a control.
|
||||
|
||||
Run A: a fresh (empty) wiki -> an uninformed hypothesis; the persona approves with NEW realization
|
||||
knowledge (``marker`` in ``persona_rationale``). ``promote_verdict`` lifts that verdict into the
|
||||
wiki; ``seed_store_from_bundle`` re-reads the wiki; Run B's Step-1 ExpeL fold then carries the
|
||||
marker into its hypothesis prompt. The two runs use SEPARATE sinks so each prompt set is
|
||||
inspected independently."""
|
||||
example = load_persona_example()
|
||||
if marker is None:
|
||||
marker = example.marker
|
||||
if persona_rationale is None:
|
||||
persona_rationale = example.rationale
|
||||
if marker not in persona_rationale:
|
||||
raise ValueError("marker must be a substring of persona_rationale (the carried payload)")
|
||||
|
||||
|
|
@ -168,7 +175,7 @@ async def simulate_learning_loop(
|
|||
shutil.copytree(bundle_dir, copy)
|
||||
copy_s = str(copy)
|
||||
replies = {"proposer": _VALID_PROPOSAL, "checker": _CHECKER_APPROVE}
|
||||
verdict_input = {"decision": "approved", "rationale": persona_rationale}
|
||||
verdict_input = {"decision": example.decision, "rationale": persona_rationale}
|
||||
|
||||
# Run A — empty wiki isolates the persona's NEW knowledge.
|
||||
sink_a: list[str] = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue