Two clients named Sharma — Atlas asks instead of guessing

When a name resolves to more than one firm entity, the broker no longer auto-picks. This Story teaches the Atlas brain to consume that signal: ask which one, surface the firm-scoped candidates, and re-issue the read with the chosen id.
Story-1808 · RFD-AGT-003 D14 (MD-11 brain side) · branch issue-1808-agents-d14-clarify-consume · 2026-06-27

The functions half (pramaan-functions #480, merged as Story-1805) added a new outcome to the server-tool execute response: when a name like "Sharma" matches more than one client under the caller's RLS, the broker returns NEEDS_CLARIFICATION with a typed ClarificationChoice[] instead of silently picking one. This Story is the brain half — Atlas now recognises that outcome, turns it into a clarification turn, and on counsel's selection re-issues query_matters with the chosen entity id.

One rule defines the whole Story (MD-11): asking which one is a clarification, not a refusal. Atlas never auto-picks (a wrong client is a confidentiality leak), and never declines (the data is reachable — counsel just has to disambiguate). The brain holds no DB creds and authors no SQL: it reads a typed signal and passes back a typed id.

1 · The contract, mirrored — no free-text parse

The candidate shape is custody-owned in the shared package and restated in the brain (the agent ships as a self-contained CodeZip and cannot import the repo root at runtime), fenced against drift by a parity test — the same discipline RenderSelection uses.

# agent_contracts/clarification.py — canonical, mirrors functions#1805
class ClarificationChoice(BaseModel):
    entity_type: str   # kind matched, e.g. 'client'
    id: str            # firm-scoped id the brain passes BACK to disambiguate
    label: str         # disambiguating display label

# agents/atlas/models.py — DockToolResult (the consumed ServerToolExecuteResponse)
outcome: Literal["executed", "needs_clarification", "denied", "proposal"]
choices: list[ClarificationChoice] | None   # present ONLY on needs_clarification
Why a typed type, not a string scrape: the AC forbids free-text parsing of the candidates. The ids the brain passes back are authority handles — fishing them out of prose would be both fragile and a place for a fabricated id to sneak in. The parity test (tests/test_dock_chat.py::test_clarification_choice_matches_canonical_contract) fails the moment the brain copy drifts from the #1805 contract.

2 · The brain reads the signal — and is told what to do with it

On the answer step the broker feeds prior results back as AUTHORIZED TOOL RESULTS. A needs_clarification result now renders its candidates — label and id — and the FETCH-DATA job (agents/atlas/prompts/dock_chat.py) tells Atlas exactly how to act.

# the rendered prompt line (prompts/dock_chat.py)
- query_matters: NEEDS_CLARIFICATION — the name matched more than one; ask counsel
  WHICH ONE (do NOT pick yourself), then re-issue query_matters with the chosen id:
    - Sharma & Co · Mumbai  (client id: cl-101)
    - R. Sharma (individual) · Delhi  (client id: cl-202)

The system-prompt instruction: "if a tool result comes back as NEEDS_CLARIFICATION … do NOT pick one yourself and do NOT refuse — ask counsel which one … When counsel names their choice, re-issue the SAME tool with that candidate's id as the matching param (a client candidate → client_id = its id)."

3 · The D12 evals — clarify, not auto-pick, not refuse

Two golden Dock cases land in agents/atlas/evals/dock_cases.jsonl, gated by the deterministic DockTurnMatchesExpectation (no LLM judge — clarify/auto-pick/refuse is exactly checkable).

CaseCounselGate
clarify"how many matters for Sharma?" (ambiguous)NO tool intent (no auto-pick), surfaces the distinguishing labels, never refuses
re-issue"the Mumbai one, Sharma & Co"EMITS query_matters with client_id=cl-101 — the chosen id

The gate gained a clarify branch and the tool_call branch now checks pinned params, so the re-issue can't pass by emitting any query_matters — it must carry the chosen id.

# the clarify gate bites both ways (tests/test_dock_evals.py)
auto_pick = turn(tool_intents=[query_matters(client_id="cl-101")])
assert gate(auto_pick).value is False   # picked instead of asking ⇒ fail
bare = turn("Which one did you mean?")        # no labels surfaced
assert gate(bare).value is False
asked = turn("Which one? Sharma & Co (Mumbai) or R. Sharma (Delhi)?")
assert gate(asked).value is True

4 · Invariants held

5 · Verification

ruff check agents/ agent_contracts/ tests/     # All checks passed!
uv run --group dev pytest -q                  # 525 passed (agents/atlas)
pytest tests/ -q                              # 117 passed, 2 skipped (governance)
# new gates: clarify branch + re-issue params in test_dock_evals.py; parity fence in test_dock_chat.py

The deterministic eval structure is the CI gate and is green. The live model pass (python -m evals.dock_run) needs OPENROUTER_API_KEY and runs against these same cases.