203 lines · 5.5 KB · TypeScript
RawDownload
/** * ChunkFactory service - Creates individual chunk objects from messages. * * Responsibilities: * - Build UserChunk from user messages * - Build SystemChunk from command output messages * - Build CompactChunk from summary messages * - Build AIChunk from buffered AI messages * - Calculate timing and metrics for chunks */ import {  type EnhancedAIChunk,  type EnhancedCompactChunk,  type EnhancedSystemChunk,  type EnhancedUserChunk,  type ParsedMessage,  type Process,} from '@main/types';import { calculateStepContext } from '@main/utils/contextAccumulator';import { calculateMetrics } from '@main/utils/jsonl';import { fillTimelineGaps } from '@main/utils/timelineGapFilling'; import { linkProcessesToAIChunk } from './ProcessLinker';import { extractSemanticStepsFromAIChunk } from './SemanticStepExtractor';import { buildSemanticStepGroups } from './SemanticStepGrouper';import { buildToolExecutions } from './ToolExecutionBuilder'; /** * Generate a stable chunk ID based on message UUID. * Using the message UUID ensures IDs are consistent across re-parses. */function generateStableChunkId(prefix: string, message: ParsedMessage): string {  return `${prefix}-${message.uuid}`;} /** * Build a UserChunk from a user message. */export function buildUserChunk(message: ParsedMessage): EnhancedUserChunk {  const id = generateStableChunkId('user', message);  const metrics = calculateMetrics([message]);   return {    id,    chunkType: 'user',    userMessage: message,    startTime: message.timestamp,    endTime: message.timestamp,    durationMs: 0,    metrics,    rawMessages: [message],  };} /** * Build a SystemChunk from a command output message. */export function buildSystemChunk(message: ParsedMessage): EnhancedSystemChunk {  const id = generateStableChunkId('system', message);  const commandOutput = extractCommandOutput(message);  const metrics = calculateMetrics([message]);   return {    id,    chunkType: 'system',    message,    commandOutput,    startTime: message.timestamp,    endTime: message.timestamp,    durationMs: 0,    metrics,    rawMessages: [message],  };} /** * Build a CompactChunk from a compact summary message. */export function buildCompactChunk(message: ParsedMessage): EnhancedCompactChunk {  const id = generateStableChunkId('compact', message);  const metrics = calculateMetrics([message]);   return {    id,    chunkType: 'compact',    message,    startTime: message.timestamp,    endTime: message.timestamp,    durationMs: 0,    metrics,    rawMessages: [message],  };} /** * Extract command output from <local-command-stdout> tag. */function extractCommandOutput(message: ParsedMessage): string {  const content = typeof message.content === 'string' ? message.content : '';  const match = /<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/.exec(content);  const matchStderr = /<local-command-stderr>([\s\S]*?)<\/local-command-stderr>/.exec(content);  if (match) {    return match[1];  }  if (matchStderr) {    return matchStderr[1];  }  return content;} /** * Build an AIChunk from buffered AI messages. */export function buildAIChunkFromBuffer(  responses: ParsedMessage[],  subagents: Process[],  allMessages: ParsedMessage[]): EnhancedAIChunk {  // Use first response message's UUID for stable ID  const id =    responses.length > 0 ? generateStableChunkId('ai', responses[0]) : `ai-empty-${Date.now()}`; // Fallback for edge case  const { startTime, endTime, durationMs } = calculateAIChunkTiming(responses);  const metrics = calculateMetrics(responses);  const toolExecutions = buildToolExecutions(responses);   // Collect sidechain messages for this time range  const sidechainMessages = collectSidechainMessages(allMessages, startTime, endTime);   const chunk: EnhancedAIChunk = {    id,    chunkType: 'ai',    responses,    startTime,    endTime,    durationMs,    metrics,    processes: [],    sidechainMessages,    toolExecutions,    semanticSteps: [],    rawMessages: responses,  };   // Link processes to this chunk  linkProcessesToAIChunk(chunk, subagents);   // Extract semantic steps using the extracted module  chunk.semanticSteps = extractSemanticStepsFromAIChunk(chunk);  chunk.semanticSteps = fillTimelineGaps({    steps: chunk.semanticSteps,    chunkStartTime: chunk.startTime,    chunkEndTime: chunk.endTime,  });  calculateStepContext(chunk.semanticSteps, chunk.rawMessages);  chunk.semanticStepGroups = buildSemanticStepGroups(chunk.semanticSteps);   return chunk;} /** * Calculate timing for AI chunks (responses only, no user message). */function calculateAIChunkTiming(responses: ParsedMessage[]): {  startTime: Date;  endTime: Date;  durationMs: number;} {  if (responses.length === 0) {    const now = new Date();    return { startTime: now, endTime: now, durationMs: 0 };  }   const startTime = responses[0].timestamp;  let endTime = startTime;  for (const resp of responses) {    if (resp.timestamp > endTime) {      endTime = resp.timestamp;    }  }   return {    startTime,    endTime,    durationMs: endTime.getTime() - startTime.getTime(),  };} /** * Collect sidechain messages in a time range. */function collectSidechainMessages(  messages: ParsedMessage[],  startTime: Date,  endTime: Date | undefined): ParsedMessage[] {  return messages.filter((m) => {    if (!m.isSidechain) return false;    if (m.timestamp < startTime) return false;    if (endTime && m.timestamp >= endTime) return false;    return true;  });}