@acpus/runtime 0.8.0 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/admission/input.d.ts +5 -0
  3. package/dist/admission/input.d.ts.map +1 -0
  4. package/dist/admission/input.js +17 -0
  5. package/dist/admission/input.js.map +1 -0
  6. package/dist/artifacts/rewrite.d.ts +7 -0
  7. package/dist/artifacts/rewrite.d.ts.map +1 -0
  8. package/dist/artifacts/rewrite.js +28 -0
  9. package/dist/artifacts/rewrite.js.map +1 -0
  10. package/dist/control/agent-overrides.d.ts +4 -0
  11. package/dist/control/agent-overrides.d.ts.map +1 -0
  12. package/dist/control/agent-overrides.js +40 -0
  13. package/dist/control/agent-overrides.js.map +1 -0
  14. package/dist/daemon/loop.d.ts +16 -0
  15. package/dist/daemon/loop.d.ts.map +1 -0
  16. package/dist/daemon/loop.js +191 -0
  17. package/dist/daemon/loop.js.map +1 -0
  18. package/dist/daemon/sessions.d.ts +23 -0
  19. package/dist/daemon/sessions.d.ts.map +1 -0
  20. package/dist/daemon/sessions.js +190 -0
  21. package/dist/daemon/sessions.js.map +1 -0
  22. package/dist/daemon/socket.d.ts +82 -0
  23. package/dist/daemon/socket.d.ts.map +1 -0
  24. package/dist/daemon/socket.js +341 -0
  25. package/dist/daemon/socket.js.map +1 -0
  26. package/dist/daemon/tick.d.ts +9 -0
  27. package/dist/daemon/tick.d.ts.map +1 -0
  28. package/dist/daemon/tick.js +18 -0
  29. package/dist/daemon/tick.js.map +1 -0
  30. package/dist/evaluation/admissible.d.ts +2 -0
  31. package/dist/evaluation/admissible.d.ts.map +1 -0
  32. package/dist/evaluation/admissible.js +50 -0
  33. package/dist/evaluation/admissible.js.map +1 -0
  34. package/dist/evaluation/evaluator.d.ts +14 -0
  35. package/dist/evaluation/evaluator.d.ts.map +1 -0
  36. package/dist/evaluation/evaluator.js +34 -0
  37. package/dist/evaluation/evaluator.js.map +1 -0
  38. package/dist/evaluation/loop-limit.d.ts +4 -0
  39. package/dist/evaluation/loop-limit.d.ts.map +1 -0
  40. package/dist/evaluation/loop-limit.js +9 -0
  41. package/dist/evaluation/loop-limit.js.map +1 -0
  42. package/dist/evaluation/schema.d.ts +4 -0
  43. package/dist/evaluation/schema.d.ts.map +1 -0
  44. package/dist/evaluation/schema.js +92 -0
  45. package/dist/evaluation/schema.js.map +1 -0
  46. package/dist/execution/advance.d.ts +18 -0
  47. package/dist/execution/advance.d.ts.map +1 -0
  48. package/dist/execution/advance.js +57 -0
  49. package/dist/execution/advance.js.map +1 -0
  50. package/dist/execution/agent-node.d.ts +23 -0
  51. package/dist/execution/agent-node.d.ts.map +1 -0
  52. package/dist/execution/agent-node.js +512 -0
  53. package/dist/execution/agent-node.js.map +1 -0
  54. package/dist/execution/duration.d.ts +2 -0
  55. package/dist/execution/duration.d.ts.map +1 -0
  56. package/dist/execution/duration.js +15 -0
  57. package/dist/execution/duration.js.map +1 -0
  58. package/dist/execution/ir.d.ts +3 -0
  59. package/dist/execution/ir.d.ts.map +1 -0
  60. package/dist/execution/ir.js +46 -0
  61. package/dist/execution/ir.js.map +1 -0
  62. package/dist/execution/scheduler.d.ts +59 -0
  63. package/dist/execution/scheduler.d.ts.map +1 -0
  64. package/dist/execution/scheduler.js +306 -0
  65. package/dist/execution/scheduler.js.map +1 -0
  66. package/dist/execution/task-executor.d.ts +14 -0
  67. package/dist/execution/task-executor.d.ts.map +1 -0
  68. package/dist/execution/task-executor.js +186 -0
  69. package/dist/execution/task-executor.js.map +1 -0
  70. package/dist/hooks/config.d.ts +32 -0
  71. package/dist/hooks/config.d.ts.map +1 -0
  72. package/dist/hooks/config.js +112 -0
  73. package/dist/hooks/config.js.map +1 -0
  74. package/dist/hooks/context.d.ts +50 -0
  75. package/dist/hooks/context.d.ts.map +1 -0
  76. package/dist/hooks/context.js +116 -0
  77. package/dist/hooks/context.js.map +1 -0
  78. package/dist/hooks/events.d.ts +22 -0
  79. package/dist/hooks/events.d.ts.map +1 -0
  80. package/dist/hooks/events.js +41 -0
  81. package/dist/hooks/events.js.map +1 -0
  82. package/dist/hooks/journal.d.ts +21 -19
  83. package/dist/hooks/journal.d.ts.map +1 -1
  84. package/dist/hooks/journal.js +1 -48
  85. package/dist/hooks/journal.js.map +1 -1
  86. package/dist/hooks/loader.d.ts +33 -39
  87. package/dist/hooks/loader.d.ts.map +1 -1
  88. package/dist/hooks/loader.js +79 -95
  89. package/dist/hooks/loader.js.map +1 -1
  90. package/dist/hooks/runner.d.ts +14 -41
  91. package/dist/hooks/runner.d.ts.map +1 -1
  92. package/dist/hooks/runner.js +177 -145
  93. package/dist/hooks/runner.js.map +1 -1
  94. package/dist/index.d.ts +13 -28
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +13 -33
  97. package/dist/index.js.map +1 -1
  98. package/dist/runs/advance-runtime.d.ts +50 -0
  99. package/dist/runs/advance-runtime.d.ts.map +1 -0
  100. package/dist/runs/advance-runtime.js +82 -0
  101. package/dist/runs/advance-runtime.js.map +1 -0
  102. package/dist/runs/use-cases.d.ts +111 -0
  103. package/dist/runs/use-cases.d.ts.map +1 -0
  104. package/dist/runs/use-cases.js +539 -0
  105. package/dist/runs/use-cases.js.map +1 -0
  106. package/dist/scheduler/advance.d.ts +65 -0
  107. package/dist/scheduler/advance.d.ts.map +1 -0
  108. package/dist/scheduler/advance.js +435 -0
  109. package/dist/scheduler/advance.js.map +1 -0
  110. package/dist/scheduler/control.d.ts +49 -0
  111. package/dist/scheduler/control.d.ts.map +1 -0
  112. package/dist/scheduler/control.js +212 -0
  113. package/dist/scheduler/control.js.map +1 -0
  114. package/dist/scheduler/event-codec.d.ts +4 -0
  115. package/dist/scheduler/event-codec.d.ts.map +1 -0
  116. package/dist/scheduler/event-codec.js +57 -0
  117. package/dist/scheduler/event-codec.js.map +1 -0
  118. package/dist/scheduler/events.d.ts +193 -0
  119. package/dist/scheduler/events.d.ts.map +1 -0
  120. package/dist/scheduler/events.js +13 -0
  121. package/dist/scheduler/events.js.map +1 -0
  122. package/dist/scheduler/fork-seed.d.ts +38 -0
  123. package/dist/scheduler/fork-seed.d.ts.map +1 -0
  124. package/dist/scheduler/fork-seed.js +590 -0
  125. package/dist/scheduler/fork-seed.js.map +1 -0
  126. package/dist/scheduler/identity.d.ts +8 -0
  127. package/dist/scheduler/identity.d.ts.map +1 -0
  128. package/dist/scheduler/identity.js +49 -0
  129. package/dist/scheduler/identity.js.map +1 -0
  130. package/dist/scheduler/ir-walk.d.ts +3 -0
  131. package/dist/scheduler/ir-walk.d.ts.map +1 -0
  132. package/dist/scheduler/ir-walk.js +25 -0
  133. package/dist/scheduler/ir-walk.js.map +1 -0
  134. package/dist/scheduler/limiter.d.ts +7 -0
  135. package/dist/scheduler/limiter.d.ts.map +1 -0
  136. package/dist/scheduler/limiter.js +10 -0
  137. package/dist/scheduler/limiter.js.map +1 -0
  138. package/dist/scheduler/materialize.d.ts +7 -0
  139. package/dist/scheduler/materialize.d.ts.map +1 -0
  140. package/dist/scheduler/materialize.js +756 -0
  141. package/dist/scheduler/materialize.js.map +1 -0
  142. package/dist/scheduler/membership.d.ts +12 -0
  143. package/dist/scheduler/membership.d.ts.map +1 -0
  144. package/dist/scheduler/membership.js +74 -0
  145. package/dist/scheduler/membership.js.map +1 -0
  146. package/dist/scheduler/node-executor.d.ts +15 -0
  147. package/dist/scheduler/node-executor.d.ts.map +1 -0
  148. package/dist/scheduler/node-executor.js +69 -0
  149. package/dist/scheduler/node-executor.js.map +1 -0
  150. package/dist/scheduler/runtime-runner.d.ts +44 -0
  151. package/dist/scheduler/runtime-runner.d.ts.map +1 -0
  152. package/dist/scheduler/runtime-runner.js +155 -0
  153. package/dist/scheduler/runtime-runner.js.map +1 -0
  154. package/dist/scheduler/scope.d.ts +6 -0
  155. package/dist/scheduler/scope.d.ts.map +1 -0
  156. package/dist/scheduler/scope.js +56 -0
  157. package/dist/scheduler/scope.js.map +1 -0
  158. package/dist/scheduler/store-port.d.ts +211 -0
  159. package/dist/scheduler/store-port.d.ts.map +1 -0
  160. package/dist/scheduler/store-port.js +28 -0
  161. package/dist/scheduler/store-port.js.map +1 -0
  162. package/dist/scheduler/transitions.d.ts +89 -0
  163. package/dist/scheduler/transitions.d.ts.map +1 -0
  164. package/dist/scheduler/transitions.js +967 -0
  165. package/dist/scheduler/transitions.js.map +1 -0
  166. package/dist/scheduler/types.d.ts +146 -0
  167. package/dist/scheduler/types.d.ts.map +1 -0
  168. package/dist/{executors → scheduler}/types.js.map +1 -1
  169. package/dist/store/store.d.ts +390 -0
  170. package/dist/store/store.d.ts.map +1 -0
  171. package/dist/store/store.js +3053 -0
  172. package/dist/store/store.js.map +1 -0
  173. package/dist/visualization/overlay.d.ts +86 -0
  174. package/dist/visualization/overlay.d.ts.map +1 -0
  175. package/dist/visualization/overlay.js +161 -0
  176. package/dist/visualization/overlay.js.map +1 -0
  177. package/package.json +19 -37
  178. package/README.md +0 -7
  179. package/dist/agent-telemetry.d.ts +0 -56
  180. package/dist/agent-telemetry.d.ts.map +0 -1
  181. package/dist/agent-telemetry.js +0 -304
  182. package/dist/agent-telemetry.js.map +0 -1
  183. package/dist/artifacts.d.ts +0 -45
  184. package/dist/artifacts.d.ts.map +0 -1
  185. package/dist/artifacts.js +0 -163
  186. package/dist/artifacts.js.map +0 -1
  187. package/dist/attempt-artifacts.d.ts +0 -35
  188. package/dist/attempt-artifacts.d.ts.map +0 -1
  189. package/dist/attempt-artifacts.js +0 -90
  190. package/dist/attempt-artifacts.js.map +0 -1
  191. package/dist/client.d.ts +0 -82
  192. package/dist/client.d.ts.map +0 -1
  193. package/dist/client.js +0 -200
  194. package/dist/client.js.map +0 -1
  195. package/dist/evaluator.d.ts +0 -36
  196. package/dist/evaluator.d.ts.map +0 -1
  197. package/dist/evaluator.js +0 -101
  198. package/dist/evaluator.js.map +0 -1
  199. package/dist/executors/agent.d.ts +0 -103
  200. package/dist/executors/agent.d.ts.map +0 -1
  201. package/dist/executors/agent.js +0 -516
  202. package/dist/executors/agent.js.map +0 -1
  203. package/dist/executors/mock-program.d.ts +0 -26
  204. package/dist/executors/mock-program.d.ts.map +0 -1
  205. package/dist/executors/mock-program.js +0 -100
  206. package/dist/executors/mock-program.js.map +0 -1
  207. package/dist/executors/output-preview.d.ts +0 -2
  208. package/dist/executors/output-preview.d.ts.map +0 -1
  209. package/dist/executors/output-preview.js +0 -20
  210. package/dist/executors/output-preview.js.map +0 -1
  211. package/dist/executors/program.d.ts +0 -30
  212. package/dist/executors/program.d.ts.map +0 -1
  213. package/dist/executors/program.js +0 -230
  214. package/dist/executors/program.js.map +0 -1
  215. package/dist/executors/types.d.ts +0 -32
  216. package/dist/executors/types.d.ts.map +0 -1
  217. package/dist/fork.d.ts +0 -74
  218. package/dist/fork.d.ts.map +0 -1
  219. package/dist/fork.js +0 -278
  220. package/dist/fork.js.map +0 -1
  221. package/dist/hooks/payload.d.ts +0 -17
  222. package/dist/hooks/payload.d.ts.map +0 -1
  223. package/dist/hooks/payload.js +0 -45
  224. package/dist/hooks/payload.js.map +0 -1
  225. package/dist/interpreter.d.ts +0 -168
  226. package/dist/interpreter.d.ts.map +0 -1
  227. package/dist/interpreter.js +0 -1747
  228. package/dist/interpreter.js.map +0 -1
  229. package/dist/keys.d.ts +0 -77
  230. package/dist/keys.d.ts.map +0 -1
  231. package/dist/keys.js +0 -382
  232. package/dist/keys.js.map +0 -1
  233. package/dist/run-control.d.ts +0 -45
  234. package/dist/run-control.d.ts.map +0 -1
  235. package/dist/run-control.js +0 -255
  236. package/dist/run-control.js.map +0 -1
  237. package/dist/run-id.d.ts +0 -3
  238. package/dist/run-id.d.ts.map +0 -1
  239. package/dist/run-id.js +0 -10
  240. package/dist/run-id.js.map +0 -1
  241. package/dist/state-machine.d.ts +0 -31
  242. package/dist/state-machine.d.ts.map +0 -1
  243. package/dist/state-machine.js +0 -102
  244. package/dist/state-machine.js.map +0 -1
  245. package/dist/store.d.ts +0 -92
  246. package/dist/store.d.ts.map +0 -1
  247. package/dist/store.js +0 -420
  248. package/dist/store.js.map +0 -1
  249. package/dist/supervisor-app.d.ts +0 -27
  250. package/dist/supervisor-app.d.ts.map +0 -1
  251. package/dist/supervisor-app.js +0 -694
  252. package/dist/supervisor-app.js.map +0 -1
  253. package/dist/supervisor-discovery.d.ts +0 -18
  254. package/dist/supervisor-discovery.d.ts.map +0 -1
  255. package/dist/supervisor-discovery.js +0 -180
  256. package/dist/supervisor-discovery.js.map +0 -1
  257. package/dist/supervisor-entry.d.ts +0 -11
  258. package/dist/supervisor-entry.d.ts.map +0 -1
  259. package/dist/supervisor-entry.js +0 -50
  260. package/dist/supervisor-entry.js.map +0 -1
  261. package/dist/supervisor-lock.d.ts +0 -26
  262. package/dist/supervisor-lock.d.ts.map +0 -1
  263. package/dist/supervisor-lock.js +0 -49
  264. package/dist/supervisor-lock.js.map +0 -1
  265. package/dist/supervisor-runner.d.ts +0 -11
  266. package/dist/supervisor-runner.d.ts.map +0 -1
  267. package/dist/supervisor-runner.js +0 -128
  268. package/dist/supervisor-runner.js.map +0 -1
  269. package/dist/types.d.ts +0 -375
  270. package/dist/types.d.ts.map +0 -1
  271. package/dist/types.js +0 -7
  272. package/dist/types.js.map +0 -1
  273. package/dist/validate-input.d.ts +0 -27
  274. package/dist/validate-input.d.ts.map +0 -1
  275. package/dist/validate-input.js +0 -56
  276. package/dist/validate-input.js.map +0 -1
  277. package/dist/validate-signal.d.ts +0 -14
  278. package/dist/validate-signal.d.ts.map +0 -1
  279. package/dist/validate-signal.js +0 -31
  280. package/dist/validate-signal.js.map +0 -1
  281. package/dist/workflow-context.d.ts +0 -4
  282. package/dist/workflow-context.d.ts.map +0 -1
  283. package/dist/workflow-context.js +0 -11
  284. package/dist/workflow-context.js.map +0 -1
  285. package/dist/workflow-outputs.d.ts +0 -9
  286. package/dist/workflow-outputs.d.ts.map +0 -1
  287. package/dist/workflow-outputs.js +0 -60
  288. package/dist/workflow-outputs.js.map +0 -1
  289. /package/dist/{executors → scheduler}/types.js +0 -0
@@ -1,1747 +0,0 @@
1
- import { parseDurationMs, compileWorkflow, hashIrNode, realPathOrUndefined } from "@acpus/core";
2
- import { readFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
4
- import { ExpressionEvaluator } from "./evaluator.js";
5
- import { appendDynamicFrame, appendDynamicFrames, nestedParallelBranchDynamic, resolveNodeKey, staticNodePathFromKey, withNodeKeyPrefix } from "./keys.js";
6
- import { canTransition, transition, createInitialNodeState } from "./state-machine.js";
7
- import { ArtifactStore } from "./artifacts.js";
8
- import { AttemptArtifactRecorder } from "./attempt-artifacts.js";
9
- import { renderAgentRequestPrompt, renderAgentSessionKey } from "./executors/agent.js";
10
- import { validateInput } from "./validate-input.js";
11
- import { validateSignalPayload } from "./validate-signal.js";
12
- import { RunControl, abortedNodeError, isPausedContinuationState } from "./run-control.js";
13
- import { evaluateTemplatedValue, evaluateWorkflowOutputs } from "./workflow-outputs.js";
14
- import { randomBytes } from "node:crypto";
15
- import pLimit from "p-limit";
16
- import { AgentTelemetryAccumulator, upsertAgentAttemptTelemetry } from "./agent-telemetry.js";
17
- import { buildWorkflowExpressionContext } from "./workflow-context.js";
18
- import { HookFailureError } from "./hooks/runner.js";
19
- import { HookJournal } from "./hooks/journal.js";
20
- import { basePayload, runScope, withNodeFields } from "./hooks/payload.js";
21
- import { isRunTerminal } from "./types.js";
22
- import { join } from "node:path";
23
- /**
24
- * The core IR interpreter that drives state transitions, orchestrates
25
- * execution, and persists state.
26
- */
27
- export class WorkflowInterpreter {
28
- store;
29
- evaluator;
30
- agentExecutor;
31
- programExecutor;
32
- artifactStore;
33
- attemptArtifacts;
34
- runControl;
35
- maxConcurrency;
36
- sleep;
37
- /** Frozen hook runner for this Run; undefined disables all hook machinery. */
38
- hookRunner;
39
- /** Lazily-created per-Run injector journal (only when injectors fire). */
40
- hookJournal;
41
- /**
42
- * Per-node leaf execution metadata for hook onNodeComplete/Error payloads,
43
- * keyed by node key. Populated by executeProgram/executeAgent and deleted by
44
- * executeNode once the node's terminal lifecycle events have fired, so the map
45
- * never accumulates across a long-lived Run.
46
- */
47
- leafMeta = new Map();
48
- /** Pending external-decision resolvers for Signal Nodes awaiting a payload,
49
- * keyed by "runId:nodeKey". An entry exists only while a node is `awaiting`.
50
- * The resolver validates the payload against the node's output schema and
51
- * throws on mismatch, leaving the node `awaiting`. */
52
- signalResolvers = new Map();
53
- constructor(store, agentExecutor, programExecutor, options) {
54
- this.store = store;
55
- this.evaluator = new ExpressionEvaluator({ nowTimestamp: options?.nowTimestamp });
56
- this.agentExecutor = agentExecutor;
57
- this.programExecutor = programExecutor;
58
- this.artifactStore = new ArtifactStore(store.getBaseDir());
59
- this.attemptArtifacts = new AttemptArtifactRecorder(this.artifactStore);
60
- this.runControl = new RunControl(store);
61
- this.maxConcurrency = options?.maxConcurrency ?? 10;
62
- this.sleep = options?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
63
- this.hookRunner = options?.hookRunner;
64
- }
65
- /**
66
- * Start a new workflow run to completion (init + execute). Convenience for
67
- * callers that want to await the terminal state.
68
- */
69
- async start(ir, opts) {
70
- const meta = this.initRun(ir, opts);
71
- return this.runToCompletion(ir, opts, meta.runId);
72
- }
73
- /**
74
- * Initialize a run (freeze IR + input, write running meta) and return the
75
- * initial running state synchronously, without executing nodes.
76
- */
77
- initRun(ir, opts) {
78
- const validatedInput = validateInput(ir.input, opts.input);
79
- const runId = opts.runId ?? generateRunId();
80
- return this.store.initRun(runId, ir, validatedInput, {
81
- workflowRef: opts.workflowRef,
82
- workflowSourcePath: opts.workflowSourcePath,
83
- agentOverrides: opts.agentOverrides,
84
- submissionWarnings: opts.submissionWarnings,
85
- skipHooks: opts.skipHooks
86
- });
87
- }
88
- /**
89
- * Execute a previously-initialized run to its terminal state.
90
- */
91
- async runToCompletion(ir, opts, runId) {
92
- const meta = this.store.readRunMeta(runId);
93
- const scope = this.hookRunner ? runScope(ir, meta) : undefined;
94
- // `beforeRun` fires only at the first execution entry (no node states yet),
95
- // never on retry/resume where the Run is already underway.
96
- const isFirstExecution = this.store.listNodeStates(runId).length === 0;
97
- if (scope && isFirstExecution) {
98
- await this.emitRunEvent("beforeRun", scope, (p) => {
99
- p.input = opts.input;
100
- p.run_attempt = meta.runAttempt;
101
- p.ir_digest = meta.irDigest;
102
- });
103
- }
104
- const runStartedAt = Date.now();
105
- try {
106
- const ctx = this.buildContext(ir, opts.input, runId);
107
- await this.executeNode(ir.root, ctx, runId, {});
108
- meta.output = evaluateWorkflowOutputs(ir, ctx, this.evaluator);
109
- meta.error = undefined;
110
- meta.status = "completed";
111
- }
112
- catch (error) {
113
- const rootState = this.store.readNodeState(runId, resolveNodeKey(ir.root.keyTemplate));
114
- if (rootState?.state === "paused") {
115
- meta.status = "paused";
116
- }
117
- else if (rootState?.state === "cancelled") {
118
- meta.status = "cancelled";
119
- }
120
- else {
121
- meta.status = "failed";
122
- meta.output = undefined;
123
- meta.error = errorMessage(error);
124
- if (rootState?.state === "completed") {
125
- rootState.state = "failed";
126
- rootState.error = meta.error;
127
- rootState.completedAt = new Date().toISOString();
128
- this.store.writeTerminalNodeState(runId, rootState);
129
- }
130
- }
131
- }
132
- finally {
133
- // Clean up per-runId scheduling guards when the run settles,
134
- // so they don't leak memory across runs.
135
- this.runControl.clearSchedulingGuards(runId);
136
- }
137
- meta.updatedAt = new Date().toISOString();
138
- this.store.writeRunMeta(runId, meta);
139
- // `afterRun` fires once the Run reaches a terminal status.
140
- if (scope && isRunTerminal(meta.status)) {
141
- await this.emitRunEvent("afterRun", scope, (p) => {
142
- p.run_status = meta.status;
143
- p.run_attempt = meta.runAttempt;
144
- p.ir_digest = meta.irDigest;
145
- p.duration_ms = Date.now() - runStartedAt;
146
- if (meta.output !== undefined)
147
- p.output = meta.output;
148
- if (meta.error !== undefined)
149
- p.error = meta.error;
150
- });
151
- }
152
- return meta;
153
- }
154
- /**
155
- * Reset any nodes persisted as `running` (or `awaiting`) back to `pending`
156
- * (crash recovery). Safe to call when adopting a Run after a supervisor
157
- * restart: in-memory abort controllers and signal resolvers are gone, so a
158
- * node marked `running`/`awaiting` on disk has no live execution and must be
159
- * re-runnable. An `awaiting` Signal Node re-registers its resolver and waits
160
- * for a fresh external decision on re-execution.
161
- */
162
- recoverStaleNodes(runId) {
163
- this.runControl.recoverStaleNodes(runId);
164
- }
165
- /**
166
- * Deterministically replay a persisted Run and verify its reconstructed
167
- * Node topology.
168
- *
169
- * Re-walks the frozen IR snapshot (never the mutable YAML), feeding recorded
170
- * per-node outputs back into the expression context so control-flow decisions
171
- * (switch branches, loop rounds, fanout lanes) are re-derived. No agents or
172
- * programs are executed, no disk writes occur, and the walk is pinned to the
173
- * recorded runId + a frozen clock for self-determinism. The set of node keys
174
- * the re-walk reaches is compared against what was persisted; per-node
175
- * terminal-state and output equivalence are out of scope for this milestone.
176
- */
177
- replay(runId) {
178
- const ir = this.store.readIr(runId);
179
- const input = this.store.readInput(runId);
180
- if (!ir || !input) {
181
- throw new Error(`Run ${runId} not found`);
182
- }
183
- // Pin determinism to the run's frozen creation clock. `createdAt` is written
184
- // once at run init and never mutated (unlike `updatedAt`), so it is a stable
185
- // deterministic-clock source for re-deriving any now()-dependent values.
186
- // NOTE: aligning the execution-time clock to this same value (so now()-driven
187
- // control flow replays identically) is a follow-up; R3 replay verifies node
188
- // topology (reached key set), which does not depend on wall-clock time.
189
- const meta = this.store.readRunMeta(runId);
190
- const evaluator = new ExpressionEvaluator({ nowTimestamp: meta?.createdAt });
191
- // Persisted (recorded) node states, keyed by node key.
192
- const recorded = new Map();
193
- for (const s of this.store.listNodeStates(runId))
194
- recorded.set(s.nodeKey, s);
195
- // States reached by the deterministic re-walk.
196
- const reached = new Map();
197
- const ctx = this.buildContext(ir, input, runId);
198
- this.replayNode(ir.root, ctx, runId, {}, undefined, recorded, reached, evaluator);
199
- // Compare the set of node keys reached by the re-walk against what was
200
- // persisted. `reached` carries each node's recorded state for reporting; the
201
- // effective check is topological (which keys the interpretation reaches),
202
- // since recorded outputs are replayed rather than recomputed.
203
- const mismatches = [];
204
- for (const [key, recordedState] of recorded) {
205
- const actual = reached.get(key);
206
- if (actual === undefined) {
207
- mismatches.push({ nodeKey: key, kind: "missing-in-replay", expected: recordedState.state });
208
- }
209
- else if (actual !== recordedState.state) {
210
- mismatches.push({ nodeKey: key, kind: "state", expected: recordedState.state, actual });
211
- }
212
- }
213
- for (const [key, actual] of reached) {
214
- if (!recorded.has(key)) {
215
- mismatches.push({ nodeKey: key, kind: "unexpected-in-replay", actual });
216
- }
217
- }
218
- return { runId, ok: mismatches.length === 0, mismatches };
219
- }
220
- /**
221
- * Read-only re-walk used by {@link replay}. Mirrors the control-flow dispatch
222
- * of executeNode but never executes leaves: it replays recorded outputs into
223
- * the context and records the reconstructed node key → state. Concurrent
224
- * containers (parallel/fanout) descend only into the lanes/branches that were
225
- * actually recorded (racy joins do not run every branch).
226
- */
227
- replayNode(node, ctx, runId, dynamic, keyPrefix, recorded, reached, evaluator) {
228
- const resolved = resolveNodeKey(node.keyTemplate, dynamic);
229
- const nodeKey = withNodeKeyPrefix(keyPrefix, resolved);
230
- const rec = recorded.get(nodeKey);
231
- // A node absent from the recording was never reached on the original walk
232
- // (e.g. an untaken switch branch); skip it so we don't fabricate topology.
233
- if (!rec)
234
- return;
235
- reached.set(nodeKey, rec.state);
236
- // Feed the recorded output into the step context so downstream decisions
237
- // re-derive identically. Leaves contribute their output; containers below
238
- // populate ctx.steps for their own children as they descend.
239
- if (rec.output !== undefined)
240
- ctx.steps[node.id] = expressionOutputForNode(node, rec.output);
241
- switch (node.kind) {
242
- case "pipeline":
243
- for (const child of node.children ?? []) {
244
- this.replayNode(child, ctx, runId, dynamic, keyPrefix, recorded, reached, evaluator);
245
- }
246
- break;
247
- case "parallel":
248
- (node.branches ?? []).forEach((branch) => {
249
- const branchDynamic = nestedParallelBranchDynamic(dynamic, branch.id);
250
- const branchCtx = { ...ctx, steps: { ...ctx.steps } };
251
- this.replayNode(branch.child, branchCtx, runId, branchDynamic, keyPrefix, recorded, reached, evaluator);
252
- });
253
- break;
254
- case "fanout": {
255
- const overExpr = node.metadata.over;
256
- const items = overExpr ? evaluator.evaluateOverExpression(overExpr, ctx) : [];
257
- items.forEach((item, index) => {
258
- const keyCtx = { ...ctx, item, item_index: index };
259
- const itemId = this.extractItemId(item, node.metadata.key, index, keyCtx, evaluator);
260
- const itemDynamic = appendDynamicFrame(dynamic, { fanoutItemId: itemId, laneId: String(index) });
261
- const itemCtx = { ...keyCtx, steps: { ...ctx.steps }, item_id: itemId };
262
- for (const child of node.children ?? []) {
263
- this.replayNode(child, itemCtx, runId, itemDynamic, keyPrefix, recorded, reached, evaluator);
264
- }
265
- });
266
- break;
267
- }
268
- case "if":
269
- case "switch":
270
- for (const branch of node.branches ?? []) {
271
- const taken = !branch.when || Boolean(evaluator.evaluateExpression(branch.when, ctx));
272
- if (taken) {
273
- this.replayNode(branch.child, ctx, runId, dynamic, keyPrefix, recorded, reached, evaluator);
274
- break;
275
- }
276
- }
277
- break;
278
- case "loop": {
279
- const untilExpr = node.metadata.until;
280
- const maxIterations = node.metadata.max_iterations ?? 100;
281
- let lastOutput;
282
- for (let iter = 0; iter < maxIterations; iter++) {
283
- const loopCtx = { ...ctx, loop: { iter, last: lastOutput } };
284
- const loopDynamic = appendDynamicFrame(dynamic, { loopRound: iter });
285
- if (untilExpr && iter > 0 && evaluator.evaluateExpression(untilExpr, loopCtx))
286
- break;
287
- // Only descend while this round's children were actually recorded.
288
- let anyChildReached = false;
289
- for (const child of node.children ?? []) {
290
- const before = reached.size;
291
- this.replayNode(child, loopCtx, runId, loopDynamic, keyPrefix, recorded, reached, evaluator);
292
- if (reached.size > before)
293
- anyChildReached = true;
294
- const childKey = withNodeKeyPrefix(keyPrefix, resolveNodeKey(child.keyTemplate, loopDynamic));
295
- const recordedOutput = recorded.get(childKey)?.output;
296
- lastOutput = recordedOutput !== undefined ? primaryOutput(expressionOutputForNode(child, recordedOutput)) : lastOutput;
297
- }
298
- if (!anyChildReached)
299
- break;
300
- }
301
- break;
302
- }
303
- case "subworkflow":
304
- // The child root was executed under this node's key as prefix; descend
305
- // using recorded child node states (frozen child IR is not re-read).
306
- for (const [key, state] of recorded) {
307
- if (key.startsWith(`${nodeKey}/`) && !reached.has(key)) {
308
- reached.set(key, state.state);
309
- }
310
- }
311
- break;
312
- // run.agent / run.program / run.signal are leaves: their recorded state
313
- // was already captured above; nothing further to descend.
314
- default:
315
- break;
316
- }
317
- }
318
- /**
319
- * Retry a failed executable Node. This is a local repair operation for the
320
- * target executable only; Run-level retry remains the operation that restores
321
- * Workflow progress from failed composite ancestors.
322
- */
323
- async retryNode(runId, nodeKey) {
324
- const { state, ir, input } = this.runControl.prepareNodeRetry(runId, nodeKey);
325
- // Resolve the node IR *before* mutating state. Subworkflow child IR is
326
- // compiled on demand and never persisted, so a child key like
327
- // `workflow/sub/child` is unresolvable here; reject explicitly rather than
328
- // silently no-op and leave the node stuck in a pending state.
329
- const node = this.findNodeByKey(ir.root, nodeKey);
330
- if (!node) {
331
- throw new Error(`Cannot retry node ${nodeKey}: its definition was not found in the run's IR (subworkflow child nodes are not individually retryable)`);
332
- }
333
- // Control-plane reset (failed → pending), not a business-lifecycle
334
- // transition. `attempt` is incremented by executeNode.
335
- this.runControl.resetNodeForRetry(runId, state);
336
- const ctx = this.buildContext(ir, input, runId);
337
- const retryTarget = this.hydrateContextForNodeRetry(ir.root, ctx, runId, nodeKey, {});
338
- // Restore the parent dynamic value-context captured at first execution.
339
- const retryCtx = retryTarget?.ctx ?? ctx;
340
- this.restoreDynamicContext(retryCtx, state.dynamicContext);
341
- // Retry re-runs the executable as a continuation: agents keep the same acpx
342
- // session name and receive the fixed continuation prompt. The original full
343
- // node key is preserved for stable identity.
344
- await this.executeNode(node, retryCtx, runId, retryTarget?.dynamic ?? {}, undefined, true, nodeKey);
345
- }
346
- // ─── Run-level controls ─────────────────────────────────────────
347
- /**
348
- * Pause an entire Run. Validates Run is `running`, sets scheduling guard,
349
- * pauses all running nodes, and updates Run metadata to `paused`.
350
- */
351
- pauseRun(runId) {
352
- this.runControl.pauseRun(runId);
353
- }
354
- /**
355
- * Cancel an entire Run. Validates Run is `running` or `paused`, sets
356
- * scheduling guard, cancels running nodes, marks pending nodes as cancelled,
357
- * and updates Run metadata to `cancelled`.
358
- */
359
- cancelRun(runId) {
360
- this.runControl.cancelRun(runId);
361
- }
362
- /**
363
- * Resume an entire paused Run. Validates Run is `paused`, clears scheduling
364
- * guards, recovers stale nodes, and re-executes from root.
365
- */
366
- async resumeRun(runId) {
367
- this.runControl.resumeRun(runId);
368
- }
369
- /**
370
- * Retry a failed Run. Validates Run is `failed`, resets failed materialized
371
- * nodes to pending (preserving completed), clears scheduling guards, and
372
- * re-executes from root. Same Run ID, no new Run.
373
- */
374
- retryRun(runId) {
375
- this.runControl.retryRun(runId);
376
- }
377
- // ─── Node execution dispatch ──────────────────────────────────
378
- async executeNode(node, ctx, runId, dynamic, keyPrefix, isContinuation, overrideNodeKey) {
379
- // On continuation/retry the full resolved node key is supplied directly so the
380
- // node's stable identity (and thus the agent's acpx session name) survives
381
- // across loop/fanout/lane/subworkflow dynamics that are not re-derived here.
382
- const resolved = resolveNodeKey(node.keyTemplate, dynamic);
383
- const nodeKey = overrideNodeKey ?? withNodeKeyPrefix(keyPrefix, resolved);
384
- // Check if already completed (from prior run)
385
- const existing = this.store.readNodeState(runId, nodeKey);
386
- if (existing?.state === "completed") {
387
- const expressionOutput = expressionOutputForNode(node, existing.output);
388
- ctx.steps[node.id] = expressionOutput;
389
- return expressionOutput;
390
- }
391
- if (existing?.state === "cancelled" || existing?.state === "paused") {
392
- throw new NodeAbortedError(nodeKey, existing.state);
393
- }
394
- const continuation = Boolean(isContinuation || isPausedContinuationState(existing));
395
- // Initialize state
396
- const definitionHash = hashIrNode(node, { workflow: ctx.workflow });
397
- const state = existing ?? createInitialNodeState(nodeKey, node.id, node.kind, definitionHash);
398
- if (!state.definitionHash)
399
- state.definitionHash = definitionHash;
400
- if (state.state === "failed") {
401
- throw new Error(`Node ${nodeKey} is in failed state`);
402
- }
403
- const schedulingAbort = this.runControl.applySchedulingGuard(runId, state);
404
- if (schedulingAbort) {
405
- throw new NodeAbortedError(nodeKey, schedulingAbort);
406
- }
407
- // Set up abort controller
408
- const controller = new AbortController();
409
- this.runControl.registerAbortController(runId, nodeKey, controller);
410
- // Transition to running
411
- const fromState = state.state;
412
- if (canTransition(state.state, "running")) {
413
- state.state = transition(state.state, "running");
414
- }
415
- state.attempt++;
416
- state.startedAt = new Date().toISOString();
417
- // Snapshot the parent dynamic value-context (fanout item / loop round) for
418
- // executable leaves so retry/continuation can re-render their command/prompt
419
- // without the parent re-deriving item/loop. Only captured on fresh entry
420
- // (retry/continuation restore it from disk into ctx).
421
- if (!continuation && (node.kind === "run.agent" || node.kind === "run.program")) {
422
- const snapshot = this.captureDynamicContext(ctx);
423
- if (snapshot)
424
- state.dynamicContext = snapshot;
425
- }
426
- this.store.writeNodeState(runId, state);
427
- if (fromState !== "running") {
428
- await this.emitNodeLifecycle(runId, "onNodeStart", node, nodeKey, state, dynamic, ctx, fromState);
429
- }
430
- try {
431
- let output;
432
- let completeScope = false;
433
- // Artifact refs produced by a leaf execution in this call frame. Kept as a
434
- // local (not a shared field) so concurrent parallel/fanout siblings can't
435
- // clobber each other's refs.
436
- let artifactRefs;
437
- switch (node.kind) {
438
- case "pipeline":
439
- output = await this.executePipeline(node, ctx, runId, dynamic, keyPrefix);
440
- break;
441
- case "run.agent": {
442
- const leaf = await this.executeAgent(node, ctx, runId, controller.signal, nodeKey, continuation);
443
- output = leaf.output;
444
- artifactRefs = leaf.artifactRefs;
445
- break;
446
- }
447
- case "run.program": {
448
- const leaf = await this.executeProgram(node, ctx, runId, controller.signal, nodeKey);
449
- output = leaf.output;
450
- artifactRefs = leaf.artifactRefs;
451
- break;
452
- }
453
- case "parallel":
454
- output = await this.executeParallel(node, ctx, runId, dynamic, nodeKey, keyPrefix);
455
- break;
456
- case "fanout":
457
- output = await this.executeFanout(node, ctx, runId, dynamic, nodeKey, keyPrefix);
458
- break;
459
- case "if":
460
- output = await this.executeIf(node, ctx, runId, dynamic, keyPrefix);
461
- break;
462
- case "switch":
463
- output = await this.executeSwitch(node, ctx, runId, dynamic, keyPrefix);
464
- break;
465
- case "loop":
466
- output = await this.executeLoop(node, ctx, runId, dynamic, keyPrefix);
467
- break;
468
- case "guard": {
469
- const guard = this.executeGuard(node, ctx);
470
- output = guard.output;
471
- completeScope = guard.completeScope;
472
- break;
473
- }
474
- case "run.signal":
475
- output = await this.executeSignal(node, ctx, runId, controller.signal, nodeKey);
476
- break;
477
- case "subworkflow":
478
- output = await this.executeSubworkflow(node, ctx, runId, dynamic, nodeKey, state);
479
- break;
480
- default:
481
- throw new Error(`Unknown node kind: ${node.kind}`);
482
- }
483
- const expressionOutput = expressionOutputForNode(node, output);
484
- // If a Run-level resume already re-entered this node, a late result from
485
- // the old attempt must not overwrite the newer attempt's state.
486
- if (this.runControl.isStaleAttemptOnDisk(runId, nodeKey, state.attempt, state.startedAt)) {
487
- return expressionOutput;
488
- }
489
- this.runControl.syncInFrameAttemptFromDisk(runId, nodeKey, state);
490
- this.syncAgentTelemetryFromDisk(runId, nodeKey, state);
491
- // Transition to completed unless an external pause/cancel already changed
492
- // this node's state on disk while we were awaiting the leaf.
493
- const abortedState = this.runControl.readAbortedStateOnDisk(runId, nodeKey);
494
- if (abortedState) {
495
- throw new NodeAbortedError(nodeKey, abortedState, artifactRefs, output);
496
- }
497
- state.state = "completed";
498
- state.error = undefined;
499
- state.output = output;
500
- if (artifactRefs)
501
- state.artifactRefs = artifactRefs;
502
- state.completedAt = new Date().toISOString();
503
- // A Signal Node completes from `awaiting`, every other leaf from `running`.
504
- const completeFrom = node.kind === "run.signal" ? "awaiting" : "running";
505
- this.store.writeTerminalNodeState(runId, state);
506
- await this.emitNodeLifecycle(runId, "onNodeComplete", node, nodeKey, state, dynamic, ctx, completeFrom);
507
- // Add output to step context
508
- ctx.steps[node.id] = expressionOutput;
509
- if (completeScope) {
510
- throw new ScopeCompleted(expressionOutput);
511
- }
512
- return expressionOutput;
513
- }
514
- catch (error) {
515
- if (error instanceof ScopeCompleted) {
516
- throw error;
517
- }
518
- if (error instanceof NodeAbortedError) {
519
- if (this.runControl.isStaleAttemptOnDisk(runId, nodeKey, state.attempt, state.startedAt)) {
520
- throw error;
521
- }
522
- this.runControl.syncInFrameAttemptFromDisk(runId, nodeKey, state);
523
- // Transition this node to the same state as the child that was aborted.
524
- state.state = error.state === "paused" ? "paused" : "cancelled";
525
- state.error = abortedNodeError(error.state);
526
- // Preserve any output + partial Agent artifacts from the aborted leaf.
527
- if (error.output !== undefined)
528
- state.output = error.output;
529
- if (error.artifactRefs)
530
- state.artifactRefs = error.artifactRefs;
531
- this.syncAgentTelemetryFromDisk(runId, nodeKey, state);
532
- if (state.state === "cancelled") {
533
- this.store.writeTerminalNodeState(runId, state);
534
- }
535
- else {
536
- this.store.writeNodeState(runId, state);
537
- }
538
- await this.emitNodeLifecycle(runId, error.state === "paused" ? "onNodePaused" : "onNodeCancelled", node, nodeKey, state, dynamic, ctx, "running");
539
- throw error;
540
- }
541
- // Don't clobber an external pause/cancel that landed while this node's
542
- // leaf was failing; keep the control-plane abort as the observed outcome.
543
- if (this.runControl.isStaleAttemptOnDisk(runId, nodeKey, state.attempt, state.startedAt)) {
544
- throw error;
545
- }
546
- this.runControl.syncInFrameAttemptFromDisk(runId, nodeKey, state);
547
- const abortedState = this.runControl.readAbortedStateOnDisk(runId, nodeKey);
548
- if (abortedState) {
549
- throw new NodeAbortedError(nodeKey, abortedState);
550
- }
551
- state.state = "failed";
552
- state.error = error instanceof Error ? error.message : String(error);
553
- // Preserve artifacts and invalid output a leaf produced before failing.
554
- if (error instanceof LeafExecutionError) {
555
- if (error.artifactRefs)
556
- state.artifactRefs = error.artifactRefs;
557
- if (error.output !== undefined)
558
- state.output = error.output;
559
- }
560
- if (error instanceof GuardFailureError) {
561
- state.output = error.output;
562
- }
563
- this.syncAgentTelemetryFromDisk(runId, nodeKey, state);
564
- state.completedAt = new Date().toISOString();
565
- this.store.writeTerminalNodeState(runId, state);
566
- await this.emitNodeLifecycle(runId, "onNodeError", node, nodeKey, state, dynamic, ctx, "running", error);
567
- throw error;
568
- }
569
- finally {
570
- this.runControl.clearInFlightNode(runId, nodeKey);
571
- // Drop per-node leaf hook metadata so the map never grows across a Run.
572
- this.clearLeafMeta(nodeKey);
573
- }
574
- }
575
- // ─── Kind-specific execution ───────────────────────────────────
576
- async executePipeline(node, ctx, runId, dynamic, keyPrefix) {
577
- const children = node.children ?? [];
578
- if (node.metadata.implicit === true) {
579
- for (const child of children) {
580
- try {
581
- await this.executeNode(child, ctx, runId, dynamic, keyPrefix);
582
- }
583
- catch (error) {
584
- if (error instanceof ScopeCompleted)
585
- return { output: { ...ctx.steps } };
586
- throw error;
587
- }
588
- }
589
- return { output: { ...ctx.steps } };
590
- }
591
- const frame = { ...ctx, steps: { ...ctx.steps } };
592
- let lastOutput;
593
- for (const child of children) {
594
- try {
595
- lastOutput = await this.executeNode(child, frame, runId, dynamic, keyPrefix);
596
- }
597
- catch (error) {
598
- if (error instanceof ScopeCompleted)
599
- return { output: primaryOutput(error.output) };
600
- throw error;
601
- }
602
- }
603
- if (isRecord(node.metadata.outputs)) {
604
- return { output: evaluateWorkflowOutputs({ outputs: node.metadata.outputs }, frame, this.evaluator) };
605
- }
606
- return { output: primaryOutput(lastOutput) };
607
- }
608
- async executeAgent(node, ctx, runId, signal, nodeKey, continuation) {
609
- const retry = node.metadata.retry;
610
- const hasOutputSchema = node.metadata.output !== undefined;
611
- const maxRetries = typeof retry?.max === "number" ? retry.max : hasOutputSchema ? 2 : 0;
612
- const backoffMs = retry?.backoff ? parseDurationMs(retry.backoff) : 5e3;
613
- const allArtifactRefs = [...(this.store.readNodeState(runId, nodeKey)?.artifactRefs ?? [])];
614
- // Run `beforeAgentExec` once per Agent Step execution (per persisted attempt),
615
- // not per internal parse/schema auto-retry iteration. The returned prompt
616
- // prefix is prepended to every rendered prompt below.
617
- const injectedPrompt = await this.runInjector("beforeAgentExec", node, ctx, runId, nodeKey, (p) => {
618
- p.agent_use = node.metadata.agent?.use;
619
- p.is_continuation = Boolean(continuation);
620
- });
621
- // `attempt` is local to this executor call and controls parse/schema
622
- // auto-retry. The persisted `state.attempt` is the durable attempt sequence
623
- // used for node state and artifact filenames across pause/resume/manual retry.
624
- for (let attempt = 0;; attempt++) {
625
- const attemptNo = this.store.readNodeState(runId, nodeKey)?.attempt ?? attempt + 1;
626
- let preparedPrompt;
627
- let renderedSessionKey;
628
- try {
629
- preparedPrompt = renderAgentRequestPrompt(node, ctx, this.evaluator, Boolean(continuation), attempt > 0);
630
- renderedSessionKey = renderAgentSessionKey(node, ctx, this.evaluator);
631
- }
632
- catch (error) {
633
- const use = node.metadata.agent?.use ?? "?";
634
- throw new LeafExecutionError(`Agent step '${node.id}' (use: ${use}) failed (config): Failed to evaluate agent configuration template: ${error instanceof Error ? error.message : String(error)}`);
635
- }
636
- if (injectedPrompt?.prependPrompt) {
637
- preparedPrompt = `${injectedPrompt.prependPrompt}\n\n${preparedPrompt}`;
638
- }
639
- const rawAcpDebug = process.env.ACPUS_AGENT_RAW_ACP_DEBUG === "1";
640
- const liveArtifacts = this.attemptArtifacts.startAgentAttempt(runId, nodeKey, attemptNo, preparedPrompt, { rawAcpDebug });
641
- this.attemptArtifacts.mergeAttemptRefs(allArtifactRefs, liveArtifacts.artifactRefs);
642
- this.publishRenderedPrompt(runId, nodeKey, preparedPrompt);
643
- if (attempt === 0 && renderedSessionKey !== undefined) {
644
- this.publishRenderedAgentSessionKey(runId, nodeKey, renderedSessionKey);
645
- }
646
- const streamDiagnostics = [];
647
- let sawStdoutStream = false;
648
- const activity = new AgentTelemetryAccumulator({
649
- attempt: attemptNo,
650
- inputText: preparedPrompt,
651
- inputArtifactRef: liveArtifacts.promptRef,
652
- onTelemetry: (attemptTelemetry) => {
653
- this.publishAgentTelemetry(runId, nodeKey, attemptTelemetry);
654
- }
655
- });
656
- this.publishRunningAgentAttempt(runId, nodeKey, allArtifactRefs, activity.snapshot("running"));
657
- const result = await this.agentExecutor.execute({
658
- kind: "agent",
659
- node,
660
- context: ctx,
661
- signal,
662
- nodeKey,
663
- prompt: preparedPrompt,
664
- sessionKey: renderedSessionKey,
665
- continuation,
666
- retry: attempt > 0,
667
- onStream: (stream, chunk) => {
668
- if (stream === "stdout" && chunk.length > 0) {
669
- sawStdoutStream = true;
670
- if (rawAcpDebug) {
671
- try {
672
- this.attemptArtifacts.appendAgentRawAcpDebug(runId, nodeKey, attemptNo, chunk);
673
- }
674
- catch (error) {
675
- streamDiagnostics.push(`failed to append raw ACP debug stream: ${error instanceof Error ? error.message : String(error)}`);
676
- }
677
- }
678
- try {
679
- activity.append(chunk);
680
- }
681
- catch (error) {
682
- streamDiagnostics.push(`failed to parse live agent telemetry: ${error instanceof Error ? error.message : String(error)}`);
683
- }
684
- }
685
- }
686
- });
687
- try {
688
- if (!sawStdoutStream && result.stdout !== undefined)
689
- activity.append(result.stdout);
690
- if (result.acpxRecordId)
691
- activity.setAcpxRecordId(result.acpxRecordId);
692
- if (result.cwd)
693
- activity.setCwd(result.cwd);
694
- activity.flush();
695
- }
696
- catch (error) {
697
- streamDiagnostics.push(`failed to parse live agent telemetry: ${error instanceof Error ? error.message : String(error)}`);
698
- }
699
- if (result.responseText !== undefined)
700
- activity.setResponseText(result.responseText);
701
- const abortIntent = result.partial ? this.runControl.abortIntent(runId, nodeKey) : undefined;
702
- const finalAttemptState = abortIntent
703
- ?? (result.failureKind || (result.error && !result.partial) ? "failed" : "completed");
704
- const completedAt = new Date().toISOString();
705
- // Persist per-attempt human-readable agent IO plus diagnostics
706
- // when the executor exposed them. Refs flow back to executeNode via the
707
- // return value / thrown error (no shared mutable field).
708
- const finalized = result.responseText !== undefined || result.stderr !== undefined
709
- ? this.attemptArtifacts.finalizeAgentAttempt(runId, nodeKey, attemptNo, {
710
- responseText: result.responseText,
711
- stderr: result.stderr,
712
- diagnostics: streamDiagnostics
713
- })
714
- : result.artifactRefs;
715
- const artifactRefs = Array.isArray(finalized) ? finalized : finalized?.artifactRefs;
716
- const responseRef = !Array.isArray(finalized) ? finalized?.responseRef : undefined;
717
- if (responseRef)
718
- activity.setOutputArtifactRef(responseRef);
719
- const finalTelemetry = activity.snapshot(finalAttemptState, completedAt);
720
- const telemetryRef = this.attemptArtifacts.writeAgentTelemetry(runId, nodeKey, attemptNo, finalTelemetry);
721
- this.publishAgentTelemetry(runId, nodeKey, finalTelemetry);
722
- if (artifactRefs)
723
- this.attemptArtifacts.mergeAttemptRefs(allArtifactRefs, artifactRefs);
724
- this.attemptArtifacts.mergeAttemptRefs(allArtifactRefs, [telemetryRef]);
725
- if (result.partial) {
726
- // Operator abort → carry output + partial Agent artifact refs on the abort error;
727
- // executeNode persists the paused/cancelled state.
728
- throw new NodeAbortedError(nodeKey, abortIntent ?? "paused", allArtifactRefs.length > 0 ? allArtifactRefs : undefined, result.output);
729
- }
730
- // parse/schema failures are retryable while attempts remain.
731
- const retryable = result.failureKind === "parse" || result.failureKind === "schema";
732
- if (retryable && attempt < maxRetries) {
733
- const state = this.store.readNodeState(runId, nodeKey);
734
- if (state) {
735
- state.attempt++;
736
- this.store.writeNodeState(runId, state);
737
- }
738
- if (backoffMs > 0)
739
- await this.sleep(backoffMs);
740
- continue;
741
- }
742
- // Stash agent detail for hook onNodeComplete/Error payloads (terminal attempt).
743
- if (this.hookRunner) {
744
- const agent = node.metadata.agent;
745
- this.leafMeta.set(nodeKey, {
746
- failureKind: result.failureKind,
747
- agentModel: agent?.model,
748
- agentType: agent?.type,
749
- agentPolicy: node.metadata.policy ?? agent?.policy,
750
- sessionKey: renderedSessionKey,
751
- agentExitCode: result.exitCode,
752
- agentResponseText: result.responseText
753
- });
754
- }
755
- if (result.failureKind || (result.error && !result.partial)) {
756
- const use = node.metadata.agent?.use ?? "?";
757
- throw new LeafExecutionError(`Agent step '${node.id}' (use: ${use}) failed${result.failureKind ? ` (${result.failureKind})` : ""}: ${result.error ?? "unknown"}`, allArtifactRefs.length > 0 ? allArtifactRefs : artifactRefs, result.output);
758
- }
759
- // Agent output is wrapped in an envelope for parity with program steps.
760
- return {
761
- output: { output: result.output },
762
- artifactRefs: allArtifactRefs.length > 0 ? allArtifactRefs : artifactRefs
763
- };
764
- }
765
- }
766
- async executeProgram(node, ctx, runId, signal, nodeKey) {
767
- // Run `beforeProgramExec` before the executor call. cmd/env render inside
768
- // the executor, so injected env is passed through ExecutionRequest and the
769
- // executor merges it into the subprocess environment.
770
- const injected = await this.runInjector("beforeProgramExec", node, ctx, runId, nodeKey);
771
- const result = await this.programExecutor.execute({
772
- kind: "program",
773
- node,
774
- context: ctx,
775
- signal,
776
- nodeKey,
777
- injectedEnv: injected?.env
778
- });
779
- // Stash rendered command/env/output for hook onNodeComplete/Error payloads.
780
- if (this.hookRunner) {
781
- this.leafMeta.set(nodeKey, {
782
- failureKind: result.failureKind,
783
- command: result.command,
784
- shell: result.shell,
785
- subprocessEnv: result.subprocessEnv,
786
- exitCode: result.exitCode,
787
- stdout: result.stdout,
788
- stderr: result.stderr
789
- });
790
- }
791
- // Operator abort → paused/cancelled (carry output on the abort error).
792
- if (result.partial) {
793
- throw new NodeAbortedError(nodeKey, this.runControl.abortIntent(runId, nodeKey), undefined, result.output);
794
- }
795
- // Always persist stdout/stderr as artifacts (even when empty). An artifact
796
- // write failure is itself non-recoverable.
797
- const artifactRefs = this.attemptArtifacts.writeProgramArtifacts(runId, nodeKey, result.stdout ?? "", result.stderr ?? "");
798
- // Non-recoverable failures fail the node.
799
- if (result.failureKind) {
800
- throw new LeafExecutionError(`Program step '${node.id}' failed (${result.failureKind}): ${result.error ?? "unknown"}`, artifactRefs);
801
- }
802
- // exit code is allow-listed by `expect.exit_code` (default `[0]`); other
803
- return { output: { output: result.output, exit_code: result.exitCode ?? 0 }, artifactRefs };
804
- }
805
- publishRunningAgentAttempt(runId, nodeKey, artifactRefs, attemptTelemetry) {
806
- const state = this.store.readNodeState(runId, nodeKey);
807
- if (!state)
808
- return;
809
- state.artifactRefs = [...artifactRefs];
810
- state.agentTelemetry = upsertAgentAttemptTelemetry(state.agentTelemetry, attemptTelemetry);
811
- this.store.writeNodeState(runId, state);
812
- }
813
- publishRenderedAgentSessionKey(runId, nodeKey, renderedSessionKey) {
814
- const state = this.store.readNodeState(runId, nodeKey);
815
- if (!state)
816
- return;
817
- state.renderedSessionKey = renderedSessionKey;
818
- this.store.writeNodeState(runId, state);
819
- }
820
- publishRenderedPrompt(runId, nodeKey, renderedPrompt) {
821
- const state = this.store.readNodeState(runId, nodeKey);
822
- if (!state)
823
- return;
824
- state.renderedPrompt = renderedPrompt;
825
- this.store.writeNodeState(runId, state);
826
- }
827
- publishAgentTelemetry(runId, nodeKey, attemptTelemetry) {
828
- const state = this.store.readNodeState(runId, nodeKey);
829
- if (!state)
830
- return;
831
- state.agentTelemetry = upsertAgentAttemptTelemetry(state.agentTelemetry, attemptTelemetry);
832
- this.store.writeNodeState(runId, state);
833
- }
834
- syncAgentTelemetryFromDisk(runId, nodeKey, state) {
835
- const persisted = this.store.readNodeState(runId, nodeKey);
836
- state.agentTelemetry = persisted?.agentTelemetry ?? state.agentTelemetry;
837
- state.renderedSessionKey = persisted?.renderedSessionKey ?? state.renderedSessionKey;
838
- state.renderedPrompt = persisted?.renderedPrompt ?? state.renderedPrompt;
839
- }
840
- async executeParallel(node, ctx, runId, dynamic, nodeKey, keyPrefix) {
841
- const branches = node.branches ?? [];
842
- const maxConcurrency = node.metadata.max_concurrency ?? this.maxConcurrency;
843
- const join = node.metadata.join ?? "all";
844
- if (join === "all") {
845
- branches.forEach((branch) => {
846
- const branchDynamic = this.withExecutionContext(dynamic, nestedParallelBranchDynamic(dynamic, branch.id));
847
- this.materializePendingNode(runId, branch.child, ctx, branchDynamic, keyPrefix);
848
- });
849
- }
850
- const limit = pLimit(maxConcurrency);
851
- const branchPromises = branches.map((branch) => limit(async () => {
852
- const branchDynamic = this.withExecutionContext(dynamic, nestedParallelBranchDynamic(dynamic, branch.id));
853
- let output;
854
- try {
855
- output = await this.executeNode(branch.child, { ...ctx, steps: { ...ctx.steps } }, runId, branchDynamic, keyPrefix);
856
- }
857
- catch (error) {
858
- if (error instanceof ScopeCompleted) {
859
- output = error.output;
860
- }
861
- else {
862
- throw error;
863
- }
864
- }
865
- return { branch, output };
866
- }));
867
- if (join === "race") {
868
- try {
869
- // First branch to settle wins; losers are not cancelled but silently
870
- // consumed so their later rejection doesn't surface as unhandled.
871
- const winner = await Promise.race(branchPromises);
872
- branchPromises.forEach((p) => void p.catch(() => undefined));
873
- const mapOutput = { [winner.branch.id]: primaryOutput(winner.output) };
874
- return { output: mapOutput };
875
- }
876
- catch (error) {
877
- if (!(error instanceof NodeAbortedError)) {
878
- this.runControl.cancelDescendantsInScope(runId, nodeKey);
879
- }
880
- branchPromises.forEach((p) => void p.catch(() => undefined));
881
- throw error;
882
- }
883
- }
884
- // join: all — collect every branch output keyed by step id.
885
- // fail-fast: Promise.all rejects on the first branch failure. Before
886
- // rethrowing, actively cancel the still-running sibling branches so they
887
- // don't linger in "running" state (they belong to a parallel that has
888
- // already failed). Siblings transition to "cancelled".
889
- let results;
890
- try {
891
- results = await Promise.all(branchPromises);
892
- }
893
- catch (error) {
894
- if (!(error instanceof NodeAbortedError)) {
895
- // Genuine failure or cancel — fast-stop: cancel still-running siblings
896
- this.runControl.cancelDescendantsInScope(runId, nodeKey);
897
- }
898
- branchPromises.forEach((p) => void p.catch(() => undefined));
899
- throw error;
900
- }
901
- const mapOutput = {};
902
- for (const { branch, output } of results) {
903
- mapOutput[branch.id] = primaryOutput(output);
904
- }
905
- return { output: mapOutput };
906
- }
907
- async executeFanout(node, ctx, runId, dynamic, nodeKey, keyPrefix) {
908
- const overExpr = node.metadata.over;
909
- if (!overExpr) {
910
- throw new Error(`fanout node ${node.id} missing 'over' expression`);
911
- }
912
- const items = this.evaluator.evaluateOverExpression(overExpr, ctx);
913
- const maxConcurrency = node.metadata.max_concurrency ?? this.maxConcurrency;
914
- const join = node.metadata.join ?? "all";
915
- const quorum = node.metadata.quorum;
916
- const successCriteria = node.metadata.success_criteria;
917
- const body = node.children?.[0];
918
- if (!body) {
919
- throw new Error(`fanout node ${node.id} missing body pipeline`);
920
- }
921
- const limit = pLimit(maxConcurrency);
922
- // Success target (how many lanes must succeed). Default follows join.
923
- const defaultMinSuccess = join === "race" ? 1 : join === "quorum" ? (quorum ?? 1) : items.length;
924
- const minSuccess = successCriteria?.min_success ?? defaultMinSuccess;
925
- const lanePlan = items.map((item, index) => {
926
- const keyCtx = { ...ctx, item, item_index: index };
927
- const itemId = this.extractItemId(item, node.metadata.key, index, keyCtx, this.evaluator);
928
- const laneDynamic = appendDynamicFrame({}, { fanoutItemId: itemId, laneId: String(index) });
929
- return {
930
- itemId,
931
- keyCtx,
932
- itemDynamic: laneDynamic
933
- };
934
- });
935
- if (join === "all") {
936
- for (const lane of lanePlan) {
937
- const itemDynamic = this.withExecutionContext(dynamic, appendDynamicFrames(dynamic, lane.itemDynamic));
938
- this.materializePendingNode(runId, body, lane.keyCtx, itemDynamic, keyPrefix);
939
- }
940
- }
941
- // Fail-fast: once enough lanes have failed that the success target is
942
- // unreachable (failures > total - minSuccess), abort the whole fanout —
943
- // cancel every still-running/pending lane subtree and reject to short
944
- // circuit the wait, instead of waiting for doomed lanes to finish.
945
- // race/quorum tolerate per-lane failure until their target is impossible.
946
- const maxFailures = items.length - minSuccess;
947
- let failures = 0;
948
- let failFastTriggered = false;
949
- // Each lane resolves to a LaneResult. A tolerable failure is captured (does
950
- // not reject) so the join/min_success logic can run. A NodeAbortedError
951
- // (operator pause/cancel) re-throws so it propagates to the parent.
952
- const lanePromises = lanePlan.map((lane) => limit(async () => {
953
- const itemDynamic = this.withExecutionContext(dynamic, appendDynamicFrames(dynamic, lane.itemDynamic));
954
- const itemCtx = {
955
- ...lane.keyCtx,
956
- steps: { ...ctx.steps },
957
- item_id: lane.itemId
958
- };
959
- try {
960
- const laneOutput = await this.executeNode(body, itemCtx, runId, itemDynamic, keyPrefix);
961
- return { ok: true, output: primaryOutput(laneOutput) };
962
- }
963
- catch (error) {
964
- if (error instanceof ScopeCompleted)
965
- return { ok: true, output: primaryOutput(error.output) };
966
- if (error instanceof NodeAbortedError) {
967
- throw error;
968
- }
969
- failures++;
970
- if (failures > maxFailures) {
971
- // Success target is now unreachable → fail fast: cancel all
972
- // still-running lanes of THIS fanout (scoped to the parent dynamic,
973
- // so it spans every lane), then reject to short-circuit the wait.
974
- if (!failFastTriggered) {
975
- failFastTriggered = true;
976
- this.runControl.cancelDescendantsInScope(runId, nodeKey);
977
- }
978
- throw error;
979
- }
980
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
981
- }
982
- }));
983
- // Wait strategy.
984
- const settled = await this.waitForFanout(lanePromises, join, quorum);
985
- const successes = settled.filter((r) => r.ok);
986
- if (successes.length < minSuccess) {
987
- throw new Error(`fanout ${node.id}: ${successes.length} successful lanes, requires ${minSuccess}`);
988
- }
989
- // outputMerge: "array" of successful lane outputs.
990
- return { output: successes.map((r) => r.output) };
991
- }
992
- /**
993
- * Resolve fanout lanes per the wait strategy. Lanes never reject on normal
994
- * failure (captured as LaneResult); only NodeAbortedError rejects, which we
995
- * let propagate. Losing/excess lanes are silently consumed.
996
- */
997
- async waitForFanout(lanePromises, join, quorum) {
998
- if (join === "race") {
999
- const first = await Promise.race(lanePromises);
1000
- lanePromises.forEach((p) => void p.catch(() => undefined));
1001
- return [first];
1002
- }
1003
- if (join === "quorum") {
1004
- const target = Math.min(quorum ?? lanePromises.length, lanePromises.length);
1005
- return new Promise((resolve, reject) => {
1006
- const collected = [];
1007
- for (const p of lanePromises) {
1008
- p.then((r) => {
1009
- collected.push(r);
1010
- if (collected.length >= target)
1011
- resolve(collected.slice());
1012
- }, (err) => reject(err));
1013
- }
1014
- });
1015
- }
1016
- // join: all — wait for every lane. A lane only rejects under fail-fast
1017
- // (first failure) or NodeAbortedError; either way Promise.all short-circuits
1018
- // and the rejection propagates to fail the fanout immediately.
1019
- return Promise.all(lanePromises);
1020
- }
1021
- materializePendingNode(runId, node, ctx, dynamic, keyPrefix) {
1022
- const resolved = resolveNodeKey(node.keyTemplate, dynamic);
1023
- const nodeKey = withNodeKeyPrefix(keyPrefix, resolved);
1024
- if (this.store.readNodeState(runId, nodeKey))
1025
- return;
1026
- this.store.writeNodeState(runId, createInitialNodeState(nodeKey, node.id, node.kind, hashIrNode(node, { workflow: ctx.workflow })));
1027
- }
1028
- async executeSwitch(node, ctx, runId, dynamic, keyPrefix) {
1029
- const branches = node.branches ?? [];
1030
- for (const branch of branches) {
1031
- if (branch.when && !this.evaluator.evaluateExpression(branch.when, ctx))
1032
- continue;
1033
- // Default branch has no condition and is reached only after earlier cases fail.
1034
- try {
1035
- return { output: primaryOutput(await this.executeNode(branch.child, ctx, runId, dynamic, keyPrefix)) };
1036
- }
1037
- catch (error) {
1038
- if (error instanceof ScopeCompleted)
1039
- return { output: primaryOutput(error.output) };
1040
- throw error;
1041
- }
1042
- }
1043
- throw new Error(`Switch node ${node.id}: no branch matched and no default`);
1044
- }
1045
- async executeIf(node, ctx, runId, dynamic, keyPrefix) {
1046
- const branches = node.branches ?? [];
1047
- for (const branch of branches) {
1048
- if (branch.when && !this.evaluator.evaluateExpression(branch.when, ctx))
1049
- continue;
1050
- try {
1051
- return { output: primaryOutput(await this.executeNode(branch.child, ctx, runId, dynamic, keyPrefix)) };
1052
- }
1053
- catch (error) {
1054
- if (error instanceof ScopeCompleted)
1055
- return { output: primaryOutput(error.output) };
1056
- throw error;
1057
- }
1058
- }
1059
- return { output: {} };
1060
- }
1061
- async executeLoop(node, ctx, runId, dynamic, keyPrefix) {
1062
- const untilExpr = node.metadata.until;
1063
- const maxIterations = node.metadata.max_iterations ?? 100;
1064
- const body = node.children?.[0];
1065
- if (!body) {
1066
- throw new Error(`loop node ${node.id} missing body pipeline`);
1067
- }
1068
- let lastOutput;
1069
- for (let iter = 0; iter < maxIterations; iter++) {
1070
- const loopCtx = {
1071
- ...ctx,
1072
- loop: { iter, last: lastOutput }
1073
- };
1074
- const loopDynamic = this.withExecutionContext(dynamic, appendDynamicFrame(dynamic, { loopRound: iter }));
1075
- // Check until condition (skip on first iteration)
1076
- if (untilExpr && iter > 0) {
1077
- const done = this.evaluator.evaluateExpression(untilExpr, loopCtx);
1078
- if (done)
1079
- break;
1080
- }
1081
- try {
1082
- lastOutput = primaryOutput(await this.executeNode(body, loopCtx, runId, loopDynamic, keyPrefix));
1083
- }
1084
- catch (error) {
1085
- if (error instanceof ScopeCompleted)
1086
- return { output: primaryOutput(error.output) };
1087
- throw error;
1088
- }
1089
- }
1090
- return { output: lastOutput ?? {} };
1091
- }
1092
- executeGuard(node, ctx) {
1093
- const when = node.metadata.when;
1094
- if (!when) {
1095
- throw new Error(`guard node ${node.id} missing 'when' expression`);
1096
- }
1097
- const matched = Boolean(this.evaluator.evaluateExpression(when, ctx));
1098
- const action = (matched ? node.metadata.then : node.metadata.else);
1099
- if (action !== "continue" && action !== "fail" && action !== "complete") {
1100
- throw new Error(`guard node ${node.id}: action must be continue, fail, or complete`);
1101
- }
1102
- const guardOutput = { matched, action };
1103
- if (action === "fail") {
1104
- const messageTemplate = node.metadata.message;
1105
- const message = typeof messageTemplate === "string" ? this.evaluator.evaluateTemplate(messageTemplate, ctx) : undefined;
1106
- if (message !== undefined)
1107
- guardOutput.message = message;
1108
- throw new GuardFailureError(message ?? `Guard '${node.id}' failed`, { output: guardOutput });
1109
- }
1110
- return { output: { output: guardOutput }, completeScope: action === "complete" };
1111
- }
1112
- async executeSignal(node, ctx, runId, signal, nodeKey) {
1113
- const timeout = node.metadata.timeout;
1114
- const onTimeout = node.metadata.on_timeout;
1115
- const defaultPayload = node.metadata.default;
1116
- const outputSchema = node.metadata.output;
1117
- const timeoutMs = timeout ? parseDurationMs(timeout) : undefined;
1118
- const resolverKey = `${runId}:${nodeKey}`;
1119
- // Render the prompt the operator must act on. The Signal Node is the one
1120
- // place a human is expected to inject data, so the rendered prompt (and the
1121
- // expected payload schema, already on node.metadata.output) must be visible
1122
- // in `runs show` / TUI. Persist it the same way Agent prompts are surfaced.
1123
- const promptTemplate = node.metadata.prompt;
1124
- if (typeof promptTemplate === "string") {
1125
- let renderedPrompt;
1126
- try {
1127
- renderedPrompt = this.evaluator.evaluateTemplate(promptTemplate, ctx);
1128
- }
1129
- catch (error) {
1130
- throw new LeafExecutionError(`Signal step '${node.id}' failed (config): Failed to evaluate signal prompt template: ${error instanceof Error ? error.message : String(error)}`);
1131
- }
1132
- this.publishRenderedPrompt(runId, nodeKey, renderedPrompt);
1133
- }
1134
- // Enter the `awaiting` state: the node is now blocked on an external
1135
- // decision. This is deliberately distinct from operator `paused` (see state
1136
- // machine); a signal payload resolves it, a cancel aborts it.
1137
- const enterState = this.store.readNodeState(runId, nodeKey);
1138
- if (enterState && canTransition(enterState.state, "awaiting")) {
1139
- const from = enterState.state;
1140
- enterState.state = transition(enterState.state, "awaiting");
1141
- this.store.writeNodeState(runId, enterState);
1142
- // Surface the running → awaiting transition to onStateChange observers.
1143
- await this.emitNodeLifecycle(runId, "onStateChange", node, nodeKey, enterState, this.dynamicFromCtx(ctx), ctx, from);
1144
- }
1145
- return new Promise((resolve, reject) => {
1146
- const onAbort = () => {
1147
- cleanup();
1148
- // Honor the operator intent recorded by Run-level pause/cancel. An
1149
- // awaiting signal node is normally cancelled (awaiting → cancelled).
1150
- reject(new NodeAbortedError(nodeKey, this.runControl.abortIntent(runId, nodeKey)));
1151
- };
1152
- const timer = timeoutMs
1153
- ? setTimeout(() => {
1154
- cleanup();
1155
- // The compiler guarantees on_timeout is "fail" or "default" whenever
1156
- // timeout is set. "default" resolves with the literal default payload
1157
- // (already schema-checked at compile time); "fail" rejects.
1158
- if (onTimeout === "default") {
1159
- resolve({ output: defaultPayload ?? {} });
1160
- }
1161
- else {
1162
- reject(new Error(`Signal timed out after ${timeout} (on_timeout: fail)`));
1163
- }
1164
- }, timeoutMs)
1165
- : undefined;
1166
- signal.addEventListener("abort", onAbort, { once: true });
1167
- const cleanup = () => {
1168
- if (timer)
1169
- clearTimeout(timer);
1170
- signal.removeEventListener("abort", onAbort);
1171
- this.signalResolvers.delete(resolverKey);
1172
- };
1173
- // External decision channel: an operator/system `signal` resolves the
1174
- // node with a structured payload. With no `timeout` configured, the node
1175
- // waits indefinitely for this (or a cancel). The resolver validates the
1176
- // payload against the declared output schema; an invalid payload throws
1177
- // back to the caller (the HTTP endpoint maps it to 422) and the node
1178
- // stays `awaiting` because cleanup() has not run.
1179
- this.signalResolvers.set(resolverKey, (payload) => {
1180
- validateSignalPayload(outputSchema, payload);
1181
- cleanup();
1182
- resolve({ output: payload });
1183
- });
1184
- });
1185
- }
1186
- /**
1187
- * Submit an external decision payload to a Signal Node that is currently
1188
- * `awaiting`. Validates the payload against the node's output schema (when
1189
- * declared) and resolves the in-memory promise registered by executeSignal,
1190
- * which lets executeNode transition the node `awaiting → completed` with the
1191
- * payload as output. Throws if the node is not awaiting (no live resolver) or
1192
- * if the payload fails schema validation (the node stays `awaiting`).
1193
- */
1194
- submitSignal(runId, nodeKey, payload) {
1195
- const resolver = this.signalResolvers.get(`${runId}:${nodeKey}`);
1196
- if (!resolver) {
1197
- throw new Error(`Node ${nodeKey} is not awaiting a signal`);
1198
- }
1199
- // resolver throws SignalPayloadValidationError on a non-conforming payload
1200
- // without consuming the awaiting state.
1201
- resolver(payload);
1202
- }
1203
- async executeSubworkflow(node, ctx, runId, dynamic, nodeKey, state) {
1204
- const specPath = node.metadata.subworkflow;
1205
- const inputSpec = node.metadata.input;
1206
- const baseDir = dynamic.currentSpecPath
1207
- ? dirname(dynamic.currentSpecPath)
1208
- : (this.store.readIr(runId)?.source.path ? dirname(this.store.readIr(runId).source.path) : process.cwd());
1209
- const childAbs = resolve(baseDir, specPath);
1210
- const childReal = this.resolveExistingSourcePath(childAbs, `Subworkflow path '${specPath}' does not exist or is not readable`);
1211
- // Per-path guard: parallel siblings may call the same child independently.
1212
- const currentPaths = dynamic.subworkflowPaths;
1213
- if (currentPaths?.has(childReal)) {
1214
- throw new Error(`Subworkflow cycle detected for '${specPath}'`);
1215
- }
1216
- // Compile the child spec at runtime (file I/O lives in the runtime layer).
1217
- const source = readFileSync(childReal, "utf8");
1218
- const compiled = compileWorkflow(source, {
1219
- sourcePath: childReal,
1220
- includeResolver: (includePath, fromPath) => {
1221
- const dir = fromPath ? dirname(resolve(fromPath)) : process.cwd();
1222
- const includeAbs = resolve(dir, includePath);
1223
- const includeReal = this.resolveExistingSourcePath(includeAbs, `Include path '${includePath}' does not exist or is not readable`);
1224
- return readFileSync(includeReal, "utf8");
1225
- }
1226
- });
1227
- if (!compiled.ok || !compiled.ir) {
1228
- throw new Error(`Subworkflow '${specPath}' failed to compile: ${compiled.diagnostics.map((d) => d.message).join(", ")}`);
1229
- }
1230
- // Evaluate the declared input map against the current context. A field that
1231
- // is a single ${{ }} expression keeps its native type; otherwise it is a
1232
- // template string.
1233
- const childInput = {};
1234
- for (const [key, value] of Object.entries(inputSpec ?? {})) {
1235
- childInput[key] = evaluateTemplatedValue(value, ctx, this.evaluator);
1236
- }
1237
- // Validate subworkflow input against the child IR's compiled input schema.
1238
- const validatedChildInput = validateInput(compiled.ir.input, childInput);
1239
- state.input = validatedChildInput;
1240
- this.store.writeNodeState(runId, state);
1241
- const childPaths = new Set(currentPaths);
1242
- childPaths.add(childReal);
1243
- const childDynamic = { ...dynamic, subworkflowPaths: childPaths, currentSpecPath: childReal };
1244
- const childCtx = this.buildContext(compiled.ir, validatedChildInput, runId);
1245
- await this.executeNode(compiled.ir.root, childCtx, runId, childDynamic, nodeKey);
1246
- return { output: evaluateWorkflowOutputs(compiled.ir, childCtx, this.evaluator) };
1247
- }
1248
- resolveExistingSourcePath(path, message) {
1249
- const realPath = realPathOrUndefined(path);
1250
- if (realPath === undefined) {
1251
- throw new Error(message);
1252
- }
1253
- return realPath;
1254
- }
1255
- // ─── Helpers ────────────────────────────────────────────────────
1256
- withExecutionContext(parent, dynamic) {
1257
- return {
1258
- ...dynamic,
1259
- subworkflowPaths: parent.subworkflowPaths,
1260
- currentSpecPath: parent.currentSpecPath
1261
- };
1262
- }
1263
- buildContext(ir, input, runId) {
1264
- return { input, steps: {}, workflow: buildWorkflowExpressionContext(ir), run_id: runId };
1265
- }
1266
- hydrateContextForNodeRetry(node, ctx, runId, targetNodeKey, dynamic, keyPrefix) {
1267
- const nodeKey = withNodeKeyPrefix(keyPrefix, resolveNodeKey(node.keyTemplate, dynamic));
1268
- if (nodeKey === targetNodeKey)
1269
- return { ctx, dynamic };
1270
- const state = this.store.readNodeState(runId, nodeKey);
1271
- if (state?.state === "completed" && state.output !== undefined && node.metadata.implicit !== true) {
1272
- ctx.steps[node.id] = expressionOutputForNode(node, state.output);
1273
- return undefined;
1274
- }
1275
- if (node.kind === "pipeline") {
1276
- const frame = node.metadata.implicit === true ? ctx : { ...ctx, steps: { ...ctx.steps } };
1277
- for (const child of node.children ?? []) {
1278
- const found = this.hydrateContextForNodeRetry(child, frame, runId, targetNodeKey, dynamic, keyPrefix);
1279
- if (found)
1280
- return found;
1281
- }
1282
- return undefined;
1283
- }
1284
- if (node.kind === "parallel") {
1285
- for (const branch of node.branches ?? []) {
1286
- const branchCtx = { ...ctx, steps: { ...ctx.steps } };
1287
- const branchDynamic = nestedParallelBranchDynamic(dynamic, branch.id);
1288
- const found = this.hydrateContextForNodeRetry(branch.child, branchCtx, runId, targetNodeKey, branchDynamic, keyPrefix);
1289
- if (found)
1290
- return found;
1291
- }
1292
- return undefined;
1293
- }
1294
- for (const child of node.children ?? []) {
1295
- const found = this.hydrateContextForNodeRetry(child, ctx, runId, targetNodeKey, dynamic, keyPrefix);
1296
- if (found)
1297
- return found;
1298
- }
1299
- for (const branch of node.branches ?? []) {
1300
- const found = this.hydrateContextForNodeRetry(branch.child, ctx, runId, targetNodeKey, dynamic, keyPrefix);
1301
- if (found)
1302
- return found;
1303
- }
1304
- return undefined;
1305
- }
1306
- /** Extract the parent dynamic value-context (fanout item / loop round) from a context, if any. */
1307
- captureDynamicContext(ctx) {
1308
- const snapshot = {};
1309
- let has = false;
1310
- if (ctx.item !== undefined) {
1311
- snapshot.item = ctx.item;
1312
- has = true;
1313
- }
1314
- if (ctx.item_id !== undefined) {
1315
- snapshot.item_id = ctx.item_id;
1316
- has = true;
1317
- }
1318
- if (ctx.item_index !== undefined) {
1319
- snapshot.item_index = ctx.item_index;
1320
- has = true;
1321
- }
1322
- if (ctx.loop !== undefined) {
1323
- snapshot.loop = ctx.loop;
1324
- has = true;
1325
- }
1326
- return has ? snapshot : undefined;
1327
- }
1328
- /** Merge a persisted dynamic value-context back into a rebuilt context (retry/continuation). */
1329
- restoreDynamicContext(ctx, snapshot) {
1330
- if (!snapshot)
1331
- return;
1332
- if (snapshot.item !== undefined)
1333
- ctx.item = snapshot.item;
1334
- if (snapshot.item_id !== undefined)
1335
- ctx.item_id = snapshot.item_id;
1336
- if (snapshot.item_index !== undefined)
1337
- ctx.item_index = snapshot.item_index;
1338
- if (snapshot.loop !== undefined)
1339
- ctx.loop = snapshot.loop;
1340
- }
1341
- findNodeByKey(root, nodeKey) {
1342
- const staticPath = staticNodePathFromKey(nodeKey);
1343
- return this.findNodeByPath(root, staticPath);
1344
- }
1345
- findNodeByPath(root, staticPath) {
1346
- if (root.nodePath.join("/") === staticPath)
1347
- return root;
1348
- for (const child of root.children ?? []) {
1349
- const found = this.findNodeByPath(child, staticPath);
1350
- if (found)
1351
- return found;
1352
- }
1353
- for (const branch of root.branches ?? []) {
1354
- const found = this.findNodeByPath(branch.child, staticPath);
1355
- if (found)
1356
- return found;
1357
- }
1358
- return undefined;
1359
- }
1360
- extractItemId(item, keyExpr, index, ctx, evaluator) {
1361
- if (keyExpr) {
1362
- const result = evaluator.evaluateTemplate(keyExpr, ctx);
1363
- if (result)
1364
- return result;
1365
- }
1366
- return String(index ?? 0);
1367
- }
1368
- // ─── Hook integration ───────────────────────────────────────────
1369
- /** Run scope for the active Run, derived from the frozen IR + metadata. */
1370
- resolveRunScope(runId) {
1371
- if (!this.hookRunner)
1372
- return undefined;
1373
- const ir = this.store.readIr(runId);
1374
- const meta = this.store.readRunMeta(runId);
1375
- if (!ir || !meta)
1376
- return undefined;
1377
- return runScope(ir, meta);
1378
- }
1379
- /** Lazily create the per-Run injector journal. */
1380
- journalFor(runId) {
1381
- if (!this.hookJournal) {
1382
- this.hookJournal = new HookJournal(join(this.store.getBaseDir(), runId));
1383
- }
1384
- return this.hookJournal;
1385
- }
1386
- /**
1387
- * Run an injector for a node, journaling each handler invocation. Returns the
1388
- * merged InjectorResult, or undefined when no hook runner / no handlers.
1389
- * An injector failure under `fail` policy propagates as HookFailureError,
1390
- * which executeNode maps to a node failure with failureKind hook_failure.
1391
- */
1392
- async runInjector(name, node, ctx, runId, nodeKey, enrich) {
1393
- if (!this.hookRunner || !this.hookRunner.hasInjector(name))
1394
- return undefined;
1395
- const scope = this.resolveRunScope(runId);
1396
- if (!scope)
1397
- return undefined;
1398
- const state = this.store.readNodeState(runId, nodeKey);
1399
- const nodeAttempt = state?.attempt ?? 1;
1400
- const isRetry = nodeAttempt > 1;
1401
- const payload = withNodeFields(basePayload(scope, name), node, nodeKey, state, this.dynamicFromCtx(ctx), ctx);
1402
- payload.is_retry = isRetry;
1403
- // Note: `beforeAgentExec` runs before the prompt is rendered, so we do NOT
1404
- // surface state.renderedPrompt here — it would be absent on first execution
1405
- // and stale on retry. The rendered prompt is carried by lifecycle events.
1406
- enrich?.(payload);
1407
- const journal = this.journalFor(runId);
1408
- return this.hookRunner.runInjector(name, payload, (handlerIndex, result, durationMs) => {
1409
- journal.append({
1410
- node_key: nodeKey,
1411
- injector: name,
1412
- handler_index: handlerIndex,
1413
- node_attempt: nodeAttempt,
1414
- is_retry: isRetry,
1415
- prepend_prompt: name === "beforeAgentExec" ? (result.prependPrompt ?? null) : null,
1416
- env: result.env ?? null,
1417
- timestamp: new Date().toISOString(),
1418
- duration_ms: durationMs
1419
- });
1420
- });
1421
- }
1422
- /** Fire a node lifecycle event plus onStateChange for an actual transition. */
1423
- async emitNodeLifecycle(runId, name, node, nodeKey, state, dynamic, ctx, fromState, error) {
1424
- if (!this.hookRunner)
1425
- return;
1426
- const firesSpecific = this.hookRunner.hasEvent(name);
1427
- const firesStateChange = this.hookRunner.hasEvent("onStateChange");
1428
- if (!firesSpecific && !firesStateChange)
1429
- return;
1430
- const scope = this.resolveRunScope(runId);
1431
- if (!scope)
1432
- return;
1433
- const hookFailure = error instanceof HookFailureError;
1434
- const build = (eventName) => {
1435
- const p = withNodeFields(basePayload(scope, eventName), node, nodeKey, state, dynamic, ctx);
1436
- if (state.output !== undefined)
1437
- p.output = state.output;
1438
- if (state.startedAt && state.completedAt) {
1439
- p.duration_ms = Date.parse(state.completedAt) - Date.parse(state.startedAt);
1440
- }
1441
- // beforeAgentExec aside, lifecycle events carry the rendered prompt.
1442
- if (state.renderedPrompt)
1443
- p.prompt = state.renderedPrompt;
1444
- if (state.renderedSessionKey)
1445
- p.session_key = state.renderedSessionKey;
1446
- if (node.kind === "run.agent") {
1447
- const telemetry = hookAgentTelemetry(state);
1448
- if (telemetry)
1449
- p.agent_telemetry = telemetry;
1450
- }
1451
- if (error !== undefined)
1452
- p.error = error instanceof Error ? error.message : String(error);
1453
- this.fillParentFields(runId, node, nodeKey, p);
1454
- fillCompositeFields(node, p);
1455
- const leaf = this.leafMeta.get(nodeKey);
1456
- if (leaf) {
1457
- if (leaf.failureKind !== undefined)
1458
- p.failure_kind = leaf.failureKind;
1459
- // Program detail.
1460
- if (leaf.command !== undefined)
1461
- p.command = leaf.command;
1462
- if (leaf.shell !== undefined)
1463
- p.shell = leaf.shell;
1464
- if (leaf.subprocessEnv !== undefined)
1465
- p.subprocess_env = leaf.subprocessEnv;
1466
- if (leaf.exitCode !== undefined)
1467
- p.exit_code = leaf.exitCode;
1468
- if (leaf.stdout !== undefined)
1469
- p.stdout = leaf.stdout;
1470
- if (leaf.stderr !== undefined)
1471
- p.stderr = leaf.stderr;
1472
- // Agent detail.
1473
- if (leaf.agentModel !== undefined)
1474
- p.agent_model = leaf.agentModel;
1475
- if (leaf.agentType !== undefined)
1476
- p.agent_type = leaf.agentType;
1477
- if (leaf.agentPolicy !== undefined)
1478
- p.agent_policy = leaf.agentPolicy;
1479
- if (leaf.sessionKey !== undefined)
1480
- p.session_key = leaf.sessionKey;
1481
- if (leaf.agentExitCode !== undefined)
1482
- p.agent_exit_code = leaf.agentExitCode;
1483
- if (leaf.agentResponseText !== undefined)
1484
- p.agent_response_text = leaf.agentResponseText;
1485
- }
1486
- else if (hookFailure) {
1487
- // A node that failed because an injector failed under `fail` policy.
1488
- p.failure_kind = "hook_failure";
1489
- }
1490
- return p;
1491
- };
1492
- // The specific lifecycle event (onNodeStart/Complete/... ); onStateChange is
1493
- // never emitted via this branch — it always carries from/to below.
1494
- if (firesSpecific && name !== "onStateChange")
1495
- await this.hookRunner.emitEvent(name, build(name));
1496
- // onStateChange only fires when the state field actually changed.
1497
- if (firesStateChange && fromState !== state.state) {
1498
- const p = build("onStateChange");
1499
- p.from_state = fromState;
1500
- p.to_state = state.state;
1501
- await this.hookRunner.emitEvent("onStateChange", p);
1502
- }
1503
- }
1504
- /** Fire a run-level event (beforeRun / afterRun). */
1505
- async emitRunEvent(name, scope, enrich) {
1506
- if (!this.hookRunner || !this.hookRunner.hasEvent(name))
1507
- return;
1508
- const payload = basePayload(scope, name);
1509
- enrich(payload);
1510
- await this.hookRunner.emitEvent(name, payload);
1511
- }
1512
- /** Recover the dynamic key dimensions from an expression context. */
1513
- dynamicFromCtx(ctx) {
1514
- const dynamic = {};
1515
- if (ctx.loop?.iter !== undefined)
1516
- dynamic.loopRound = ctx.loop.iter;
1517
- if (ctx.item_id !== undefined)
1518
- dynamic.fanoutItemId = ctx.item_id;
1519
- return dynamic;
1520
- }
1521
- /**
1522
- * Populate parent_node_key/parent_node_kind from the Run's frozen IR. The
1523
- * parent's resolved key shares this node's dynamic prefix, so we map the
1524
- * static parent path onto the dynamic portion of the child key.
1525
- */
1526
- fillParentFields(runId, node, nodeKey, payload) {
1527
- const ir = this.store.readIr(runId);
1528
- if (!ir)
1529
- return;
1530
- const parent = findParentNode(ir.root, node.id);
1531
- if (!parent || parent.id === ir.root.id && parent.kind === "pipeline") {
1532
- // The workflow root is an implicit container; only surface real parents.
1533
- if (!parent || parent.nodePath.length === 0)
1534
- return;
1535
- }
1536
- if (!parent)
1537
- return;
1538
- payload.parent_node_kind = parent.kind;
1539
- // The child static path is parent path + child id; the parent's resolved key
1540
- // is the child key with the trailing static `/childId` segment removed.
1541
- const childStatic = staticNodePathFromKey(nodeKey);
1542
- const childIdSeg = `/${node.id}`;
1543
- if (childStatic.endsWith(childIdSeg)) {
1544
- const idx = nodeKey.lastIndexOf(childIdSeg);
1545
- if (idx > 0)
1546
- payload.parent_node_key = nodeKey.slice(0, idx);
1547
- }
1548
- }
1549
- /** Drop per-node leaf metadata once its terminal events have fired. */
1550
- clearLeafMeta(nodeKey) {
1551
- this.leafMeta.delete(nodeKey);
1552
- }
1553
- }
1554
- function hookAgentTelemetry(state) {
1555
- const telemetry = state.agentTelemetry;
1556
- const attempt = telemetry?.attempts.find((item) => item.attempt === telemetry.currentAttempt)
1557
- ?? telemetry?.attempts[telemetry.attempts.length - 1];
1558
- if (!attempt)
1559
- return undefined;
1560
- const result = {
1561
- attempt: attempt.attempt,
1562
- state: attempt.state,
1563
- updated_at: attempt.updatedAt
1564
- };
1565
- if (attempt.completedAt)
1566
- result.completed_at = attempt.completedAt;
1567
- if (attempt.context) {
1568
- result.context = {
1569
- used: attempt.context.used,
1570
- size: attempt.context.size,
1571
- updated_at: attempt.context.updatedAt
1572
- };
1573
- }
1574
- if (attempt.tokenUsage) {
1575
- result.token_usage = {
1576
- source: attempt.tokenUsage.source
1577
- };
1578
- if (attempt.tokenUsage.inputTokens !== undefined)
1579
- result.token_usage.input_tokens = attempt.tokenUsage.inputTokens;
1580
- if (attempt.tokenUsage.outputTokens !== undefined)
1581
- result.token_usage.output_tokens = attempt.tokenUsage.outputTokens;
1582
- if (attempt.tokenUsage.cachedReadTokens !== undefined)
1583
- result.token_usage.cached_read_tokens = attempt.tokenUsage.cachedReadTokens;
1584
- if (attempt.tokenUsage.cachedWriteTokens !== undefined)
1585
- result.token_usage.cached_write_tokens = attempt.tokenUsage.cachedWriteTokens;
1586
- if (attempt.tokenUsage.thoughtTokens !== undefined)
1587
- result.token_usage.thought_tokens = attempt.tokenUsage.thoughtTokens;
1588
- if (attempt.tokenUsage.totalTokens !== undefined)
1589
- result.token_usage.total_tokens = attempt.tokenUsage.totalTokens;
1590
- }
1591
- return result;
1592
- }
1593
- function primaryOutput(value) {
1594
- return isRecord(value) && "output" in value ? value.output : value;
1595
- }
1596
- function expressionOutputForNode(node, output) {
1597
- if ((node.kind !== "run.agent" && node.kind !== "run.program") || !isRecord(output)) {
1598
- return output;
1599
- }
1600
- const schema = node.metadata.output;
1601
- if (!isRecord(schema) || !("output" in output)) {
1602
- return output;
1603
- }
1604
- return {
1605
- ...output,
1606
- output: projectValueBySchema(output.output, schema)
1607
- };
1608
- }
1609
- function projectValueBySchema(value, schema) {
1610
- if (schema.type === "array") {
1611
- if (!Array.isArray(value) || !isRecord(schema.items))
1612
- return value;
1613
- return value.map((item) => projectValueBySchema(item, schema.items));
1614
- }
1615
- if (schema.type !== "object" || !isRecord(value)) {
1616
- return value;
1617
- }
1618
- if (!isRecord(schema.properties)) {
1619
- return {};
1620
- }
1621
- const projected = {};
1622
- for (const [key, childSchema] of Object.entries(schema.properties)) {
1623
- if (Object.prototype.hasOwnProperty.call(value, key)) {
1624
- projected[key] = isRecord(childSchema) ? projectValueBySchema(value[key], childSchema) : value[key];
1625
- }
1626
- }
1627
- return projected;
1628
- }
1629
- function isRecord(value) {
1630
- return value !== null && typeof value === "object" && !Array.isArray(value);
1631
- }
1632
- /** Generate a locally sortable run ID: yyyyMMddHHmmss + 20 uppercase hex chars. */
1633
- export function generateRunId(now = new Date()) {
1634
- const pad = (n) => String(n).padStart(2, "0");
1635
- const timestamp = [
1636
- now.getFullYear(),
1637
- pad(now.getMonth() + 1),
1638
- pad(now.getDate()),
1639
- pad(now.getHours()),
1640
- pad(now.getMinutes()),
1641
- pad(now.getSeconds())
1642
- ].join("");
1643
- return `${timestamp}${randomBytes(10).toString("hex").toUpperCase()}`;
1644
- }
1645
- /** Find the immediate parent IR node of `childId`, or undefined for the root. */
1646
- function findParentNode(node, childId) {
1647
- for (const child of node.children ?? []) {
1648
- if (child.id === childId)
1649
- return node;
1650
- const found = findParentNode(child, childId);
1651
- if (found)
1652
- return found;
1653
- }
1654
- for (const branch of node.branches ?? []) {
1655
- if (branch.child.id === childId)
1656
- return node;
1657
- const found = findParentNode(branch.child, childId);
1658
- if (found)
1659
- return found;
1660
- }
1661
- return undefined;
1662
- }
1663
- /** Populate composite-container-specific payload fields from node metadata. */
1664
- function fillCompositeFields(node, p) {
1665
- const m = node.metadata;
1666
- switch (node.kind) {
1667
- case "parallel":
1668
- if (typeof m.join === "string")
1669
- p.join_strategy = m.join;
1670
- if (typeof m.max_concurrency === "number")
1671
- p.max_concurrency = m.max_concurrency;
1672
- break;
1673
- case "fanout":
1674
- if (typeof m.join === "string")
1675
- p.join_strategy = m.join;
1676
- if (typeof m.max_concurrency === "number")
1677
- p.max_concurrency = m.max_concurrency;
1678
- break;
1679
- case "loop":
1680
- if (typeof m.max_iterations === "number")
1681
- p.max_iterations = m.max_iterations;
1682
- break;
1683
- case "subworkflow":
1684
- if (typeof m.subworkflow === "string")
1685
- p.subworkflow_spec_path = m.subworkflow;
1686
- break;
1687
- case "run.signal":
1688
- if (typeof m.timeout === "string")
1689
- p.signal_timeout = m.timeout;
1690
- if (typeof m.on_timeout === "string")
1691
- p.signal_on_timeout = m.on_timeout;
1692
- break;
1693
- default:
1694
- break;
1695
- }
1696
- }
1697
- class NodeAbortedError extends Error {
1698
- nodeKey;
1699
- state;
1700
- artifactRefs;
1701
- output;
1702
- constructor(nodeKey, state,
1703
- /** Artifact refs (e.g. partial Agent response/telemetry) to persist on the aborted node. */
1704
- artifactRefs,
1705
- /** Partial output captured before the abort. */
1706
- output) {
1707
- super(`Node ${nodeKey} aborted: ${state}`);
1708
- this.nodeKey = nodeKey;
1709
- this.state = state;
1710
- this.artifactRefs = artifactRefs;
1711
- this.output = output;
1712
- this.name = "NodeAbortedError";
1713
- }
1714
- }
1715
- class ScopeCompleted extends Error {
1716
- output;
1717
- constructor(output) {
1718
- super("Scope completed by guard");
1719
- this.output = output;
1720
- this.name = "ScopeCompleted";
1721
- }
1722
- }
1723
- function errorMessage(error) {
1724
- return error instanceof Error ? error.message : String(error);
1725
- }
1726
- class GuardFailureError extends Error {
1727
- output;
1728
- constructor(message, output) {
1729
- super(message);
1730
- this.output = output;
1731
- this.name = "GuardFailureError";
1732
- }
1733
- }
1734
- /** A non-recoverable leaf failure that still carries artifacts written before failing. */
1735
- class LeafExecutionError extends Error {
1736
- artifactRefs;
1737
- output;
1738
- constructor(message, artifactRefs,
1739
- /** Invalid output captured before the leaf failed validation/execution. */
1740
- output) {
1741
- super(message);
1742
- this.artifactRefs = artifactRefs;
1743
- this.output = output;
1744
- this.name = "LeafExecutionError";
1745
- }
1746
- }
1747
- //# sourceMappingURL=interpreter.js.map