Skip to content

Commit 3cc6b2a

Browse files
author
Grapedge
authored
feat(kimi-code): carry turn trace id and copilot stats in rating surveys (#3907)
1 parent 60f2a63 commit 3cc6b2a

24 files changed

Lines changed: 895 additions & 81 deletions

‎apps/kimi-code/src/tui/controllers/editor-keyboard.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export class EditorKeyboardController {
285285
};
286286

287287
editor.onOpenExternalEditor = () => {
288-
host.surveyController.closeSilently();
288+
host.surveyController.notifyDisplaced();
289289
host.track('shortcut_editor');
290290
void this.openExternalEditor();
291291
};

‎apps/kimi-code/src/tui/controllers/session-event-handler.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ export class SessionEventHandler {
302302
case 'compaction.blocked': break;
303303
case 'compaction.cancelled': this.handleCompactionCancel(event, sendQueued); break;
304304
case 'subagent.spawned':
305+
this.host.surveyController.notifySubagentSpawned(event);
306+
this.subAgentEventHandler.handleLifecycleEvent(event); break;
305307
case 'subagent.started':
306308
case 'subagent.suspended':
307309
case 'subagent.completed':
@@ -619,7 +621,7 @@ export class SessionEventHandler {
619621

620622
private handleToolCall(event: ToolCallStartedEvent): void {
621623
const { streamingUI } = this.host;
622-
this.host.surveyController.notifyToolCallStarted();
624+
this.host.surveyController.notifyToolCallStarted(event.toolCallId, event.name);
623625
streamingUI.flushNow();
624626
const { turnId, step } = streamingUI.getTurnContext();
625627
const toolCall: ToolCallBlockData = {
@@ -683,6 +685,7 @@ export class SessionEventHandler {
683685

684686
private handleToolResult(event: ToolResultEvent): void {
685687
const { streamingUI } = this.host;
688+
this.host.surveyController.notifyToolCallEnded(event.toolCallId);
686689
streamingUI.flushNow();
687690
this.clearStepRetry();
688691
const resultData: ToolResultBlockData = {

‎apps/kimi-code/src/tui/controllers/survey-controller.ts‎

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ import type { BtwPanelController } from './btw-panel';
5353
export interface SurveyHost {
5454
readonly state: TUIState;
5555
readonly btwPanelController: BtwPanelController;
56-
track(event: string, props?: Record<string, unknown>): void;
56+
track(
57+
event: string,
58+
props?: Record<string, unknown>,
59+
context?: { readonly sessionId?: string },
60+
): void;
5761
}
5862

5963
function resolveKfcModelId(appState: TUIState['appState']): string | undefined {
@@ -64,6 +68,31 @@ function resolveKfcModelId(appState: TUIState['appState']): string | undefined {
6468
return entry.model;
6569
}
6670

71+
interface SurveyAppearanceSnapshot {
72+
readonly sessionId: string;
73+
readonly fields: SurveyEventEnvironmentFields;
74+
}
75+
76+
interface ResumedAgentSeed {
77+
readonly type: string;
78+
readonly profileName?: string;
79+
readonly swarmItem?: string;
80+
readonly sessionInit?: string;
81+
}
82+
83+
const TOWER_WORKER_PROFILE_NAME = 'tower-worker';
84+
const SESSION_INIT_PARENT_TOOL_CALL_ID = 'generate-agents-md';
85+
86+
function recordModel(models: Set<string>, model: string | undefined): void {
87+
if (model === undefined || model.length === 0) return;
88+
models.add(model);
89+
}
90+
91+
function joinModels(models: ReadonlySet<string>): string | undefined {
92+
if (models.size === 0) return undefined;
93+
return [...models].toSorted().join(',');
94+
}
95+
6796
export interface SurveyControllerDeps {
6897
readonly config?: () => SurveyPopupConfig;
6998
readonly monotonicNow?: () => number;
@@ -136,6 +165,13 @@ export class SurveyController {
136165
private configRefreshedAt = 0;
137166
private coldRefreshAttemptedAt = 0;
138167
private configRegion: string | undefined;
168+
private pendingTraceId: string | undefined;
169+
private appearanceSnapshot: SurveyAppearanceSnapshot | undefined;
170+
private subagentCount = 0;
171+
private subagentModels = new Set<string>();
172+
private swarmRunCount = 0;
173+
private swarmModels = new Set<string>();
174+
private readonly toolCallFamilies = new Map<string, string>();
139175

140176
constructor(
141177
private readonly host: SurveyHost,
@@ -148,6 +184,7 @@ export class SurveyController {
148184
}
149185

150186
reset(): void {
187+
this.notifyDisplaced();
151188
this.generation += 1;
152189
this.clearIdleTimer();
153190
this.clearDigitTimer();
@@ -167,6 +204,13 @@ export class SurveyController {
167204
this.appearanceConfig = undefined;
168205
this.currentTurnUserOrigin = undefined;
169206
this.evaluationPending = false;
207+
this.pendingTraceId = undefined;
208+
this.appearanceSnapshot = undefined;
209+
this.subagentCount = 0;
210+
this.subagentModels = new Set();
211+
this.swarmRunCount = 0;
212+
this.swarmModels = new Set();
213+
this.toolCallFamilies.clear();
170214
const generation = this.generation;
171215
this.cooldownReady = false;
172216
void (this.deps.readGlobalLastShown ?? defaultDeps.readGlobalLastShown)()
@@ -194,15 +238,19 @@ export class SurveyController {
194238
this.currentTurnUserOrigin = userOrigin;
195239
this.idleSince = undefined;
196240
this.clearIdleTimer();
197-
if (this.machine.phase !== 'closed') this.applyAction({ type: 'close-silently' });
241+
this.notifyDisplaced();
198242
if (userOrigin) {
199243
this.userTurnCount += 1;
200244
this.evaluationPending = false;
245+
this.pendingTraceId = undefined;
201246
}
202247
}
203248

204-
notifyTurnEnded(): void {
205-
if (this.currentTurnUserOrigin === true) this.evaluationPending = true;
249+
notifyTurnEnded(traceId?: string): void {
250+
if (this.currentTurnUserOrigin === true) {
251+
this.evaluationPending = true;
252+
this.pendingTraceId = traceId;
253+
}
206254
this.currentTurnUserOrigin = undefined;
207255
if (!this.evaluationPending) return;
208256
this.idleSince = this.now();
@@ -214,8 +262,52 @@ export class SurveyController {
214262
}, SURVEY_IDLE_EVALUATION_DELAY_MS);
215263
}
216264

217-
notifyToolCallStarted(): void {
265+
notifyToolCallStarted(toolCallId: string, toolName: string): void {
218266
this.toolCallCount += 1;
267+
this.toolCallFamilies.set(toolCallId, toolName);
268+
if (toolName === 'AgentSwarm') this.swarmRunCount += 1;
269+
}
270+
271+
notifyToolCallEnded(toolCallId: string): void {
272+
this.toolCallFamilies.delete(toolCallId);
273+
}
274+
275+
notifySubagentSpawned(event: {
276+
readonly parentToolCallId?: string;
277+
readonly swarmIndex?: number;
278+
readonly model?: string;
279+
}): void {
280+
if (event.parentToolCallId === SESSION_INIT_PARENT_TOOL_CALL_ID) return;
281+
const toolName =
282+
event.parentToolCallId === undefined
283+
? undefined
284+
: this.toolCallFamilies.get(event.parentToolCallId);
285+
if (toolName === 'Agent') {
286+
this.subagentCount += 1;
287+
recordModel(this.subagentModels, event.model);
288+
return;
289+
}
290+
if (toolName === 'AgentSwarm') {
291+
recordModel(this.swarmModels, event.model);
292+
return;
293+
}
294+
if (toolName !== undefined) return;
295+
if (event.swarmIndex !== undefined) {
296+
recordModel(this.swarmModels, event.model);
297+
return;
298+
}
299+
this.subagentCount += 1;
300+
recordModel(this.subagentModels, event.model);
301+
}
302+
303+
seedFromResumedAgents(agents: Readonly<Record<string, ResumedAgentSeed>>): void {
304+
this.subagentCount = Object.values(agents).filter(
305+
(agent) =>
306+
agent.type === 'sub' &&
307+
agent.swarmItem === undefined &&
308+
agent.sessionInit === undefined &&
309+
agent.profileName !== TOWER_WORKER_PROFILE_NAME,
310+
).length;
219311
}
220312

221313
notifyCompactionFinished(): void {
@@ -224,17 +316,15 @@ export class SurveyController {
224316

225317
notifyInputModeChanged(mode: 'prompt' | 'bash'): void {
226318
if (mode !== 'bash') return;
227-
if (this.machine.phase === 'closed') return;
319+
this.notifyDisplaced();
320+
}
321+
322+
notifyDisplaced(): void {
228323
if (this.machine.phase === 'open') {
229324
this.applyAction({ type: 'abandon' });
230325
return;
231326
}
232-
this.applyAction({ type: 'close-silently' });
233-
}
234-
235-
closeSilently(): void {
236-
if (this.machine.phase === 'closed') return;
237-
this.applyAction({ type: 'close-silently' });
327+
if (this.machine.phase !== 'closed') this.applyAction({ type: 'close-silently' });
238328
}
239329

240330
handlePreInput(data: string): boolean {
@@ -465,6 +555,9 @@ export class SurveyController {
465555
};
466556
const shownAt = this.now();
467557
this.appearanceConfig = config;
558+
const traceId = this.pendingTraceId;
559+
this.pendingTraceId = undefined;
560+
this.appearanceSnapshot = this.captureSnapshot(traceId);
468561
this.applyAction({ type: 'open', appearance });
469562
if (this.machine.phase !== 'open') return;
470563
this.openedAt = shownAt;
@@ -497,19 +590,22 @@ export class SurveyController {
497590
switch (effect.type) {
498591
case 'report': {
499592
if (appearance === undefined) return;
500-
this.host.track(
501-
SURVEY_EVENT_NAMES[appearance.survey],
502-
buildSurveyEventProperties(
503-
{
504-
event_type: effect.eventType,
505-
appearance_id: appearance.appearanceId,
506-
appearance_index: appearance.appearanceIndex,
507-
response: effect.response,
508-
},
509-
this.environmentFields(),
510-
this.appearanceConfig ?? (this.deps.config ?? defaultDeps.config)(),
511-
),
593+
const properties = buildSurveyEventProperties(
594+
{
595+
event_type: effect.eventType,
596+
appearance_id: appearance.appearanceId,
597+
appearance_index: appearance.appearanceIndex,
598+
response: effect.response,
599+
},
600+
this.appearanceSnapshot?.fields ?? this.environmentFields(),
601+
this.appearanceConfig ?? (this.deps.config ?? defaultDeps.config)(),
512602
);
603+
const sessionId = this.appearanceSnapshot?.sessionId ?? '';
604+
if (sessionId.length > 0) {
605+
this.host.track(SURVEY_EVENT_NAMES[appearance.survey], properties, { sessionId });
606+
} else {
607+
this.host.track(SURVEY_EVENT_NAMES[appearance.survey], properties);
608+
}
513609
return;
514610
}
515611
case 'schedule': {
@@ -593,6 +689,18 @@ export class SurveyController {
593689
this.host.state.ui.requestRender();
594690
}
595691

692+
private captureSnapshot(traceId: string | undefined): SurveyAppearanceSnapshot {
693+
const { appState } = this.host.state;
694+
const kfcModelId = resolveKfcModelId(appState);
695+
return {
696+
sessionId: appState.sessionId,
697+
fields: {
698+
...this.environmentFields(),
699+
kfc_trace_id: kfcModelId === undefined ? undefined : traceId,
700+
},
701+
};
702+
}
703+
596704
private environmentFields(): SurveyEventEnvironmentFields {
597705
const { appState } = this.host.state;
598706
return {
@@ -605,6 +713,10 @@ export class SurveyController {
605713
compaction_count: this.compactionCount,
606714
permission_mode: appState.permissionMode,
607715
thinking_effort: appState.thinkingEffort,
716+
subagent_count: this.subagentCount,
717+
subagent_models: joinModels(this.subagentModels),
718+
swarm_run_count: this.swarmRunCount,
719+
swarm_models: joinModels(this.swarmModels),
608720
};
609721
}
610722

‎apps/kimi-code/src/tui/kimi-tui.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,16 @@ export class KimiTUI {
420420
*/
421421
public exitForegroundTask: ((exitCode: number) => Promise<void>) | undefined;
422422

423-
track(event: string, properties?: Parameters<KimiHarness['track']>[1]): void {
424-
this.harness.track(event, properties);
423+
track(
424+
event: string,
425+
properties?: Parameters<KimiHarness['track']>[1],
426+
context?: { readonly sessionId?: string },
427+
): void {
428+
if (context === undefined) {
429+
this.harness.track(event, properties);
430+
return;
431+
}
432+
this.harness.trackWithContext(event, properties, { sessionId: context.sessionId });
425433
}
426434

427435
constructor(harness: KimiHarness, startupInput: KimiTUIStartupInput) {
@@ -836,6 +844,7 @@ export class KimiTUI {
836844
this.applyStartupPermissionAndPlanToAppState();
837845
}
838846
const resumeState = this.session?.getResumeState();
847+
this.surveyController.seedFromResumedAgents(resumeState?.sessionMetadata.agents ?? {});
839848
if (resumeState?.warning !== undefined) {
840849
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
841850
}
@@ -1758,7 +1767,7 @@ export class KimiTUI {
17581767

17591768
handleTurnEnded(event: TurnEndedEvent): void {
17601769
this.staging.handleTurnEnded(event);
1761-
this.surveyController.notifyTurnEnded();
1770+
this.surveyController.notifyTurnEnded(event.traceId);
17621771
}
17631772

17641773
releaseStagingMedia(mediaAttachmentIds: readonly number[]): void {
@@ -2683,6 +2692,7 @@ export class KimiTUI {
26832692
this.sessionEventHandler.startSubscription();
26842693
}
26852694
const resumeState = session.getResumeState();
2695+
this.surveyController.seedFromResumedAgents(resumeState?.sessionMetadata.agents ?? {});
26862696
if (resumeState?.warning !== undefined) {
26872697
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
26882698
}
@@ -2714,6 +2724,7 @@ export class KimiTUI {
27142724
}
27152725
this.sessionEventHandler.startSubscription();
27162726
const resumeState = session.getResumeState();
2727+
this.surveyController.seedFromResumedAgents(resumeState?.sessionMetadata.agents ?? {});
27172728
if (resumeState?.warning !== undefined) {
27182729
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
27192730
}
@@ -3751,7 +3762,7 @@ export class KimiTUI {
37513762
// =========================================================================
37523763

37533764
mountEditorReplacement(panel: Component & Focusable): void {
3754-
this.surveyController.closeSilently();
3765+
this.surveyController.notifyDisplaced();
37553766
this.state.editorReplacementMounted = true;
37563767
this.state.editorContainer.clear();
37573768
this.state.editorContainer.addChild(panel);

‎apps/kimi-code/src/tui/utils/survey-policy.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,18 @@ export interface SurveyEventCoreFields {
345345
export interface SurveyEventEnvironmentFields {
346346
readonly current_model: string;
347347
readonly kfc_model_id?: string;
348+
readonly kfc_trace_id?: string;
348349
readonly user_turn_count: number;
349350
readonly cumulative_tokens: number;
350351
readonly virtual_context_tokens: number;
351352
readonly tool_call_count: number;
352353
readonly compaction_count: number;
353354
readonly permission_mode: string;
354355
readonly thinking_effort: string;
356+
readonly subagent_count: number;
357+
readonly subagent_models?: string;
358+
readonly swarm_run_count: number;
359+
readonly swarm_models?: string;
355360
}
356361

357362
export function buildSurveyEventProperties(

0 commit comments

Comments
 (0)