UNPKG

mcp-ai-agent-guidelines

Version:

A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices

255 lines 9.83 kB
/** * Tool Invoker - Core runtime for A2A tool orchestration * * Provides the execution layer for tool-to-tool invocations with: * - Timeout enforcement * - Recursion depth guards * - Context propagation * - Execution logging * - Error handling and recovery */ import { addExecutionLogEntry, createChildContext, getRemainingChainTime, hasChainTimedOut, hashInput, } from "./a2a-context.js"; import { ChainTimeoutError, RecursionDepthError, ToolInvocationError, ToolTimeoutError, } from "./a2a-errors.js"; import { logger } from "./logger.js"; import { toolRegistry } from "./tool-registry.js"; /** * Invoke a tool with A2A context support * * This is the primary entry point for tool-to-tool invocations. * It handles: * - Context creation/propagation * - Recursion depth checking * - Timeout enforcement * - Execution logging * - Error handling * * @param toolName - Name of the tool to invoke * @param args - Arguments to pass to the tool * @param context - A2A context (optional for top-level, required for nested) * @param options - Invocation options * @returns Tool execution result * @throws RecursionDepthError if maximum depth exceeded * @throws ChainTimeoutError if chain timeout exceeded * @throws ToolTimeoutError if tool timeout exceeded * @throws ToolInvocationError if tool execution fails */ export async function invokeTool(toolName, args, context, options) { // If context provided, create child context; otherwise this is top-level let executionContext = context; if (context) { try { executionContext = createChildContext(context, toolName); } catch (error) { if (error instanceof Error && error.message.includes("depth")) { const depthError = new RecursionDepthError(context.depth + 1, context.maxDepth, { toolName, parentTool: context.parentToolName }); throw depthError; } throw error; } } // Check chain timeout if context exists if (executionContext && hasChainTimedOut(executionContext)) { const error = new ChainTimeoutError(executionContext.chainTimeoutMs || 0, executionContext.executionLog.length, { toolName }); throw error; } const startTime = Date.now(); const inputHash = hashInput(args); // Check for duplicate invocations if requested if (options?.deduplicate && executionContext) { const duplicate = findDuplicateInvocation(executionContext, toolName, inputHash); if (duplicate) { logger.debug(`Skipping duplicate invocation of ${toolName}`, { inputHash, originalTimestamp: duplicate.timestamp, }); // Return cached result from log return { success: duplicate.status === "success", data: { cached: true, outputSummary: duplicate.outputSummary }, metadata: { toolName, durationMs: 0, timestamp: new Date(), }, }; } } // Determine timeout (use option, context, or chain remaining time) let timeoutMs = options?.timeoutMs || executionContext?.timeoutMs; if (executionContext) { const remainingTime = getRemainingChainTime(executionContext); if (remainingTime !== undefined && timeoutMs) { timeoutMs = Math.min(timeoutMs, remainingTime); } } // Execute tool with timeout try { const result = await executeWithTimeout(() => toolRegistry.invoke(toolName, args, executionContext), timeoutMs, toolName); const durationMs = Date.now() - startTime; // Log successful execution if (executionContext) { addExecutionLogEntry(executionContext, { toolName, inputHash, outputSummary: summarizeOutput(result.data), durationMs, status: result.success ? "success" : "error", errorDetails: result.error, parentToolName: executionContext.parentToolName, }); } logger.info(`Tool '${toolName}' executed successfully`, { durationMs, depth: executionContext?.depth, correlationId: executionContext?.correlationId, }); return result; } catch (error) { const durationMs = Date.now() - startTime; // Handle custom error handler if provided if (options?.onError) { try { const recoveryResult = await options.onError(error instanceof Error ? error : new Error(String(error))); // Log error with recovery if (executionContext) { addExecutionLogEntry(executionContext, { toolName, inputHash, outputSummary: summarizeOutput(recoveryResult.data), durationMs, status: "success", errorDetails: `Recovered: ${error instanceof Error ? error.message : String(error)}`, parentToolName: executionContext.parentToolName, }); } return recoveryResult; } catch (recoveryError) { // Recovery failed, continue with original error handling logger.error("Error recovery failed", { toolName, originalError: error instanceof Error ? error.message : String(error), recoveryError: recoveryError instanceof Error ? recoveryError.message : String(recoveryError), }); } } // Log failed execution if (executionContext) { addExecutionLogEntry(executionContext, { toolName, inputHash, outputSummary: "", durationMs, status: "error", errorDetails: error instanceof Error ? error.message : String(error), parentToolName: executionContext.parentToolName, }); } // Re-throw as ToolInvocationError if not already an A2A error if (error instanceof RecursionDepthError || error instanceof ChainTimeoutError || error instanceof ToolTimeoutError) { throw error; } throw new ToolInvocationError(toolName, error instanceof Error ? error.message : String(error), { durationMs, depth: executionContext?.depth, correlationId: executionContext?.correlationId, }); } } /** * Execute a function with timeout * * @param fn - Function to execute * @param timeoutMs - Timeout in milliseconds (undefined = no timeout) * @param toolName - Tool name for error reporting * @returns Function result * @throws ToolTimeoutError if timeout exceeded */ async function executeWithTimeout(fn, timeoutMs, toolName) { if (!timeoutMs) { return fn(); } return Promise.race([ fn(), new Promise((_, reject) => { setTimeout(() => { reject(new ToolTimeoutError(toolName, timeoutMs)); }, timeoutMs); }), ]); } /** * Window size for deduplication checking * Only check the last N entries to avoid excessive searching */ const DEDUPLICATION_WINDOW_SIZE = 10; /** * Find duplicate invocation in execution log * * @param context - A2A context * @param toolName - Tool name * @param inputHash - Input hash to match * @returns Matching log entry or undefined */ function findDuplicateInvocation(context, toolName, inputHash) { // Look for recent invocations (within last DEDUPLICATION_WINDOW_SIZE entries) // to avoid excessive searching while still catching common duplicates const recentLog = context.executionLog.slice(-DEDUPLICATION_WINDOW_SIZE); return recentLog.find((entry) => entry.toolName === toolName && entry.inputHash === inputHash && entry.status === "success"); } /** * Create a summary of tool output for logging * * @param output - Tool output * @returns Summary string (max 200 chars) */ function summarizeOutput(output) { if (!output) { return ""; } const str = JSON.stringify(output); return str.length > 200 ? `${str.substring(0, 200)}...` : str; } /** * Batch invoke multiple tools in parallel * * @param invocations - Array of tool invocations * @param context - Optional A2A context * @returns Array of results (in same order as invocations) */ export async function batchInvoke(invocations, context) { const promises = invocations.map((inv) => invokeTool(inv.toolName, inv.args, context, inv.options)); return Promise.all(promises); } /** * Invoke tools sequentially, passing output from one to the next * * @param chain - Array of tool invocations with optional transform functions * @param context - Optional A2A context * @param initialInput - Initial input for first tool * @returns Final tool result */ export async function invokeSequence(chain, context, initialInput) { let currentInput = initialInput; let lastResult = { success: true, data: currentInput }; for (const step of chain) { // Apply transform if provided const args = step.transform ? step.transform(currentInput) : currentInput; lastResult = await invokeTool(step.toolName, args, context, step.options); if (!lastResult.success) { // Stop on first failure return lastResult; } currentInput = lastResult.data; } return lastResult; } //# sourceMappingURL=tool-invoker.js.map