/** * Tool Linking Engine - Link tool calls to their results * * Matches tool_call steps with their corresponding tool_result steps * and builds LinkedToolItem structures for display. */import { estimateTokens, formatToolInput, formatToolResult, toDate } from './aiGroupHelpers';import type { ParsedMessage, SemanticStep } from '../types/data';import type { LinkedToolItem } from '../types/groups';/** * Link tool calls to their results and build a map of LinkedToolItems. * * Strategy: * 1. Iterate through steps to find all tool_call steps * 2. For each tool call, search for matching tool_result by ID * - Tool result step IDs are set to the tool_use_id, matching the call's ID * 3. Build LinkedToolItem with preview text * 4. Include orphaned calls (calls without results) * 5. For Skill tool calls, extract skill instructions from responses * * @param steps - Semantic steps from the AI Group * @param responses - Optional raw messages for extracting skill instructions * @returns Map of tool call ID to LinkedToolItem */export function linkToolCallsToResults( steps: SemanticStep[], responses?: ParsedMessage[]): Map<string, LinkedToolItem> { const linkedTools = new Map<string, LinkedToolItem>(); // First pass: collect all tool calls const toolCalls = steps.filter((step) => step.type === 'tool_call'); // Build a map of result steps by their ID for fast lookup const resultStepsById = new Map<string, SemanticStep>(); for (const step of steps) { if (step.type === 'tool_result') { resultStepsById.set(step.id, step); } } // Build a map of skill instructions by source tool use ID // Skill tools have follow-up isMeta:true messages with instructions starting with "Base directory for this skill:" const skillInstructionsById = new Map<string, string>(); if (responses) { for (const msg of responses) { // Extract skill instructions if (msg.type === 'user' && msg.isMeta && msg.sourceToolUseID && Array.isArray(msg.content)) { for (const block of msg.content) { if (block.type === 'text' && block.text) { const text = block.text; if (text.startsWith('Base directory for this skill:')) { skillInstructionsById.set(msg.sourceToolUseID, text); } } } } } } for (const callStep of toolCalls) { const toolCallId = callStep.id; const toolName = callStep.content.toolName ?? 'Unknown'; const toolInput = callStep.content.toolInput ?? {}; // Search for matching tool result by ID // Tool result steps have their ID set to the tool_use_id (same as call ID) const resultStep = resultStepsById.get(toolCallId); // Convert timestamps to proper Date objects (handles IPC serialization) const callStartTime = toDate(callStep.startTime); const resultStartTime = resultStep ? toDate(resultStep.startTime) : undefined; // Get skill instructions for Skill tool calls const skillInstructions = toolName === 'Skill' ? skillInstructionsById.get(toolCallId) : undefined; // Calculate callTokens directly from tool name + input // This reflects what actually enters the context window (not proportioned output_tokens) const callTokens = estimateTokens(toolName + JSON.stringify(toolInput)); const linkedItem: LinkedToolItem = { id: toolCallId, name: toolName, input: toolInput as Record<string, unknown>, callTokens, // Token count for tool call (what Claude generated) result: resultStep ? { content: resultStep.content.toolResultContent ?? '', isError: resultStep.content.isError ?? false, toolUseResult: resultStep.content.toolUseResult, tokenCount: resultStep.content.tokenCount, // Pre-computed token count for result } : undefined, inputPreview: formatToolInput(toolInput as Record<string, unknown>), outputPreview: resultStep ? formatToolResult(resultStep.content.toolResultContent ?? '') : undefined, startTime: callStartTime, endTime: resultStartTime, durationMs: resultStartTime ? resultStartTime.getTime() - callStartTime.getTime() : undefined, isOrphaned: !resultStep, skillInstructions, skillInstructionsTokenCount: skillInstructions ? estimateTokens(skillInstructions) : undefined, }; linkedTools.set(toolCallId, linkedItem); } return linkedTools;}
/**
* Tool Linking Engine - Link tool calls to their results
*
* Matches tool_call steps with their corresponding tool_result steps
* and builds LinkedToolItem structures for display.
*/
import { estimateTokens, formatToolInput, formatToolResult, toDate } from './aiGroupHelpers';
import type { ParsedMessage, SemanticStep } from '../types/data';
import type { LinkedToolItem } from '../types/groups';
/**
* Link tool calls to their results and build a map of LinkedToolItems.
*
* Strategy:
* 1. Iterate through steps to find all tool_call steps
* 2. For each tool call, search for matching tool_result by ID
* - Tool result step IDs are set to the tool_use_id, matching the call's ID
* 3. Build LinkedToolItem with preview text
* 4. Include orphaned calls (calls without results)
* 5. For Skill tool calls, extract skill instructions from responses
*
* @param steps - Semantic steps from the AI Group
* @param responses - Optional raw messages for extracting skill instructions
* @returns Map of tool call ID to LinkedToolItem
*/
export function linkToolCallsToResults(
steps: SemanticStep[],
responses?: ParsedMessage[]
): Map<string, LinkedToolItem> {
const linkedTools = new Map<string, LinkedToolItem>();
// First pass: collect all tool calls
const toolCalls = steps.filter((step) => step.type === 'tool_call');
// Build a map of result steps by their ID for fast lookup
const resultStepsById = new Map<string, SemanticStep>();
for (const step of steps) {
if (step.type === 'tool_result') {
resultStepsById.set(step.id, step);
}
}
// Build a map of skill instructions by source tool use ID
// Skill tools have follow-up isMeta:true messages with instructions starting with "Base directory for this skill:"
const skillInstructionsById = new Map<string, string>();
if (responses) {
for (const msg of responses) {
// Extract skill instructions
if (msg.type === 'user' && msg.isMeta && msg.sourceToolUseID && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === 'text' && block.text) {
const text = block.text;
if (text.startsWith('Base directory for this skill:')) {
skillInstructionsById.set(msg.sourceToolUseID, text);
}
}
}
}
}
}
for (const callStep of toolCalls) {
const toolCallId = callStep.id;
const toolName = callStep.content.toolName ?? 'Unknown';
const toolInput = callStep.content.toolInput ?? {};
// Search for matching tool result by ID
// Tool result steps have their ID set to the tool_use_id (same as call ID)
const resultStep = resultStepsById.get(toolCallId);
// Convert timestamps to proper Date objects (handles IPC serialization)
const callStartTime = toDate(callStep.startTime);
const resultStartTime = resultStep ? toDate(resultStep.startTime) : undefined;
// Get skill instructions for Skill tool calls
const skillInstructions =
toolName === 'Skill' ? skillInstructionsById.get(toolCallId) : undefined;
// Calculate callTokens directly from tool name + input
// This reflects what actually enters the context window (not proportioned output_tokens)
const callTokens = estimateTokens(toolName + JSON.stringify(toolInput));
const linkedItem: LinkedToolItem = {
id: toolCallId,
name: toolName,
input: toolInput as Record<string, unknown>,
callTokens, // Token count for tool call (what Claude generated)
result: resultStep
? {
content: resultStep.content.toolResultContent ?? '',
isError: resultStep.content.isError ?? false,
toolUseResult: resultStep.content.toolUseResult,
tokenCount: resultStep.content.tokenCount, // Pre-computed token count for result
}
: undefined,
inputPreview: formatToolInput(toolInput as Record<string, unknown>),
outputPreview: resultStep
? formatToolResult(resultStep.content.toolResultContent ?? '')
: undefined,
startTime: callStartTime,
endTime: resultStartTime,
durationMs: resultStartTime ? resultStartTime.getTime() - callStartTime.getTime() : undefined,
isOrphaned: !resultStep,
skillInstructions,
skillInstructionsTokenCount: skillInstructions
? estimateTokens(skillInstructions)
: undefined,
};
linkedTools.set(toolCallId, linkedItem);
}
return linkedTools;
}