@@ -53,7 +53,11 @@ import type { BtwPanelController } from './btw-panel';
5353export 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
5963function 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+
6796export 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
0 commit comments