feat(agent): dynamic tool calling on what's available — one availability-gated palette for both shells - #201
Conversation
…ity-gated palette for both shells
Kev, after the phone-vs-Mac voice read (2026-09-03): "how do we lean out on
Mac? We want consistency — by dynamic tool calling on what's available."
The Mac and the phone each hand-built their own tool list; the Mac's carried
tools whose backing might not be there (open_link with the web toggle off,
delegate_deep on a machine whose dive never reaches Big, battery_status on a
desktop, the knowledge trio over an empty corpus).
- ToolPalettePolicy (M1K3Chat, pure, TDD red-first, 8 tests incl. a
live-name pin): Availability{corpusHasItems, webAllowed, deepBrainAvailable,
hasBattery} → excludedNames / filter. Names are strings for the same reason
SelfQueryGate's are; the pin guards drift.
- Mac: applied INSIDE interactiveAgentTools, the builder the launch warm and
the live turn share, so the warmed prefix can't drift from the turn's set.
deepBrainAvailable reads the SAME gate the dive handler uses
(DeepDiveTarget.plan over LocalModelInventory + physical RAM); hasBattery is
the IOKit probe, read once. open_link now rides the web toggle — a page
render reaches the internet too, so the "only thing that reaches the
internet" guarantee holds.
- iOS: the responder's list passes the same policy (corpus + web; this shell
offers no bigger brain and no battery tool).
- Deliberately NOT per-question routing: the palette is a PersonaPrefixCache
key (a different set = ~6 s cold prefill, 2-entry cache — ROADMAP's "tune
the grounding, never the palette"). Stable facts only. Docs updated
(CONTEXT_TOOLS_PLAN, ROADMAP).
Tests: M1K3ChatTests 524 green (+8). Mac app + iOS simulator shells build.
Verify-owed: Kev's felt A/B, phone vs Mac over voice on real devices.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2kJAPEHTFU34uKiGArFSP
# ── MurphySig template (uncomment lines below to sign this commit) ──────
# Significant change detected. If this commit encodes a decision worth
# remembering — architecture, tradeoff, non-obvious fix — sign it.
# Spec: https://murphysig.dev/spec
#
# MurphySig:
# Signed: Kev + claude-fable-5-1, 2026-09-03
# Format: MurphySig v0.1
# Context: <why this exists — the thought, not the diff>
# Confidence: <0.0-1.0> — <what's uncertain>
# Open: <unresolved questions>
# ────────────────────────────────────────────────────────────────────────
✅ Deploy Preview for m1k3 canceled.
|
|
Review — dynamic tool palette ( Nice piece of work overall: the policy is a clean pure/testable seam ( 1. New synchronous I/O now runs on
Before this PR,
Because Each individual call is probably sub-millisecond in practice, but it's a real regression against the stated principle ('no blocking the main actor during a model load') and it's paid twice for no reason —
2. Minor nits (non-blocking)
Swift 6 / architecture / provenance — clean
|
…the #201 review fold claude-review's one real finding: the availability facts are real I/O (a SQLite count, a directory listing, IOKit once) and the warm path called the builder twice, synchronously, on the main actor. Now: - interactiveAgentTools takes an optional `availability:`; nil computes live (the per-turn path). New nonisolated helpers `paletteAvailability(store:)` and `webSearchAllowed()` so both the builder and the warm read the same facts the same way. - warmPersonaPrefixAfterLoad builds both palettes INSIDE its detached task, from one Availability — no main-actor I/O, no double read. - ToolPalettePolicy.corpusHasItems(count:) hoists the fail-open default out of both shells, documented (an unreadable count keeps the tools), red-first pinned (nil → true, 0 → false, 3 → true). - iOS: the inert `hasBattery: true` says why it's inert. Tests: ToolPalettePolicyTests 9 green. Mac app + iOS simulator shells build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K2kJAPEHTFU34uKiGArFSP # ── MurphySig template (uncomment lines below to sign this commit) ────── # Significant change detected. If this commit encodes a decision worth # remembering — architecture, tradeoff, non-obvious fix — sign it. # Spec: https://murphysig.dev/spec # # MurphySig: # Signed: Kev + claude-fable-5-1, 2026-09-03 # Format: MurphySig v0.1 # Context: <why this exists — the thought, not the diff> # Confidence: <0.0-1.0> — <what's uncertain> # Open: <unresolved questions> # ────────────────────────────────────────────────────────────────────────
# Conflicts: # macos/M1K3iOSApp/AppCore.swift
…proval window 5 s → 60 s (#192) (#203) Five hits in three days (latest: PR #201, two attempts in a row, 5.6–6.8 s each). The scripted transport needs three 1 ms polls; the 5 s approvalWindow is only a deadline — but under `swift test --parallel` on the CI runner the actor hops alone can exceed it, and the ceremony then honestly reports "not approved". Sixty seconds keeps the deadline as a hang guard without racing the scheduler. Test-only change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K2kJAPEHTFU34uKiGArFSP # ── MurphySig ────────────────────────────────────────────────────── # Significant decision in this commit? Sign it. https://murphysig.dev/spec # ───────────────────────────────────────────────────────────────────
Review — #201 (dynamic tool-calling palette)Went through both commits ( Concurrency ✅
Architecture ✅
Correctness
Tests ✅ — 9/9 new tests read as meaningful, not padding: each exclusion reason gets its own test, Nit (non-blocking): the iOS-side web-toggle check ( Provenance — new file signed Nothing blocking. Good example of applying the fix inside the one shared builder so the warm and the live turn structurally can't drift. |
Why
Kev, after the phone-vs-Mac voice read (2026-09-03): "how do we lean out on Mac? We want consistency — by dynamic tool calling on what's available." Each shell hand-built its own tool list, and the Mac's carried tools whose backing might not be there:
open_linkwith the web toggle off (a page render reaches the internet too),delegate_deepon a machine whose dive never reaches Big,battery_statuson a desktop, the knowledge trio over an empty corpus.What
ToolPalettePolicy(M1K3Chat, pure, TDD red-first — 8 tests including a live-name pin):Availability{corpusHasItems, webAllowed, deepBrainAvailable, hasBattery}→excludedNames/filter. Names are strings for the same reasonSelfQueryGate's are; the pin guards drift.interactiveAgentTools, the one builder the launch warm and the live turn share, so the warmed persona prefix can never drift from the turn's set.deepBrainAvailablereads the same gate the dive handler uses (DeepDiveTarget.planoverLocalModelInventory+ physical RAM);hasBatteryis the IOKit probe, read once.open_linknow rides the web toggle, so the "only thing that reaches the internet" guarantee holds.CONTEXT_TOOLS_PLAN.md,ROADMAP.md.What this deliberately is NOT
Per-question routing. The palette is a
PersonaPrefixCachekey: a different tool set is a ~6 s cold prefill against a 2-entry cache (ROADMAP: "tune the grounding, never the palette"). So the inputs are stable facts only — a toggle, a download, a first document — never the query. If Kev's felt A/B says the Mac still carries too much, the next lever is the prefix-cache shape (capacity, or tools rendered after the cached persona), which is a measured decision, not a flip.Product choice worth a glance
delegate_deepleaves the palette on a Mac where the dive would only run on the resident brain ("buying time, not extra intelligence" — the tool's own description). That's the literal reading of "on what's available"; the affordance is one toggle-free download away (Big) and comes back on the next turn. Easy to revert if the buy-time dive is wanted without Big.Tests / verification
M1K3ChatTests524 green (+8).🤖 Generated with Claude Code
https://claude.ai/code/session_01K2kJAPEHTFU34uKiGArFSP