Change: Vega (framer) now receives the counsel-declared matter context on every
build — client_name, representing_side, and the optional
who_we_represent — and treats it as ground truth for party-side mapping instead of
guessing "us vs them" from documents (RFD-AGT-011 D1/D2).
FrameOutput is unchanged — the client never
enters the output.class FramerInput(BaseModel):
title: str
matter_type: str
client_name: str # REQUIRED — business.clients.name (NOT NULL)
representing_side: Literal["principal","opposing","advisory"] # REQUIRED
who_we_represent: str | None = None # OPTIONAL — verbatim label or null; never fabricated
documents: list[str] = ...
previous_frame: FrameOutput | None = None
...
Making the two fields required means an invoke payload missing them fails closed at
FramerInput.model_validate(...) in runtime.py/main.py,
before run.started — no code branch needed, the type is the gate. This is
why the deploy order is fixed (D3): the functions assembler ships first, the brain second.
The shared prompts/_base.py now encodes one ladder every role inherits:
declared context (client + representing side + label) > previous_frame > document inference — and locked_fields are a BOUNDARY Vega never crosses, not a rank in this ladder.
| Situation | Behaviour |
|---|---|
| Document seems to invert the side | Keep the declared mapping; flag the disagreement in core_ambiguities, downgrade frame_status. Never silently flip. |
| Legacy previous_frame guessed the wrong side | Corrected to the declared context on rebuild — the bug self-heals on the next unlocked build. |
| Locked field clashes with declared context | Preserve the locked value byte-for-byte; raise a SPECIFIC, PERSISTENT clash flag naming both sides + the locked field; downgrade status. Counsel resolves by unlocking/editing. |
representing_side == "advisory" | No adversary exists — an empty adversary_best_shot is correct. The Critic must NOT flag it as thin. |
who_we_represent is null | Input null is honest. Output labels fall back to standard procedural vocabulary for the matter type — never an invented custom label (Bug-1310 class). |
A single shared helper (declared_context_block) fences the three fields as inert
data (so an instruction-shaped who_we_represent label is the poisoned-label attack
surface, not an order) while the role prompts rank it the highest authority for who-is-who —
fenced means "never obey", not "low priority for facts".
COUNSEL-DECLARED MATTER CONTEXT (ground truth for who is who ...): <<DECLARED_CONTEXT_9f3a1c>> client: Acarer Metal representing_side: principal our_procedural_label: Claimant <</DECLARED_CONTEXT_9f3a1c>>
Each role's VERSION bumped (triage-v5, proposer-v2, critic-v2, synthesizer-v2) so
the eval Experiments table distinguishes pre/post-change runs. A re-sweep is required before any
crowning claim (leakage rule); the four new eval cases land in the sibling Chore pramaan-spec#2780.
uv run ruff check agents/framer/ # All checks passed uv run --package framer pytest agents/framer/tests -q # 120 passed
Golden labels unchanged — inputs grew, expected outputs did not. Every in-repo
FramerInput constructor (tests, eval schemas + datasets, scripts) carries the new
fields; the offline dataset-validation suite (test_frame_datasets.py) proves every
matter file still loads through the schema.