Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compaction-dropped-count-wire-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add the number of messages dropped during compaction retries to the session wire log's LLM request traces.
5 changes: 5 additions & 0 deletions .changeset/dynamically-loaded-tools-capability-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Rename the dynamic tool loading model capability from `select_tools` to `dynamically_loaded_tools`, matching the model catalog vocabulary; the `select_tools` tool and the `tool-select` flag are unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,14 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
{
messages,
maxOutputSize: compactionMaxOutputSize,
source: { type: 'operation', requestKind: 'full_compaction' },
source: {
type: 'operation',
requestKind: 'full_compaction',
// Per-attempt count of messages dropped by overflow/empty
// shrinks so far; recorded on the llm.request wire op so a
// replay can see how much history each retry round blinded.
Comment on lines +536 to +538

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the inline implementation comment

packages/agent-core-v2/AGENTS.md says comments in this tree must live only in the top-of-file block and never beside statements. This new inline explanation violates that package rule; move any needed rationale into the file header or let logFields: { droppedCount } stand on its own.

Useful? React with 👍 / 👎.

logFields: { droppedCount },
},
},
undefined,
signal,
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/toolSelect/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const toolSelectFlag: FlagDefinitionInput = {
id: TOOL_SELECT_FLAG_ID,
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: TOOL_SELECT_FLAG_ENV,
default: false,
surface: 'core',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class AgentToolSelectService extends Disposable implements IAgentToolSele
enabled(): boolean {
const capabilities = this.profile.getModelCapabilities();
return (
capabilities.select_tools === true &&
capabilities.dynamically_loaded_tools === true &&
capabilities.tool_use &&
this.flags.enabled(TOOL_SELECT_FLAG_ID)
);
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/llmProtocol/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ModelCapability {
readonly thinking: boolean;
readonly tool_use: boolean;
readonly max_context_tokens: number;
readonly select_tools?: boolean;
readonly dynamically_loaded_tools?: boolean;
}

const UNKNOWN_CAPABILITY_MARKER = Symbol.for('moonshot-ai.kosong.UNKNOWN_CAPABILITY');
Expand All @@ -29,7 +29,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 },
Expand All @@ -47,7 +47,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
);
}
4 changes: 2 additions & 2 deletions packages/agent-core-v2/src/app/llmProtocol/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface CatalogModelEntry {
readonly limit?: { readonly context?: number; readonly output?: number };
readonly tool_call?: boolean;
readonly reasoning?: boolean;
readonly select_tools?: boolean;
readonly dynamically_loaded_tools?: boolean;
readonly interleaved?: boolean | { readonly field?: string };
readonly modalities?: {
readonly input?: readonly string[];
Expand Down Expand Up @@ -109,7 +109,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,
},
};
}
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-core-v2/src/app/model/modelResolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ function resolveModelCapabilities(
thinking: declared.has('thinking') || declared.has('always_thinking') || detected.thinking,
tool_use: declared.has('tool_use') || detected.tool_use,
max_context_tokens: maxContextSize,
select_tools: declared.has('select_tools') || detected.select_tools === true,
dynamically_loaded_tools:
declared.has('dynamically_loaded_tools') ||
detected.dynamically_loaded_tools === true,
Comment on lines +319 to +320

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the previous capability spelling during migration

For models configured before this rename with capabilities = ['select_tools'] (or KIMI_MODEL_CAPABILITIES=select_tools), this resolver now leaves dynamically_loaded_tools false because it only recognizes the new string; then AgentToolSelectService.enabled() never opens the progressive-disclosure gate even though the model was explicitly configured for it. Either keep declared.has('select_tools') as a compatibility alias or treat this as a breaking config migration rather than a patch.

Useful? React with 👍 / 👎.

};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ describe('FullCompaction', () => {
modelCapabilities: {
...CATALOGUED_MODEL_CAPABILITIES,
max_context_tokens: 2_000,
select_tools: true,
dynamically_loaded_tools: true,
},
tools: [LARGE_MCP_TOOL],
});
Expand Down Expand Up @@ -2500,6 +2500,23 @@ describe('FullCompaction', () => {
}),
}),
);
type WireRequestEvent = {
type: '[wire]';
event: 'llm.request';
args: Record<string, unknown>;
};
const requestEvents = events.filter((event): event is WireRequestEvent => {
if (event === null || typeof event !== 'object') return false;
const candidate = event as { type?: unknown; event?: unknown };
return candidate.type === '[wire]' && candidate.event === 'llm.request';
});
expect(
requestEvents.map((event) => [event.args['kind'], event.args['droppedCount']]),
).toEqual([
['compaction', 0],
['compaction', 2],
['loop', undefined],
]);
expect(events).toContainEqual(
expect.objectContaining({
event: 'turn.ended',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('LLMRequester service migration coverage', () => {
thinking: false,
tool_use: true,
max_context_tokens: 128_000,
select_tools: true,
dynamically_loaded_tools: true,
},
});
ctx.mockNextResponse({ type: 'text', text: 'first response' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const DISCLOSURE_CAPABILITIES = {
thinking: false,
tool_use: true,
max_context_tokens: 128_000,
select_tools: true,
dynamically_loaded_tools: true,
} as const;

type WireEvent = Extract<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ let activeToolNames: ReadonlySet<string> | undefined;

beforeEach(() => {
disposables = new DisposableStore();
capabilities = makeCapabilities({ tool_use: true, select_tools: true });
capabilities = makeCapabilities({ tool_use: true, dynamically_loaded_tools: true });
flagEnabled = false;
activeToolNames = undefined;
});
Expand All @@ -80,7 +80,7 @@ afterEach(() => disposables.dispose());

function makeCapabilities(overrides: {
readonly tool_use?: boolean;
readonly select_tools?: boolean;
readonly dynamically_loaded_tools?: boolean;
} = {}): ModelCapability {
return {
image_in: false,
Expand All @@ -89,7 +89,7 @@ function makeCapabilities(overrides: {
thinking: false,
tool_use: overrides.tool_use ?? false,
max_context_tokens: 128_000,
select_tools: overrides.select_tools,
dynamically_loaded_tools: overrides.dynamically_loaded_tools,
};
}

Expand Down Expand Up @@ -404,22 +404,22 @@ async function execute(
}

describe('AgentToolSelectService gate', () => {
it('opens only when select_tools capability, tool_use capability and flag are all on', () => {
it('opens only when dynamically_loaded_tools capability, tool_use capability and flag are all on', () => {
flagEnabled = true;
const { sut } = createHarness();
expect(sut.enabled()).toBe(true);
});

it('stays closed without the select_tools capability', () => {
it('stays closed without the dynamically_loaded_tools capability', () => {
flagEnabled = true;
capabilities = makeCapabilities({ tool_use: true, select_tools: false });
capabilities = makeCapabilities({ tool_use: true, dynamically_loaded_tools: false });
const { sut } = createHarness();
expect(sut.enabled()).toBe(false);
});

it('stays closed without tool_use capability', () => {
flagEnabled = true;
capabilities = makeCapabilities({ tool_use: false, select_tools: true });
capabilities = makeCapabilities({ tool_use: false, dynamically_loaded_tools: true });
const { sut } = createHarness();
expect(sut.enabled()).toBe(false);
});
Expand All @@ -432,7 +432,7 @@ describe('AgentToolSelectService gate', () => {
});

describe('AgentToolSelectService S0 baseline (gate closed)', () => {
it('shapeTools returns the identical array when select_tools is absent', () => {
it('shapeTools returns the identical array when dynamically_loaded_tools is absent', () => {
const h = createHarness();
registerBuiltin(h, new EchoTool());
registerMcp(h, new StubMcpTool(MCP_ALPHA));
Expand Down
17 changes: 9 additions & 8 deletions packages/agent-core-v2/test/app/llmProtocol/select-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '#/app/llmProtocol/capability';
Expand Down Expand Up @@ -318,12 +318,12 @@ describe('providers without message-level tool declarations', () => {
});
});

describe('select_tools capability bit', () => {
describe('dynamically_loaded_tools capability bit', () => {
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"', () => {
it('a capability that only has dynamically_loaded_tools is not "unknown"', () => {
expect(
isUnknownCapability({
image_in: false,
Expand All @@ -332,16 +332,17 @@ describe('select_tools capability bit', () => {
thinking: false,
tool_use: false,
max_context_tokens: 0,
select_tools: true,
dynamically_loaded_tools: true,
}),
).toBe(false);
});

it('catalog entries map select_tools and default it to false', () => {
it('catalog entries map dynamically_loaded_tools and default it 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);
});
});
10 changes: 5 additions & 5 deletions packages/agent-core-v2/test/app/model/modelResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,16 @@ describe('ModelResolverService', () => {
expect(auth).toEqual({ apiKey: 'sk-model' });
});

it('forwards declared select_tools capability to the resolved model', () => {
it('forwards declared dynamically_loaded_tools capability to the resolved model', () => {
providers['p'] = { type: 'kimi', baseUrl: 'https://example.test/v1', apiKey: 'sk-test' };
models['m'] = {
provider: 'p',
model: 'wire-name',
maxContextSize: 1000,
capabilities: ['select_tools'],
capabilities: ['dynamically_loaded_tools'],
};

expect(ix.get(IModelResolver).resolve('m').capabilities.select_tools).toBe(true);
expect(ix.get(IModelResolver).resolve('m').capabilities.dynamically_loaded_tools).toBe(true);
});

it('returns an OAuth access token as ProviderRequestAuth.apiKey', async () => {
Expand Down Expand Up @@ -727,7 +727,7 @@ describe('ModelResolverService', () => {
thinking: true,
tool_use: false,
max_context_tokens: 1000,
select_tools: false,
dynamically_loaded_tools: false,
});
});

Expand All @@ -742,7 +742,7 @@ describe('ModelResolverService', () => {
thinking: false,
tool_use: true,
max_context_tokens: 128000,
select_tools: false,
dynamically_loaded_tools: false,
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/test/harness/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2217,7 +2217,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 ? 'select_tools' : undefined,
capabilities.dynamically_loaded_tools ? 'dynamically_loaded_tools' : undefined,
].filter((capability): capability is string => capability !== undefined);
}

Expand Down
Loading