UNPKG

mcp-ai-agent-guidelines

Version:

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

337 lines 12.5 kB
/** * Execution Controller - Orchestration strategies for A2A tool chaining * * Provides declarative execution patterns: * - Sequential execution (one after another) * - Parallel execution (concurrent with Promise.all) * - Parallel with join (merge results) * - Conditional branching * - Retry with exponential backoff */ import { ExecutionStrategyError, OrchestrationError } from "./a2a-errors.js"; import { logger } from "./logger.js"; import { invokeTool } from "./tool-invoker.js"; /** * Execute a chain of tools according to the execution plan * * @param plan - Execution plan * @param context - A2A context * @returns Chain execution result */ export async function executeChain(plan, context) { const startTime = Date.now(); const stepResults = new Map(); logger.info("Starting chain execution", { strategy: plan.strategy, stepCount: plan.steps.length, correlationId: context.correlationId, }); try { switch (plan.strategy) { case "sequential": await executeSequential(plan.steps, context, stepResults, plan.onError); break; case "parallel": await executeParallel(plan.steps, context, stepResults, plan.onError); break; case "parallel-with-join": await executeParallelWithJoin(plan.steps, context, stepResults, plan.onError); break; case "conditional": await executeConditional(plan.steps, context, stepResults, plan.onError); break; case "retry-with-backoff": await executeWithRetry(plan.steps, context, stepResults, plan.onError, plan.retryConfig || getDefaultRetryConfig()); break; default: throw new ExecutionStrategyError(plan.strategy, `Unknown execution strategy: ${plan.strategy}`); } // Calculate summary const summary = calculateSummary(stepResults, startTime, context); // Determine final output const finalOutput = getFinalOutput(plan.steps, stepResults); return { success: summary.failedSteps === 0, stepResults, finalOutput, summary, }; } catch (error) { // Try fallback if configured if (plan.onError === "fallback" && plan.fallbackTool) { logger.warn("Chain execution failed, trying fallback", { fallbackTool: plan.fallbackTool, error: error instanceof Error ? error.message : String(error), }); try { const fallbackResult = await invokeTool(plan.fallbackTool, plan.fallbackArgs || {}, context); stepResults.set("fallback", fallbackResult); const summary = calculateSummary(stepResults, startTime, context); return { success: fallbackResult.success, stepResults, finalOutput: fallbackResult.data, summary, }; } catch (fallbackError) { logger.error("Fallback execution also failed", { fallbackTool: plan.fallbackTool, error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError), }); } } const summary = calculateSummary(stepResults, startTime, context); return { success: false, stepResults, error: error instanceof Error ? error.message : String(error), summary, }; } } /** * Execute steps sequentially */ async function executeSequential(steps, context, results, onError) { for (const step of steps) { // Check dependencies if (!areDependenciesMet(step, results)) { logger.warn(`Skipping step ${step.id} due to unmet dependencies`); results.set(step.id, { success: false, error: "Dependencies not met", }); continue; } try { // Get args (potentially transformed from previous output) const args = step.transform ? step.transform(getPreviousOutput(step, results)) : step.args; const result = await invokeTool(step.toolName, args, context, step.options); results.set(step.id, result); if (!result.success && onError === "abort") { throw new OrchestrationError(`Step ${step.id} failed: ${result.error}`, { stepId: step.id }); } } catch (error) { if (onError === "abort") { throw error; } if (onError === "skip") { logger.warn(`Step ${step.id} failed, skipping`, { error: error instanceof Error ? error.message : String(error), }); results.set(step.id, { success: false, error: error instanceof Error ? error.message : String(error), }); } } } } /** * Execute steps in parallel */ async function executeParallel(steps, context, results, onError) { // Group steps by dependency level const levels = groupStepsByDependency(steps); // Execute each level in sequence, but steps within level in parallel for (const levelSteps of levels) { const promises = levelSteps.map(async (step) => { try { const args = step.transform ? step.transform(getPreviousOutput(step, results)) : step.args; const result = await invokeTool(step.toolName, args, context, step.options); return { stepId: step.id, result }; } catch (error) { if (onError === "abort") { throw error; } return { stepId: step.id, result: { success: false, error: error instanceof Error ? error.message : String(error), }, }; } }); const levelResults = await Promise.all(promises); for (const { stepId, result } of levelResults) { results.set(stepId, result); if (!result.success && onError === "abort") { throw new OrchestrationError(`Step ${stepId} failed: ${result.error}`, { stepId, }); } } } } /** * Execute steps in parallel and join/merge results */ async function executeParallelWithJoin(steps, context, results, onError) { // Execute all independent steps in parallel await executeParallel(steps, context, results, onError); // Store merged output in shared state const mergedOutput = Array.from(results.values()) .filter((r) => r.success) .map((r) => r.data); context.sharedState.set("merged_results", mergedOutput); } /** * Execute steps with conditional branching */ async function executeConditional(steps, context, results, onError) { for (const step of steps) { // Check condition if present if (step.condition && !step.condition(context.sharedState)) { logger.debug(`Skipping step ${step.id} due to condition`); continue; } // Check dependencies if (!areDependenciesMet(step, results)) { continue; } try { const args = step.transform ? step.transform(getPreviousOutput(step, results)) : step.args; const result = await invokeTool(step.toolName, args, context, step.options); results.set(step.id, result); if (!result.success && onError === "abort") { throw new OrchestrationError(`Step ${step.id} failed: ${result.error}`, { stepId: step.id }); } } catch (error) { if (onError === "abort") { throw error; } results.set(step.id, { success: false, error: error instanceof Error ? error.message : String(error), }); } } } /** * Execute steps with retry and exponential backoff */ async function executeWithRetry(steps, context, results, onError, retryConfig) { for (const step of steps) { if (!areDependenciesMet(step, results)) { continue; } let lastError; let delayMs = retryConfig.initialDelayMs; for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) { try { const args = step.transform ? step.transform(getPreviousOutput(step, results)) : step.args; const result = await invokeTool(step.toolName, args, context, step.options); results.set(step.id, result); if (!result.success && onError === "abort") { throw new OrchestrationError(`Step ${step.id} failed: ${result.error}`, { stepId: step.id }); } break; // Success, exit retry loop } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < retryConfig.maxRetries) { logger.warn(`Step ${step.id} failed, retrying in ${delayMs}ms`, { attempt: attempt + 1, maxRetries: retryConfig.maxRetries, }); await sleep(delayMs); delayMs = Math.min(delayMs * retryConfig.backoffMultiplier, retryConfig.maxDelayMs); } } } // All retries failed if (lastError) { if (onError === "abort") { throw lastError; } results.set(step.id, { success: false, error: lastError.message, }); } } } /** * Helper functions */ function areDependenciesMet(step, results) { if (!step.dependencies || step.dependencies.length === 0) { return true; } return step.dependencies.every((depId) => results.has(depId) && results.get(depId)?.success); } function getPreviousOutput(step, results) { if (!step.dependencies || step.dependencies.length === 0) { return undefined; } // Return output from first dependency const depId = step.dependencies[0]; return results.get(depId)?.data; } function groupStepsByDependency(steps) { const levels = []; const processed = new Set(); while (processed.size < steps.length) { const currentLevel = steps.filter((step) => !processed.has(step.id) && (!step.dependencies || step.dependencies.every((dep) => processed.has(dep)))); if (currentLevel.length === 0) { throw new OrchestrationError("Circular dependency detected in execution plan"); } levels.push(currentLevel); for (const step of currentLevel) { processed.add(step.id); } } return levels; } function getFinalOutput(steps, results) { // Return output from last successful step for (let i = steps.length - 1; i >= 0; i--) { const result = results.get(steps[i].id); if (result?.success) { return result.data; } } return undefined; } function calculateSummary(results, startTime, context) { const values = Array.from(results.values()); // Count skipped steps from execution log const skippedCount = context.executionLog.filter((entry) => entry.status === "skipped").length; return { totalSteps: values.length, successfulSteps: values.filter((r) => r.success).length, failedSteps: values.filter((r) => !r.success).length, skippedSteps: skippedCount, totalDurationMs: Date.now() - startTime, }; } function getDefaultRetryConfig() { return { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000, backoffMultiplier: 2, }; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } //# sourceMappingURL=execution-controller.js.map