Blake learns to label the record

A document carries two lifecycle identities (EPIC-1876): an annexure label the drafter sets at filing ("Annexure A"), and an exhibit number + party role the tribunal sets at the hearing ("C-1", "R-2", "Respondent No. 2"). Story-1883 teaches Blake — PRAMAAN's Document Registrar — to read a matter's documents and propose all three per jurisdiction. He proposes; the broker writes the marking under RLS + audit, attributing it to the agent run. He never admits, never labels a privileged document, and never guesses.
Story-1883 · EPIC-1876 · RFD-DOC-003 D4/D7 · depends on #1878 (the junction table) · pramaan-docs ADR-128 (truth boundary) · ADR-117 (Pydantic AI soul) · ADR-124 (manual spans) · repo pramaan-agents · branch issue-1883-blake-labeling-pass

The write target — documents.document_exhibit_marking — already exists (Story-1878, merged): one row per marking event, firm-RLS'd, with exhibit_label, exhibit_party_role + exhibit_party_role_canonical, a lifecycle_status CHECK in (marked, admitted, rejected), and a privilege trigger that blocks a marking on a privileged document. Story-1883 is the AGENT half: Blake reasons over each document and emits typed proposals the broker turns into rows. Blake holds no DB credentials and calls no tool (ADR-128); his brain stays config + soul.

The naive fix that detonates: "let Blake fill in a marking row and write it." That puts schema-of-truth in the brain, lets a model set lifecycle_status='admitted' (a tribunal act), and forces a whole-document verdict — so one uncertain attribute would block the two confident ones. The shape that holds: three states PER ATTRIBUTE (proposed · open-question · accepted), proposals as typed intents the broker executes, and two guarantees Blake's code can never break (DR-1 never admits/rejects, DR-2 never labels privilege).

1 · Blake = the registrar agent; labeling = a new reasoning pass

"Blake" is agents/registrar/ — he already does classify and govern. Labeling is a third capability built the canonical way (ADR-117): a hand-owned Pydantic AI agent over a FLAT generation DTO (Bug-1357 — DeepSeek drops deep required fields), an output FUNCTION that assembles the rich turn, and a completeness validator that fails-toward-retry.

# agents/registrar/agent.py
_label_agent = Agent(name=BLAKE.display_name,
    output_type=PromptedOutput(_assemble_label_turn),
    system_prompt=LABEL_SYSTEM, defer_model_check=True)
_label_agent.output_validator(_require_grounded_proposals)

2 · Three states, PER ATTRIBUTE — never a whole-document refusal

The model emits, for each of annexure_label / exhibit_number / party_role, a state restricted to proposed | open-question. A confident exhibit number rides even when the party role is unclear; the unclear one becomes an open-question with a question and no value. accepted is the human act — it is in the wire union but the brain's Literal can't produce it.

# models.py — the brain's emit-state Literal (no 'accepted', no lifecycle word)
_LabelEmitState = Literal["proposed", "open-question"]

# agent.py — the anti-guess completeness gate
def _require_grounded_proposals(turn):
    # a 'proposed' attribute MUST carry a grounded value (party_role also its canonical token)
    # else ModelRetry -> the model fills it from the document, or downgrades to open-question
Why open-question never carries a value (D7): "unsure" must route to the inbox, not to a plausible guess. The validator makes "never guess" structural: a proposed-but-blank attribute can't survive the pass.

3 · Proposals become typed intents; the broker writes the row

policy.py turns the per-attribute turn into intents. A proposed exhibit number / party role → one propose_exhibit_marking (→ the junction); a proposed annexure label → propose_annexure_label (→ documents); each open-question → create_registrar_review_item (the inbox). Per attribute, not per document.

# policy.intents_for_label — the marking proposal's args
{"document_id":…, "proceeding_id":…,
 "source": "agent",            # NEVER recorded_by_user_id — the agent run owns it
 "lifecycle_status": "marked",  # DR-1: never admitted/rejected (a tribunal act)
 "exhibit_label": "C-1",
 "exhibit_party_role": "Petitioner", "exhibit_party_role_canonical": "initiating_party"}

The intent stamps policy gated — group-D and LOCKED, so a marking can never ship auto (it's not a name; it's a document identity). The runtime stamps agent_run_id on the turn so an entire run is revertable in one click (revert itself is Story-1884 — not built here). _assert_safe_lifecycle refuses an admitted/rejected status at the boundary even if a future edit tried to set one.

4 · Party role: display + the canonical 4-token axis (D4)

Jurisdictions use different party words; the canonical axis is the same everywhere. The prompt maps the local name onto exactly one token. No DB CHECK on the axis (app-layer — D4), so the closed set is the agent's contract.

Canonical tokenIndia (IN) display names — prompt-hardcoded
initiating_partyPetitioner / Plaintiff / Claimant / Complainant / Appellant / Applicant / Prosecution
responding_partyRespondent / Defendant / Accused / Opposite Party
adjudicatorThe Court / Tribunal / Arbitrator / Bench
third_partyIntervenor / Impleaded / Witness / Amicus

The vocabulary is prompt-hardcoded and launched country-by-country (prompts/label.py _JURISDICTION_VOCAB). An unknown jurisdiction is not an error — Blake files every attribute as an open-question and proposes nothing (never a guessed scheme).

5 · DR-2 — a privileged document is never labeled

The orchestrator checks the canonical privilege_flag first, before any model call: a privileged document yields a skipped_privileged turn with no attributes and no intents. The DB trigger (Story-1878) also blocks it; Blake refuses to even reason about it.

# agent.stream_label_turn
if label_input.privilege_flag:        # DR-2 — skip upstream, NO model call, NO proposal
    yield _skipped_privileged_turn(label_input)
    return

6 · Proof — the repo test gate

tests/test_registrar_label_capability.py (offline, no model call) covers the three required acceptance cases plus wiring:

WhatTest
(a) per-attribute open-question — confident attr proposed, unsure one to inbox, marking carries only the confidenttest_open_question_routes_to_inbox_marking_carries_only_confidentpass
(b) agent never proposes admitted/rejected — type can't express it; every marking is 'marked'; the guard refuses tribunal actstest_model_output_cannot_express_admitted_or_rejected, …_marked_never_admitted_or_rejected, …_assert_safe_lifecycle…pass
(c) privileged document skipped — no model call, no proposaltest_privileged_document_is_skipped_with_no_model_callpass
provenance: source='agent', no user id, gated; jurisdiction vocab; manifest wiring…_provenance_is_agent_run…, …_jurisdiction_vocab…, …_intent_tools_present_in_manifestpass
# agents/registrar/
pytest tests/ -q   → 226 passed   (16 new label tests)
ruff check agents/ → All checks passed
scripts/check_brain_egress.py · check_prompt_leakage.py → clean
What this Story deliberately does NOT do: revert logic (Story-1884), the UI review surface (W3), and the broker-side tool handlers that perform the actual document_exhibit_marking write (pramaan-functions). Blake's job is to propose honestly; everything else is downstream of the wire.