@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
599 lines (598 loc) • 22.8 kB
JavaScript
/**
* Agent Coordinator - Manages coordination between agents in a network
*
* The Coordinator is responsible for:
* - Managing agent lifecycle and state
* - Coordinating task execution across multiple agents
* - Handling agent dependencies and execution order
* - Managing shared context and state between agents
*/
import { EventEmitter } from "events";
import { randomUUID } from "crypto";
import { withTimeout } from "../../utils/async/withTimeout.js";
import { logger } from "../../utils/logger.js";
/**
* Agent Coordinator - Orchestrates multi-agent execution
*/
export class AgentCoordinator {
agents = new Map();
config;
emitter;
activeExecutions = new Map();
executionHistory = [];
roundRobinCursor = 0;
constructor(config) {
this.config = {
strategy: "sequential",
maxConcurrency: 3,
agentTimeout: 60000,
continueOnFailure: false,
...config,
};
this.emitter = new EventEmitter();
logger.debug("[AgentCoordinator] Created with config", {
strategy: this.config.strategy,
maxConcurrency: this.config.maxConcurrency,
});
}
/**
* Register an agent with the coordinator
*/
registerAgent(agent) {
this.agents.set(agent.id, agent);
logger.debug(`[AgentCoordinator] Registered agent: ${agent.name}`);
}
/**
* Unregister an agent
*/
unregisterAgent(agentId) {
this.agents.delete(agentId);
}
/**
* Get all registered agents
*/
getAgents() {
return Array.from(this.agents.values());
}
/**
* Get agent status
*/
getAgentStatus(agentId) {
const agent = this.agents.get(agentId);
return agent?.getStatus();
}
/**
* Execute a coordinated task across agents
*/
async coordinate(task, options) {
const startTime = Date.now();
const executionId = randomUUID();
const config = { ...this.config, ...options };
const context = {
currentStep: 0,
previousResults: new Map(),
sharedState: new Map(),
metadata: {
startTime,
strategy: config.strategy,
executionId,
},
};
this.emitter.emit("coordination:start", { executionId, task });
try {
let result;
switch (config.strategy) {
case "sequential":
result = await this.executeSequential(task, context, config);
break;
case "parallel":
result = await this.executeParallel(task, context, config);
break;
case "pipeline":
result = await this.executePipeline(task, context, config);
break;
case "roundRobin":
result = await this.executeRoundRobin(task, context, config);
break;
case "leastBusy":
result = await this.executeLeastBusy(task, context, config);
break;
case "custom":
if (!config.customCoordinator) {
throw new Error("Custom coordinator function required for custom strategy");
}
result = await config.customCoordinator(this.getAgents(), task, context);
break;
default:
throw new Error(`Unknown coordination strategy: ${config.strategy}`);
}
this.emitter.emit("coordination:complete", { executionId, result });
return result;
}
catch (error) {
const errorResult = {
success: false,
agentResults: context.previousResults,
steps: this.executionHistory,
errors: [
{
agentId: "coordinator",
error: error instanceof Error ? error.message : String(error),
},
],
duration: Date.now() - startTime,
metadata: {
executionId,
strategy: config.strategy,
agentsExecuted: context.previousResults.size,
agentsFailed: 1,
},
};
this.emitter.emit("coordination:error", { executionId, error });
return errorResult;
}
}
/**
* Execute agents sequentially
*/
async executeSequential(task, context, config) {
const agents = this.getAgents();
if (agents.length === 0) {
return {
success: false,
agentResults: context.previousResults,
steps: [],
finalOutput: "",
errors: [{ agentId: "coordinator", error: "No agents registered" }],
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: config.strategy ?? "sequential",
agentsExecuted: 0,
agentsFailed: 0,
},
};
}
const errors = [];
const steps = [];
let currentInput = task;
let finalOutput = "";
for (let i = 0; i < agents.length; i++) {
const agent = agents[i];
context.currentStep = i;
const stepStart = Date.now();
try {
const result = await this.executeAgentWithTimeout(agent, currentInput, context);
context.previousResults.set(agent.id, result);
steps.push({
index: i,
primitive: { type: "agent", id: agent.id, name: agent.name },
input: currentInput,
output: result.content,
duration: Date.now() - stepStart,
usage: result.usage,
timestamp: stepStart,
});
if (result.status === "success") {
currentInput = result.content;
finalOutput = result.content;
}
else if (result.error) {
errors.push({ agentId: agent.id, error: result.error });
if (!config.continueOnFailure) {
break;
}
}
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push({ agentId: agent.id, error: errorMsg });
steps.push({
index: i,
primitive: { type: "agent", id: agent.id, name: agent.name },
input: currentInput,
error: errorMsg,
duration: Date.now() - stepStart,
timestamp: stepStart,
});
if (!config.continueOnFailure) {
break;
}
}
}
return {
success: errors.length === 0,
agentResults: context.previousResults,
steps,
finalOutput,
errors,
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: "sequential",
agentsExecuted: context.previousResults.size,
agentsFailed: errors.length,
},
};
}
/**
* Execute agents in parallel
*/
async executeParallel(task, context, config) {
const agents = this.getAgents();
if (agents.length === 0) {
return {
success: false,
agentResults: context.previousResults,
steps: [],
finalOutput: "",
errors: [{ agentId: "coordinator", error: "No agents registered" }],
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: config.strategy ?? "parallel",
agentsExecuted: 0,
agentsFailed: 0,
},
};
}
const errors = [];
const steps = [];
const maxConcurrency = Math.max(1, config.maxConcurrency ?? 3);
// Split agents into batches based on concurrency
const batches = [];
for (let i = 0; i < agents.length; i += maxConcurrency) {
batches.push(agents.slice(i, i + maxConcurrency));
}
let stepIndex = 0;
const allResults = [];
for (const batch of batches) {
const batchPromises = batch.map(async (agent) => {
const stepStart = Date.now();
const currentIndex = stepIndex++;
try {
const result = await this.executeAgentWithTimeout(agent, task, context);
context.previousResults.set(agent.id, result);
steps.push({
index: currentIndex,
primitive: { type: "agent", id: agent.id, name: agent.name },
input: task,
output: result.content,
duration: Date.now() - stepStart,
usage: result.usage,
timestamp: stepStart,
});
if (result.status === "success") {
allResults.push(result.content);
}
else if (result.error) {
errors.push({ agentId: agent.id, error: result.error });
}
return result;
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push({ agentId: agent.id, error: errorMsg });
steps.push({
index: currentIndex,
primitive: { type: "agent", id: agent.id, name: agent.name },
input: task,
error: errorMsg,
duration: Date.now() - stepStart,
timestamp: stepStart,
});
return null;
}
});
await Promise.all(batchPromises);
}
// Combine results
const finalOutput = allResults.join("\n\n---\n\n");
return {
success: errors.length === 0,
agentResults: context.previousResults,
steps,
finalOutput,
errors,
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: "parallel",
agentsExecuted: context.previousResults.size,
agentsFailed: errors.length,
},
};
}
/**
* Execute agents in a pipeline (output feeds into next)
*/
async executePipeline(task, context, config) {
// Pipeline is essentially sequential with explicit input chaining
return this.executeSequential(task, context, config);
}
/**
* Execute using round-robin distribution
*/
async executeRoundRobin(task, context, _config) {
const agents = this.getAgents();
if (agents.length === 0) {
return {
success: false,
agentResults: new Map(),
steps: [],
errors: [{ agentId: "coordinator", error: "No agents registered" }],
duration: 0,
metadata: {
executionId: context.metadata.executionId,
strategy: "roundRobin",
agentsExecuted: 0,
agentsFailed: 0,
},
};
}
// For round robin, select the next agent in rotation using persistent cursor
const selectedIndex = this.roundRobinCursor % agents.length;
this.roundRobinCursor++;
const agent = agents[selectedIndex];
const stepStart = Date.now();
const result = await this.executeAgentWithTimeout(agent, task, context);
context.previousResults.set(agent.id, result);
const step = {
index: 0,
primitive: { type: "agent", id: agent.id, name: agent.name },
input: task,
output: result.content,
duration: Date.now() - stepStart,
usage: result.usage,
timestamp: stepStart,
};
return {
success: result.status === "success",
agentResults: context.previousResults,
steps: [step],
finalOutput: result.content,
errors: result.error ? [{ agentId: agent.id, error: result.error }] : [],
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: "roundRobin",
agentsExecuted: 1,
agentsFailed: result.status === "error" ? 1 : 0,
},
};
}
/**
* Execute using least busy agent
*/
async executeLeastBusy(task, context, _config) {
const agents = this.getAgents();
if (agents.length === 0) {
return {
success: false,
agentResults: new Map(),
steps: [],
errors: [{ agentId: "coordinator", error: "No agents registered" }],
duration: 0,
metadata: {
executionId: context.metadata.executionId,
strategy: "leastBusy",
agentsExecuted: 0,
agentsFailed: 0,
},
};
}
// Find the least busy agent (fewest active executions or lowest execution count)
let leastBusyAgent = agents[0];
let lowestCount = leastBusyAgent.getStatus().executionCount;
for (const agent of agents) {
const status = agent.getStatus();
if (status.executionCount < lowestCount) {
lowestCount = status.executionCount;
leastBusyAgent = agent;
}
}
const stepStart = Date.now();
const result = await this.executeAgentWithTimeout(leastBusyAgent, task, context);
context.previousResults.set(leastBusyAgent.id, result);
const step = {
index: 0,
primitive: {
type: "agent",
id: leastBusyAgent.id,
name: leastBusyAgent.name,
},
input: task,
output: result.content,
duration: Date.now() - stepStart,
usage: result.usage,
timestamp: stepStart,
};
return {
success: result.status === "success",
agentResults: context.previousResults,
steps: [step],
finalOutput: result.content,
errors: result.error
? [{ agentId: leastBusyAgent.id, error: result.error }]
: [],
duration: Date.now() - context.metadata.startTime,
metadata: {
executionId: context.metadata.executionId,
strategy: "leastBusy",
agentsExecuted: 1,
agentsFailed: result.status === "error" ? 1 : 0,
},
};
}
/**
* Execute an agent with timeout
*/
async executeAgentWithTimeout(agent, input, context) {
const timeout = this.config.agentTimeout ?? 60000;
const executionPromise = agent.execute(input, {
context: Object.fromEntries(context.sharedState),
traceId: context.metadata.executionId,
});
// Track active execution
this.activeExecutions.set(agent.id, executionPromise);
try {
return await withTimeout(executionPromise, timeout, `Agent execution timeout after ${timeout}ms`);
}
finally {
this.activeExecutions.delete(agent.id);
}
}
/**
* Execute multiple task assignments with dependencies
*/
async executeWithDependencies(assignments) {
const startTime = Date.now();
const executionId = randomUUID();
const context = {
currentStep: 0,
previousResults: new Map(),
sharedState: new Map(),
metadata: {
startTime,
strategy: "custom",
executionId,
},
};
const errors = [];
const steps = [];
const completed = new Set();
const failed = new Set();
const pending = new Map(assignments.map((a) => [a.agent.id, a]));
let stepCounter = 0;
while (pending.size > 0) {
// Find assignments whose dependencies are satisfied
const ready = [];
for (const [_agentId, assignment] of pending) {
const deps = assignment.dependencies ?? [];
if (deps.every((d) => completed.has(d))) {
ready.push(assignment);
}
}
if (ready.length === 0 && pending.size > 0) {
// Circular dependency or unmet dependency
errors.push({
agentId: "coordinator",
error: "Unresolvable dependencies detected",
});
break;
}
// Sort by priority and execute
ready.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
// Execute in parallel batches
const maxConcurrency = Math.max(1, this.config.maxConcurrency ?? 3);
for (let i = 0; i < ready.length; i += maxConcurrency) {
const batch = ready.slice(i, i + maxConcurrency);
const batchPromises = batch.map(async (assignment) => {
const stepStart = Date.now();
const stepIndex = stepCounter++;
// Fail fast if any dependency already failed
const deps = assignment.dependencies ?? [];
const hasFailedDep = deps.some((d) => failed.has(d));
if (hasFailedDep) {
const failedDepId = deps.find((d) => failed.has(d));
errors.push({
agentId: assignment.agent.id,
error: `Dependency '${failedDepId}' failed`,
});
failed.add(assignment.agent.id);
pending.delete(assignment.agent.id);
return null;
}
try {
const result = await this.executeAgentWithTimeout(assignment.agent, assignment.input, context);
context.previousResults.set(assignment.agent.id, result);
pending.delete(assignment.agent.id);
steps.push({
index: stepIndex,
primitive: {
type: "agent",
id: assignment.agent.id,
name: assignment.agent.name,
},
input: assignment.input,
output: result.content,
duration: Date.now() - stepStart,
usage: result.usage,
timestamp: stepStart,
});
if (result.error) {
errors.push({
agentId: assignment.agent.id,
error: result.error,
});
failed.add(assignment.agent.id);
}
else {
completed.add(assignment.agent.id);
}
return result;
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push({ agentId: assignment.agent.id, error: errorMsg });
failed.add(assignment.agent.id);
pending.delete(assignment.agent.id);
steps.push({
index: stepIndex,
primitive: {
type: "agent",
id: assignment.agent.id,
name: assignment.agent.name,
},
input: assignment.input,
error: errorMsg,
duration: Date.now() - stepStart,
timestamp: stepStart,
});
return null;
}
});
await Promise.all(batchPromises);
}
}
// Combine final outputs
const finalOutputs = [];
for (const result of context.previousResults.values()) {
if (result.content) {
finalOutputs.push(result.content);
}
}
return {
success: errors.length === 0,
agentResults: context.previousResults,
steps,
finalOutput: finalOutputs.join("\n\n"),
errors,
duration: Date.now() - startTime,
metadata: {
executionId,
strategy: "custom",
agentsExecuted: completed.size,
agentsFailed: errors.length,
},
};
}
/**
* Update coordinator configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
}
/**
* Subscribe to coordinator events
*/
on(event, handler) {
this.emitter.on(event, handler);
}
/**
* Unsubscribe from coordinator events
*/
off(event, handler) {
this.emitter.off(event, handler);
}
}