flowchart TD
NOTE["ROUTER / PLANNER — for each message it decides: run a tool, and which one — or just reply.<br/>TWO engines try IN ORDER — (1) a deterministic RULE planner with NO LLM, then (2) an LLM router only if (1) finds nothing.<br/>Both emit ONE ToolPlan {decision, tool, action, args}. Reads can auto-run; writes only ever PROPOSE an approval chip."]
subgraph ENTRY["1 · ENTRY — runtime.py"]
direction TB
E1["process_account_turn — the one brain · the message is ingested into memory first"]
E2["_handle_natural_language_tool_intent_result · deterministic FAST-PATHS first — screenshot · email-list · calendar-read"]
E3["_run_bounded_tool_loop · up to tool_router_max_steps (default 1) · re-enters the planner after each READ"]
E1 --> E2 --> E3
end
subgraph CTX["2 · CONTEXT fed to the planner"]
direction TB
C1["assembled in _plan_tool_intent_async<br/>today's date + the owner's timezone"]
C2["recent SERVER-ordered ids — last gmail.list message-ids · last calendar.list event-ids · last drive.list file-ids<br/>this is how 'the first one' / 'it' / '#2' resolve — the model never supplies an id (INV-022)"]
C3["flags — self-code-mode · the per-arm natural-language flags (calendar-create, drive-rename, reminder-snooze, ...)"]
C1 ~~~ C2 ~~~ C3
end
subgraph ENG1["3 · ENGINE 1 — DETERMINISTIC planner · no LLM · tried FIRST"]
direction TB
D0["planner/deterministic.py<br/>normalize_text → extract_entities"]
D1["_deny_tool_json — reject a user trying to hand-write a raw tool call"]
D2["_capability_plan — 'what can you do?' answered straight from the REGISTRY truth, never the web"]
D3["_browser_plan — open a named site or URL"]
D4["_google_workspace_plan — gmail · calendar · drive · tasks<br/>resolves referents ('the first one', 'it', '#2') against the cached server ids from step 2"]
D5["_reminder_plan · _todo_plan · _weather_plan"]
D6["_web_search_plan — last-resort regex match"]
D0 --> D1 --> D2 --> D3 --> D4 --> D5 --> D6
end
subgraph ENG2["4 · ENGINE 2 — LLM ROUTER · only if Engine 1 finds nothing"]
direction TB
L0["shortlist · select_candidates — BM25 over each tool's usage_examples → a top-5 candidate menu"]
L1["build the prompt — a router role + the candidate ToolSpecs' descriptions · the user message is framed as DATA, not instructions"]
L2["LLM call · tool_router_model (its OWN model, e.g. DeepSeek Flash) · 4s timeout · NOT streaming · 2 retries on 429/5xx only"]
L3["strict JSON decode → {tool, action, query/args} · extra keys rejected · garbage ⇒ none, fall through to plain chat"]
VAR["two variants — route_readonly = read-only allowlist ONLY (web_search, browser_agent, weather)<br/>route_via_engine = registry-driven, CAN propose write-chips · default OFF + shadow-first"]
L0 --> L1 --> L2 --> L3 --> VAR
end
subgraph GATES["5 · GATES · checked on EVERY pick · 3 layers"]
direction TB
G1["classify_tier — the ONE source of truth → ALLOWLIST · CONFIRMATION · MUSTLIST · BLOCKLIST"]
G2["positive allowlist gate — the pick must be in a LITERAL allowed set for its lane"]
G3["validate_tool_plan — tool exists + enabled · INV-022 blocks operator-only (morpheus_drain_/codex_) · INV-023 blocks hard-delete of client data"]
G4["final LITERAL re-assert — the last check cannot loosen even if a gate above is changed"]
G1 --> G2 --> G3 --> G4
end
DEC{"ToolPlan<br/>decision"}
ENTRY --> CTX
CTX --> ENG1
ENG1 -->|matched a rule| GATES
ENG1 -->|no rule matched| ENG2
ENG2 --> GATES
GATES --> DEC
DEC -->|runs a read| RTOOL["TOOL · _execute_tool_action runs it now — gmail.list · weather · web_search · self_code_read<br/>emits ToolCallStart then ToolCallEnd · result captured"]
DEC -->|instant-write| RINST["INSTANT WRITE — gmail.mark_read ONLY (ADR 0153, reversible) · runs chiplessly"]
DEC -->|proposes a write| RPROP["PROPOSE · mints an approval CHIP — one-shot token, 10-min TTL · emits an action hint · WAITS, never auto-runs<br/>gmail.send · calendar.cancel_event · drive.rename · reminder.create ..."]
DEC -->|asks back| RCLAR["CLARIFY — replies with a question, e.g. 'which reminder did you mean?' · no tool runs"]
DEC -->|no tool| RANS["ANSWER — hands straight to the main chat LLM to reply in words"]
RTOOL -->|read accumulates| LOOPB["loop re-enters the planner with the fresh read in context · dedupe on (tool, action, query) · a write ENDS the loop"]
LOOPB -. next step .-> ENG1
RPROP -->|operator approves| OPC["chip redeemed → the write executes exactly once"]
RTOOL --> MAIN
RINST --> MAIN
OPC --> MAIN
RANS --> MAIN
MAIN["6 · MAIN CHAT LLM — composes the final reply from the tool result + memory recall"]
MAIN --> OUT(["reply streamed to the user · SSE"])
RCLAR --> OUT
L2 -. every call logged .-> RDB[("DB · steelmoth.llm_usage_event · steelmoth.chat_turns")]
LESSON["DESIGN NOTE — the assistant's 'what can you do' self-description is now DERIVED from this SAME registry (ADR 0178), one source of truth enforced by a fitness test. The old split caused false 'I don't have that tool' denials — keep it unified in any redesign."]
D2 -. same registry truth .-> LESSON
subgraph EX["WORKED EXAMPLES · where each message goes"]
direction TB
X1["'what is the weather in Perth?'<br/>Engine 1 _weather_plan → weather.get_current_today → ALLOWLIST → runs now"]
X2["'cancel the first one' (a calendar list was just shown)<br/>Engine 1 resolves 'first one' to the cached event-id → calendar.cancel_event → CONFIRMATION → approval chip"]
X3["'email Marko that I'm running late'<br/>gmail.send → CONFIRMATION → approval chip · never auto-sends"]
X4["'mark it read'<br/>gmail.mark_read → CONFIRMATION but INSTANT-WRITE carve-out → runs chiplessly"]
X5["'what is the latest news on the budget?'<br/>Engine 1 finds nothing → Engine 2 LLM router → web_search.search → ALLOWLIST → runs"]
X6["'is Marko related to me?'<br/>relationship guard demotes contact.dossier → no tool → memory recall answers in words"]
X7["'cancel the first one' with NO list shown yet<br/>nothing to resolve against → CLARIFY → 'which one did you mean?'"]
X1 ~~~ X2 ~~~ X3 ~~~ X4 ~~~ X5 ~~~ X6 ~~~ X7
end
DEC -. worked examples .-> EX
style ENTRY fill:#eef3ff,stroke:#2f6df6,stroke-width:2px,color:#2f6df6
style CTX fill:#f1f5f9,stroke:#475569,stroke-width:2px,color:#475569
style ENG1 fill:#ecfeff,stroke:#0c8ea4,stroke-width:3px,color:#0c8ea4
style ENG2 fill:#eef3ff,stroke:#2f6df6,stroke-width:3px,color:#2f6df6
style GATES fill:#fdecec,stroke:#e5484d,stroke-width:3px,color:#e5484d
style EX fill:#f1f5f9,stroke:#475569,stroke-width:2px,color:#475569
classDef blN fill:#ffffff,stroke:#2f6df6,color:#27272a,stroke-width:1px
classDef tlN fill:#ffffff,stroke:#0c8ea4,color:#27272a,stroke-width:1px
classDef gnN fill:#ffffff,stroke:#1f9d63,color:#27272a,stroke-width:1px
classDef amN fill:#ffffff,stroke:#c2570c,color:#27272a,stroke-width:1px
classDef goN fill:#ffffff,stroke:#b45309,color:#27272a,stroke-width:1px
classDef slN fill:#ffffff,stroke:#475569,color:#27272a,stroke-width:1px
class NOTE,C1,C2,C3,LOOPB,RDB,X1,X2,X3,X4,X5,X6,X7 slN
class E1,E2,E3,D0,D1,D3,D4,D5,D6,L0,L1,L2,L3,VAR,G1,G2,G3,G4,DEC blN
class D2 tlN
class RTOOL,RINST,MAIN,OPC,LESSON gnN
class RPROP,RCLAR amN
class RANS,OUT goN