diff --git a/.changeset/dynamically-loaded-tools-capability.md b/.changeset/dynamically-loaded-tools-capability.md new file mode 100644 index 0000000000..1ff8e84f24 --- /dev/null +++ b/.changeset/dynamically-loaded-tools-capability.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/kosong": patch +"@moonshot-ai/kimi-code": patch +"@moonshot-ai/kimi-code-sdk": patch +--- + +Rename the dynamic tool loading model capability from `select_tools` to `dynamically_loaded_tools`. diff --git a/apps/kimi-code/scripts/update-catalog.mjs b/apps/kimi-code/scripts/update-catalog.mjs index 41cfca6fa3..ee43eeafc1 100644 --- a/apps/kimi-code/scripts/update-catalog.mjs +++ b/apps/kimi-code/scripts/update-catalog.mjs @@ -24,6 +24,10 @@ const KEEP_MODEL = new Set([ "reasoning", "interleaved", "modalities", + // Message-level tool declarations capability — kosong's + // catalogModelToCapability reads it; stripping it here would silently + // disable tool-select for catalog-imported aliases. + "dynamically_loaded_tools", ]); function resolveOutputFile(args) { diff --git a/packages/agent-core/src/agent/context/dynamic-tools.ts b/packages/agent-core/src/agent/context/dynamic-tools.ts index da4d2282b3..577095231b 100644 --- a/packages/agent-core/src/agent/context/dynamic-tools.ts +++ b/packages/agent-core/src/agent/context/dynamic-tools.ts @@ -39,10 +39,10 @@ export function isLoadableToolsAnnouncement(message: ContextMessage): boolean { * Shape a history for a consumer that must not see dynamic-tool protocol * context: drop the loadable-tools announcements and strip `message.tools` * (dropping the message entirely when nothing else remains). Two callers: - * - projection for a model without the `select_tools` capability (mid-session - * model switch — the canonical history keeps its shape, only the outgoing - * view changes; announcements would be noise and even reference a - * select_tools tool the model does not have); + * - projection for a model without the dynamically-loaded-tools capability + * (mid-session model switch — the canonical history keeps its shape, only + * the outgoing view changes; announcements would be noise and even + * reference a select_tools tool the model does not have); * - the compaction summarizer input (schemas and announcements are protocol * context, not conversation — summarizing them wastes tokens and risks * leaking schema text into the summary). diff --git a/packages/agent-core/src/agent/context/index.ts b/packages/agent-core/src/agent/context/index.ts index b8951ba937..fcf9950517 100644 --- a/packages/agent-core/src/agent/context/index.ts +++ b/packages/agent-core/src/agent/context/index.ts @@ -376,11 +376,12 @@ export class ContextMemory { project(messages: readonly ContextMessage[], options?: ProjectOptions): Message[] { // Shape for the current model BEFORE projecting: a model without the - // select_tools capability must not see dynamic-tool schema messages or - // loadable-tools announcements (the canonical history keeps them; only - // this outgoing view is shaped). Must run pre-projection — project() - // strips `origin`, the only anchor for the announcements. setModel never - // rewrites history, so a mid-session switch degrades/upgrades losslessly. + // dynamically-loaded-tools capability must not see dynamic-tool schema + // messages or loadable-tools announcements (the canonical history keeps + // them; only this outgoing view is shaped). Must run pre-projection — + // project() strips `origin`, the only anchor for the announcements. + // setModel never rewrites history, so a mid-session switch + // degrades/upgrades losslessly. const shaped = this.agent.toolSelectEnabled ? messages : stripDynamicToolContext(messages); const anomalies: ProjectionAnomaly[] = []; const result = project(this.agent.microCompaction.compact(shaped), { diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 6b9d7cf19f..db3e6de282 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -236,18 +236,19 @@ export class Agent { /** * Single decision point for select_tools progressive disclosure. All three - * gates must be open: the model declares the `select_tools` capability, the - * model declares `tool_use` (a model without tool use registering - * select_tools is a contradiction), and the `tool-select` experimental flag - * is on. Every consumer — top-level tools[] convergence, select_tools - * registration, manifest announcements, projection shaping — reads this - * instead of re-deriving the conditions, so degradation is lossless: any - * closed gate reproduces the inline behavior byte-for-byte. + * gates must be open: the model has the `dynamically_loaded_tools` + * capability (message-level tool declarations), the model declares + * `tool_use` (a model without tool use loading tools dynamically is a + * contradiction), and the `tool-select` experimental flag is on. Every + * consumer — top-level tools[] convergence, select_tools registration, + * manifest announcements, projection shaping — reads this instead of + * re-deriving the conditions, so degradation is lossless: any closed gate + * reproduces the inline behavior byte-for-byte. */ get toolSelectEnabled(): boolean { const capability = this.config.modelCapabilities; return ( - capability.select_tools === true && + capability.dynamically_loaded_tools === true && capability.tool_use && this.experimentalFlags.enabled('tool-select') ); diff --git a/packages/agent-core/src/flags/registry.ts b/packages/agent-core/src/flags/registry.ts index 8a5fd1b3e8..dbec75b80b 100644 --- a/packages/agent-core/src/flags/registry.ts +++ b/packages/agent-core/src/flags/registry.ts @@ -27,7 +27,7 @@ export const FLAG_DEFINITIONS = [ id: 'tool-select', title: 'Tool select (progressive tool disclosure)', description: - 'Keep MCP tool schemas out of the immutable top-level tools[]; the model loads them on demand via the select_tools tool. Only takes effect on models whose capability catalog declares select_tools.', + 'Keep MCP tool schemas out of the immutable top-level tools[]; the model loads them on demand via the select_tools tool. Only takes effect on models whose capability catalog declares dynamically loaded tools.', env: 'KIMI_CODE_EXPERIMENTAL_TOOL_SELECT', default: false, surface: 'core', diff --git a/packages/agent-core/src/session/provider-manager.ts b/packages/agent-core/src/session/provider-manager.ts index b67aef3189..072ff44b11 100644 --- a/packages/agent-core/src/session/provider-manager.ts +++ b/packages/agent-core/src/session/provider-manager.ts @@ -233,10 +233,12 @@ function resolveModelCapabilities( thinking: declared.has('thinking') || declared.has('always_thinking') || detected.thinking, tool_use: declared.has('tool_use') || detected.tool_use, max_context_tokens: alias.maxContextSize, - // Message-level tool declarations (select_tools progressive disclosure). - // Every field here must be merged explicitly — a capability registered in + // Message-level tool declarations ("dynamically loaded tools"). Every + // field here must be merged explicitly — a capability registered in // kosong that is not forwarded here never reaches the agent. - select_tools: declared.has('select_tools') || detected.select_tools === true, + dynamically_loaded_tools: + declared.has('dynamically_loaded_tools') || + detected.dynamically_loaded_tools === true, }; } diff --git a/packages/agent-core/test/agent/harness/agent.ts b/packages/agent-core/test/agent/harness/agent.ts index f3ccef4c35..06041c2599 100644 --- a/packages/agent-core/test/agent/harness/agent.ts +++ b/packages/agent-core/test/agent/harness/agent.ts @@ -1094,7 +1094,7 @@ function capabilityNames(capabilities: ModelCapability | undefined): string[] { capabilities.audio_in ? 'audio_in' : undefined, capabilities.thinking ? 'thinking' : undefined, capabilities.tool_use ? 'tool_use' : undefined, - capabilities.select_tools === true ? 'select_tools' : undefined, + capabilities.dynamically_loaded_tools === true ? 'dynamically_loaded_tools' : undefined, ].filter((capability): capability is string => capability !== undefined); } diff --git a/packages/agent-core/test/agent/tool-select.e2e.test.ts b/packages/agent-core/test/agent/tool-select.e2e.test.ts index e26a0082f6..288f132189 100644 --- a/packages/agent-core/test/agent/tool-select.e2e.test.ts +++ b/packages/agent-core/test/agent/tool-select.e2e.test.ts @@ -2,7 +2,7 @@ * select_tools progressive disclosure — end-to-end agent tests. * * Uses the scripted-generate harness: real ToolManager/turn loop/context, fake - * LLM. The three-condition gate (model capability.select_tools × + * LLM. The three-condition gate (model capability.dynamically_loaded_tools × * capability.tool_use × `tool-select` flag) is driven through the alias * capability declarations and an injected FlagResolver. * @@ -34,7 +34,7 @@ const DISCLOSURE_CAPABILITIES = { thinking: false, tool_use: true, max_context_tokens: 256_000, - select_tools: true, + dynamically_loaded_tools: true, } as const; const INLINE_PROVIDER = { type: 'kimi', apiKey: 'test-key', model: 'inline-model' } as const; diff --git a/packages/kosong/src/capability.ts b/packages/kosong/src/capability.ts index 27d004860d..2f2299b88f 100644 --- a/packages/kosong/src/capability.ts +++ b/packages/kosong/src/capability.ts @@ -16,12 +16,13 @@ export interface ModelCapability { readonly tool_use: boolean; readonly max_context_tokens: number; /** - * Model accepts message-level tool declarations (`messages[].tools`), the - * primitive behind select_tools progressive disclosure. Absent means - * unsupported: only models explicitly catalogued or declared with this - * capability may ever receive a message carrying `tools`. + * Model accepts message-level tool declarations (`messages[].tools`) — the + * "dynamically loaded tools" wire feature that clients can drive with + * progressive tool disclosure. Absent means unsupported: only models + * explicitly catalogued or declared with this capability may ever receive a + * message carrying `tools`. */ - readonly select_tools?: boolean; + readonly dynamically_loaded_tools?: boolean; } const UNKNOWN_CAPABILITY_MARKER = Symbol.for('moonshot-ai.kosong.UNKNOWN_CAPABILITY'); @@ -40,7 +41,7 @@ export const UNKNOWN_CAPABILITY: ModelCapability = Object.freeze( thinking: false, tool_use: false, max_context_tokens: 0, - select_tools: false, + dynamically_loaded_tools: false, }, UNKNOWN_CAPABILITY_MARKER, { value: true }, @@ -58,7 +59,7 @@ export function isUnknownCapability(capability: ModelCapability): boolean { !capability.audio_in && !capability.thinking && !capability.tool_use && - capability.select_tools !== true && + capability.dynamically_loaded_tools !== true && capability.max_context_tokens === 0 ); } diff --git a/packages/kosong/src/catalog.ts b/packages/kosong/src/catalog.ts index ff1fb94379..ebb2e1daad 100644 --- a/packages/kosong/src/catalog.ts +++ b/packages/kosong/src/catalog.ts @@ -14,7 +14,7 @@ export interface CatalogModelEntry { readonly tool_call?: boolean; readonly reasoning?: boolean; /** Accepts message-level tool declarations (`messages[].tools`). Defaults to false. */ - readonly select_tools?: boolean; + readonly dynamically_loaded_tools?: boolean; readonly interleaved?: boolean | { readonly field?: string }; readonly modalities?: { readonly input?: readonly string[]; @@ -138,7 +138,7 @@ export function catalogModelToCapability(model: CatalogModelEntry): CatalogModel thinking: Boolean(model.reasoning), tool_use: model.tool_call ?? true, max_context_tokens: context, - select_tools: model.select_tools === true, + dynamically_loaded_tools: model.dynamically_loaded_tools === true, }, }; } diff --git a/packages/kosong/src/index.ts b/packages/kosong/src/index.ts index 01662bcbc7..e6bd57e627 100644 --- a/packages/kosong/src/index.ts +++ b/packages/kosong/src/index.ts @@ -34,7 +34,7 @@ export { KimiChatProvider } from './providers/kimi'; export type { ExtraBody, GenerationKwargs, KimiOptions, ThinkingConfig } from './providers/kimi'; // Model capability matrix -export { UNKNOWN_CAPABILITY, isUnknownCapability } from './capability'; +export { isUnknownCapability, UNKNOWN_CAPABILITY } from './capability'; export type { ModelCapability } from './capability'; // Model catalog (models.dev-style) metadata diff --git a/packages/kosong/test/catalog.test.ts b/packages/kosong/test/catalog.test.ts index 5afe778236..87538b92a3 100644 --- a/packages/kosong/test/catalog.test.ts +++ b/packages/kosong/test/catalog.test.ts @@ -82,7 +82,7 @@ describe('catalogModelToCapability', () => { thinking: true, tool_use: true, max_context_tokens: 200000, - select_tools: false, + dynamically_loaded_tools: false, }, }); }); diff --git a/packages/kosong/test/select-tools.test.ts b/packages/kosong/test/select-tools.test.ts index 1ce7ff3af7..9d2706eb96 100644 --- a/packages/kosong/test/select-tools.test.ts +++ b/packages/kosong/test/select-tools.test.ts @@ -7,7 +7,7 @@ * normalization and the `$` builtin branch shared with top-level tools); * - `Tool.deferred` stripping in `generate()` (single strip point for every * provider call — the marker itself must never reach the wire); - * - the `select_tools` capability bit (unknown/default-off semantics). + * - the `dynamically_loaded_tools` capability bit (unknown/default-off semantics). */ import { UNKNOWN_CAPABILITY, isUnknownCapability } from '#/capability'; @@ -324,30 +324,32 @@ describe('providers without message-level tool declarations', () => { }); }); -describe('select_tools capability bit', () => { +describe('dynamically_loaded_tools capability bit', () => { + const BASE_CAPABILITY = { + image_in: false, + video_in: false, + audio_in: false, + thinking: false, + tool_use: false, + max_context_tokens: 0, + }; + it('defaults to false on UNKNOWN_CAPABILITY', () => { - expect(UNKNOWN_CAPABILITY.select_tools).toBe(false); + expect(UNKNOWN_CAPABILITY.dynamically_loaded_tools).toBe(false); }); - it('a capability that only has select_tools is not "unknown"', () => { - expect( - isUnknownCapability({ - image_in: false, - video_in: false, - audio_in: false, - thinking: false, - tool_use: false, - max_context_tokens: 0, - select_tools: true, - }), - ).toBe(false); + it('a capability that only has the bit set is not "unknown"', () => { + expect(isUnknownCapability({ ...BASE_CAPABILITY, dynamically_loaded_tools: true })).toBe( + false, + ); }); - it('catalog entries map select_tools and default it to false', () => { + it('catalog entries map the capability, defaulting to false', () => { const base = { id: 'm', limit: { context: 1000 } }; - expect(catalogModelToCapability(base)?.capability.select_tools).toBe(false); + expect(catalogModelToCapability(base)?.capability.dynamically_loaded_tools).toBe(false); expect( - catalogModelToCapability({ ...base, select_tools: true })?.capability.select_tools, + catalogModelToCapability({ ...base, dynamically_loaded_tools: true })?.capability + .dynamically_loaded_tools, ).toBe(true); }); }); diff --git a/packages/node-sdk/src/catalog.ts b/packages/node-sdk/src/catalog.ts index f7a215e7c9..592a1647f4 100644 --- a/packages/node-sdk/src/catalog.ts +++ b/packages/node-sdk/src/catalog.ts @@ -47,7 +47,7 @@ function capabilityToStrings(capability: ModelCapability): string[] | undefined if (capability.audio_in) caps.push('audio_in'); if (capability.thinking) caps.push('thinking'); if (capability.tool_use) caps.push('tool_use'); - if (capability.select_tools === true) caps.push('select_tools'); + if (capability.dynamically_loaded_tools === true) caps.push('dynamically_loaded_tools'); return caps.length > 0 ? caps : undefined; } diff --git a/packages/node-sdk/test/config.test.ts b/packages/node-sdk/test/config.test.ts index 0601950548..9484facf1f 100644 --- a/packages/node-sdk/test/config.test.ts +++ b/packages/node-sdk/test/config.test.ts @@ -338,7 +338,7 @@ describe('KimiHarness config API', () => { id: 'tool-select', title: 'Tool select (progressive tool disclosure)', description: - 'Keep MCP tool schemas out of the immutable top-level tools[]; the model loads them on demand via the select_tools tool. Only takes effect on models whose capability catalog declares select_tools.', + 'Keep MCP tool schemas out of the immutable top-level tools[]; the model loads them on demand via the select_tools tool. Only takes effect on models whose capability catalog declares dynamically loaded tools.', surface: 'core', env: 'KIMI_CODE_EXPERIMENTAL_TOOL_SELECT', defaultEnabled: false,