UNPKG

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
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 };