trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
1,060 lines (1,052 loc) • 37.3 kB
JavaScript
import {
PROVENANCE,
init_canonical_op
} from "./chunk-RUMOVKR4.js";
// src/core/agents/harness.ts
init_canonical_op();
var AGENT_CTX = { provenance: PROVENANCE.agent };
var AgentHarness = class {
kernel;
toolHandlers = /* @__PURE__ */ new Map();
config;
runCounter = 0;
constructor(kernel, config) {
this.kernel = kernel;
this.config = {
recordDecisions: true,
maxDecisionsPerRun: 100,
...config
};
}
// ... (existing methods)
/**
* Execute an autonomous task run using the configured LLM provider.
*
* When the configured contextManager is a GraphContextManager, conversations
* are automatically created (or resumed) and linked to the agent run. Pass
* `opts.conversationId` to resume an existing conversation.
*/
async runAgentTask(agentId, input, opts) {
if (!this.config.llmProvider) {
throw new Error("AgentHarness: No llmProvider configured for autonomous tasks.");
}
const agent = this.getAgent(agentId);
if (!agent) throw new Error(`Agent "${agentId}" not found.`);
const runId = await this.startRun(agentId, input);
const context = this.config.contextManager;
if (context && typeof context.createConversation === "function") {
const gcm = context;
if (opts?.conversationId) {
await gcm.resumeConversation(opts.conversationId);
} else {
const convId = await gcm.createConversation({
title: opts?.conversationTitle ?? input.slice(0, 80),
agentId,
model: agent.model
});
await this.kernel.addLink(runId, "hasConversation", convId, AGENT_CTX);
}
}
if (context && agent.systemPrompt) {
context.addMessage({ role: "system", content: agent.systemPrompt });
}
if (context) {
context.addMessage({ role: "user", content: input });
}
let turnCount = 0;
const maxTurns = agent.maxTokens ?? 10;
try {
while (turnCount < maxTurns) {
turnCount++;
const messages = context ? context.getHistory() : [
...agent.systemPrompt ? [{ role: "system", content: agent.systemPrompt }] : [],
{ role: "user", content: input }
];
const response = await this.config.llmProvider.complete(messages, {
model: agent.model,
temperature: agent.temperature,
tools: this._getAvailableTools(agent.tools)
});
const message = response.choices[0].message;
if (context) context.addMessage(message);
if (message.tool_calls && message.tool_calls.length > 0) {
let planPending = false;
for (const call of message.tool_calls) {
const result = await this.invokeTool(runId, call.function.name, JSON.parse(call.function.arguments));
if (context) {
context.addMessage({
role: "tool",
tool_call_id: call.id,
name: call.function.name,
content: result.success ? JSON.stringify(result.output) : result.error ?? "Unknown error"
});
}
if (result.success && result.output && typeof result.output === "object" && result.output._planPending) {
planPending = true;
}
}
if (planPending) {
await this.kernel.updateEntity(runId, { status: "plan_pending" }, AGENT_CTX);
return runId;
}
continue;
}
await this.completeRun(runId, message.content ?? "", response.usage?.total_tokens);
return runId;
}
throw new Error(`Agent run exceeded maximum turns (${maxTurns}).`);
} catch (err) {
await this.failRun(runId, err.message);
throw err;
}
}
_getAvailableTools(toolIds) {
return this.listTools().filter((t) => toolIds.includes(t.id)).map((t) => ({
type: "function",
function: {
name: t.id,
description: t.description,
parameters: t.schema ? JSON.parse(t.schema) : { type: "object", properties: {} }
}
}));
}
// -------------------------------------------------------------------------
// Agent CRUD (via kernel entities)
// -------------------------------------------------------------------------
async createAgent(def) {
const id = def.id ?? `agent:${def.name.toLowerCase().replace(/\s+/g, "-")}`;
await this.kernel.createEntity(id, "Agent", {
name: def.name,
...def.description ? { description: def.description } : {},
...def.model ? { model: def.model } : {},
...def.provider ? { provider: def.provider } : {},
...def.systemPrompt ? { systemPrompt: def.systemPrompt } : {},
status: def.status ?? "active"
}, void 0, AGENT_CTX);
if (def.capabilities) {
for (const cap of def.capabilities) {
await this.kernel.addLink(id, "hasCapability", cap, AGENT_CTX);
}
}
if (def.tools) {
for (const tool of def.tools) {
await this.kernel.addLink(id, "hasTool", tool, AGENT_CTX);
}
}
return this.getAgent(id);
}
getAgent(id) {
const entity = this.kernel.getEntity(id);
if (!entity || entity.type !== "Agent") return null;
const store = this.kernel.getStore();
const capLinks = store.getLinksByEntityAndAttribute(id, "hasCapability");
const toolLinks = store.getLinksByEntityAndAttribute(id, "hasTool");
return {
id: entity.id,
name: String(entity.facts.find((f) => f.a === "name")?.v ?? ""),
description: entity.facts.find((f) => f.a === "description")?.v,
model: entity.facts.find((f) => f.a === "model")?.v,
provider: entity.facts.find((f) => f.a === "provider")?.v,
systemPrompt: entity.facts.find((f) => f.a === "systemPrompt")?.v,
status: entity.facts.find((f) => f.a === "status")?.v ?? "active",
capabilities: capLinks.map((l) => l.e2),
tools: toolLinks.map((l) => l.e2)
};
}
listAgents(status) {
const entities = this.kernel.listEntities(
"Agent",
status ? { status } : void 0
);
return entities.map((e) => this.getAgent(e.id)).filter((a) => a !== null);
}
// -------------------------------------------------------------------------
// Tool registration
// -------------------------------------------------------------------------
async registerTool(def, handler) {
const id = def.id ?? `tool:${def.name.toLowerCase().replace(/\s+/g, "-")}`;
if (!this.kernel.getEntity(id)) {
await this.kernel.createEntity(id, "Tool", {
name: def.name,
...def.description ? { description: def.description } : {},
...def.schema ? { schema: def.schema } : {},
...def.endpoint ? { endpoint: def.endpoint } : {}
}, void 0, AGENT_CTX);
}
this.toolHandlers.set(id, handler);
return id;
}
getToolHandler(toolId) {
return this.toolHandlers.get(toolId);
}
listTools() {
return this.kernel.listEntities("Tool").map((e) => ({
id: e.id,
name: String(e.facts.find((f) => f.a === "name")?.v ?? ""),
description: e.facts.find((f) => f.a === "description")?.v,
schema: e.facts.find((f) => f.a === "schema")?.v,
endpoint: e.facts.find((f) => f.a === "endpoint")?.v
}));
}
// -------------------------------------------------------------------------
// Run management
// -------------------------------------------------------------------------
async startRun(agentId, input) {
const agent = this.getAgent(agentId);
if (!agent) throw new Error(`Agent "${agentId}" not found.`);
const runId = `run:${agentId.replace("agent:", "")}:${Date.now()}:${++this.runCounter}`;
await this.kernel.createEntity(runId, "AgentRun", {
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
status: "running",
...input ? { input } : {}
}, void 0, AGENT_CTX);
await this.kernel.addLink(runId, "executedBy", agentId, AGENT_CTX);
return runId;
}
async completeRun(runId, output, tokenCount) {
const updates = {
status: "completed",
completedAt: (/* @__PURE__ */ new Date()).toISOString()
};
if (output) updates.output = output;
if (tokenCount !== void 0) updates.totalTokens = tokenCount;
await this.kernel.updateEntity(runId, updates, AGENT_CTX);
}
async failRun(runId, error) {
await this.kernel.updateEntity(runId, {
status: "failed",
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
output: `Error: ${error}`
}, AGENT_CTX);
}
getRun(runId) {
const entity = this.kernel.getEntity(runId);
if (!entity || entity.type !== "AgentRun") return null;
const store = this.kernel.getStore();
const agentLink = store.getLinksByEntityAndAttribute(runId, "executedBy");
const agentId = agentLink[0]?.e2 ?? "";
const decisionLinks = store.getLinksByAttribute("belongsToRun");
const decisionIds = decisionLinks.filter((l) => l.e2 === runId).map((l) => l.e1);
const decisions = decisionIds.map((did) => this._buildDecisionTrace(did)).filter(Boolean);
const get = (a) => entity.facts.find((f) => f.a === a)?.v;
return {
id: runId,
agentId,
startedAt: String(get("startedAt") ?? ""),
completedAt: get("completedAt"),
status: get("status") ?? "running",
input: get("input"),
output: get("output"),
totalTokens: get("totalTokens"),
promptTokens: get("promptTokens"),
completionTokens: get("completionTokens"),
decisions
};
}
listRuns(agentId) {
const runs = this.kernel.listEntities("AgentRun");
return runs.map((e) => this.getRun(e.id)).filter((r) => r !== null).filter((r) => !agentId || r.agentId === agentId).sort(
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
);
}
// -------------------------------------------------------------------------
// Decision trace recording
// -------------------------------------------------------------------------
async recordDecision(runId, toolName, input, output, opts) {
const run = this.getRun(runId);
if (!run) throw new Error(`Run "${runId}" not found.`);
const decId = `decision:${runId.replace("run:", "")}:${Date.now()}`;
await this.kernel.createEntity(decId, "DecisionTrace", {
toolName,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
...input ? { input: JSON.stringify(input) } : {},
...output ? { output } : {},
...opts?.rationale ? { rationale: opts.rationale } : {},
...opts?.alternatives ? { alternatives: JSON.stringify(opts.alternatives) } : {}
}, void 0, AGENT_CTX);
await this.kernel.addLink(decId, "belongsToRun", runId, AGENT_CTX);
await this.kernel.addLink(decId, "madeBy", run.agentId, AGENT_CTX);
if (opts?.relatedEntities) {
for (const eid of opts.relatedEntities) {
await this.kernel.addLink(decId, "relatedTo", eid, AGENT_CTX);
}
}
return decId;
}
/**
* Invoke a registered tool within a run, auto-recording a decision trace.
*/
async invokeTool(runId, toolId, input, opts) {
const handler = this.toolHandlers.get(toolId);
if (!handler)
throw new Error(`No handler registered for tool "${toolId}".`);
const result = await handler(input);
if (this.config.recordDecisions) {
const toolEntity = this.kernel.getEntity(toolId);
const toolName = toolEntity ? String(toolEntity.facts.find((f) => f.a === "name")?.v ?? toolId) : toolId;
await this.recordDecision(
runId,
toolName,
input,
result.success ? String(result.output ?? "") : `Error: ${result.error}`,
opts
);
}
return result;
}
getDecisionChain(entityId) {
const store = this.kernel.getStore();
const links = store.getLinksByAttribute("relatedTo");
const decisionIds = links.filter((l) => l.e2 === entityId).map((l) => l.e1);
return decisionIds.map((did) => this._buildDecisionTrace(did)).filter(Boolean);
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
_buildDecisionTrace(decId) {
const entity = this.kernel.getEntity(decId);
if (!entity) return null;
const get = (a) => entity.facts.find((f) => f.a === a)?.v;
const store = this.kernel.getStore();
const runLink = store.getLinksByEntityAndAttribute(decId, "belongsToRun");
const agentLink = store.getLinksByEntityAndAttribute(decId, "madeBy");
const relatedLinks = store.getLinksByEntityAndAttribute(decId, "relatedTo");
let inputParsed;
const inputRaw = get("input");
if (inputRaw) {
try {
inputParsed = JSON.parse(inputRaw);
} catch {
inputParsed = { raw: inputRaw };
}
}
let alternatives;
const altRaw = get("alternatives");
if (altRaw) {
try {
alternatives = JSON.parse(altRaw);
} catch {
alternatives = [altRaw];
}
}
return {
id: decId,
runId: runLink[0]?.e2 ?? "",
agentId: agentLink[0]?.e2 ?? "",
toolName: String(get("toolName") ?? ""),
input: inputParsed,
output: get("output"),
rationale: get("rationale"),
alternatives,
timestamp: String(get("timestamp") ?? ""),
relatedEntities: relatedLinks.map((l) => l.e2)
};
}
};
// src/core/agents/worker-pool.ts
init_canonical_op();
var AGENT_CTX2 = { provenance: PROVENANCE.agent };
var WorkerPool = class {
kernel = null;
harness = null;
kernelFactory = null;
config;
queue = [];
active = /* @__PURE__ */ new Map();
stopped = false;
pollTimer = null;
listeners = [];
constructor(kernel, harness, config) {
if (typeof kernel === "function") {
this.kernelFactory = kernel;
} else {
this.kernel = kernel;
this.harness = harness ?? null;
}
this.config = {
concurrency: 1,
pollIntervalMs: 500,
...config
};
}
/** Lazily resolve and expose the kernel (for DAGScheduler & consumers). */
async ensureKernel() {
return this._ensureKernel();
}
async _ensureKernel() {
if (!this.kernel && this.kernelFactory) {
this.kernel = await this.kernelFactory();
}
if (!this.kernel) throw new Error("WorkerPool: No kernel available");
return this.kernel;
}
async _ensureHarness() {
if (!this.harness) {
this.harness = new AgentHarness(await this._ensureKernel());
}
return this.harness;
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
start() {
if (this.pollTimer) return;
this.stopped = false;
this.pollTimer = setInterval(() => this._tick(), this.config.pollIntervalMs);
}
stop() {
this.stopped = true;
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
getStatus() {
return {
active: this.active.size,
queued: this.queue.length,
maxConcurrency: this.config.concurrency,
running: this.pollTimer !== null
};
}
// ---------------------------------------------------------------------------
// Event bus
// ---------------------------------------------------------------------------
on(listener) {
this.listeners.push(listener);
}
off(listener) {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) this.listeners.splice(idx, 1);
}
_emit(event) {
for (const listener of this.listeners) {
try {
listener(event);
} catch {
}
}
}
// ---------------------------------------------------------------------------
// Queue operations
// ---------------------------------------------------------------------------
async enqueue(agentId, input, opts, runId) {
const resolvedRunId = runId ?? `run:${agentId.replace("agent:", "")}:${Date.now()}`;
const task = {
id: `task:${resolvedRunId}`,
agentId,
runId: resolvedRunId,
input,
status: "queued",
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
};
this.queue.push(task);
await this._saveTask(task);
this._emit({ type: "task:queued", task });
return resolvedRunId;
}
async cancel(runId) {
const idx = this.queue.findIndex((t) => t.runId === runId);
if (idx !== -1) {
const [task] = this.queue.splice(idx, 1);
task.status = "cancelled";
task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
await this._updateTask(task);
this._emit({ type: "task:cancelled", task });
return;
}
const active = this.active.get(runId);
if (active) {
active.status = "cancelled";
active.completedAt = (/* @__PURE__ */ new Date()).toISOString();
this.active.delete(runId);
try {
const h = await this._ensureHarness();
await h.failRun(runId, "Cancelled by user");
} catch {
}
await this._updateTask(active);
this._emit({ type: "task:cancelled", task: active });
}
}
async pause(runId) {
const active = this.active.get(runId);
if (!active) return;
active.status = "paused";
try {
const k = await this._ensureKernel();
await k.updateEntity(runId, { status: "paused" }, AGENT_CTX2);
} catch {
}
await this._updateTask(active);
this._emit({ type: "task:paused", task: active });
}
async resume(runId) {
const task = this.queue.find((t) => t.runId === runId) ?? this.active.get(runId);
if (!task) return;
if (this.active.has(runId)) {
task.status = "running";
try {
const k = await this._ensureKernel();
await k.updateEntity(runId, { status: "running" }, AGENT_CTX2);
} catch {
}
await this._updateTask(task);
this._emit({ type: "task:resumed", task });
} else if (task.status === "queued" || task.status === "paused") {
task.status = "queued";
this.active.delete(runId);
if (!this.queue.find((t) => t.runId === runId)) {
this.queue.push(task);
}
await this._updateTask(task);
this._emit({ type: "task:resumed", task });
}
}
getQueue() {
return [...this.queue];
}
getActiveJobs() {
return [...this.active.values()];
}
getTask(runId) {
return this.queue.find((t) => t.runId === runId) ?? this.active.get(runId);
}
// ---------------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------------
_tick() {
if (this.stopped) return;
while (this.active.size < this.config.concurrency && this.queue.length > 0) {
const task = this.queue.shift();
if (task.status === "cancelled") continue;
this._execute(task);
}
}
async _execute(task) {
task.status = "running";
task.startedAt = (/* @__PURE__ */ new Date()).toISOString();
this.active.set(task.runId, task);
this._emit({ type: "task:started", task });
try {
if (this.config.simulate) {
task.status = "completed";
this._emit({ type: "task:completed", task });
} else {
const h = await this._ensureHarness();
await h.runAgentTask(task.agentId, task.input);
task.status = "completed";
this._emit({ type: "task:completed", task });
}
} catch (err) {
task.status = "failed";
task.error = err.message;
this._emit({ type: "task:failed", task, error: err.message });
} finally {
task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
this.active.delete(task.runId);
await this._updateTask(task);
}
}
// ---------------------------------------------------------------------------
// Persistence (optional — enabled via config.persistToGraph)
// ---------------------------------------------------------------------------
/** Restore queued/paused tasks from the graph. Call after construction. */
async restore() {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
const entities = k.listEntities("WorkerPoolTask");
for (const e of entities) {
const get = (a) => e.facts.find((f) => f.a === a)?.v;
const status = get("status");
if (status !== "queued" && status !== "paused") continue;
const task = {
id: e.id,
agentId: String(get("agentId") ?? ""),
runId: String(get("runId") ?? ""),
input: String(get("input") ?? ""),
status,
queuedAt: String(get("queuedAt") ?? (/* @__PURE__ */ new Date()).toISOString()),
startedAt: get("startedAt"),
completedAt: get("completedAt"),
error: get("error")
};
this.queue.push(task);
}
} catch {
}
}
async _saveTask(task) {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
if (!k.getEntity(task.id)) {
await k.createEntity(task.id, "WorkerPoolTask", {
agentId: task.agentId,
runId: task.runId,
input: task.input,
status: task.status,
queuedAt: task.queuedAt
}, void 0, AGENT_CTX2);
}
} catch {
}
}
async _updateTask(task) {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
const updates = {
status: task.status
};
if (task.startedAt) updates.startedAt = task.startedAt;
if (task.completedAt) updates.completedAt = task.completedAt;
if (task.error) updates.error = task.error;
await k.updateEntity(task.id, updates, AGENT_CTX2);
} catch {
}
}
};
// src/core/agents/dag-scheduler.ts
init_canonical_op();
// src/core/agents/edge-evaluator.ts
function evaluateCondition(condition, ctx) {
const trimmed = condition.trim();
if (!trimmed || trimmed === "true") return true;
if (trimmed === "false") return false;
const { sourceStep } = ctx;
const statusEq = trimmed.match(/^status\s*==\s*"(.+)"$/);
if (statusEq) return sourceStep.status === statusEq[1];
const statusNeq = trimmed.match(/^status\s*!=\s*"(.+)"$/);
if (statusNeq) return sourceStep.status !== statusNeq[1];
const outputContains = trimmed.match(/^output\s+contains\s+"(.+)"$/);
if (outputContains) return (sourceStep.result ?? "").includes(outputContains[1]);
const outputMatches = trimmed.match(/^output\s+matches\s+"(.+)"$/);
if (outputMatches) {
try {
return new RegExp(outputMatches[1]).test(sourceStep.result ?? "");
} catch {
return false;
}
}
return false;
}
function evaluateEdge(fromStep, condition) {
if (!condition || condition.trim() === "") return { passed: true };
const passed = evaluateCondition(condition, { sourceStep: fromStep });
return { passed, condition };
}
// src/core/agents/gate-keeper.ts
init_canonical_op();
import { exec } from "child_process";
import { promisify } from "util";
var execAsync = promisify(exec);
var AGENT_CTX3 = { provenance: PROVENANCE.agent };
async function runShellCommand(command) {
try {
const { stdout, stderr } = await execAsync(command, { timeout: 3e4 });
return { exitCode: 0, stdout, stderr };
} catch (err) {
return {
exitCode: err.code ?? 1,
stdout: err.stdout ?? "",
stderr: err.stderr ?? err.message ?? ""
};
}
}
function buildResult(passed, message, gate) {
return {
passed,
message,
action: passed ? "continue" : gate.onFail,
retryStepId: passed ? void 0 : gate.retryStepId,
failRouteEdgeId: passed ? void 0 : gate.failRouteEdgeId
};
}
async function evaluateTestGate(gate, step) {
const command = gate.criteria?.trim();
if (!command) {
return buildResult(false, "Test gate has no command in criteria", gate);
}
const { exitCode, stdout, stderr } = await runShellCommand(command);
if (exitCode === 0) {
return buildResult(true, `Test passed: ${stdout.slice(0, 200)}`, gate);
}
return buildResult(false, `Test failed (exit ${exitCode}): ${stderr.slice(0, 500)}`, gate);
}
async function evaluateManualGate(gate, _step, kernel) {
if (kernel) {
const handoffId = `handoff:gate:${_step.step.id}:${Date.now()}`;
try {
await kernel.createEntity(handoffId, "Handoff", {
type: "approval",
status: "pending",
sourceStep: _step.step.id,
gateName: gate.type,
criteria: gate.criteria ?? ""
}, void 0, AGENT_CTX3);
} catch {
}
return {
passed: false,
message: `Manual approval required \u2014 Handoff created: ${handoffId}`,
action: "stop",
retryStepId: gate.retryStepId,
failRouteEdgeId: gate.failRouteEdgeId
};
}
return {
passed: false,
message: "Manual approval required \u2014 no kernel available to create Handoff",
action: "stop",
retryStepId: gate.retryStepId,
failRouteEdgeId: gate.failRouteEdgeId
};
}
async function evaluateAcCheckGate(gate, step) {
const criteria = gate.criteria?.trim();
if (!criteria) {
return buildResult(true, "No acceptance criteria defined", gate);
}
const output = step.result ?? "";
const keywords = criteria.split(/\s+/).filter(Boolean);
const matched = keywords.filter((kw) => output.toLowerCase().includes(kw.toLowerCase()));
const ratio = matched.length / keywords.length;
if (ratio >= 0.5) {
return buildResult(true, `AC check passed (${matched.length}/${keywords.length} keywords matched)`, gate);
}
return buildResult(false, `AC check failed (${matched.length}/${keywords.length} keywords matched)`, gate);
}
async function evaluateSemanticDiffGate(_gate, _step) {
return buildResult(true, "Semantic diff gate passed (v1: no-op)", _gate);
}
async function evaluateGate(gate, step, kernel) {
switch (gate.type) {
case "test":
return evaluateTestGate(gate, step);
case "manual":
return evaluateManualGate(gate, step, kernel);
case "ac_check":
return evaluateAcCheckGate(gate, step);
case "semantic_diff":
return evaluateSemanticDiffGate(gate, step);
default:
return { passed: false, message: `Unknown gate type: ${gate.type}`, action: "stop" };
}
}
// src/core/agents/dag-scheduler.ts
var AGENT_CTX4 = { provenance: PROVENANCE.agent };
function detectCycle(workflow) {
const visited = /* @__PURE__ */ new Set();
const inStack = /* @__PURE__ */ new Set();
const adjacency = /* @__PURE__ */ new Map();
for (const step of workflow.steps) {
adjacency.set(step.id, step.dependsOn ?? []);
}
function dfs(node) {
visited.add(node);
inStack.add(node);
const deps = adjacency.get(node) ?? [];
for (const dep of deps) {
if (!adjacency.has(dep)) continue;
if (!visited.has(dep)) {
const cycle = dfs(dep);
if (cycle) return cycle;
} else if (inStack.has(dep)) {
return [...inStack].slice([...inStack].indexOf(dep)).concat(node).join(" \u2192 ");
}
}
inStack.delete(node);
return null;
}
for (const step of workflow.steps) {
if (!visited.has(step.id)) {
const cycle = dfs(step.id);
if (cycle) return cycle;
}
}
return null;
}
var DAGScheduler = class {
pool;
config;
runs = /* @__PURE__ */ new Map();
boundHandler;
constructor(pool, config) {
this.pool = pool;
this.config = { failOnError: true, persistToGraph: false, enableEdgeRouting: false, enableGates: false, ...config };
this.boundHandler = (event) => this._onPoolEvent(event);
this.pool.on(this.boundHandler);
}
/** Restore in-progress DAGRuns from the graph. Call after construction. */
async restore() {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
for (const e of k.listEntities("DAGRun")) {
const get = (a) => e.facts.find((f) => f.a === a)?.v;
const status = get("status");
if (status !== "running") continue;
const stepsRaw = get("steps");
const steps = typeof stepsRaw === "string" ? JSON.parse(stepsRaw) : Array.isArray(stepsRaw) ? stepsRaw : [];
this.runs.set(e.id, {
workflowId: String(get("workflowId") ?? e.id),
status: "running",
steps,
startedAt: String(get("startedAt") ?? (/* @__PURE__ */ new Date()).toISOString())
});
}
for (const run of this.runs.values()) {
this._evaluate(run);
}
} catch {
}
}
async _ensureKernel() {
return this.pool.ensureKernel();
}
async _saveRun(run) {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
if (!k.getEntity(run.workflowId)) {
await k.createEntity(run.workflowId, "DAGRun", {
workflowId: run.workflowId,
status: run.status,
steps: JSON.stringify(run.steps),
startedAt: run.startedAt
}, void 0, AGENT_CTX4);
}
} catch {
}
}
async _updateRun(run) {
if (!this.config.persistToGraph) return;
try {
const k = await this._ensureKernel();
const updates = {
status: run.status,
steps: JSON.stringify(run.steps)
};
if (run.completedAt) updates.completedAt = run.completedAt;
await k.updateEntity(run.workflowId, updates, AGENT_CTX4);
} catch {
}
}
dispose() {
this.pool.off(this.boundHandler);
}
// ---------------------------------------------------------------------------
// Workflow execution
// ---------------------------------------------------------------------------
async run(workflow) {
const cycle = detectCycle(workflow);
if (cycle) {
throw new Error(`Workflow cycle detected: ${cycle}`);
}
const runId = workflow.id;
const steps = workflow.steps.map((s) => ({
step: s,
status: "pending"
}));
const run = {
workflowId: runId,
status: "running",
steps,
startedAt: (/* @__PURE__ */ new Date()).toISOString()
};
this.runs.set(runId, run);
await this._saveRun(run);
this._evaluate(run);
return runId;
}
getRun(runId) {
return this.runs.get(runId);
}
listRuns() {
return [...this.runs.values()];
}
/** Wait for a run to complete (terminal status). Returns the final run state. */
async waitForRun(runId, pollMs = 100) {
return new Promise((resolve) => {
const check = () => {
const run = this.runs.get(runId);
if (!run || run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
resolve(run ?? { workflowId: runId, status: "failed", steps: [], startedAt: "" });
} else {
setTimeout(check, pollMs);
}
};
check();
});
}
// ---------------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------------
_evaluate(run) {
if (run.status !== "running") return;
const stepById = new Map(run.steps.map((rs) => [rs.step.id, rs]));
const edgeIndex = this.config.enableEdgeRouting ? this._buildEdgeIndex(run) : null;
for (const rs of run.steps) {
if (rs.status !== "pending") continue;
const deps = rs.step.dependsOn ?? [];
const allMet = deps.every((depId) => {
const dep = stepById.get(depId);
if (!dep || dep.status !== "completed") return false;
if (edgeIndex) {
const condition = edgeIndex.get(depId)?.get(rs.step.id);
if (condition !== void 0) {
const result = evaluateEdge(dep, condition);
return result.passed;
}
}
return true;
});
if (allMet) {
rs.status = "ready";
}
}
for (const rs of run.steps) {
if (rs.status !== "ready") continue;
const skippedDep = (rs.step.dependsOn ?? []).find((depId) => {
const dep = stepById.get(depId);
return dep && dep.status === "failed";
});
if (skippedDep && this.config.failOnError) {
rs.status = "skipped";
continue;
}
this._execute(run, rs);
}
}
_buildEdgeIndex(run) {
const idx = /* @__PURE__ */ new Map();
for (const rs of run.steps) {
for (const edge of rs.step.edges ?? []) {
if (!idx.has(rs.step.id)) idx.set(rs.step.id, /* @__PURE__ */ new Map());
idx.get(rs.step.id).set(edge.targetStepId, edge.condition);
}
}
return idx;
}
_isPreGate(type) {
return type === "manual";
}
async _execute(run, step) {
if (this.config.enableGates && step.step.gate && this._isPreGate(step.step.gate.type)) {
const kernel = this.config.enableGates ? await this._ensureKernel().catch(() => void 0) : void 0;
const result = await evaluateGate(step.step.gate, step, kernel);
if (!result.passed) {
step.status = "failed";
step.error = `Gate failed: ${result.message}`;
step.completedAt = (/* @__PURE__ */ new Date()).toISOString();
if (result.action === "retry" && result.retryStepId) {
const retryTarget = run.steps.find((rs) => rs.step.id === result.retryStepId);
if (retryTarget) {
retryTarget.status = "pending";
retryTarget.error = void 0;
retryTarget.completedAt = void 0;
}
}
this._failWorkflow(run, step);
return;
}
}
step.status = "running";
step.startedAt = (/* @__PURE__ */ new Date()).toISOString();
try {
step.runId = `step:${step.step.id}:${Date.now()}`;
await this.pool.enqueue(step.step.agentId, step.step.input, void 0, step.runId);
} catch (err) {
step.status = "failed";
step.error = err.message;
step.completedAt = (/* @__PURE__ */ new Date()).toISOString();
this._failWorkflow(run, step);
}
}
async _onPoolEvent(event) {
if (event.type !== "task:completed" && event.type !== "task:failed" && event.type !== "task:cancelled") return;
for (const run of this.runs.values()) {
if (run.status !== "running") continue;
for (const rs of run.steps) {
if (rs.runId !== event.task.runId) continue;
if (event.type === "task:completed" || event.type === "task:cancelled") {
rs.status = event.type === "task:completed" ? "completed" : "failed";
rs.error = event.type === "task:cancelled" ? "Cancelled" : void 0;
rs.result = event.result ?? event.output ?? rs.result;
rs.completedAt = (/* @__PURE__ */ new Date()).toISOString();
if (rs.status === "failed" && this.config.failOnError) {
this._failWorkflow(run, rs);
await this._updateRun(run);
return;
}
if (this.config.enableGates && rs.step.gate && !this._isPreGate(rs.step.gate.type) && rs.status === "completed") {
const kernel = this.config.enableGates ? await this._ensureKernel().catch(() => void 0) : void 0;
const result = await evaluateGate(rs.step.gate, rs, kernel);
if (!result.passed) {
rs.status = "failed";
rs.error = `Post-gate failed: ${result.message}`;
if (result.action === "retry" && result.retryStepId) {
const retryTarget = run.steps.find((s) => s.step.id === result.retryStepId);
if (retryTarget) {
retryTarget.status = "pending";
retryTarget.error = void 0;
retryTarget.completedAt = void 0;
}
}
if (this.config.failOnError) {
this._failWorkflow(run, rs);
await this._updateRun(run);
return;
}
}
}
this._evaluate(run);
this._checkCompletion(run);
} else if (event.type === "task:failed") {
rs.status = "failed";
rs.error = event.error;
rs.result = event.result ?? event.output ?? rs.result;
rs.completedAt = (/* @__PURE__ */ new Date()).toISOString();
if (this.config.failOnError) {
this._failWorkflow(run, rs);
await this._updateRun(run);
return;
}
this._evaluate(run);
this._checkCompletion(run);
}
await this._updateRun(run);
return;
}
}
}
_checkCompletion(run) {
const terminal = run.steps.every(
(rs) => rs.status === "completed" || rs.status === "failed" || rs.status === "skipped"
);
if (terminal) {
run.status = run.steps.some((rs) => rs.status === "failed") ? "failed" : "completed";
run.completedAt = (/* @__PURE__ */ new Date()).toISOString();
}
}
_failWorkflow(run, source) {
run.status = "failed";
run.completedAt = (/* @__PURE__ */ new Date()).toISOString();
for (const rs of run.steps) {
if (rs.status === "pending" || rs.status === "ready") {
rs.status = "skipped";
}
}
}
};
export {
AgentHarness,
WorkerPool,
evaluateCondition,
evaluateEdge,
evaluateGate,
detectCycle,
DAGScheduler
};