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
JavaScript
/**
* 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