224 lines
6.8 KiB
TypeScript
224 lines
6.8 KiB
TypeScript
/**
|
|
* ToolResultExtractor service - Extracts tool results from messages.
|
|
*
|
|
* Provides utilities for:
|
|
* - Building tool_use maps for linking results to calls
|
|
* - Building tool_result maps for token estimation
|
|
* - Estimating token counts from content
|
|
* - Extracting tool results from various message formats
|
|
*/
|
|
|
|
import { isToolResultContent, type ParsedMessage } from '@main/types';
|
|
import { countContentTokens } from '@main/utils/tokenizer';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Extracted tool result information for trigger matching.
|
|
*/
|
|
export interface ExtractedToolResult {
|
|
toolUseId: string;
|
|
isError: boolean;
|
|
content: string | unknown[];
|
|
toolName?: string;
|
|
}
|
|
|
|
/**
|
|
* Tool use information from assistant messages.
|
|
*/
|
|
export interface ToolUseInfo {
|
|
name: string;
|
|
input: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Tool result information for token estimation.
|
|
*/
|
|
export interface ToolResultInfo {
|
|
content: string | unknown[];
|
|
isError: boolean;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Map Building
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Builds a map of tool_use_id to tool_use content.
|
|
* This allows linking tool_results back to their tool_use calls to check tool names.
|
|
*/
|
|
export function buildToolUseMap(messages: ParsedMessage[]): Map<string, ToolUseInfo> {
|
|
const map = new Map<string, ToolUseInfo>();
|
|
|
|
for (const message of messages) {
|
|
if (message.type !== 'assistant') continue;
|
|
|
|
// Check content array for tool_use blocks
|
|
if (Array.isArray(message.content)) {
|
|
for (const block of message.content) {
|
|
if (block.type === 'tool_use') {
|
|
const toolUse = block;
|
|
map.set(toolUse.id, {
|
|
name: toolUse.name,
|
|
input: toolUse.input || {},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check toolCalls if present
|
|
if (message.toolCalls) {
|
|
for (const toolCall of message.toolCalls) {
|
|
map.set(toolCall.id, {
|
|
name: toolCall.name,
|
|
input: toolCall.input || {},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Builds a map of tool_use_id to tool_result content.
|
|
* Used for estimating output tokens per tool_use.
|
|
*/
|
|
export function buildToolResultMap(messages: ParsedMessage[]): Map<string, ToolResultInfo> {
|
|
const map = new Map<string, ToolResultInfo>();
|
|
|
|
for (const message of messages) {
|
|
// Check content array for tool_result blocks
|
|
if (Array.isArray(message.content)) {
|
|
for (const block of message.content) {
|
|
if (isToolResultContent(block)) {
|
|
map.set(block.tool_use_id, {
|
|
content: block.content,
|
|
isError: block.is_error === true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check toolResults array if present
|
|
if (message.toolResults) {
|
|
for (const toolResult of message.toolResults) {
|
|
map.set(toolResult.toolUseId, {
|
|
content: toolResult.content,
|
|
isError: toolResult.isError === true,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Also check toolUseResult if present (enriched data)
|
|
if (message.toolUseResult && message.sourceToolUseID) {
|
|
const content = extractContentFromToolUseResult(message.toolUseResult);
|
|
const isError =
|
|
message.toolUseResult.isError === true || message.toolUseResult.is_error === true;
|
|
map.set(message.sourceToolUseID, { content, isError });
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Token Estimation
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Estimates token count from content using the shared tokenizer.
|
|
* Uses the same calculation as ChunkBuilder for consistency with UI display.
|
|
*/
|
|
export function estimateTokens(content: string | unknown[] | Record<string, unknown>): number {
|
|
if (typeof content === 'string') {
|
|
return countContentTokens(content);
|
|
}
|
|
// For objects/arrays, use countContentTokens which handles JSON.stringify
|
|
return countContentTokens(content as unknown[]);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tool Result Extraction
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Extracts content string from toolUseResult.
|
|
*/
|
|
function extractContentFromToolUseResult(toolUseResult: Record<string, unknown>): string {
|
|
if (typeof toolUseResult.error === 'string') {
|
|
return toolUseResult.error;
|
|
}
|
|
if (typeof toolUseResult.stderr === 'string' && toolUseResult.stderr.trim()) {
|
|
return toolUseResult.stderr;
|
|
}
|
|
if (typeof toolUseResult.content === 'string') {
|
|
return toolUseResult.content;
|
|
}
|
|
if (typeof toolUseResult.message === 'string') {
|
|
return toolUseResult.message;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Extracts tool results from a message.
|
|
* Handles multiple patterns of tool result storage.
|
|
*
|
|
* @param message - The parsed message to extract from
|
|
* @param findToolNameFn - Function to find tool name by tool use ID
|
|
*/
|
|
export function extractToolResults(
|
|
message: ParsedMessage,
|
|
findToolNameFn: (message: ParsedMessage, toolUseId: string) => string | null
|
|
): ExtractedToolResult[] {
|
|
const results: ExtractedToolResult[] = [];
|
|
|
|
// Pattern 1: Check toolResults array on ParsedMessage
|
|
if (message.toolResults && message.toolResults.length > 0) {
|
|
for (const toolResult of message.toolResults) {
|
|
results.push({
|
|
toolUseId: toolResult.toolUseId,
|
|
isError: toolResult.isError === true,
|
|
content: toolResult.content,
|
|
toolName: findToolNameFn(message, toolResult.toolUseId) ?? undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Pattern 2: Check toolUseResult field (enriched data from entry)
|
|
if (message.toolUseResult) {
|
|
const toolUseResult = message.toolUseResult;
|
|
const hasError = toolUseResult.isError === true || toolUseResult.is_error === true;
|
|
const toolUseId =
|
|
(typeof toolUseResult.toolUseId === 'string' ? toolUseResult.toolUseId : undefined) ??
|
|
message.sourceToolUseID;
|
|
|
|
if (toolUseId) {
|
|
results.push({
|
|
toolUseId,
|
|
isError: hasError,
|
|
content: extractContentFromToolUseResult(toolUseResult),
|
|
toolName: typeof toolUseResult.toolName === 'string' ? toolUseResult.toolName : undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Pattern 3: Check content blocks for tool_result
|
|
if (Array.isArray(message.content)) {
|
|
for (const block of message.content) {
|
|
if (isToolResultContent(block)) {
|
|
results.push({
|
|
toolUseId: block.tool_use_id,
|
|
isError: block.is_error === true,
|
|
content: block.content,
|
|
toolName: findToolNameFn(message, block.tool_use_id) ?? undefined,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|