mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-18 05:34:23 +00:00
fix(agent-core): re-register builtin tools when provider resolves late (#1688)
When the model provider becomes resolvable only after the agent starts (async OAuth / managed free-tokens registration), the builtin tool table stayed empty: initializeBuiltinTools was gated on hasProvider at fixed checkpoints and none fired. loopTools now self-heals on read once a provider resolves, so tool calls no longer fail with "Tool not found".
This commit is contained in:
parent
d158e0a7ac
commit
94c0ef89d2
3 changed files with 58 additions and 0 deletions
5
.changeset/fix-late-provider-builtin-tools.md
Normal file
5
.changeset/fix-late-provider-builtin-tools.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue