diff --git a/.changeset/compaction-dropped-count-wire-trace.md b/.changeset/compaction-dropped-count-wire-trace.md new file mode 100644 index 0000000000..6215dbd123 --- /dev/null +++ b/.changeset/compaction-dropped-count-wire-trace.md @@ -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. diff --git a/.changeset/dynamically-loaded-tools-capability-rename.md b/.changeset/dynamically-loaded-tools-capability-rename.md new file mode 100644 index 0000000000..c2ec3927ea --- /dev/null +++ b/.changeset/dynamically-loaded-tools-capability-rename.md @@ -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. diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index 3705f20d64..93fd8d0548 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -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. + logFields: { droppedCount }, + }, }, undefined, signal, diff --git a/packages/agent-core-v2/src/agent/toolSelect/flag.ts b/packages/agent-core-v2/src/agent/toolSelect/flag.ts index 4015a2b49c..c773314035 100644 --- a/packages/agent-core-v2/src/agent/toolSelect/flag.ts +++ b/packages/agent-core-v2/src/agent/toolSelect/flag.ts @@ -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', diff --git a/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts b/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts index 02529fc3f0..8e605a643a 100644 --- a/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts +++ b/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts @@ -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) ); diff --git a/packages/agent-core-v2/src/app/llmProtocol/capability.ts b/packages/agent-core-v2/src/app/llmProtocol/capability.ts index 52a3950126..de54d89bea 100644 --- a/packages/agent-core-v2/src/app/llmProtocol/capability.ts +++ b/packages/agent-core-v2/src/app/llmProtocol/capability.ts @@ -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'); @@ -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 }, @@ -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 ); } diff --git a/packages/agent-core-v2/src/app/llmProtocol/catalog.ts b/packages/agent-core-v2/src/app/llmProtocol/catalog.ts index 744de6229c..4fee00397e 100644 --- a/packages/agent-core-v2/src/app/llmProtocol/catalog.ts +++ b/packages/agent-core-v2/src/app/llmProtocol/catalog.ts @@ -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[]; @@ -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, }, }; } diff --git a/packages/agent-core-v2/src/app/model/modelResolverService.ts b/packages/agent-core-v2/src/app/model/modelResolverService.ts index 8eb737e577..175ce24943 100644 --- a/packages/agent-core-v2/src/app/model/modelResolverService.ts +++ b/packages/agent-core-v2/src/app/model/modelResolverService.ts @@ -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, }; } diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index 092bac1d09..b5f4d8f4fe 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -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], }); @@ -2500,6 +2500,23 @@ describe('FullCompaction', () => { }), }), ); + type WireRequestEvent = { + type: '[wire]'; + event: 'llm.request'; + args: Record; + }; + 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', diff --git a/packages/agent-core-v2/test/agent/llmRequester/llmRequester.test.ts b/packages/agent-core-v2/test/agent/llmRequester/llmRequester.test.ts index d547667600..5317c96eb8 100644 --- a/packages/agent-core-v2/test/agent/llmRequester/llmRequester.test.ts +++ b/packages/agent-core-v2/test/agent/llmRequester/llmRequester.test.ts @@ -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' }); diff --git a/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts b/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts index 09f62146bd..db6e07250f 100644 --- a/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts +++ b/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts @@ -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< diff --git a/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts b/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts index 068eed0f30..0e30e87d00 100644 --- a/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts +++ b/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts @@ -71,7 +71,7 @@ let activeToolNames: ReadonlySet | 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; }); @@ -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, @@ -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, }; } @@ -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); }); @@ -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)); diff --git a/packages/agent-core-v2/test/app/llmProtocol/select-tools.test.ts b/packages/agent-core-v2/test/app/llmProtocol/select-tools.test.ts index a0d55ac5fd..15fe14b13e 100644 --- a/packages/agent-core-v2/test/app/llmProtocol/select-tools.test.ts +++ b/packages/agent-core-v2/test/app/llmProtocol/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 '#/app/llmProtocol/capability'; @@ -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, @@ -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); }); }); diff --git a/packages/agent-core-v2/test/app/model/modelResolver.test.ts b/packages/agent-core-v2/test/app/model/modelResolver.test.ts index f1101da0ba..db9f76b103 100644 --- a/packages/agent-core-v2/test/app/model/modelResolver.test.ts +++ b/packages/agent-core-v2/test/app/model/modelResolver.test.ts @@ -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 () => { @@ -727,7 +727,7 @@ describe('ModelResolverService', () => { thinking: true, tool_use: false, max_context_tokens: 1000, - select_tools: false, + dynamically_loaded_tools: false, }); }); @@ -742,7 +742,7 @@ describe('ModelResolverService', () => { thinking: false, tool_use: true, max_context_tokens: 128000, - select_tools: false, + dynamically_loaded_tools: false, }); }); }); diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index b50fe23336..691cd956ba 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -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); }