feat(agent): add bounded execution controller

This commit is contained in:
2026-09-15 11:51:50 +08:00
parent ec998005ac
commit eaec144ee7
7 changed files with 772 additions and 2 deletions
+9
View File
@@ -8,6 +8,8 @@ AI Parallel v2 uses a live iframe workspace instead of mirroring model responses
Workspace extension page
├─ Provider Task Runtime
│ └─ Provider Adapter Contract → shared iframe/tab transport
├─ Agent Execution Controller
│ └─ bounded planner / executor / reviewer aggregation
├─ ChatGPT iframe
├─ DeepSeek iframe
├─ 智谱 iframe
@@ -33,6 +35,13 @@ tracks each operation in memory, bounds retry attempts, exposes timeout and
cancellation states, and keeps deterministic provider failures visible. Task
history and response snapshots are not persisted.
The shared Agent Execution Controller provides an explicit, bounded parallel
execution model for planner, executor, and reviewer roles. It allows at most
eight agents per execution and three concurrent provider tasks by default. Each
agent still runs through the Provider Task Runtime and Adapter Contract; one
failure is isolated and reported in the aggregate. Prompts and responses remain
in memory only, with no autonomous loop or background execution.
## Install from a release
Download and extract `ai-parallel-browser-extension-vX.Y.Z.zip`. Open
+1 -1
View File
@@ -4,6 +4,6 @@
"version": "2.1.4",
"description": "Manifest V3 workspace for comparing AI web chats in parallel.",
"scripts": {
"check": "node --check shared/provider-catalog.js && node --check shared/provider-adapter-contract.js && node --check shared/provider-task-runtime.js && node --check service-worker.js && node --check popup.js && node --check content/frame-bridge.js && node --check workspace/workspace.js"
"check": "node --check shared/provider-catalog.js && node --check shared/provider-adapter-contract.js && node --check shared/provider-task-runtime.js && node --check shared/agent-execution-controller.js && node --check service-worker.js && node --check popup.js && node --check content/frame-bridge.js && node --check workspace/workspace.js"
}
}
@@ -0,0 +1,517 @@
(() => {
const REQUIRED_ADAPTER_METHODS = Object.freeze([
"sendPrompt",
"collectResponse",
"newChat",
"healthCheck"
]);
const ROLES = Object.freeze(["planner", "executor", "reviewer"]);
const STATUS = Object.freeze({
IDLE: "IDLE",
QUEUED: "QUEUED",
RUNNING: "RUNNING",
SUCCESS: "SUCCESS",
PARTIAL: "PARTIAL",
FAILED: "FAILED",
CANCELLED: "CANCELLED"
});
const AGENT_STATUS = Object.freeze({
PENDING: "PENDING",
RUNNING: "RUNNING",
SUCCESS: "SUCCESS",
FAILED: "FAILED",
TIMEOUT: "TIMEOUT",
CANCELLED: "CANCELLED"
});
const TERMINAL_STATUSES = new Set([
STATUS.SUCCESS,
STATUS.PARTIAL,
STATUS.FAILED,
STATUS.CANCELLED
]);
const DEFAULT_MAX_CONCURRENCY = 3;
const DEFAULT_MAX_AGENTS = 8;
const DEFAULT_TIMEOUT_MS = 30000;
const DEFAULT_MAX_HISTORY = 50;
let executionSequence = 0;
function defaultIdFactory() {
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
executionSequence += 1;
return `agent-execution-${Date.now()}-${executionSequence}`;
}
function positiveInteger(value, fallback) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.max(1, Math.floor(number)) : fallback;
}
function asMessage(error, fallback = "Agent execution failed") {
if (error instanceof Error && error.message) return error.message;
if (typeof error === "string" && error.trim()) return error.trim();
if (error && typeof error.message === "string" && error.message.trim()) return error.message.trim();
if (error && typeof error.error === "string" && error.error.trim()) return error.error.trim();
return fallback;
}
function asErrorSnapshot(error, fallback) {
return {
message: asMessage(error, fallback),
code: typeof error?.code === "string" ? error.code : null,
retryable: error?.retryable === true
};
}
function adapterIsValid(adapter) {
const contract = globalThis.AIParallelProviderAdapterContract;
if (contract?.validateProviderAdapter) return contract.validateProviderAdapter(adapter);
return Boolean(
adapter
&& typeof adapter.id === "string"
&& typeof adapter.providerId === "string"
&& REQUIRED_ADAPTER_METHODS.every((method) => typeof adapter[method] === "function")
);
}
function toAdapterMap(adapters) {
if (adapters instanceof Map) return new Map(adapters);
if (adapters && typeof adapters === "object") return new Map(Object.entries(adapters));
return new Map();
}
function snapshotAgent(agent) {
return {
id: agent.id,
providerId: agent.providerId,
role: agent.role,
taskId: agent.taskId,
status: agent.status,
attempt: agent.attempt,
maxAttempts: agent.maxAttempts,
startedAt: agent.startedAt,
finishedAt: agent.finishedAt,
error: agent.error ? { ...agent.error } : null,
response: agent.response
};
}
function snapshotExecution(execution) {
return {
id: execution.id,
taskId: execution.taskId,
strategy: execution.strategy,
status: execution.status,
ok: execution.status === STATUS.SUCCESS || execution.status === STATUS.PARTIAL,
cancelRequested: execution.cancelRequested,
startedAt: execution.startedAt,
finishedAt: execution.finishedAt,
maxConcurrency: execution.maxConcurrency,
scope: { ...execution.scope },
agents: execution.agents.map(snapshotAgent),
responses: execution.responses.map((response) => ({ ...response })),
error: execution.error
};
}
class AgentExecutionController {
constructor({
taskRuntime,
adapters,
maxConcurrency = DEFAULT_MAX_CONCURRENCY,
maxAgents = DEFAULT_MAX_AGENTS,
timeoutMs = DEFAULT_TIMEOUT_MS,
maxHistory = DEFAULT_MAX_HISTORY,
idFactory = defaultIdFactory,
now = () => Date.now()
} = {}) {
if (!taskRuntime || typeof taskRuntime.run !== "function") {
throw new TypeError("Agent execution controller requires a provider task runtime");
}
this.taskRuntime = taskRuntime;
this.adapters = toAdapterMap(adapters);
this.maxConcurrency = positiveInteger(maxConcurrency, DEFAULT_MAX_CONCURRENCY);
this.maxAgents = Math.min(positiveInteger(maxAgents, DEFAULT_MAX_AGENTS), DEFAULT_MAX_AGENTS);
this.timeoutMs = positiveInteger(timeoutMs, DEFAULT_TIMEOUT_MS);
this.maxHistory = positiveInteger(maxHistory, DEFAULT_MAX_HISTORY);
this.idFactory = idFactory;
this.now = now;
this.executions = new Map();
this.finishedExecutionIds = [];
this.listeners = new Set();
for (const adapter of this.adapters.values()) {
if (!adapterIsValid(adapter)) {
throw new TypeError("Agent execution controller received an invalid provider adapter");
}
}
}
subscribe(listener) {
if (typeof listener !== "function") throw new TypeError("Agent execution listener must be a function");
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
createExecution({
taskId,
strategy = "parallel",
prompt,
agents,
scope = {},
maxConcurrency = this.maxConcurrency,
timeoutMs = this.timeoutMs
} = {}) {
if (strategy !== "parallel") throw new TypeError("Only the parallel agent strategy is supported");
if (typeof prompt !== "string" || !prompt.trim()) throw new TypeError("Agent execution requires a prompt");
if (!Array.isArray(agents) || agents.length === 0) {
throw new TypeError("Agent execution requires at least one agent");
}
if (agents.length > this.maxAgents) {
throw new RangeError(`Agent execution is limited to ${this.maxAgents} agents`);
}
const executionId = String(taskId || this.idFactory());
if (!executionId.trim()) throw new TypeError("Agent execution requires a task ID");
if (this.executions.has(executionId)) throw new Error(`Agent execution already exists: ${executionId}`);
const normalizedConcurrency = Math.min(
positiveInteger(maxConcurrency, this.maxConcurrency),
this.maxAgents
);
const normalizedTimeout = positiveInteger(timeoutMs, this.timeoutMs);
const normalizedAgents = agents.map((agent, index) => this.normalizeAgent(agent, index, executionId));
const agentIds = new Set();
for (const agent of normalizedAgents) {
if (agentIds.has(agent.id)) throw new Error(`Agent IDs must be unique: ${agent.id}`);
agentIds.add(agent.id);
}
const scopeInput = scope && typeof scope === "object" ? scope : {};
const execution = {
id: executionId,
taskId: executionId,
strategy,
status: STATUS.IDLE,
cancelRequested: false,
startedAt: null,
finishedAt: null,
maxConcurrency: normalizedConcurrency,
scope: {
id: String(scopeInput.id || executionId),
label: String(scopeInput.label || "bounded-agent-execution"),
providerIds: normalizedAgents.map((agent) => agent.providerId),
roles: normalizedAgents.map((agent) => agent.role)
},
agents: normalizedAgents,
responses: [],
error: null
};
const record = {
execution,
prompt: prompt.trim(),
timeoutMs: normalizedTimeout,
nextAgentIndex: 0,
activeCount: 0,
activeTasks: new Map(),
cancelReason: "Agent execution cancelled",
cancelRequested: false,
completed: false,
resolve: null,
promise: null,
pump: null
};
Object.defineProperty(execution, "run", {
enumerable: false,
value: () => this.start(execution.id)
});
Object.defineProperty(execution, "cancel", {
enumerable: false,
value: (reason) => this.cancel(execution.id, reason)
});
this.executions.set(execution.id, record);
this.notify(execution);
return execution;
}
run(options) {
const execution = this.createExecution(options);
const promise = this.start(execution.id);
promise.execution = execution;
promise.executionId = execution.id;
promise.cancel = execution.cancel;
return promise;
}
start(executionOrId) {
const executionId = typeof executionOrId === "string" ? executionOrId : executionOrId?.id;
const record = this.executions.get(executionId);
if (!record) return Promise.reject(new Error("Unknown agent execution"));
if (record.promise) return record.promise;
record.promise = new Promise((resolve) => {
record.resolve = resolve;
});
this.setStatus(record.execution, STATUS.QUEUED);
Promise.resolve().then(() => this.runRecord(record));
return record.promise;
}
cancel(executionOrId, reason = "Agent execution cancelled") {
const executionId = typeof executionOrId === "string" ? executionOrId : executionOrId?.id;
const record = this.executions.get(executionId);
if (!record || record.completed) return false;
record.cancelRequested = true;
record.cancelReason = asMessage(reason, "Agent execution cancelled");
record.execution.cancelRequested = true;
this.cancelQueuedAgents(record);
for (const task of record.activeTasks.values()) task.cancel?.(record.cancelReason);
this.notify(record.execution);
if (record.activeCount === 0) this.finish(record, STATUS.CANCELLED);
return true;
}
getExecution(executionId) {
const record = this.executions.get(String(executionId));
return record ? snapshotExecution(record.execution) : null;
}
listExecutions({ includeFinished = true } = {}) {
return [...this.executions.values()]
.filter((record) => includeFinished || !TERMINAL_STATUSES.has(record.execution.status))
.map((record) => snapshotExecution(record.execution));
}
normalizeAgent(agent, index, executionId) {
if (!agent || typeof agent !== "object") throw new TypeError(`Agent ${index + 1} is invalid`);
if (typeof agent.providerId !== "string" || !agent.providerId.trim()) {
throw new TypeError(`Agent ${index + 1} requires a provider ID`);
}
if (!ROLES.includes(agent.role)) {
throw new TypeError(`Agent ${index + 1} role must be planner, executor, or reviewer`);
}
const adapter = this.adapterFor(agent.providerId);
if (!adapter) throw new Error(`Provider adapter not found: ${agent.providerId}`);
const id = String(agent.id || `${executionId}:agent-${index + 1}`);
return {
id,
providerId: agent.providerId,
role: agent.role,
taskId: null,
status: AGENT_STATUS.PENDING,
attempt: 0,
maxAttempts: adapter.capabilities?.retry === true ? 2 : 1,
startedAt: null,
finishedAt: null,
error: null,
response: null
};
}
adapterFor(providerId) {
const direct = this.adapters.get(providerId);
if (direct?.providerId === providerId) return direct;
for (const adapter of this.adapters.values()) {
if (adapter?.providerId === providerId) return adapter;
}
return null;
}
runRecord(record) {
if (record.completed) return;
if (record.cancelRequested) {
this.cancelQueuedAgents(record);
this.finish(record, STATUS.CANCELLED);
return;
}
record.execution.startedAt = this.now();
this.setStatus(record.execution, STATUS.RUNNING);
record.pump = () => this.pump(record);
record.pump();
}
pump(record) {
if (record.completed) return;
if (record.cancelRequested) {
this.cancelQueuedAgents(record);
if (record.activeCount === 0) this.finish(record, STATUS.CANCELLED);
return;
}
while (
record.activeCount < record.execution.maxConcurrency
&& record.nextAgentIndex < record.execution.agents.length
) {
const agent = record.execution.agents[record.nextAgentIndex++];
record.activeCount += 1;
const agentPromise = this.startAgent(record, agent);
agentPromise.then(
() => this.agentFinished(record, agent.id),
() => this.agentFinished(record, agent.id)
);
}
if (record.nextAgentIndex >= record.execution.agents.length && record.activeCount === 0) {
this.finish(record, this.finalStatus(record));
}
}
agentFinished(record, agentId) {
record.activeCount = Math.max(0, record.activeCount - 1);
record.activeTasks.delete(agentId);
if (!record.completed) record.pump?.();
}
async startAgent(record, agent) {
const adapter = this.adapterFor(agent.providerId);
agent.status = AGENT_STATUS.RUNNING;
agent.startedAt = this.now();
this.notify(record.execution);
try {
if (record.cancelRequested) {
this.markAgentCancelled(agent, record.cancelReason);
return;
}
const taskPromise = this.taskRuntime.run({
providerId: agent.providerId,
operation: `agent-${agent.role}`,
timeoutMs: record.timeoutMs,
maxAttempts: agent.maxAttempts,
retryOn: (error) => error?.retryable === true,
execute: ({ signal, attempt }) => adapter.sendPrompt(
this.scopedPrompt(record.prompt, record.execution, agent),
{
signal,
attempt,
timeoutMs: record.timeoutMs,
executionId: record.execution.id,
scopeId: record.execution.scope.id,
agentId: agent.id,
role: agent.role
}
)
});
record.activeTasks.set(agent.id, taskPromise);
agent.taskId = taskPromise.taskId || taskPromise.task?.id || null;
this.notify(record.execution);
const result = await taskPromise;
agent.attempt = taskPromise.task?.attempt || agent.attempt;
if (record.cancelRequested || result?.status === "CANCELLED") {
this.markAgentCancelled(agent, record.cancelReason);
} else if (result?.ok === false) {
agent.status = result.status === "TIMEOUT" ? AGENT_STATUS.TIMEOUT : AGENT_STATUS.FAILED;
agent.error = asErrorSnapshot(result, result.status || "Agent task failed");
} else {
agent.status = AGENT_STATUS.SUCCESS;
agent.response = result?.response ?? result;
agent.error = null;
}
} catch (error) {
agent.attempt = agent.attempt || 1;
if (record.cancelRequested || error?.code === "TASK_CANCELLED") {
this.markAgentCancelled(agent, record.cancelReason);
} else {
agent.status = error?.code === "TASK_TIMEOUT" ? AGENT_STATUS.TIMEOUT : AGENT_STATUS.FAILED;
agent.error = asErrorSnapshot(error);
}
} finally {
agent.finishedAt = this.now();
this.notify(record.execution);
}
}
scopedPrompt(prompt, execution, agent) {
return [
`You are the ${agent.role} agent in a bounded AI Parallel execution.`,
`Execution scope: ${execution.scope.label} (${execution.scope.id}).`,
"Follow the requested scope and return only the useful result.",
"",
prompt
].join("\n");
}
markAgentCancelled(agent, reason) {
agent.status = AGENT_STATUS.CANCELLED;
agent.error = asErrorSnapshot({ code: "AGENT_CANCELLED", retryable: false }, reason);
agent.response = null;
}
cancelQueuedAgents(record) {
for (let index = record.nextAgentIndex; index < record.execution.agents.length; index += 1) {
const agent = record.execution.agents[index];
if (agent.status !== AGENT_STATUS.PENDING) continue;
this.markAgentCancelled(agent, record.cancelReason);
agent.finishedAt = this.now();
}
record.nextAgentIndex = record.execution.agents.length;
this.notify(record.execution);
}
finalStatus(record) {
if (record.cancelRequested) return STATUS.CANCELLED;
const succeeded = record.execution.agents.filter((agent) => agent.status === AGENT_STATUS.SUCCESS).length;
return succeeded === record.execution.agents.length
? STATUS.SUCCESS
: succeeded > 0 ? STATUS.PARTIAL : STATUS.FAILED;
}
finish(record, status) {
if (record.completed) return;
record.completed = true;
const execution = record.execution;
execution.status = status;
execution.finishedAt = this.now();
execution.error = status === STATUS.SUCCESS || status === STATUS.PARTIAL
? null
: status === STATUS.CANCELLED ? record.cancelReason : "All agent tasks failed";
execution.responses = execution.agents
.filter((agent) => agent.status === AGENT_STATUS.SUCCESS)
.map((agent) => ({
agentId: agent.id,
providerId: agent.providerId,
role: agent.role,
response: agent.response
}));
const result = snapshotExecution(execution);
record.prompt = null;
record.activeTasks.clear();
this.finishedExecutionIds.push(execution.id);
while (this.finishedExecutionIds.length > this.maxHistory) {
this.executions.delete(this.finishedExecutionIds.shift());
}
this.notify(execution);
record.resolve?.(result);
}
setStatus(execution, status) {
if (execution.status === status) return;
execution.status = status;
this.notify(execution);
}
notify(execution) {
const snapshot = snapshotExecution(execution);
for (const listener of this.listeners) {
try { listener(snapshot); } catch { /* Observers must not affect agent execution. */ }
}
}
}
globalThis.AIParallelAgentExecutionController = Object.freeze({
ROLES,
STATUS,
AGENT_STATUS,
TERMINAL_STATUSES: Object.freeze([...TERMINAL_STATUSES]),
AgentExecutionController,
createAgentExecutionController(options) {
return new AgentExecutionController(options);
}
});
})();
@@ -134,6 +134,7 @@
<script src="../shared/provider-catalog.js"></script>
<script src="../shared/provider-adapter-contract.js"></script>
<script src="../shared/provider-task-runtime.js"></script>
<script src="../shared/agent-execution-controller.js"></script>
<script src="context-utils.js"></script>
<script src="workspace.js"></script>
</body>
+13
View File
@@ -11,6 +11,10 @@ AI Parallel Workspace (extension page)
├─ Provider Task Runtime
│ ├─ bounded retry / timeout / cancel
│ └─ in-memory task state and observation
├─ Agent Execution Controller
│ ├─ explicit planner / executor / reviewer scope
│ ├─ bounded parallel scheduling
│ └─ in-memory result aggregation and failure isolation
├─ ChatGPT iframe
├─ DeepSeek iframe
├─ 智谱 iframe
@@ -76,6 +80,15 @@ Tasks expose `IDLE`, `QUEUED`, `RUNNING`, `SUCCESS`, `FAILED`, `TIMEOUT`, and
only explicitly retryable transport or timeout failures are retried. Task history
and response snapshots are not written to extension storage.
The Agent Execution Controller is a user-triggered, in-memory layer above the
Provider Task Runtime. It accepts an explicit `parallel` scope containing no
more than eight planner, executor, or reviewer agents, schedules at most three
agents concurrently by default, and sends every agent prompt through the
selected Provider Adapter and Runtime. A failed agent is isolated so remaining
agents can complete; the aggregate is `SUCCESS`, `PARTIAL`, `FAILED`, or
`CANCELLED`. It does not create autonomous loops, persist prompts/responses, or
bypass the provider message boundary.
## Provider boundaries
Provider-specific DOM knowledge is isolated in `content/providers/<provider>.js`,
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "2.1.4",
"description": "Monorepo for parallel AI workflows across browser, web and desktop clients.",
"scripts": {
"check": "node --check apps/browser-extension/shared/provider-catalog.js && node --check apps/browser-extension/shared/provider-adapter-contract.js && node --check apps/browser-extension/shared/provider-task-runtime.js && node --check apps/browser-extension/service-worker.js && node --check apps/browser-extension/popup.js && node --check apps/browser-extension/content/providers/core.js && node --check apps/browser-extension/content/providers/chatgpt.js && node --check apps/browser-extension/content/providers/deepseek.js && node --check apps/browser-extension/content/providers/qwen.js && node --check apps/browser-extension/content/providers/kimi.js && node --check apps/browser-extension/content/providers/zhipu.js && node --check apps/browser-extension/content/providers/claude.js && node --check apps/browser-extension/content/providers/gemini.js && node --check apps/browser-extension/content/providers/grok.js && node --check apps/browser-extension/content/frame-bridge.js && node --check apps/browser-extension/workspace/context-utils.js && node --check apps/browser-extension/workspace/workspace.js",
"check": "node --check apps/browser-extension/shared/provider-catalog.js && node --check apps/browser-extension/shared/provider-adapter-contract.js && node --check apps/browser-extension/shared/provider-task-runtime.js && node --check apps/browser-extension/shared/agent-execution-controller.js && node --check apps/browser-extension/service-worker.js && node --check apps/browser-extension/popup.js && node --check apps/browser-extension/content/providers/core.js && node --check apps/browser-extension/content/providers/chatgpt.js && node --check apps/browser-extension/content/providers/deepseek.js && node --check apps/browser-extension/content/providers/qwen.js && node --check apps/browser-extension/content/providers/kimi.js && node --check apps/browser-extension/content/providers/zhipu.js && node --check apps/browser-extension/content/providers/claude.js && node --check apps/browser-extension/content/providers/gemini.js && node --check apps/browser-extension/content/providers/grok.js && node --check apps/browser-extension/content/frame-bridge.js && node --check apps/browser-extension/workspace/context-utils.js && node --check apps/browser-extension/workspace/workspace.js",
"test": "node --test tests/*.test.js",
"package:extension": "bash scripts/package-extension.sh"
}
+230
View File
@@ -0,0 +1,230 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const extensionRoot = path.join(__dirname, "..", "apps", "browser-extension");
function loadController() {
const context = { console, setTimeout, clearTimeout, AbortController };
context.globalThis = context;
vm.createContext(context);
for (const file of [
"shared/provider-task-runtime.js",
"shared/provider-adapter-contract.js",
"shared/agent-execution-controller.js"
]) {
vm.runInContext(fs.readFileSync(path.join(extensionRoot, file), "utf8"), context, { filename: file });
}
return context;
}
function createAdapter(providerId, sendPrompt, { retry = false } = {}) {
return {
id: providerId,
providerId,
capabilities: { retry },
sendPrompt,
collectResponse() { return Promise.resolve({ ok: true }); },
newChat() { return Promise.resolve({ ok: true }); },
healthCheck() { return Promise.resolve({ ok: true }); }
};
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
test("agent execution runs through provider runtime with bounded parallelism", async () => {
const context = loadController();
const runtime = context.AIParallelProviderTaskRuntime.createProviderTaskRuntime({
defaultTimeoutMs: 1000,
idFactory: (() => {
let sequence = 0;
return () => `provider-task-${++sequence}`;
})()
});
let active = 0;
let maxActive = 0;
const calls = [];
const adapters = {};
for (const providerId of ["chatgpt", "claude", "gemini", "grok"]) {
adapters[providerId] = createAdapter(providerId, (prompt, adapterContext) => {
active += 1;
maxActive = Math.max(maxActive, active);
calls.push({ providerId, prompt, context: adapterContext });
return new Promise((resolve) => setTimeout(() => {
active -= 1;
resolve({ ok: true, response: { content: `${providerId} result` } });
}, 8));
});
}
const controller = context.AIParallelAgentExecutionController.createAgentExecutionController({
taskRuntime: runtime,
adapters,
maxConcurrency: 2,
maxAgents: 8,
idFactory: () => "execution-1"
});
const events = [];
controller.subscribe((snapshot) => events.push(snapshot.status));
const promise = controller.run({
taskId: "research-task",
prompt: "Compare the current approaches and identify the strongest result.",
scope: { id: "research-scope", label: "bounded research" },
agents: [
{ providerId: "chatgpt", role: "planner" },
{ providerId: "claude", role: "executor" },
{ providerId: "gemini", role: "executor" },
{ providerId: "grok", role: "reviewer" }
]
});
const result = await promise;
assert.equal(result.status, "SUCCESS");
assert.equal(result.ok, true);
assert.equal(result.strategy, "parallel");
assert.equal(result.scope.id, "research-scope");
assert.equal(result.maxConcurrency, 2);
assert.equal(result.agents.length, 4);
assert.ok(result.agents.every((agent) => agent.status === "SUCCESS"));
assert.equal(result.responses.length, 4);
assert.equal("prompt" in result, false);
assert.equal(maxActive, 2);
assert.equal(calls.length, 4);
assert.ok(calls.every(({ prompt }) => prompt.includes("Compare the current approaches")));
assert.ok(calls.every(({ context: adapterContext }) => adapterContext.executionId === "research-task"));
assert.ok(calls.every(({ context: adapterContext }) => adapterContext.scopeId === "research-scope"));
assert.deepEqual(
[...runtime.listTasks().map((task) => task.operation)].sort(),
["agent-executor", "agent-executor", "agent-planner", "agent-reviewer"]
);
assert.deepEqual(events.slice(0, 3), ["IDLE", "QUEUED", "RUNNING"]);
assert.equal(events.at(-1), "SUCCESS");
});
test("one agent failure is isolated and produces a partial aggregate", async () => {
const context = loadController();
const runtime = context.AIParallelProviderTaskRuntime.createProviderTaskRuntime({
defaultTimeoutMs: 1000,
idFactory: (() => {
let sequence = 0;
return () => `provider-task-${++sequence}`;
})()
});
const calls = [];
const adapters = {
chatgpt: createAdapter("chatgpt", () => Promise.resolve({
ok: true,
response: { content: "planned" }
})),
claude: createAdapter("claude", () => {
calls.push("failed");
return Promise.resolve({ ok: false, error: "provider unavailable", code: "UNAVAILABLE" });
}),
gemini: createAdapter("gemini", () => {
calls.push("continued");
return Promise.resolve({ ok: true, response: { content: "reviewed" } });
})
};
const controller = context.AIParallelAgentExecutionController.createAgentExecutionController({
taskRuntime: runtime,
adapters,
maxConcurrency: 1,
idFactory: () => "execution-2"
});
const result = await controller.run({
prompt: "Run each bounded role independently.",
agents: [
{ providerId: "chatgpt", role: "planner" },
{ providerId: "claude", role: "executor" },
{ providerId: "gemini", role: "reviewer" }
]
});
assert.equal(result.status, "PARTIAL");
assert.equal(result.agents.filter((agent) => agent.status === "SUCCESS").length, 2);
assert.equal(result.agents.find((agent) => agent.providerId === "claude").status, "FAILED");
assert.equal(result.agents.find((agent) => agent.providerId === "claude").error.message, "provider unavailable");
assert.equal(result.responses.length, 2);
assert.equal("prompt" in result, false);
assert.deepEqual(calls, ["failed", "continued"]);
assert.equal(runtime.listTasks({ includeFinished: false }).length, 0);
});
test("cancelling an execution cancels active and queued agents", async () => {
const context = loadController();
const runtime = context.AIParallelProviderTaskRuntime.createProviderTaskRuntime({
defaultTimeoutMs: 1000,
idFactory: () => "provider-task-cancel"
});
let providerStarted;
const providerStartedPromise = new Promise((resolve) => { providerStarted = resolve; });
const adapters = {
chatgpt: createAdapter("chatgpt", (_prompt, { signal }) => new Promise((resolve) => {
providerStarted();
signal.addEventListener("abort", () => resolve({
ok: false,
error: "cancelled",
code: "TASK_CANCELLED"
}), { once: true });
})),
claude: createAdapter("claude", () => Promise.resolve({ ok: true }))
};
const controller = context.AIParallelAgentExecutionController.createAgentExecutionController({
taskRuntime: runtime,
adapters,
maxConcurrency: 1,
idFactory: () => "execution-3"
});
const promise = controller.run({
prompt: "This execution should be cancelled.",
agents: [
{ providerId: "chatgpt", role: "planner" },
{ providerId: "claude", role: "reviewer" }
]
});
await providerStartedPromise;
assert.equal(promise.cancel("user stopped the run"), true);
const result = await promise;
assert.equal(result.status, "CANCELLED");
assert.equal(result.error, "user stopped the run");
assert.ok(result.agents.every((agent) => agent.status === "CANCELLED"));
assert.equal(controller.cancel("execution-3"), false);
await delay(0);
});
test("agent scope validation enforces bounded roles and agent count", () => {
const context = loadController();
const runtime = context.AIParallelProviderTaskRuntime.createProviderTaskRuntime();
const adapter = createAdapter("chatgpt", () => Promise.resolve({ ok: true }));
const controller = context.AIParallelAgentExecutionController.createAgentExecutionController({
taskRuntime: runtime,
adapters: { chatgpt: adapter },
maxAgents: 2
});
assert.throws(() => controller.createExecution({
prompt: "x",
agents: [{ providerId: "chatgpt", role: "scheduler" }]
}), /planner, executor, or reviewer/);
assert.throws(() => controller.createExecution({
prompt: "x",
agents: [
{ providerId: "chatgpt", role: "planner" },
{ providerId: "chatgpt", role: "executor" },
{ providerId: "chatgpt", role: "reviewer" }
]
}), /limited to 2 agents/);
assert.throws(() => controller.createExecution({
strategy: "sequential",
prompt: "x",
agents: [{ providerId: "chatgpt", role: "planner" }]
}), /parallel/);
});