6.2 KiB
SF Lazy Tool Surface Design Draft
Status: draft. Scope: design only.
Prior Art
Claude Code and the local connector/tooling pattern use a small always-available discovery tool and defer detailed tool schemas until the model asks for a relevant group. This repo already has a nearby pattern in src/resources/extensions/mcp-client/index.js:8-11: mcp_servers lists configured servers, mcp_discover lazy-connects and registers discovered tools, and mcp_call remains a fallback call path. The design below applies that deferred-tool idea to SF-owned tools.
Meta-Tool Schema
Tool name: sf_tool_search.
Input:
type SfToolSearchInput = {
query: string;
domain?: "planning" | "execution" | "memory" | "subagent" | "evidence" | "mcp" | "all";
includeLoaded?: boolean;
limit?: number;
};
Return:
type SfToolSearchResult = {
query: string;
loadedBefore: string[];
matches: Array<{
name: string;
canonicalName: string;
status: "loaded" | "available_lazy" | "deprecated_alias";
title: string;
description: string;
domain: string;
aliases?: string[];
risk?: "read" | "write" | "destructive" | "external";
schemaPreview: unknown;
loadToken: string;
}>;
loadedAfter: string[];
nextInstruction: string;
};
Behavior:
sf_tool_searchis always loaded.- It searches a manifest-backed registry of lazy tools by name, alias, description, domain, and prompt snippet.
- For matched tools, it marks them loaded for the current session and causes the next LLM turn to include their full schemas.
- It should not execute the discovered tool. It only exposes schema and usage metadata.
- For tools with dangerous semantics, it returns risk metadata but does not bypass existing permission policy.
Manifest Metadata
The current extension-manifest.json has a flat provides.tools string list. Lazy loading needs a richer backward-compatible form:
{
"provides": {
"tools": [
"complete_task",
{
"name": "memory_graph",
"lazy": true,
"domain": "memory",
"aliases": [],
"canonicalName": "memory_graph",
"promptSnippet": "Inspect related SF memory records"
}
]
}
}
Prompt-build rule:
- Eager tools are passed to the provider normally.
- Lazy tools are omitted from the provider tool array and the Available tools prompt section.
sf_tool_searchgets a compact index of lazy names/domains/descriptions, not full schemas.- When a lazy tool is loaded, its full schema and prompt guidelines are included from the next turn onward.
Suggested eager baseline:
- File/shell core:
read,edit,write,grep,glob/findcompatibility,bash,ls,lsp. - Mandatory SF flow:
checkpoint,milestone_status,save_summary,plan_milestone/futuremilestone,plan_slice/futureslice,plan_task/futuretask,complete_task,complete_slice,complete_milestone,validate_milestone,reassess_roadmap. - Delegation entrypoint:
subagent. - Meta discovery:
sf_tool_search.
Suggested lazy groups:
- Subagent follow-up:
await_subagent,cancel_subagent,read_subagent,write_subagent. - Memory/evidence:
memory_search,memory_graph,query_journal,search_evidence,sift_search,codebase_search. - Admin/runtime:
resume_agent,kill_agent,read_output. - Issue/gate/chapter helpers:
report_issue,resolve_issue,record_gate,chapter_open,chapter_close,context_board,manage_todos,audit_product. - MCP: keep
mcp_servers/mcp_discover/mcp_callunder the existing MCP lazy-connect pattern.
Session State
Loaded state belongs in the coding-agent session, not .sf/sf.db.
Shape:
type LazyToolSessionState = {
loadedLazyTools: string[];
loadedAtTurn: Record<string, number>;
searchHistory: Array<{ turn: number; query: string; loaded: string[] }>;
};
Persistence:
- In-memory state is enough for a single active session.
- If session resume should preserve loaded lazy tools, serialize into the existing session transcript/state store alongside active tool names, not project DB.
- Do not write loaded-tool state to
.sf/sf.db; this is provider/session prompt state, not project planning state.
Turn lifecycle:
- User/agent starts with eager tool set plus
sf_tool_search. - Model calls
sf_tool_search({ query }). - Tool resolves matching lazy tools and updates
loadedLazyTools. - Session calls
setActiveToolsByName([...eager, ...loadedLazyTools]). - System prompt and provider tool list are rebuilt for the next LLM call.
- Loaded tools stay loaded until session end, explicit unload, or compaction policy resets them.
Current Architecture Feasibility
Partially achievable today, but full lazy prompt-build requires upstream coding-agent changes.
Current support:
packages/coding-agent/src/core/agent-session.ts:956-974can set active tools by name and rebuild the system prompt for the next turn.packages/coding-agent/src/core/extensions/loader.ts:534-548can register/unregister extension tools and refresh the runtime registry.packages/coding-agent/src/core/system-prompt.ts:157-178already builds the visible tool list from selected tool names.- MCP discovery already lazy-connects and can register external tools after discovery.
Missing pieces:
- Tool metadata has no first-class
lazy,aliases,canonicalName, ordomainfields in the active manifest flow. extension-manifest.jsonis a string list, so prompt-build cannot know which schemas to suppress.sf_tool_searchneeds a safe way to mutate active tools from inside a tool call or queue the mutation for the next turn.- Prompt guidelines are appended for all registered tools today; lazy tools must not leak full usage guidance before loading.
- Provider telemetry should preserve old name, canonical name, and alias name for deprecation accounting.
Conclusion: a minimal SF-only prototype can register all tools but hide lazy ones from setActiveToolsByName until sf_tool_search loads them. A production implementation needs coding-agent metadata support so lazy schemas, prompt snippets, aliases, and deprecation state are handled consistently across SF, MCP, web/RPC, and provider compatibility code.