117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|