@@ -3,12 +3,16 @@ import path from 'path';
33
44import { logger } from '../utils' ;
55import {
6- ApiHistoryEntry ,
7- CONTROL_MARKER ,
8- MCP_MARKER ,
9- TaskSegment ,
10- UIMessage ,
11- } from './types' ;
6+ createTimestampBoundaries ,
7+ extractConversationHistoryIndex ,
8+ filterRelevantMessages ,
9+ findTaskBoundaries ,
10+ getLastValidMessageTimestamp ,
11+ getTimestampFromApiEntry ,
12+ identifyTestType ,
13+ validateTaskBoundaries ,
14+ } from './metrics-utils' ;
15+ import { ApiHistoryEntry , TaskSegment , UIMessage } from './types' ;
1216
1317class ChatProcessor {
1418 private chatDir : string ;
@@ -45,7 +49,7 @@ class ChatProcessor {
4549 }
4650
4751 // Identify test type with validation
48- const testType = this . identifyTestType ( ) ;
52+ const testType = this . getTestType ( ) ;
4953 if ( ! testType ) {
5054 logger . warn (
5155 `Could not identify test type for ${ this . directoryId } , skipping processing` ,
@@ -95,7 +99,7 @@ class ChatProcessor {
9599 * Identify the test type (control or mcp) from the chat data
96100 * @returns {string|undefined } Test type or undefined if not identifiable
97101 */
98- identifyTestType ( ) : string | undefined {
102+ getTestType ( ) : string | undefined {
99103 if (
100104 ! this . initialized ||
101105 ! this . uiMessages ?. length ||
@@ -104,64 +108,7 @@ class ChatProcessor {
104108 return undefined ;
105109 }
106110
107- // Check UI messages first
108- for ( const message of this . uiMessages ) {
109- if ( message . type === 'say' && message . say === 'text' && message . text ) {
110- if (
111- message . text . includes (
112- "'agent-instructions/control_instructions.md'" ,
113- ) ||
114- message . text . includes (
115- '"agent-instructions/control_instructions.md"' ,
116- ) ||
117- message . text . includes ( CONTROL_MARKER )
118- ) {
119- return 'control' ;
120- }
121-
122- if (
123- message . text . includes ( "'agent-instructions/mcp_instructions.md'" ) ||
124- message . text . includes ( '"agent-instructions/mcp_instructions.md"' ) ||
125- message . text . includes ( MCP_MARKER )
126- ) {
127- return 'mcp' ;
128- }
129- }
130-
131- // Check for MCP usage
132- if ( message . type === 'say' && message . say === 'use_mcp_server' ) {
133- return 'mcp' ;
134- }
135- }
136-
137- // Check API history as backup
138- for ( const entry of this . apiHistory ) {
139- if (
140- entry . role === 'user' &&
141- entry . content &&
142- Array . isArray ( entry . content )
143- ) {
144- const content = entry . content . map ( ( c ) => c . text ?? '' ) . join ( ' ' ) ;
145-
146- if (
147- content . includes ( CONTROL_MARKER ) ||
148- content . includes ( "'agent-instructions/control_instructions.md'" ) ||
149- content . includes ( '"agent-instructions/control_instructions.md"' )
150- ) {
151- return 'control' ;
152- }
153-
154- if (
155- content . includes ( MCP_MARKER ) ||
156- content . includes ( "'agent-instructions/mcp_instructions.md'" ) ||
157- content . includes ( '"agent-instructions/mcp_instructions.md"' )
158- ) {
159- return 'mcp' ;
160- }
161- }
162- }
163-
164- return undefined ;
111+ return identifyTestType ( this . uiMessages , this . apiHistory ) ;
165112 }
166113
167114 /**
@@ -175,52 +122,12 @@ class ChatProcessor {
175122 return Promise . resolve ( [ ] ) ;
176123 }
177124
178- const taskBoundaries : TaskSegment [ ] = [ ] ;
179- const taskNumbers = new Set < number > ( ) ;
180-
181- // Find task boundaries in UI messages
182- for ( let i = 0 ; i < this . uiMessages . length ; i ++ ) {
183- const message = this . uiMessages [ i ] ;
184-
185- if ( message . type === 'say' && message . say === 'text' && message . text ) {
186- let taskNumber : number | undefined ;
187- let messageTestType = testType ;
188-
189- // Only look for task starts in specific instruction formats
190- const taskStartMatch = message . text . match (
191- / C o m p l e t e T a s k ( \d + ) u s i n g t h e (?: c o m m a n d s | t o o l s | f u n c t i o n s ) / i,
192- ) ;
193- if ( taskStartMatch ?. [ 1 ] ) {
194- taskNumber = parseInt ( taskStartMatch [ 1 ] , 10 ) ;
195-
196- // Verify this is actually a task instruction by checking for instruction file references
197- if ( message . text . includes ( 'control_instructions.md' ) ) {
198- messageTestType = 'control' ;
199- } else if ( message . text . includes ( 'mcp_instructions.md' ) ) {
200- messageTestType = 'mcp' ;
201- } else {
202- // Not a real task instruction
203- taskNumber = undefined ;
204- }
205- }
206-
207- if ( taskNumber && ! taskNumbers . has ( taskNumber ) ) {
208- taskNumbers . add ( taskNumber ) ;
209-
210- taskBoundaries . push ( {
211- taskNumber,
212- directoryId : this . directoryId ,
213- startIndex : i ,
214- startTime : message . ts as number ,
215- apiCalls : [ ] ,
216- userMessages : [ ] ,
217- endIndex : null ,
218- endTime : null ,
219- testType : messageTestType ,
220- } ) ;
221- }
222- }
223- }
125+ // Find task boundaries
126+ const taskBoundaries = findTaskBoundaries (
127+ this . uiMessages ,
128+ this . directoryId ,
129+ testType ,
130+ ) ;
224131
225132 // If no task boundaries found, return empty array
226133 if ( taskBoundaries . length === 0 ) {
@@ -229,48 +136,18 @@ class ChatProcessor {
229136 }
230137
231138 // Get the last valid message timestamp
232- let lastValidMessageTs : number | undefined ;
233- for ( let i = this . uiMessages . length - 1 ; i >= 0 ; i -- ) {
234- const message = this . uiMessages [ i ] ;
235- // Skip resume_completed_task messages as they can appear much later
236- if (
237- message ?. ts &&
238- typeof message . ts === 'number' &&
239- ! ( message . type === 'ask' && message . ask === 'resume_completed_task' )
240- ) {
241- lastValidMessageTs = message . ts ;
242- break ;
243- }
244- }
139+ const lastValidMessageTs = getLastValidMessageTimestamp ( this . uiMessages ) ;
245140
246- // Set end boundaries with validation
247- for ( let i = 0 ; i < taskBoundaries . length ; i ++ ) {
248- if ( i < taskBoundaries . length - 1 ) {
249- // For non-last tasks, end at the start of next task
250- taskBoundaries [ i ] . endIndex = taskBoundaries [ i + 1 ] . startIndex - 1 ;
251- taskBoundaries [ i ] . endTime = taskBoundaries [ i + 1 ] . startTime ;
252- } else {
253- // For the last task, use the last valid message timestamp
254- taskBoundaries [ i ] . endIndex = this . uiMessages . length - 1 ;
255- taskBoundaries [ i ] . endTime = lastValidMessageTs as number ;
256- }
257-
258- // Validate endTime is after startTime and within reasonable bounds
259- if (
260- ! taskBoundaries [ i ] . endTime ||
261- taskBoundaries [ i ] . endTime ! < taskBoundaries [ i ] . startTime ||
262- ( lastValidMessageTs && taskBoundaries [ i ] . endTime ! > lastValidMessageTs )
263- ) {
264- logger . warn (
265- `Invalid endTime detected for task ${ taskBoundaries [ i ] . taskNumber } . Using last valid message timestamp.` ,
266- ) ;
267- taskBoundaries [ i ] . endTime = lastValidMessageTs as number ;
268- }
269- }
141+ // Validate and set end boundaries
142+ const validatedBoundaries = validateTaskBoundaries (
143+ taskBoundaries ,
144+ lastValidMessageTs ,
145+ this . uiMessages . length ,
146+ ) ;
270147
271148 // Process task segments in parallel
272149 return Promise . all (
273- taskBoundaries . map ( ( task ) => this . processTaskSegment ( task ) ) ,
150+ validatedBoundaries . map ( ( task ) => this . processTaskSegment ( task ) ) ,
274151 ) ;
275152 }
276153
@@ -300,78 +177,28 @@ class ChatProcessor {
300177 `Processing ${ messages . length } messages for task ${ task . taskNumber } ` ,
301178 ) ;
302179
303- const TASK_SEGMENT_TEXTS = [
304- 'read_file' ,
305- 'codebase_search' ,
306- 'grep_search' ,
307- 'file_search' ,
308- '<function_calls>' ,
309- '<fnr>' ,
310- ] ;
311-
312180 // Filter messages to include only relevant ones
313- const relevantMessages = messages . filter ( ( msg ) => {
314- // Include API requests and MCP server requests
315- if (
316- msg . type === 'say' &&
317- ( msg . say === 'api_req_started' ||
318- msg . say === 'mcp_server_request_started' )
319- ) {
320- return true ;
321- }
322-
323- // Include tool usage messages
324- if ( msg . type === 'say' && msg . say === 'text' && msg . text ) {
325- return TASK_SEGMENT_TEXTS . some ( ( text ) => msg . text ?. includes ( text ) ) ;
326- }
327-
328- // Include user messages
329- if (
330- msg . type === 'say' &&
331- msg . say === 'text' &&
332- ( ! msg . from || msg . from === 'user' )
333- ) {
334- return true ;
335- }
336-
337- // Include all other 'say' messages for completeness
338- if ( msg . type === 'say' ) {
339- return true ;
340- }
341-
342- return false ;
343- } ) ;
181+ const relevantMessages = filterRelevantMessages ( messages ) ;
344182
345- // Create a combined task timestamp for filtering API entries
346- const startBoundary = task . startTime - 60 * 1000 ; // 1 minute before task start
347- const endBoundary = ( task . endTime as number ) + 5 * 60 * 1000 ; // 5 minutes after task end
183+ // Create timestamp boundaries for filtering API entries
184+ const [ startBoundary , endBoundary ] = createTimestampBoundaries (
185+ task . startTime ,
186+ task . endTime as number ,
187+ ) ;
348188
349189 // Filter API entries that fall within this task's time boundaries
350190 const apiEntries = this . apiHistory . filter ( ( entry ) => {
351- const timestamp = this . getTimestampFromApiEntry ( entry ) ;
191+ const timestamp = getTimestampFromApiEntry ( entry ) ;
352192 return (
353193 timestamp && timestamp >= startBoundary && timestamp <= endBoundary
354194 ) ;
355195 } ) ;
356196
357- // Extract conversation history index from UI messages if available
197+ // Extract conversation history index from UI messages
358198 for ( const msg of relevantMessages ) {
359- if ( msg . type === 'say' && msg . text ) {
360- try {
361- // First try to parse JSON to get index from structured data
362- const jsonData = JSON . parse ( msg . text ) ;
363- if ( jsonData . conversationHistoryIndex !== undefined ) {
364- msg . conversationHistoryIndex = jsonData . conversationHistoryIndex ;
365- }
366- } catch ( e ) {
367- // If JSON parsing fails, try regex
368- const indexMatch = msg . text . match (
369- / c o n v e r s a t i o n H i s t o r y I n d e x [ " \s : ] + ( \d + ) / i,
370- ) ;
371- if ( indexMatch ?. [ 1 ] ) {
372- msg . conversationHistoryIndex = parseInt ( indexMatch [ 1 ] , 10 ) ;
373- }
374- }
199+ const index = extractConversationHistoryIndex ( msg ) ;
200+ if ( index !== undefined ) {
201+ msg . conversationHistoryIndex = index ;
375202 }
376203 }
377204
@@ -385,36 +212,6 @@ class ChatProcessor {
385212 messageCount : relevantMessages . length ,
386213 } ;
387214 }
388-
389- /**
390- * Extract timestamp from an API entry
391- * @param {ApiHistoryEntry } entry The API entry
392- * @returns {number|undefined } Timestamp in milliseconds or undefined if not found
393- */
394- getTimestampFromApiEntry ( entry : ApiHistoryEntry ) : number | undefined {
395- if ( ! entry ?. content || ! Array . isArray ( entry . content ) ) {
396- return undefined ;
397- }
398-
399- for ( const content of entry . content ) {
400- if (
401- content . type === 'text' &&
402- content . text &&
403- content . text . includes ( 'environment_details' ) &&
404- content . text . includes ( 'Current Time' )
405- ) {
406- const match = content . text . match ( / C u r r e n t T i m e \s + ( [ ^ < ] + ) / ) ;
407- if ( match ?. [ 1 ] ) {
408- const date = new Date ( match [ 1 ] . trim ( ) ) ;
409- if ( ! Number . isNaN ( date . getTime ( ) ) ) {
410- return date . getTime ( ) ;
411- }
412- }
413- }
414- }
415-
416- return undefined ;
417- }
418215}
419216
420217export default ChatProcessor ;
0 commit comments