nexo_core/agent/built_in_deferred.rs
1//! Canonical list of built-in tools that ship deferred
2//! by default. Deferred tools are excluded from
3//! `ToolRegistry::to_tool_defs_non_deferred()` (the slice every
4//! provider shim — Anthropic / MiniMax / OpenAI / Gemini / DeepSeek
5//! / xAI / Mistral — emits in the request body) and instead surface
6//! through `ToolSearch` discovery + the
7//! `<available-deferred-tools>` synthetic block. The model fetches
8//! a deferred tool's full schema on demand via
9//! `ToolSearch(select:<name>)`.
10//!
11//! Adding a tool to [`BUILT_IN_DEFERRED_TOOLS`] is the only step
12//! required for it to participate in the `ToolSearch` budget — no
13//! per-call-site change needed. The sweep
14//! [`mark_built_in_deferred`] runs at agent boot, idempotent vs
15//! gated tools (entries not registered in this boot are silently
16//! skipped because [`ToolRegistry::set_meta`] only writes the
17//! side-channel meta map).
18//!
19//! Provider-agnostic: deferral lives at the registry layer, not in
20//! any provider shim. Switching providers does not change which
21//! tools are deferred.
22//!
23//! IRROMPIBLE refs:
24//! - `claude-code-leak/src/Tool.ts:438-449` — `shouldDefer` /
25//! `alwaysLoad` semantics. Deferred tools are sent with
26//! `defer_loading: true`; `alwaysLoad: true` is the per-tool
27//! opt-out (we don't need it today, no built-in requires turn-1
28//! appearance).
29//! - `claude-code-leak/src/tools/ToolSearchTool/prompt.ts:62-108`
30//! — `isDeferredTool` decision tree the consumer uses to pick
31//! the deferred subset. Carve-outs (`alwaysLoad`, `isMcp`,
32//! `name == TOOL_SEARCH`, KAIROS-mode Brief / SendUserFile,
33//! FORK_SUBAGENT-mode Agent) live there; we mirror only the
34//! `name == TOOL_SEARCH` carve-out today (ToolSearch itself
35//! must always load — the model needs it to discover the rest).
36//! - `claude-code-leak/src/services/api/claude.ts:1136-1253` —
37//! token-budget rationale: deferred schemas omitted from the
38//! request, `<available-deferred-tools>` block injects names +
39//! 1-line descriptions instead. Big surfaces (e.g. ~30 MCP
40//! tools) save thousands of tokens per turn.
41//! - Per-tool `shouldDefer: true` precedents in leak:
42//! * `src/tools/TodoWriteTool/TodoWriteTool.ts:51`
43//! * `src/tools/NotebookEditTool/NotebookEditTool.ts:94`
44//! * `src/tools/RemoteTriggerTool/RemoteTriggerTool.ts:50`
45//! * `src/tools/LSPTool/LSPTool.ts:136`
46//! * `src/tools/TeamCreateTool/TeamCreateTool.ts:78`
47//! * `src/tools/TeamDeleteTool/TeamDeleteTool.ts:36`
48//! * `src/tools/TaskListTool/TaskListTool.ts:52` — precedent for
49//! list/status read-only tools (we apply it to `TeamList` /
50//! `TeamStatus`).
51//! * `src/tools/SendMessageTool/SendMessageTool.ts:533` —
52//! precedent for messaging tools (we apply it to
53//! `TeamSendMessage`).
54//! * `src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts:50`
55//! * `src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts:59`
56//! - `research/`: no relevant prior art — OpenClaw is channel-side
57//! and has no `ToolSearch` / deferred-tool concept.
58
59use super::tool_registry::{ToolMeta, ToolRegistry};
60
61/// Canonical list of `(tool_name, search_hint)` for built-in
62/// tools that ship deferred. The hint feeds `ToolSearch` keyword
63/// ranking — when present it scores higher than the verbose
64/// description (mirrors leak's `searchHint:` field on the tool
65/// definition, e.g. `TaskListTool.ts:35`).
66///
67/// Out of scope (deferred to follow-up slices):
68/// - `EnterPlanMode` / `ExitPlanMode` (plan-mode flow
69/// control mid-turn warrants separate UX consideration).
70/// - 5 cron tools (surface differs from leak's 3-tool
71/// shape; defer until cron UX settles).
72/// - `WebSearch` / `WebFetch` (web-tools surface still
73/// in flux).
74pub const BUILT_IN_DEFERRED_TOOLS: &[(&str, &str)] = &[
75 ("TodoWrite", "todo, tasks, in-progress checklist"),
76 ("NotebookEdit", "jupyter, ipynb, notebook cell edit"),
77 ("RemoteTrigger", "webhook, external publish, http POST"),
78 ("Lsp", "language server, go-to-def, hover, references"),
79 ("TeamCreate", "team, parallel agents, fan-out"),
80 ("TeamDelete", "team, teardown"),
81 ("TeamSendMessage", "team, dm, broadcast"),
82 ("TeamList", "team, list active members"),
83 ("TeamStatus", "team, status, member health"),
84 ("Repl", "python, node, bash, REPL, code execution"),
85 ("ListMcpResources", "mcp, resources, discovery"),
86 ("ReadMcpResource", "mcp, resource, fetch"),
87];
88
89/// Apply `ToolMeta::deferred_with_hint(hint)` to every tool in
90/// [`BUILT_IN_DEFERRED_TOOLS`] that is registered on `registry`.
91///
92/// Idempotent in two senses:
93/// 1. Tools that aren't registered in this boot (gated off via
94/// `agent.team.enabled = false`, `agent.lsp.enabled = false`,
95/// etc.) are silently skipped — `set_meta` only writes the
96/// side-channel meta map and doesn't require a handler.
97/// 2. Calling N times has the same effect as calling once — the
98/// last write wins and all writes carry identical content.
99///
100/// Call once at agent boot, AFTER all `tools.register(...)` calls
101/// and BEFORE the registry is handed to the runtime. Calling
102/// before registration still works (meta lands in the side
103/// channel) but can be surprising — the documented call site is
104/// post-registration.
105pub fn mark_built_in_deferred(registry: &ToolRegistry) {
106 for (name, hint) in BUILT_IN_DEFERRED_TOOLS.iter() {
107 registry.set_meta(name, ToolMeta::deferred_with_hint(*hint));
108 }
109}