UNPKG

mcp-ai-agent-guidelines

Version:

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

339 lines 11.7 kB
/** * Async Patterns for A2A Orchestration * * Provides common async orchestration patterns: * - Map-Reduce for parallel processing with aggregation * - Pipeline with transformations between steps * - Conditional branching based on runtime state * - Scatter-Gather for fan-out/fan-in patterns */ import { OrchestrationError } from "./a2a-errors.js"; import { logger } from "./logger.js"; import { batchInvoke, invokeTool } from "./tool-invoker.js"; /** * Map-Reduce pattern: Apply a tool to multiple inputs in parallel, then reduce * * @param toolName - Tool to apply to each input * @param inputs - Array of inputs * @param context - A2A context * @param reducer - Function to combine results * @returns Reduced result */ export async function mapReduceTools(toolName, inputs, context, reducer) { logger.info("Starting map-reduce operation", { toolName, inputCount: inputs.length, correlationId: context.correlationId, }); // Map: Execute tool for each input in parallel const invocations = inputs.map((input) => ({ toolName, args: input, })); const results = await batchInvoke(invocations, context); // Check for failures const failures = results.filter((r) => !r.success); if (failures.length > 0) { logger.warn(`Map-reduce: ${failures.length} invocations failed`, { toolName, failureCount: failures.length, }); } // Reduce: Combine results return reducer(results); } /** * Pipeline pattern: Chain tools with transformations between steps * * @param pipeline - Array of pipeline steps * @param context - A2A context * @param initialInput - Initial input for first tool * @returns Final result */ export async function pipelineTools(pipeline, context, initialInput) { logger.info("Starting pipeline execution", { stageCount: pipeline.length, correlationId: context.correlationId, }); let currentInput = initialInput; for (let i = 0; i < pipeline.length; i++) { const stage = pipeline[i]; // Apply transform if provided const args = stage.transform ? stage.transform(currentInput) : currentInput; logger.debug(`Pipeline stage ${i + 1}/${pipeline.length}`, { toolName: stage.toolName, }); const result = await invokeTool(stage.toolName, args, context); if (!result.success) { throw new OrchestrationError(`Pipeline failed at stage ${i + 1} (${stage.toolName}): ${result.error}`, { stage: i + 1, toolName: stage.toolName }); } currentInput = result.data; } return { success: true, data: currentInput, }; } /** * Conditional branch pattern: Execute one of two tools based on a condition * * @param condition - Function to evaluate condition * @param trueBranch - Tool to execute if condition is true * @param falseBranch - Tool to execute if condition is false * @param args - Arguments for the selected tool * @param context - A2A context * @returns Result from the executed branch */ export async function branchOnCondition(condition, trueBranch, falseBranch, args, context) { const shouldExecuteTrueBranch = condition(context.sharedState); const selectedTool = shouldExecuteTrueBranch ? trueBranch : falseBranch; logger.info("Executing conditional branch", { condition: shouldExecuteTrueBranch, selectedTool, correlationId: context.correlationId, }); return invokeTool(selectedTool, args, context); } /** * Scatter-Gather pattern: Fan out to multiple tools, then gather results * * @param tools - Array of tools to invoke * @param args - Arguments (can be per-tool or shared) * @param context - A2A context * @param gatherer - Function to process gathered results * @returns Gathered result */ export async function scatterGatherTools(tools, context, gatherer) { logger.info("Starting scatter-gather operation", { toolCount: tools.length, correlationId: context.correlationId, }); // Scatter: Execute all tools in parallel const invocations = tools.map(({ toolName, args }) => ({ toolName, args, })); const results = await batchInvoke(invocations, context); // Create map of tool name to result const resultMap = new Map(); for (let i = 0; i < tools.length; i++) { resultMap.set(tools[i].toolName, results[i]); } // Gather: Process all results return gatherer(resultMap); } /** * Fan-out pattern: Execute a tool multiple times with different arguments * * @param toolName - Tool to execute * @param argsArray - Array of argument sets * @param context - A2A context * @param maxConcurrency - Maximum concurrent executions (undefined = unlimited) * @returns Array of results */ export async function fanOut(toolName, argsArray, context, maxConcurrency) { logger.info("Starting fan-out operation", { toolName, executionCount: argsArray.length, maxConcurrency, correlationId: context.correlationId, }); if (!maxConcurrency || maxConcurrency >= argsArray.length) { // Execute all in parallel const invocations = argsArray.map((args) => ({ toolName, args })); return batchInvoke(invocations, context); } // Execute with concurrency limit const results = []; const chunks = chunkArray(argsArray, maxConcurrency); for (const chunk of chunks) { const invocations = chunk.map((args) => ({ toolName, args })); const chunkResults = await batchInvoke(invocations, context); results.push(...chunkResults); } return results; } /** * Waterfall pattern: Execute tools in sequence, each using the previous result * * Similar to pipeline but without explicit transforms (tools handle previous output) * * @param tools - Array of tool names * @param context - A2A context * @param initialInput - Initial input * @returns Final result */ export async function waterfallTools(tools, context, initialInput) { logger.info("Starting waterfall execution", { toolCount: tools.length, correlationId: context.correlationId, }); let currentData = initialInput; for (const toolName of tools) { const result = await invokeTool(toolName, currentData, context); if (!result.success) { return result; // Propagate failure } currentData = result.data; } return { success: true, data: currentData, }; } /** * Race pattern: Execute multiple tools in parallel, return first success * * @param tools - Array of tools to race * @param args - Arguments (can be per-tool or shared) * @param context - A2A context * @returns First successful result */ export async function raceTools(tools, context) { logger.info("Starting race operation", { toolCount: tools.length, correlationId: context.correlationId, }); const promises = tools.map(async ({ toolName, args }) => { const result = await invokeTool(toolName, args, context); if (result.success) { return result; } throw new Error(`Tool ${toolName} failed: ${result.error}`); }); try { // Return first successful result return await Promise.race(promises); } catch (error) { // All failed return { success: false, error: `All tools failed in race: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Retry pattern: Retry a tool invocation with exponential backoff and optional jitter * * @param toolName - Tool to retry * @param args - Tool arguments * @param context - A2A context * @param maxRetries - Maximum retry attempts * @param initialDelayMs - Initial delay before first retry * @param backoffMultiplier - Multiplier for exponential backoff * @param jitterMs - Optional maximum jitter in milliseconds (default: 0) * @returns Tool result * * @remarks * Jitter is added to each retry delay to prevent thundering herd problems when * multiple tool chains fail simultaneously and retry at the same time. */ export async function retryTool(toolName, args, context, maxRetries = 3, initialDelayMs = 1000, backoffMultiplier = 2, jitterMs = 0) { let lastError; let delayMs = initialDelayMs; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await invokeTool(toolName, args, context); if (result.success) { return result; } lastError = new Error(result.error || "Tool execution failed"); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); } if (attempt < maxRetries) { // Add jitter to delay to prevent thundering herd const jitter = jitterMs > 0 ? Math.floor(Math.random() * jitterMs) : 0; const actualDelay = delayMs + jitter; logger.warn(`Retry attempt ${attempt + 1}/${maxRetries} for ${toolName}`, { delayMs: actualDelay, baseDelayMs: delayMs, jitterMs: jitter, }); await sleep(actualDelay); delayMs *= backoffMultiplier; } } return { success: false, error: `Failed after ${maxRetries} retries: ${lastError?.message}`, }; } /** * Fallback pattern: Try primary tool, fall back to secondary if it fails * * @param primaryTool - Primary tool to try * @param fallbackTool - Fallback tool if primary fails * @param args - Tool arguments * @param context - A2A context * @returns Result from primary or fallback */ export async function fallbackTool(primaryTool, fallbackTool, args, context) { logger.info("Trying primary tool", { primaryTool, fallbackTool, correlationId: context.correlationId, }); const primaryResult = await invokeTool(primaryTool, args, context); if (primaryResult.success) { return primaryResult; } logger.warn("Primary tool failed, trying fallback", { primaryTool, fallbackTool, primaryError: primaryResult.error, }); return invokeTool(fallbackTool, args, context); } /** * Helper: Chunk array into smaller arrays */ function chunkArray(array, chunkSize) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Helper: Sleep for specified milliseconds */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Common reducer functions for map-reduce */ export const reducers = { /** * Collect all successful results into an array */ collectSuccessful: (results) => results.filter((r) => r.success).map((r) => r.data), /** * Count successful executions */ countSuccessful: (results) => results.filter((r) => r.success).length, /** * Check if all executions succeeded */ allSucceeded: (results) => results.every((r) => r.success), /** * Check if any execution succeeded */ anySucceeded: (results) => results.some((r) => r.success), /** * Merge all results into a single object */ mergeResults: (results) => { const merged = {}; for (const result of results) { if (result.success && result.data) { Object.assign(merged, result.data); } } return merged; }, }; //# sourceMappingURL=async-patterns.js.map