diff --git a/.changeset/fix-late-provider-builtin-tools.md b/.changeset/fix-late-provider-builtin-tools.md new file mode 100644 index 000000000..f73755efd --- /dev/null +++ b/.changeset/fix-late-provider-builtin-tools.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix built-in tools being unavailable when the model provider becomes ready after the session starts. diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index df30c31bf..59b311639 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -830,6 +830,23 @@ export class ToolManager { get loopTools(): readonly ExecutableTool[] { if (this.loopToolsOverride !== undefined) return this.loopToolsOverride; + // Self-heal an empty builtin table. The constructor and every config- + // mutation checkpoint gate initializeBuiltinTools() on hasProvider, but a + // provider that becomes resolvable asynchronously (OAuth / managed + // free-tokens model registration) trips none of them — without this the + // agent runs with zero tools while the system prompt still advertises them. + // loopTools is re-read before every step, so the table is populated on the + // first step after the provider resolves. Steady state short-circuits on + // `builtinTools.size === 0`, so hasProvider is not evaluated per read. + if (this.builtinTools.size === 0 && this.agent.config.hasProvider) { + try { + this.initializeBuiltinTools(); + } catch (error) { + this.agent.log.warn('lazy initializeBuiltinTools failed; will retry on next read', { + error: error instanceof Error ? error.message : String(error), + }); + } + } const disclosure = this.progressiveDisclosure; const enabledMcpNames = [...this.mcpTools.keys()].filter((name) => this.isMcpToolEnabled(name), diff --git a/packages/agent-core/test/agent/tool.test.ts b/packages/agent-core/test/agent/tool.test.ts index 549bccedd..13ad93cb7 100644 --- a/packages/agent-core/test/agent/tool.test.ts +++ b/packages/agent-core/test/agent/tool.test.ts @@ -6,7 +6,9 @@ import type { ToolCall } from '@moonshot-ai/kosong'; import { describe, expect, it, vi } from 'vitest'; import { budgetToolResultForModel } from '../../src/agent/turn/tool-result-budget'; +import type { KimiConfig } from '../../src/config'; import { HookEngine } from '../../src/session/hooks'; +import { ProviderManager } from '../../src/session/provider-manager'; import type { SessionSubagentHost } from '../../src/session/subagent-host'; import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { createFakeKaos } from '../tools/fixtures/fake-kaos'; @@ -269,6 +271,40 @@ describe('Agent tools', () => { expect(ctx.agent.tools.loopTools.some((tool) => tool.name === 'AgentSwarm')).toBe(true); }); + it('self-heals the builtin tool table when the provider becomes resolvable after construction', () => { + // The ProviderManager reads this live config; it starts with no model or + // provider, so hasProvider is false at Agent construction and + // initializeBuiltinTools() is skipped — the state the asynchronous + // free-tokens / OAuth model registration produces. + const liveConfig: KimiConfig = { providers: {}, models: {} }; + const ctx = testAgent({ + providerManager: new ProviderManager({ config: () => liveConfig }), + }); + + // Aim at a model that cannot resolve yet and enable some tools. Neither call + // runs a gated re-init because hasProvider is still false, so the enabled + // tools have no builtin backing and are not dispatchable. + ctx.agent.config.update({ modelAlias: 'late-model' }); + ctx.agent.tools.setActiveTools(['Bash', 'Read', 'Glob']); + expect(ctx.agent.tools.loopTools.some((tool) => tool.name === 'Bash')).toBe(false); + + // The provider registers asynchronously: the config the ProviderManager reads + // now resolves the model, but no agent config.update fires, so none of the + // hasProvider-gated checkpoints re-run initializeBuiltinTools(). + liveConfig.providers['late-provider'] = { type: 'kimi', apiKey: 'late-key' }; + liveConfig.models!['late-model'] = { + provider: 'late-provider', + model: 'late-model', + maxContextSize: 1_000_000, + capabilities: [], + }; + + // loopTools self-heals on read: the builtin table is populated and the + // enabled tools become dispatchable instead of reporting "Tool not found". + const names = ctx.agent.tools.loopTools.map((tool) => tool.name); + expect(names).toEqual(expect.arrayContaining(['Bash', 'Read', 'Glob'])); + }); + it('routes registered user tools through tool.call request/response', async () => { const lookupCall: ToolCall = { type: 'function',