Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/subagent-id-reuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Skip subagent ids persisted from previous runs when auto-assigning `agent-N` ids, so a resumed session cannot reissue `agent-0` and collide with earlier telemetry.
7 changes: 7 additions & 0 deletions .changeset/telemetry-agent-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Emit `turn_id` and `agent_id` on turn, tool, and agent-level telemetry events so activity can be attributed to the main agent or a specific subagent within a session.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { createUserMessage, type Message } from '#/app/llmProtocol/message';
import type { Tool } from '#/app/llmProtocol/tool';
import { inputTotal, type TokenUsage } from '#/app/llmProtocol/usage';
import { IEventBus } from '#/app/event/eventBus';
import type { CompactionFinishedEvent } from '#/app/telemetry/events';
import type { CompactionFailedEvent, CompactionFinishedEvent } from '#/app/telemetry/events';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { ErrorCodes, Error2, isCodedError, isError2, toKimiErrorPayload, unwrapErrorCause } from "#/errors";
import { IWireService } from '#/wire/wire';
Expand Down Expand Up @@ -80,6 +80,7 @@ type CompactionTelemetryProperties = Pick<
>;

interface ActiveCompaction extends FullCompactionTask {
readonly originTurnId?: number;
blockedByTurn: boolean;
bgRegistration?: IDisposable;
}
Expand Down Expand Up @@ -110,6 +111,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
private readonly observedMaxContextTokensByModel = new Map<string, number>();
private lastCompactedTokenCount: number | null = null;
private consecutiveOverflowCompactions = 0;
private activeTurnId: number | undefined;
private contextInjectorService: IAgentContextInjectorService | undefined;

constructor(
Expand Down Expand Up @@ -139,6 +141,11 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
this._register(
this.eventBus.subscribe('turn.started', () => this.resetForTurn()),
);
this._register(
this.eventBus.subscribe('turn.ended', () => {
this.activeTurnId = undefined;
}),
);
this._register(
this.loopService.hooks.onWillBeginStep.register('full-compaction', async (ctx, next) => {
await this.beforeStep(ctx.signal, ctx.turnId);
Expand Down Expand Up @@ -244,7 +251,11 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
const tokenCount = this.validateCompactionStart(data.source);
this.wire.dispatch(fullCompactionBegin(data));

const active = this.createActiveCompaction(data.source, tokenCount);
const active = this.createActiveCompaction(
data.source,
tokenCount,
data.source === 'auto' ? this.activeTurnId : undefined,
);
this._compacting = active.task;
active.task.abortController.signal.addEventListener(
'abort',
Expand Down Expand Up @@ -282,6 +293,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
private createActiveCompaction(
trigger: CompactionBeginData['source'],
tokenCount: number,
originTurnId: number | undefined,
): {
readonly task: ActiveCompaction;
readonly resolve: (result: CompactionResult) => void;
Expand All @@ -300,6 +312,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
promise,
trigger,
tokenCount,
originTurnId,
blockedByTurn: false,
bgRegistration: this.activity.registerBackground('compaction', abortController),
},
Expand Down Expand Up @@ -370,6 +383,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
}

private async beforeStep(signal: AbortSignal, turnId?: number): Promise<void> {
this.activeTurnId = turnId;
this.checkAutoCompaction();
if (this.strategy.shouldBlock(this.tokenCountWithPending())) {
await this.block(signal, turnId);
Expand Down Expand Up @@ -536,6 +550,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
maxOutputSize: compactionMaxOutputSize,
source: {
type: 'operation',
turnId: active.originTurnId,
requestKind: 'full_compaction',
// Per-attempt count of messages dropped by overflow/empty
// shrinks so far; recorded on the llm.request wire op so a
Expand Down Expand Up @@ -620,6 +635,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
});

const properties: CompactionFinishedEvent = {
turn_id: active.originTurnId,
source: data.source,
tokens_before: result.tokensBefore,
tokens_after: result.tokensAfter,
Expand All @@ -635,15 +651,17 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
return result;
} catch (error) {
if (isAbortError(error)) throw error;
this.telemetry.track2('compaction_failed', {
const properties: CompactionFailedEvent = {
turn_id: active.originTurnId,
source: data.source,
tokens_before: tokensBefore,
duration_ms: Date.now() - startedAt,
round: 1,
retry_count: retryCount,
thinking_effort: thinkingEffort,
error_type: error instanceof Error ? error.name : 'Unknown',
});
};
this.telemetry.track2('compaction_failed', properties);
if (
isError2(error) &&
(error.code === ErrorCodes.AUTH_LOGIN_REQUIRED ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type LLMRequestSource =
}
| {
readonly type: 'operation';
readonly turnId?: number;
readonly requestKind?: string;
readonly logFields?: LLMRequestLogFields;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
return await this.runRequest(this.resolveRequest(overrides), onPart, signal);
} catch (error) {
this.logRequestFailure(error, overrides, signal);
this.trackApiError(error, startedAt, signal);
this.trackApiError(error, startedAt, signal, overrides.source);
throw error;
}
}
Expand All @@ -180,6 +180,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
error: unknown,
startedAt: number,
signal: AbortSignal | undefined,
source: LLMRequestSource | undefined,
): void {
if (isAbortError(error) || signal?.aborted === true) return;
const modelAlias = this.profile.data().modelAlias;
Expand All @@ -192,6 +193,8 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
protocol: model?.protocol,
retryable: isRetryableGenerateError(error),
duration_ms: Math.max(0, Date.now() - startedAt),
turn_id: source?.turnId,
request_kind: requestKindForTelemetry(source),
};
const statusCode = apiStatusCode(error);
if (statusCode !== undefined) properties['status_code'] = statusCode;
Expand Down Expand Up @@ -579,6 +582,12 @@ function logFieldsForSource(source: LLMRequestSource | undefined): LLMRequestLog
}
}

function requestKindForTelemetry(source: LLMRequestSource | undefined): string | undefined {
if (source?.type === 'turn') return 'turn';
if (source?.type === 'operation') return source.requestKind ?? 'operation';
return undefined;
}

function providerVisibleTools(tools: readonly Tool[]): readonly Tool[] {
if (!tools.some((tool) => tool.deferred === true)) return tools;
return tools.filter((tool) => tool.deferred !== true);
Expand Down
6 changes: 4 additions & 2 deletions packages/agent-core-v2/src/agent/loop/loopService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,10 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
const startedAt = Date.now();
const telemetryContext = this.telemetryContext.get();
const turnTelemetry = this.telemetry.withContext(telemetryContext);
const { mode, provider_type, protocol } = telemetryContext;
const { agent_id, mode, provider_type, protocol } = telemetryContext;
let result: TurnResult | undefined;
try {
const started: TurnStartedTelemetryEvent = { turn_id: turn.id, mode, provider_type, protocol };
const started: TurnStartedTelemetryEvent = { turn_id: turn.id, agent_id, mode, provider_type, protocol };
turnTelemetry.track2('turn_started', started);
result = await this.run({
turnId: turn.id,
Expand Down Expand Up @@ -397,6 +397,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
if (result.type !== 'completed') {
const interrupted: TurnInterruptedEvent = {
turn_id: turn.id,
agent_id,
at_step: result.steps,
mode,
interrupt_reason: interruptReasonFor(result),
Expand All @@ -408,6 +409,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
}
const ended: TurnEndedTelemetryEvent = {
turn_id: turn.id,
agent_id,
reason: result?.type ?? 'failed',
duration_ms: Date.now() - startedAt,
mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG
const evaluation = await this.policyService.evaluate(context);
if (evaluation === undefined) return undefined;
this.telemetry.track2('permission_policy_decision', {
turn_id: context.turnId,
tool_call_id: context.toolCall.id,
policy_name: evaluation.policyName,
tool_name: context.toolCall.name,
permission_mode: this.modeService.mode,
Expand Down Expand Up @@ -173,6 +175,8 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG
} catch (error) {
if (isUserCancellation(error)) throw error;
this.telemetry.track2('permission_approval_result', {
turn_id: context.turnId,
tool_call_id: context.toolCall.id,
policy_name: policyName ?? null,
tool_name: name,
permission_mode: this.modeService.mode,
Expand Down Expand Up @@ -216,6 +220,8 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG
result: response,
});
this.telemetry.track2('permission_approval_result', {
turn_id: context.turnId,
tool_call_id: context.toolCall.id,
policy_name: policyName ?? null,
tool_name: name,
permission_mode: this.modeService.mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ export class ExitPlanModeReviewAskPermissionPolicyService implements PermissionP
this.trackPlanTelemetry('plan_resolved', { outcome: 'rejected' });
}

private trackPlanTelemetry(event: 'plan_submitted', properties: PlanSubmittedEvent): void;
private trackPlanTelemetry(event: 'plan_resolved', properties: PlanResolvedEvent): void;
private trackPlanTelemetry(event: 'plan_submitted', properties: Omit<PlanSubmittedEvent, 'agent_id'>): void;
private trackPlanTelemetry(event: 'plan_resolved', properties: Omit<PlanResolvedEvent, 'agent_id'>): void;
private trackPlanTelemetry(
event: 'plan_submitted' | 'plan_resolved',
properties: PlanSubmittedEvent | PlanResolvedEvent,
properties: Omit<PlanSubmittedEvent, 'agent_id'> | Omit<PlanResolvedEvent, 'agent_id'>,
): void {
if (event === 'plan_submitted') {
this.telemetry.track2('plan_submitted', properties as PlanSubmittedEvent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export class EnterPlanModeTool implements BuiltinTool<EnterPlanModeInput> {
return { isError: true, output: `Failed to enter plan mode: ${message}` };
}

this.telemetry.track2('plan_enter_resolved', { outcome: 'auto_approved' });
this.telemetry.track2('plan_enter_resolved', {
outcome: 'auto_approved',
});
const after = await this.planMode.status();
return { output: enteredPlanModeMessage(after?.path ?? null) };
},
Expand Down
8 changes: 6 additions & 2 deletions packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,18 @@ export class ExitPlanModeTool implements BuiltinTool<ExitPlanModeInput> {
if (failed !== undefined) return failed;

if (this.permissionMode.mode === 'auto') {
this.telemetry.track2('plan_resolved', { outcome: 'auto_approved' });
this.telemetry.track2('plan_resolved', {
outcome: 'auto_approved',
});
return {
isError: false,
output: `Exited plan mode. ${formatAutoApprovedPlanForOutput(resolvedPlan.plan, resolvedPlan.path)}`,
};
}

this.telemetry.track2('plan_resolved', { outcome: 'approved' });
this.telemetry.track2('plan_resolved', {
outcome: 'approved',
});
return {
isError: false,
output: `Exited plan mode. ${formatPlanForOutput(resolvedPlan.plan, resolvedPlan.path)}`,
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core-v2/src/agent/task/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,13 +970,15 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
private recordTaskStarted(info: AgentTaskInfo): void {
this.wire.dispatch(taskStarted({ info }));
this.telemetry.track2('background_task_created', {
task_id: info.taskId,
kind: info.kind === 'process' ? 'bash' : info.kind,
});
}

private recordTaskTerminated(info: AgentTaskInfo): void {
this.wire.dispatch(taskTerminated({ info }));
this.telemetry.track2('background_task_completed', {
task_id: info.taskId,
kind: info.kind,
duration_ms: info.endedAt !== null ? info.endedAt - info.startedAt : null,
status: info.status,
Expand Down
14 changes: 9 additions & 5 deletions packages/agent-core-v2/src/agent/toolDedupe/toolDedupeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { InstantiationType } from '#/_base/di/extensions';
import { Disposable } from '#/_base/di/lifecycle';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { canonicalTelemetryArgs } from '#/_base/utils/canonical-args';
import type { ToolCallDedupDetectedEvent, ToolCallRepeatEvent } from '#/app/telemetry/events';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { IAgentLoopService } from '#/agent/loop/loop';
import { IAgentToolExecutorService, type ToolCallDupType } from '#/agent/toolExecutor/toolExecutor';
Expand Down Expand Up @@ -213,14 +214,15 @@ export class AgentToolDedupeService extends Disposable implements IAgentToolDedu
dupType: ToolCallDupType,
): void {
this.toolExecutor.recordDupType(toolCallId, dupType);
this.telemetry.track2('tool_call_dedup_detected', {
turn_id: this.activeTurnId ?? 0,
const properties: ToolCallDedupDetectedEvent = {
turn_id: this.activeTurnId,
step_no: this.activeStep,
tool_call_id: toolCallId,
tool_name: toolName,
dup_type: dupType,
args_hash: argsHash(args),
});
};
this.telemetry.track2('tool_call_dedup_detected', properties);
}

private async finalizeResult(
Expand Down Expand Up @@ -271,11 +273,13 @@ export class AgentToolDedupeService extends Disposable implements IAgentToolDedu
}

if (streak >= 2) {
this.telemetry.track2('tool_call_repeat', {
const properties: ToolCallRepeatEvent = {
turn_id: this.activeTurnId,
tool_name: toolName,
repeat_count: streak,
action,
});
};
this.telemetry.track2('tool_call_repeat', properties);
}

this.stepDeferreds.get(key)?.resolve(finalResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createDecorator } from '#/_base/di/instantiation';

export type AgentTelemetryContext = {
mode: 'agent' | 'plan';
agent_id: string;
provider_type?: string;
protocol?: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
*
* Holds the agent's ambient telemetry context (defaults to `mode: 'agent'`);
* merged into turn telemetry through `ITelemetryService.withContext` at turn
* launch. Owns no cross-domain collaborators. Bound at Agent scope.
* launch. Reads the current agent identity from `scopeContext`. Bound at Agent
* scope.
*/

import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
import {
IAgentTelemetryContextService,
type AgentTelemetryContext,
} from './agentTelemetryContext';

export class AgentTelemetryContextService implements IAgentTelemetryContextService {
declare readonly _serviceBrand: undefined;
private context: AgentTelemetryContext = { mode: 'agent' };
private context: AgentTelemetryContext;

constructor(@IAgentScopeContext scopeContext: IAgentScopeContext) {
this.context = { mode: 'agent', agent_id: scopeContext.agentId };
}

get(): AgentTelemetryContext {
return this.context;
Expand Down
Loading
Loading