Closes#3221.
Introduces a lazy factory API on ToolRegistry (registerFactory,
ensureTool, warmAll, getAllToolNames) as infrastructure for future
esbuild code-splitting (#3226). With the current single-bundle build,
the lazy API does not change startup time on its own — the primary
immediate value is fixing three pre-existing bugs uncovered while
designing it.
Bug fixes:
- Concurrent instantiation (P0): the original ensureTool had no
concurrency protection around `await factory()` — two concurrent
calls for the same tool both passed the cache check and each ran the
factory, producing two instances. AgentTool and SkillTool register
SubagentManager listeners in their constructors, so the extra
instance leaked listeners. Fix: a per-name `inflight: Map<string,
Promise<Tool>>` so concurrent ensureTool() calls share a single
promise. On factory rejection the inflight entry is cleared so a
subsequent call can retry.
- stop() resource leak: stop() only disposed tools already in
`this.tools`; tools still loading in `inflight` when stop() ran
finished afterward and were never disposed. Fix: await
Promise.allSettled(inflight.values()) before the dispose loop.
- Cache hit left stale factory: ensureTool's cache-hit branch did not
delete the factory entry, so warmAll() would re-invoke the factory
for an already-loaded tool. Fix: delete the factory on cache hit.
Additional hardening in response to review feedback:
- warmAll({ strict?: boolean }): strict mode re-throws the first
factory failure rather than swallowing it. Config.initialize() uses
strict: true so a broken built-in tool fails startup fast instead of
silently leaving a partially initialized registry; runtime-path
callers (GeminiChat, agent runtime, etc.) continue to use the
non-strict default and log failures via debugLogger.
- getAllTools() and getFunctionDeclarationsFiltered() emit a debug
warning when called while unloaded factories remain, nudging callers
toward warmAll() without hard-breaking existing code paths.
- copyDiscoveredToolsFrom() now iterates source.tools.values()
directly instead of source.getAllTools() — the copy path deals only
with already-discovered MCP/command tools and should not trigger the
unloaded-factory warning.
- MemoryTool and SkillTool config parsing was extracted into
memory-config.ts and skill-utils.ts so a factory can resolve tool
metadata without importing the tool module.
Tests:
- tool-registry.test.ts adds 128 lines covering: concurrent ensureTool
runs the factory exactly once, warmAll and ensureTool overlap,
retries succeed after a prior factory failure, stop() disposes tools
that finish loading after stop was called, and warmAll strict vs
default behavior.
- 33 existing call sites across cli, core, agents, and subagents were
updated to await warmAll() before bulk tool access.
- Track pendingConfirmationCallId in AgentToolInvocation to properly clear stale prompts
- Clear pendingConfirmation when TOOL_RESULT arrives for the pending tool (IDE diff-tab path)
- Clear pendingConfirmation via onConfirm callback (terminal UI path)
- Ensure pendingConfirmation is NOT cleared when TOOL_RESULT is for a different tool
- Prefer filePath over fileName for diff content path in Session and SubAgentTracker
- Add comprehensive tests for IDE diff-tab and terminal UI confirmation flows
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Remove acp.ts and schema.ts in favor of SDK types
- Refactor acpAgent.ts to leverage SDK client
- Update session management types and implementations
- Adjust all test cases for new SDK-based architecture
- Update integration tests and export utilities
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Rename SubAgentScope → AgentHeadless and runNonInteractive → execute
- Move agents-collab/ into agents/ with new runtime/ subdirectory
- Split subagent.ts into agent-core.ts and agent-headless.ts
- Update all event types, emitters, and statistics classes
BREAKING CHANGE: SubAgentScope renamed to AgentHeadless;
runNonInteractive() renamed to execute()
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>