Skip to content

Router / planner · tool registry · capabilities

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