agent-ecosystem/src/main/services/analysis/ChunkFactory.ts

203 lines
5.5 KiB
TypeScript

/**
* 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;
});
}