Until now Atlas had exactly one chat behaviour: wizard intake. Every chat turn ran
stream_atlas_turn, which requires an AtlasIntakeInput and assembles a Matter
Frame proposal. So when counsel opened the Agent Dock on, say, the matters page and asked "what is this page
about?", they got matter-intake clarifications — wrong. This Story gives Atlas a real Dock-chat
behaviour: given the surface context (the page counsel is on + the per-turn available_surfaces
manifest) + their message + history, it answers about the current surface and may propose a typed
navigate_to. The Wave-0 contract (Chore #1663) added the kind: "intake" | "dock_chat"
discriminator + the DockChatContext; the sibling broker (#1660) routes kind=dock_chat
here.
navigate_to ONLY (PLAN D1) — no server-plane read tools this pass; "what is this page" is
answered purely from the route-registry surface.description carried in available_surfaces
(D2 — no KB retrieval). (2) The brain proposes a typed intent and holds no router credentials; any
navigate_to is validated against the injected manifest, so it can only navigate to a surface it was
shown (and a URL-shaped key is rejected at the Pydantic boundary).| Area | Where | What it does |
|---|---|---|
| Typed contracts | agents/atlas/models.py | DockChatContext (surface_key + available_surfaces + history + counsel_message + thinking_level), the FLAT generation DTO AtlasDockOutput, and the assembled AtlasDockTurn (Markdown answer + optional NavigateTo + typed error handles). |
| The Dock brain | agents/atlas/dock_chat.py | stream_dock_chat_turn — a flat-DTO PromptedOutput agent + output-function, the SAME non-streaming bounded run() path as intake (Bug-1370), fail-closed to a TYPED error turn. Reuses route_intent.assert_surface_in_manifest for the no-invented-key guarantee. |
| The Dock prompt | agents/atlas/prompts/dock_chat.py | DOCK_CHAT_SYSTEM + builder — renders the CURRENT page's description + the navigable manifest + history + counsel's message. Answers in counsel's language (incl. Hindi). |
| Runtime kind-branch | agents/atlas/runtime.py | handle_request branches on kind BEFORE build_intake_input → the Dock path needs no wizard input. Same turn.delta→turn.completed cadence + cost/usage stamping; intake path byte-unchanged. |
| Span + entrypoint | agents/atlas/manual_span.py · main.py | New approved manual span agents.atlas.dock_chat (ADR-124). main.py exempts kind=dock_chat from the "input required" rule. |
| Evals + offline gates | agents/atlas/evals/dock_*.py · tests/test_dock_*.py | 6 golden Dock cases (3 answer-about-page on distinct surfaces, 2 navigate, 1 Hindi) graded by the deterministic DockTurnMatchesExpectation gate; offline + FunctionModel wiring tests run in CI without a key. |
// AtlasDockTurn — a page answer, optionally carrying a typed navigation { next_message_md: "This is your matters list — browse, filter, and open cases.", navigate_to: null, // a plain "what is this page" answer is_terminal: true } // "show my clients" on the matters page → the same turn, now navigating { next_message_md: "Taking you to your clients.", navigate_to: { kind:"navigate_to", surface_key:"clients", params:{} } } // a key, NEVER "/clients"
# 1. offline gates + the real PromptedOutput wiring (no key — runs in CI) cd agents/atlas && pytest tests/test_dock_chat.py tests/test_dock_evals.py -q # passed # 2. repo gate ruff check agents/atlas && python ../../scripts/check_brain_egress.py && pytest -q # 289 passed, clean # 3. golden Dock-evals LIVE against the real model (needs OPENROUTER_API_KEY) cd agents/atlas && python -m evals.dock_run
The FunctionModel test proves the full chain end-to-end without an API key: the flat DTO is validated, the
output-function assembles the AtlasDockTurn, a navigation builds a typed NavigateTo, and an
invented surface key is rejected until retries exhaust. The LIVE Dock-evals (DeepSeek v3.2 via OpenRouter)
run in CI where the key is injected.
stream_dock_chat_turn — does NOT call the intake assembler and needs no AtlasIntakeInput.surface.description in available_surfaces (D2). No KB retrieval.navigate_to, reusing the route-intent no-invented-key guarantee + URL rejection. The brain proposes; it never executes nav (D1).next_message_md → turn.completed; any exception → one TYPED fail-closed error turn, never a worker-loop crash.handle_request branches on kind → dock_chat vs the unchanged intake path; cost/usage stamping preserved.thinking_level through the SAME ZDR/residency default-deny path (provider.build_model/model_settings); no model pinned (F4).agents.atlas.dock_chat via the shared helper.navigate_to clients; one Hindi prompt → clients.kind=intake — only a new branch was added ahead of it.pramaan-agents). The broker (#1660) and app (#C) are siblings, not touched here.