Skip to main content

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}