Skip to content

Commit 18df871

Browse files
author
fengchenchen
committed
refactor(kosong)!: drop the select_tools capability spelling entirely
No catalogued model or shipped configuration ever used the capability, so there is nothing to migrate: remove the deprecated select_tools field/config-string acceptance and the hasDynamicallyLoadedTools compat helper. dynamically_loaded_tools is the only spelling — read directly where consumed. The select_tools builtin tool and the tool-select flag keep their names (client-side mechanism vocabulary).
1 parent dcdc3df commit 18df871

11 files changed

Lines changed: 25 additions & 107 deletions

File tree

.changeset/dynamically-loaded-tools-capability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"@moonshot-ai/kimi-code-sdk": patch
55
---
66

7-
Rename the dynamic tool loading model capability to its canonical name `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 — remains fully supported as a deprecated alias in model alias `capabilities` config and in catalog entries, so no configuration changes are required. The SDK's catalog-to-alias mapping now emits the canonical name.
7+
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.

apps/kimi-code/scripts/update-catalog.mjs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ const KEEP_MODEL = new Set([
2424
"reasoning",
2525
"interleaved",
2626
"modalities",
27-
// Message-level tool declarations capability (canonical + deprecated
28-
// spelling) — kosong's catalogModelToCapability reads both; stripping them
29-
// here would silently disable tool-select for catalog-imported aliases.
27+
// Message-level tool declarations capability — kosong's
28+
// catalogModelToCapability reads it; stripping it here would silently
29+
// disable tool-select for catalog-imported aliases.
3030
"dynamically_loaded_tools",
31-
"select_tools",
3231
]);
3332

3433
function resolveOutputFile(args) {

packages/agent-core/src/agent/index.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ErrorCodes, KimiError, makeErrorPayload } from '#/errors';
66
import { log } from '#/logging/logger';
77
import type { Logger } from '#/logging/types';
88
import type { AgentAPI, AgentEvent, KimiConfig, SDKAgentRPC, UsageStatus } from '#/rpc';
9-
import { generate, hasDynamicallyLoadedTools } from '@moonshot-ai/kosong';
9+
import { generate } from '@moonshot-ai/kosong';
1010

1111
import type { EnabledPluginSessionStart, PluginCommandDef } from '#/plugin';
1212
import { expandCommandArguments } from '../plugin/commands';
@@ -236,20 +236,19 @@ export class Agent {
236236

237237
/**
238238
* Single decision point for select_tools progressive disclosure. All three
239-
* gates must be open: the model has the dynamically-loaded-tools capability
240-
* (message-level tool declarations; the deprecated `select_tools` spelling
241-
* is honored via hasDynamicallyLoadedTools), the model declares `tool_use`
242-
* (a model without tool use loading tools dynamically is a contradiction),
243-
* and the `tool-select` experimental flag is on. Every consumer — top-level
244-
* tools[] convergence, select_tools registration, manifest announcements,
245-
* projection shaping — reads this instead of re-deriving the conditions, so
246-
* degradation is lossless: any closed gate reproduces the inline behavior
247-
* byte-for-byte.
239+
* gates must be open: the model has the `dynamically_loaded_tools`
240+
* capability (message-level tool declarations), the model declares
241+
* `tool_use` (a model without tool use loading tools dynamically is a
242+
* contradiction), and the `tool-select` experimental flag is on. Every
243+
* consumer — top-level tools[] convergence, select_tools registration,
244+
* manifest announcements, projection shaping — reads this instead of
245+
* re-deriving the conditions, so degradation is lossless: any closed gate
246+
* reproduces the inline behavior byte-for-byte.
248247
*/
249248
get toolSelectEnabled(): boolean {
250249
const capability = this.config.modelCapabilities;
251250
return (
252-
hasDynamicallyLoadedTools(capability) &&
251+
capability.dynamically_loaded_tools === true &&
253252
capability.tool_use &&
254253
this.experimentalFlags.enabled('tool-select')
255254
);

packages/agent-core/src/session/provider-manager.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Logger } from '#/logging/types';
22
import type { ProviderConfig as KosongProviderConfig, ModelCapability, ProviderRequestAuth } from '@moonshot-ai/kosong';
3-
import { APIStatusError, getModelCapability, hasDynamicallyLoadedTools, UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong';
3+
import { APIStatusError, getModelCapability, UNKNOWN_CAPABILITY } from '@moonshot-ai/kosong';
44
import { parseKimiCodeCustomHeaders } from '@moonshot-ai/kimi-code-oauth';
55
import {
66
effectiveModelAlias,
@@ -235,12 +235,10 @@ function resolveModelCapabilities(
235235
max_context_tokens: alias.maxContextSize,
236236
// Message-level tool declarations ("dynamically loaded tools"). Every
237237
// field here must be merged explicitly — a capability registered in
238-
// kosong that is not forwarded here never reaches the agent. The
239-
// deprecated `select_tools` spelling stays accepted in alias configs.
238+
// kosong that is not forwarded here never reaches the agent.
240239
dynamically_loaded_tools:
241240
declared.has('dynamically_loaded_tools') ||
242-
declared.has('select_tools') ||
243-
hasDynamicallyLoadedTools(detected),
241+
detected.dynamically_loaded_tools === true,
244242
};
245243
}
246244

packages/agent-core/test/agent/harness/agent.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,9 +1095,6 @@ function capabilityNames(capabilities: ModelCapability | undefined): string[] {
10951095
capabilities.thinking ? 'thinking' : undefined,
10961096
capabilities.tool_use ? 'tool_use' : undefined,
10971097
capabilities.dynamically_loaded_tools === true ? 'dynamically_loaded_tools' : undefined,
1098-
// Deliberately kept as the legacy string so tests exercising the
1099-
// deprecated field also exercise the deprecated declared-name path.
1100-
capabilities.select_tools === true ? 'select_tools' : undefined,
11011098
].filter((capability): capability is string => capability !== undefined);
11021099
}
11031100

packages/agent-core/test/agent/tool-select.e2e.test.ts

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,6 @@ const DISCLOSURE_CAPABILITIES = {
3737
dynamically_loaded_tools: true,
3838
} as const;
3939

40-
// Same capability through the deprecated field name — must keep working
41-
// (declared as the legacy `select_tools` capability string by the harness).
42-
const LEGACY_DISCLOSURE_CAPABILITIES = {
43-
image_in: false,
44-
video_in: false,
45-
audio_in: false,
46-
thinking: false,
47-
tool_use: true,
48-
max_context_tokens: 256_000,
49-
select_tools: true,
50-
} as const;
51-
5240
const INLINE_PROVIDER = { type: 'kimi', apiKey: 'test-key', model: 'inline-model' } as const;
5341
const INLINE_CAPABILITIES = {
5442
image_in: false,
@@ -232,31 +220,6 @@ describe('disclosure mode — top-level convergence and announcements', () => {
232220
expect(JSON.stringify(call.history)).toContain(GRAFANA_TOOL);
233221
});
234222

235-
it('honors the deprecated select_tools capability spelling end to end', async () => {
236-
// Declared through the harness as the legacy `select_tools` capability
237-
// string; the gate, convergence, select and dispatch must all still work.
238-
const callLog: Array<[string, unknown]> = [];
239-
const ctx = testAgent({ experimentalFlags: toolSelectFlagOn() });
240-
ctx.configure({
241-
tools: ['Read', 'mcp__*'],
242-
provider: DISCLOSURE_PROVIDER,
243-
modelCapabilities: LEGACY_DISCLOSURE_CAPABILITIES,
244-
});
245-
await registerGrafana(ctx, callLog);
246-
await ctx.rpc.setPermission({ mode: 'yolo' });
247-
expect(ctx.agent.toolSelectEnabled).toBe(true);
248-
249-
ctx.mockNextResponse({ type: 'text', text: 'loading' }, selectCall('call-1', [GRAFANA_TOOL]));
250-
ctx.mockNextResponse({ type: 'text', text: 'querying' }, mcpCall('call-2', 'errors'));
251-
ctx.mockNextResponse({ type: 'text', text: 'done' });
252-
await runTurn(ctx, 'use the tool');
253-
254-
const wireNames = ctx.llmCalls.at(-3)!.tools.map((t) => t.name);
255-
expect(wireNames).toContain('select_tools');
256-
expect(wireNames.some((n) => n.startsWith('mcp__'))).toBe(false);
257-
expect(callLog).toEqual([['query_range', { query: 'errors' }]]);
258-
});
259-
260223
it('does not re-announce when the loadable set is unchanged', async () => {
261224
const ctx = await disclosureAgent();
262225
ctx.mockNextResponse({ type: 'text', text: 'one' });

packages/kosong/src/capability.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,6 @@ export interface ModelCapability {
2323
* message carrying `tools`.
2424
*/
2525
readonly dynamically_loaded_tools?: boolean;
26-
/**
27-
* @deprecated Legacy alias of {@link ModelCapability.dynamically_loaded_tools}
28-
* (this bit originally shipped under the client-side mechanism's tool name).
29-
* Still honored everywhere via {@link hasDynamicallyLoadedTools}; write the
30-
* canonical field instead.
31-
*/
32-
readonly select_tools?: boolean;
33-
}
34-
35-
/**
36-
* True when the model accepts message-level tool declarations. Single read
37-
* point for the capability so the deprecated `select_tools` spelling keeps
38-
* working wherever the bit is consumed.
39-
*/
40-
export function hasDynamicallyLoadedTools(capability: ModelCapability): boolean {
41-
return capability.dynamically_loaded_tools === true || capability.select_tools === true;
4226
}
4327

4428
const UNKNOWN_CAPABILITY_MARKER = Symbol.for('moonshot-ai.kosong.UNKNOWN_CAPABILITY');
@@ -57,9 +41,6 @@ export const UNKNOWN_CAPABILITY: ModelCapability = Object.freeze(
5741
thinking: false,
5842
tool_use: false,
5943
max_context_tokens: 0,
60-
// Only the canonical spelling in the default output shape; the
61-
// deprecated `select_tools` field is accepted on input but never
62-
// emitted (this object reaches RPC/SDK consumers via config snapshots).
6344
dynamically_loaded_tools: false,
6445
},
6546
UNKNOWN_CAPABILITY_MARKER,
@@ -78,7 +59,7 @@ export function isUnknownCapability(capability: ModelCapability): boolean {
7859
!capability.audio_in &&
7960
!capability.thinking &&
8061
!capability.tool_use &&
81-
!hasDynamicallyLoadedTools(capability) &&
62+
capability.dynamically_loaded_tools !== true &&
8263
capability.max_context_tokens === 0
8364
);
8465
}

packages/kosong/src/catalog.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ export interface CatalogModelEntry {
1515
readonly reasoning?: boolean;
1616
/** Accepts message-level tool declarations (`messages[].tools`). Defaults to false. */
1717
readonly dynamically_loaded_tools?: boolean;
18-
/** @deprecated Legacy alias of `dynamically_loaded_tools`; still accepted. */
19-
readonly select_tools?: boolean;
2018
readonly interleaved?: boolean | { readonly field?: string };
2119
readonly modalities?: {
2220
readonly input?: readonly string[];
@@ -140,8 +138,7 @@ export function catalogModelToCapability(model: CatalogModelEntry): CatalogModel
140138
thinking: Boolean(model.reasoning),
141139
tool_use: model.tool_call ?? true,
142140
max_context_tokens: context,
143-
dynamically_loaded_tools:
144-
model.dynamically_loaded_tools === true || model.select_tools === true,
141+
dynamically_loaded_tools: model.dynamically_loaded_tools === true,
145142
},
146143
};
147144
}

packages/kosong/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export { KimiChatProvider } from './providers/kimi';
3434
export type { ExtraBody, GenerationKwargs, KimiOptions, ThinkingConfig } from './providers/kimi';
3535

3636
// Model capability matrix
37-
export { hasDynamicallyLoadedTools, isUnknownCapability, UNKNOWN_CAPABILITY } from './capability';
37+
export { isUnknownCapability, UNKNOWN_CAPABILITY } from './capability';
3838
export type { ModelCapability } from './capability';
3939

4040
// Model catalog (models.dev-style) metadata

packages/kosong/test/select-tools.test.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
* normalization and the `$` builtin branch shared with top-level tools);
88
* - `Tool.deferred` stripping in `generate()` (single strip point for every
99
* provider call — the marker itself must never reach the wire);
10-
* - the `dynamically_loaded_tools` capability bit (unknown/default-off
11-
* semantics, deprecated `select_tools` spelling honored).
10+
* - the `dynamically_loaded_tools` capability bit (unknown/default-off semantics).
1211
*/
1312

14-
import { UNKNOWN_CAPABILITY, hasDynamicallyLoadedTools, isUnknownCapability } from '#/capability';
13+
import { UNKNOWN_CAPABILITY, isUnknownCapability } from '#/capability';
1514
import { catalogModelToCapability } from '#/catalog';
1615
import { generate } from '#/generate';
1716
import { isToolDeclarationOnlyMessage } from '#/message';
@@ -337,34 +336,20 @@ describe('dynamically_loaded_tools capability bit', () => {
337336

338337
it('defaults to false on UNKNOWN_CAPABILITY', () => {
339338
expect(UNKNOWN_CAPABILITY.dynamically_loaded_tools).toBe(false);
340-
expect(hasDynamicallyLoadedTools(UNKNOWN_CAPABILITY)).toBe(false);
341339
});
342340

343-
it('honors both the canonical and the deprecated select_tools spelling', () => {
344-
expect(
345-
hasDynamicallyLoadedTools({ ...BASE_CAPABILITY, dynamically_loaded_tools: true }),
346-
).toBe(true);
347-
expect(hasDynamicallyLoadedTools({ ...BASE_CAPABILITY, select_tools: true })).toBe(true);
348-
expect(hasDynamicallyLoadedTools(BASE_CAPABILITY)).toBe(false);
349-
});
350-
351-
it('a capability that only has the bit set is not "unknown" (either spelling)', () => {
341+
it('a capability that only has the bit set is not "unknown"', () => {
352342
expect(isUnknownCapability({ ...BASE_CAPABILITY, dynamically_loaded_tools: true })).toBe(
353343
false,
354344
);
355-
expect(isUnknownCapability({ ...BASE_CAPABILITY, select_tools: true })).toBe(false);
356345
});
357346

358-
it('catalog entries map both spellings onto the canonical field, defaulting to false', () => {
347+
it('catalog entries map the capability, defaulting to false', () => {
359348
const base = { id: 'm', limit: { context: 1000 } };
360349
expect(catalogModelToCapability(base)?.capability.dynamically_loaded_tools).toBe(false);
361350
expect(
362351
catalogModelToCapability({ ...base, dynamically_loaded_tools: true })?.capability
363352
.dynamically_loaded_tools,
364353
).toBe(true);
365-
expect(
366-
catalogModelToCapability({ ...base, select_tools: true })?.capability
367-
.dynamically_loaded_tools,
368-
).toBe(true);
369354
});
370355
});

0 commit comments

Comments
 (0)