mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-25 16:46:17 +00:00
feat(agent-core-v2): record turn-level tool repeats (#3209)
* feat(agent-core-v2): record turn-level tool repeats * fix(agent-core-v2): bound turn repeat signatures
This commit is contained in:
parent
496bb6ce4e
commit
2d00599010
4 changed files with 169 additions and 2 deletions
|
|
@ -27,7 +27,7 @@
|
|||
// references become '(circular)', and class instances collapse to a '(ClassName)'
|
||||
// marker — the wire shape of an entry is the JSON projection of the type here.
|
||||
//
|
||||
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 78 keys)
|
||||
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 80 keys)
|
||||
// App
|
||||
// Workspace
|
||||
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
|
||||
|
|
@ -118,6 +118,8 @@
|
|||
// toolDedupe.originalCallIndex src/agent/toolDedupe/toolDedupeService.ts
|
||||
// toolDedupe.stepCalls src/agent/toolDedupe/toolDedupeService.ts
|
||||
// toolDedupe.syntheticCallIds src/agent/toolDedupe/toolDedupeService.ts
|
||||
// toolDedupe.turnCallRecords src/agent/toolDedupe/toolDedupeService.ts
|
||||
// toolDedupe.turnRepeatCount src/agent/toolDedupe/toolDedupeService.ts
|
||||
// toolExecutor.dupTypeTurnId src/agent/toolExecutor/toolExecutorService.ts
|
||||
// toolExecutor.toolCallDupTypes src/agent/toolExecutor/toolExecutorService.ts
|
||||
// toolSelect.pendingLoaded src/agent/toolSelect/toolSelectService.ts
|
||||
|
|
@ -1415,6 +1417,11 @@ export interface AgentStateSnapshot {
|
|||
'toolDedupe.originalCallIndex': Map<string, number>;
|
||||
'toolDedupe.stepCalls': string[];
|
||||
'toolDedupe.syntheticCallIds': Set<string>;
|
||||
'toolDedupe.turnCallRecords': Map<string, /* TurnCallRecord — packages/agent-core-v2/src/agent/toolDedupe/toolDedupeService.ts */ {
|
||||
count: number;
|
||||
lastStep: number;
|
||||
}>;
|
||||
'toolDedupe.turnRepeatCount': number;
|
||||
// src/agent/toolExecutor/toolExecutorService.ts
|
||||
'toolExecutor.dupTypeTurnId': number | undefined;
|
||||
'toolExecutor.toolCallDupTypes': Map<string, /* ToolCallDupType — packages/agent-core-v2/src/agent/toolExecutor/toolExecutor.ts */ 'same_step' | 'cross_step'>;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,18 @@ import { LifecycleScope } from '#/app/scopes';
|
|||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { defineState } from '#/state/state';
|
||||
import { canonicalTelemetryArgs } from '#/_base/utils/canonical-args';
|
||||
import type { ToolCallDedupDetectedEvent, ToolCallRepeatEvent } from '#/app/telemetry/events';
|
||||
import type {
|
||||
ToolCallDedupDetectedEvent,
|
||||
ToolCallRepeatEvent,
|
||||
ToolCallTurnRepeatEvent,
|
||||
} from '#/app/telemetry/events';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import type { LLMRequestTrace } from '#/kosong/contract/requestTrace';
|
||||
import { parseToolCallArguments } from '#/tool/tool-args-parse';
|
||||
import { IAgentLoopService } from '#/agent/loop/loop';
|
||||
import { IAgentStateService } from '#/agent/state/agentState';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { TurnEnded } from '#/agent/loop/turnOps';
|
||||
import { wrapSystemReminder } from '#/agent/systemReminder/systemReminder';
|
||||
import { IAgentToolExecutorService, type ToolCallDupType } from '#/agent/toolExecutor/toolExecutor';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
|
@ -71,10 +77,19 @@ function argsHash(args: unknown): string {
|
|||
return createHash('sha256').update(canonicalTelemetryArgs(args)).digest('hex').slice(0, 8);
|
||||
}
|
||||
|
||||
function callSignature(key: string): string {
|
||||
return createHash('sha256').update(key).digest('hex');
|
||||
}
|
||||
|
||||
interface CheckedToolCall {
|
||||
readonly syntheticResult: ToolDedupeResult | null;
|
||||
}
|
||||
|
||||
interface TurnCallRecord {
|
||||
count: number;
|
||||
lastStep: number;
|
||||
}
|
||||
|
||||
function appendReminder(result: ToolDedupeResult, reminderText: string): ToolDedupeResult {
|
||||
const output = result.output;
|
||||
let newOutput: string | ContentPart[];
|
||||
|
|
@ -128,6 +143,14 @@ export const toolDedupeActiveTurnIdKey = defineState<number | undefined>(
|
|||
() => undefined as number | undefined,
|
||||
);
|
||||
export const toolDedupeActiveStepKey = defineState<number>('toolDedupe.activeStep', () => 0);
|
||||
export const toolDedupeTurnCallRecordsKey = defineState<Map<string, TurnCallRecord>>(
|
||||
'toolDedupe.turnCallRecords',
|
||||
() => new Map(),
|
||||
);
|
||||
export const toolDedupeTurnRepeatCountKey = defineState<number>(
|
||||
'toolDedupe.turnRepeatCount',
|
||||
() => 0,
|
||||
);
|
||||
|
||||
export class AgentToolDedupeService extends Service implements IAgentToolDedupeService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
|
@ -138,6 +161,7 @@ export class AgentToolDedupeService extends Service implements IAgentToolDedupeS
|
|||
@IAgentLoopService loop: IAgentLoopService,
|
||||
@IAgentToolExecutorService private readonly toolExecutor: IAgentToolExecutorService,
|
||||
@IAgentStateService private readonly states: IAgentStateService,
|
||||
@IEventBus eventBus: IEventBus,
|
||||
) {
|
||||
super();
|
||||
this.states.contributeState(toolDedupeStepCallsKey);
|
||||
|
|
@ -148,6 +172,9 @@ export class AgentToolDedupeService extends Service implements IAgentToolDedupeS
|
|||
this.states.contributeState(toolDedupeConsecutiveCountKey);
|
||||
this.states.contributeState(toolDedupeActiveTurnIdKey);
|
||||
this.states.contributeState(toolDedupeActiveStepKey);
|
||||
this.states.contributeState(toolDedupeTurnCallRecordsKey);
|
||||
this.states.contributeState(toolDedupeTurnRepeatCountKey);
|
||||
this._register(eventBus.subscribe(TurnEnded, () => this.clearTurnRecords()));
|
||||
loop.hooks.onWillBeginStep.register('toolDedupe', async (ctx, next) => {
|
||||
this.beginStep(ctx.turnId, ctx.step);
|
||||
await next();
|
||||
|
|
@ -241,11 +268,29 @@ export class AgentToolDedupeService extends Service implements IAgentToolDedupeS
|
|||
this.states.set(toolDedupeActiveStepKey, value);
|
||||
}
|
||||
|
||||
private get turnCallRecords(): Map<string, TurnCallRecord> {
|
||||
return this.states.get(toolDedupeTurnCallRecordsKey);
|
||||
}
|
||||
|
||||
private get turnRepeatCount(): number {
|
||||
return this.states.get(toolDedupeTurnRepeatCountKey);
|
||||
}
|
||||
|
||||
private set turnRepeatCount(value: number) {
|
||||
this.states.set(toolDedupeTurnRepeatCountKey, value);
|
||||
}
|
||||
|
||||
private clearTurnRecords(): void {
|
||||
this.turnCallRecords.clear();
|
||||
this.turnRepeatCount = 0;
|
||||
}
|
||||
|
||||
private beginStep(turnId?: number, step?: number): void {
|
||||
if (turnId !== undefined && turnId !== this.activeTurnId) {
|
||||
this.activeTurnId = turnId;
|
||||
this.consecutiveKey = null;
|
||||
this.consecutiveCount = 0;
|
||||
this.clearTurnRecords();
|
||||
}
|
||||
if (step !== undefined) {
|
||||
this.activeStep = step;
|
||||
|
|
@ -275,6 +320,36 @@ export class AgentToolDedupeService extends Service implements IAgentToolDedupeS
|
|||
}
|
||||
}
|
||||
|
||||
private recordTurnRepeat(
|
||||
toolCallId: string,
|
||||
toolName: string,
|
||||
args: unknown,
|
||||
key: string,
|
||||
trace: LLMRequestTrace | undefined,
|
||||
): void {
|
||||
const signature = callSignature(key);
|
||||
const record = this.turnCallRecords.get(signature);
|
||||
if (record === undefined) {
|
||||
this.turnCallRecords.set(signature, { count: 0, lastStep: this.activeStep });
|
||||
return;
|
||||
}
|
||||
if (record.lastStep === this.activeStep) return;
|
||||
|
||||
record.count += 1;
|
||||
record.lastStep = this.activeStep;
|
||||
this.turnRepeatCount += 1;
|
||||
const properties: ToolCallTurnRepeatEvent = {
|
||||
turn_id: this.activeTurnId,
|
||||
step_no: this.activeStep,
|
||||
tool_call_id: toolCallId,
|
||||
tool_name: toolName,
|
||||
turn_repeat_count: this.turnRepeatCount,
|
||||
args_hash: argsHash(args),
|
||||
trace_id: trace?.traceId,
|
||||
};
|
||||
this.telemetry.track2('tool_call_turn_repeat', properties);
|
||||
}
|
||||
|
||||
private checkToolCall(
|
||||
toolCallId: string,
|
||||
toolName: string,
|
||||
|
|
@ -292,6 +367,7 @@ export class AgentToolDedupeService extends Service implements IAgentToolDedupeS
|
|||
this.recordDupType(toolCallId, toolName, args, 'same_step', trace);
|
||||
return { syntheticResult: DEDUPE_PLACEHOLDER_RESULT };
|
||||
}
|
||||
this.recordTurnRepeat(toolCallId, toolName, args, key, trace);
|
||||
this.stepDeferreds.set(key, makeDeferred<ToolDedupeResult>());
|
||||
this.originalCallIndex.set(toolCallId, index);
|
||||
if (this.consecutiveKey === key && this.consecutiveCount > 0) {
|
||||
|
|
|
|||
|
|
@ -325,6 +325,16 @@ export interface ToolCallRepeatEvent {
|
|||
trace_id?: string;
|
||||
}
|
||||
|
||||
export interface ToolCallTurnRepeatEvent {
|
||||
turn_id?: number;
|
||||
step_no: number;
|
||||
tool_call_id: string;
|
||||
tool_name: string;
|
||||
turn_repeat_count: number;
|
||||
args_hash: string;
|
||||
trace_id?: string;
|
||||
}
|
||||
|
||||
export interface AgentsMdReminderShownEvent {
|
||||
turn_id: number;
|
||||
tool_name: string;
|
||||
|
|
@ -830,6 +840,20 @@ export const telemetryEventDefinitions = {
|
|||
'Trace id of the LLM request that produced the repeated tool call; absent for non-Kimi protocols',
|
||||
},
|
||||
}),
|
||||
tool_call_turn_repeat: defineAgentTelemetryEvent<ToolCallTurnRepeatEvent>({
|
||||
owner: 'kimi-code',
|
||||
comment: 'A tool call reappears within the same turn.',
|
||||
properties: {
|
||||
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session; omitted when no turn is active',
|
||||
step_no: 'Step index within the turn',
|
||||
tool_call_id: 'Provider-assigned tool call id',
|
||||
tool_name: 'Registered tool name',
|
||||
turn_repeat_count: 'Number of prior-step tool-call reappearances counted in the turn',
|
||||
args_hash: 'Hash of the tool call arguments',
|
||||
trace_id:
|
||||
'Trace id of the LLM request that produced the repeated tool call; absent for non-Kimi protocols',
|
||||
},
|
||||
}),
|
||||
agents_md_reminder_shown: defineAgentTelemetryEvent<AgentsMdReminderShownEvent>({
|
||||
owner: 'kimi-code',
|
||||
comment: 'An AGENTS.md discovery reminder is appended to a tool result.',
|
||||
|
|
|
|||
|
|
@ -702,6 +702,66 @@ describe('AgentToolDedupeService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('counts interleaved tool calls across a turn without injecting a reminder', async () => {
|
||||
const h = createHarness();
|
||||
h.registry.register(new EchoTool('A'));
|
||||
h.registry.register(new EchoTool('B'));
|
||||
h.registry.register(new EchoTool('C'));
|
||||
|
||||
await runStep(h, 7, 1, [toolCall('a1', 'A', {})]);
|
||||
await runStep(h, 7, 2, [toolCall('b1', 'B', {})]);
|
||||
await runStep(h, 7, 3, [toolCall('c1', 'C', {})]);
|
||||
await runStep(h, 7, 4, [toolCall('a2', 'A', {})]);
|
||||
await runStep(h, 7, 5, [toolCall('b2', 'B', {})]);
|
||||
const [last] = await runStep(h, 7, 6, [toolCall('c2', 'C', {})]);
|
||||
|
||||
expect(last!.result.output as string).not.toContain('<system-reminder>');
|
||||
expect(telemetryEvents.filter((e) => e.event === 'tool_call_turn_repeat')).toEqual([
|
||||
expect.objectContaining({
|
||||
event: 'tool_call_turn_repeat',
|
||||
properties: expect.objectContaining({
|
||||
turn_id: 7,
|
||||
step_no: 4,
|
||||
tool_call_id: 'a2',
|
||||
tool_name: 'A',
|
||||
turn_repeat_count: 1,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
event: 'tool_call_turn_repeat',
|
||||
properties: expect.objectContaining({
|
||||
turn_id: 7,
|
||||
step_no: 5,
|
||||
tool_call_id: 'b2',
|
||||
tool_name: 'B',
|
||||
turn_repeat_count: 2,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
event: 'tool_call_turn_repeat',
|
||||
properties: expect.objectContaining({
|
||||
turn_id: 7,
|
||||
step_no: 6,
|
||||
tool_call_id: 'c2',
|
||||
tool_name: 'C',
|
||||
turn_repeat_count: 3,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(telemetryEvents.filter((e) => e.event === 'tool_call_repeat')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not carry turn repeat telemetry across turns', async () => {
|
||||
const h = createHarness();
|
||||
h.registry.register(new EchoTool('Read'));
|
||||
|
||||
await runStep(h, 7, 1, [toolCall('first', 'Read', { path: '/a' })]);
|
||||
telemetryEvents.length = 0;
|
||||
await runStep(h, 8, 1, [toolCall('new-turn', 'Read', { path: '/a' })]);
|
||||
|
||||
expect(telemetryEvents.filter((e) => e.event === 'tool_call_turn_repeat')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('merges the request trace id into dedupe and repeat telemetry', async () => {
|
||||
const h = createHarness();
|
||||
h.registry.register(new EchoTool('Read'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue