From 9e646618eff5e01fb7fe691208023ecc1dc12f03 Mon Sep 17 00:00:00 2001 From: fengchenchen Date: Wed, 8 Jul 2026 15:13:40 +0800 Subject: [PATCH 1/2] refactor(kosong): rename select_tools capability to dynamically_loaded_tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the `ModelCapability` bit from `select_tools` to `dynamically_loaded_tools` everywhere it is declared, detected, catalogued, and forwarded: kosong `ModelCapability`/catalog, agent-core capability resolution and the `toolSelectEnabled` gate, the SDK catalog-to-alias mapping, and the built-in catalog pruner's keep list. The old `select_tools` spelling is removed outright rather than kept as an alias — no catalogued model or shipped configuration used the capability, so there is nothing to migrate. Client-side vocabulary (the `select_tools` builtin tool and the `tool-select` experimental flag) is intentionally untouched. --- .../dynamically-loaded-tools-capability.md | 7 ++++ .../select-tools-discard-on-compaction.md | 2 +- apps/kimi-code/scripts/update-catalog.mjs | 4 ++ .../src/agent/context/dynamic-tools.ts | 8 ++-- .../agent-core/src/agent/context/index.ts | 11 +++--- packages/agent-core/src/agent/index.ts | 17 +++++---- packages/agent-core/src/flags/registry.ts | 2 +- .../src/session/provider-manager.ts | 8 ++-- .../agent-core/test/agent/harness/agent.ts | 2 +- .../test/agent/tool-select.e2e.test.ts | 4 +- packages/kosong/src/capability.ts | 15 ++++---- packages/kosong/src/catalog.ts | 4 +- packages/kosong/src/index.ts | 2 +- packages/kosong/test/catalog.test.ts | 2 +- packages/kosong/test/select-tools.test.ts | 38 ++++++++++--------- packages/node-sdk/src/catalog.ts | 2 +- packages/node-sdk/test/config.test.ts | 2 +- 17 files changed, 74 insertions(+), 56 deletions(-) create mode 100644 .changeset/dynamically-loaded-tools-capability.md diff --git a/.changeset/dynamically-loaded-tools-capability.md b/.changeset/dynamically-loaded-tools-capability.md new file mode 100644 index 0000000000..0fabfadbc3 --- /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 to `dynamically_loaded_tools` (the model-level ability to accept message-level tool declarations). The previous `select_tools` spelling — which named the client-side mechanism rather than the model capability — is removed outright: no catalogued model or shipped configuration ever used it, so there is nothing to migrate. Model alias `capabilities` config, catalog entries, and the SDK's catalog-to-alias mapping all use the new name. diff --git a/.changeset/select-tools-discard-on-compaction.md b/.changeset/select-tools-discard-on-compaction.md index 70bda32d5c..cbadf54f76 100644 --- a/.changeset/select-tools-discard-on-compaction.md +++ b/.changeset/select-tools-discard-on-compaction.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": patch --- -Progressive tool disclosure (`select_tools`, experimental): compaction now discards the loaded tool schemas instead of re-injecting them. After a compaction the boundary announcement re-lists every loadable tool name and the model re-selects what it still needs; a from-memory call to a no-longer-loaded tool is rejected with guidance to select it first. This keeps the post-compaction context at its minimal users+summary floor and removes the schema-rebuild budget heuristics. No effect unless the `tool-select` experimental flag and a `select_tools`-capable model are active. +Progressive tool disclosure (`select_tools`, experimental): compaction now discards the loaded tool schemas instead of re-injecting them. After a compaction the boundary announcement re-lists every loadable tool name and the model re-selects what it still needs; a from-memory call to a no-longer-loaded tool is rejected with guidance to select it first. This keeps the post-compaction context at its minimal users+summary floor and removes the schema-rebuild budget heuristics. No effect unless the `tool-select` experimental flag and a model with the dynamically-loaded-tools capability are active. 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, From 7bfca29085d64f6cc66834a03f5999d37c9bfb22 Mon Sep 17 00:00:00 2001 From: fengchenchen Date: Wed, 8 Jul 2026 16:04:24 +0800 Subject: [PATCH 2/2] chore: shorten changeset description --- .changeset/dynamically-loaded-tools-capability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dynamically-loaded-tools-capability.md b/.changeset/dynamically-loaded-tools-capability.md index 0fabfadbc3..1ff8e84f24 100644 --- a/.changeset/dynamically-loaded-tools-capability.md +++ b/.changeset/dynamically-loaded-tools-capability.md @@ -4,4 +4,4 @@ "@moonshot-ai/kimi-code-sdk": patch --- -Rename the dynamic tool loading model capability to `dynamically_loaded_tools` (the model-level ability to accept message-level tool declarations). The previous `select_tools` spelling — which named the client-side mechanism rather than the model capability — is removed outright: no catalogued model or shipped configuration ever used it, so there is nothing to migrate. Model alias `capabilities` config, catalog entries, and the SDK's catalog-to-alias mapping all use the new name. +Rename the dynamic tool loading model capability from `select_tools` to `dynamically_loaded_tools`.