* Move deferred tool discovery into tool search
Keep deferred tool discovery in the tool_search definition instead of appending it as a per-turn user message, so models do not answer or acknowledge internal discovery metadata.
Preserve deferred loading behavior: hidden tools remain hidden until loaded, tool_search stays reachable for allowlisted deferred tools, and retry paths keep loaded/deferred state.
Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tools -run 'Test.*Deferred|Test.*ToolSearch|TestRunLoadsDeferredToolThenAdvertisesNextTurn|TestRunReactiveRetryKeepsLoadedDeferredToolAndDiscovery|TestRunConnectTimeReactiveRetryKeepsLoadedDeferredToolAndDiscovery|TestAllowlistedDeferredToolsKeepToolSearchReachable|TestDisabledToolSearchFallsBackToEager'
Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tools
Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tools ./internal/cli
Tested: git diff --check
* Clarify deferred tool search selection
* Deferred (lazy) tool loading: tool_search + auto threshold for large MCP tool sets
Adds deferred tool loading so a large MCP tool set no longer sends every tool's full JSON schema each turn. When the number of visible MCP tools crosses a threshold (config tools.deferThreshold, default 10; 0 disables), their schemas are withheld and advertised compactly in a per-turn system-reminder; the model pulls a tool's full schema on demand via a new tool_search tool. Strictly additive: below the threshold behaviour is byte-identical to today. No new module dependencies.
- Eligibility: MCP tools opt in via an optional Deferred() bool interface (tools.IsDeferred); built-ins stay eager. No change to the core Tool interface.
- Formatting (internal/tools/deferred.go): compact one-liners wrapped in a <system-reminder> by BuildDeferredReminder; server label via MCPServerName().
- tool_search (internal/tools/tool_search.go): SideEffectNone/PermissionAllow/AdvertiseInAuto; select:Name1,Name2 (exact) or keyword (ranked); returns full schemas and signals the loop via Meta["load_tools"]. Operator-filter-aware (never lists tools hidden by --enabled-tools/--disabled-tools).
- Agent loop: partitionTools is the single source of truth; eligible counts only ToolVisible deferred tools; Meta["load_tools"] lifts to ToolResult.LoadedTools and unions into a per-run loaded-set advertised next turn. Reminder appended only to the per-turn request copy (never persisted), including both reactive-compaction retry paths. tool_search stays reachable when deferral is active (exempt from the EnabledTools allowlist).
- Config + wiring: tools.deferThreshold (default 10; 0 disables; negative rejected); tool_search registered and DeferThreshold threaded in exec and TUI, gated on the same visible-deferred count the loop uses.
Recreated on top of current main; resolves the SideEffectNone clash with #146 (escalate_model). Supersedes #147.
* deferred-tools: activate deferral only when tool_search is runnable; drop dead deferredTools()
Addresses #148 review:
- partitionTools gates 'active' on a registered, non-disabled, mode-advertised
tool_search (mirrors the executeToolCall dispatch gate). When the loader is
unavailable (explicitly disabled, missing, or not advertised e.g. spec-draft),
fall back to the eager/inactive path instead of hiding deferred tools behind a
loader the model can never call (the dead-end).
- Flip TestDisabledToolSearch... to assert the eager fallback; register a real
tool_search in the active-path tests that previously relied on activation
without a usable loader.
- Remove unused toolSearchTool.deferredTools().
* deferred-tools: disable deferral in exec when tool_search disabled; drop unused toolDefinitions shim
Further #148 review:
- exec computes one effective DeferThreshold, forced to 0 when tool_search is in
--disabled-tools, and feeds it to both registerToolSearchIfEligible and
agent.Options.DeferThreshold — so the registration gate and the loop partition
gate agree, and a disabled loader never enables deferral.
- Remove the unused toolDefinitions shim (golangci-lint unused; no callers).