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.
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
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)."
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).
| Case | Counsel | Gate |
|---|---|---|
| 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
query_matters stays a server-executed READ.ClarificationChoice), fenced against the #1805 contract — no free-text parse.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.