@agentproto/workflow-runtime 0.9.0 → 0.11.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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  import { runTool } from '@agentproto/driver';
2
2
  import { createHash } from 'crypto';
3
+ import { execFile } from 'child_process';
4
+ import { stat, readFile, mkdir, writeFile, appendFile, readdir } from 'fs/promises';
5
+ import { join, dirname, isAbsolute, basename, resolve } from 'path';
6
+ import { assertKnownStepRefs } from '@agentproto/workflow';
7
+ import { isRefinedKind, filterEntriesByAllOf, resolveKnowledge } from '@agentproto/corpus';
3
8
  import { parseWorkflowManifest, workflowFromManifest } from '@agentproto/workflow/manifest';
4
9
 
5
10
  /**
@@ -7,702 +12,1160 @@ import { parseWorkflowManifest, workflowFromManifest } from '@agentproto/workflo
7
12
  * AIP-15 step-walker over @agentproto/driver runTool.
8
13
  */
9
14
 
10
- var WorkflowSuspendedError = class extends Error {
11
- constructor(stepId, on) {
12
- super(
13
- `workflow suspended at step '${stepId}' awaiting [${on.join(", ")}] \u2014 no resume hook supplied`
14
- );
15
- this.stepId = stepId;
16
- this.on = on;
17
- this.name = "WorkflowSuspendedError";
15
+
16
+ // src/build-agent-step.ts
17
+ function buildAgentStep(id, fields) {
18
+ const prompt = fields.prompt;
19
+ return {
20
+ kind: "agent",
21
+ id,
22
+ ...fields.adapter !== void 0 ? { adapter: fields.adapter } : {},
23
+ ...fields.model !== void 0 ? { model: fields.model } : {},
24
+ ...fields.sessionRef !== void 0 ? { sessionRef: fields.sessionRef } : {},
25
+ ...fields.sandbox !== void 0 ? { sandbox: fields.sandbox } : {},
26
+ ...fields.cacheable ? { cacheable: true } : {},
27
+ ...fields.options !== void 0 ? { options: fields.options } : {},
28
+ prompt: typeof prompt === "function" ? prompt : ((_b) => prompt),
29
+ policy: fields.policy ?? { awaiting: "fail" },
30
+ ...fields.outputSchema !== void 0 ? { outputSchema: fields.outputSchema } : {},
31
+ ...fields.maxRetries !== void 0 ? { maxRetries: fields.maxRetries } : {},
32
+ ...fields.harness !== void 0 ? { harness: fields.harness } : {}
33
+ };
34
+ }
35
+
36
+ // src/compile-workflow.ts
37
+ var WorkflowCompileError = class extends Error {
38
+ constructor(message) {
39
+ super(`compileWorkflow: ${message}`);
40
+ this.name = "WorkflowCompileError";
18
41
  }
19
- stepId;
20
- on;
21
42
  };
22
- function view(state, item, index) {
23
- return { input: state.input, steps: state.steps, item, index };
24
- }
25
- function resolveSel(sel, b) {
26
- return typeof sel === "function" ? sel(b) : sel;
43
+ var get = (m, key) => m instanceof Map ? m.get(key) : m[key];
44
+ function dig(value, path) {
45
+ let v = value;
46
+ for (const seg of path) {
47
+ if (v == null) return void 0;
48
+ v = v[seg];
49
+ }
50
+ return v;
27
51
  }
28
- function extractJsonCandidate(raw) {
29
- const jsonFence = /```json\s*([\s\S]*?)```/g;
30
- const lastJson = lastMatch(jsonFence, raw);
31
- if (lastJson !== void 0) return lastJson;
32
- const anyFence = /```\s*([\s\S]*?)```/g;
33
- const lastAny = lastMatch(anyFence, raw);
34
- if (lastAny !== void 0) return lastAny;
35
- return raw.trim();
52
+ var REF_RE = /^\$(input|item|steps)((?:\.[^.]+)*)$/;
53
+ var REF_PREFIX_RE = /^\$(?:input|item|steps|index)((?:\.[^.$/]+)*)/;
54
+ function resolveRef(ref, b) {
55
+ if (ref === "$index") return b.index;
56
+ const m = ref.match(REF_RE);
57
+ if (!m) throw new WorkflowCompileError(`bad reference '${ref}'`);
58
+ const segs = m[2] ? m[2].slice(1).split(".") : [];
59
+ switch (m[1]) {
60
+ case "input":
61
+ return dig(b.input, segs);
62
+ case "item":
63
+ return dig(b.item, segs);
64
+ case "steps": {
65
+ const [stepId, ...rest] = segs;
66
+ if (!stepId) throw new WorkflowCompileError(`'$steps' needs a step id`);
67
+ return dig(b.steps[stepId], rest);
68
+ }
69
+ }
36
70
  }
37
- function lastMatch(re, s) {
38
- let m;
39
- let last = null;
40
- while ((m = re.exec(s)) !== null) last = m;
41
- return last ? last[1]?.trim() : void 0;
71
+ function resolveRefPrefixed(value, b) {
72
+ const m = value.match(REF_PREFIX_RE);
73
+ if (!m) return void 0;
74
+ const token = value.slice(0, m[0].length);
75
+ return { resolved: resolveRef(token, b), rest: value.slice(m[0].length) };
42
76
  }
43
- function spentUsd(state) {
44
- let total = 0;
45
- for (const c of state.costBySession.values()) total += c;
46
- return total;
77
+ function resolveValue(node, b) {
78
+ if (typeof node === "string") {
79
+ if (node.startsWith("$$")) return node.slice(1);
80
+ if (node.startsWith("$")) return resolveRef(node, b);
81
+ return node;
82
+ }
83
+ if (Array.isArray(node)) return node.map((n) => resolveValue(n, b));
84
+ if (node && typeof node === "object") {
85
+ const out = {};
86
+ for (const [k, v] of Object.entries(node))
87
+ out[k] = resolveValue(v, b);
88
+ return out;
89
+ }
90
+ return node;
47
91
  }
48
- function hashResolvedInputs(kind, resolved) {
49
- return createHash("sha256").update(`${kind}\0${JSON.stringify(resolved) ?? "undefined"}`).digest("hex");
92
+ function refPathExists(ref, b) {
93
+ const m = ref.match(REF_RE);
94
+ if (!m) return false;
95
+ const segs = m[2] ? m[2].slice(1).split(".") : [];
96
+ let v;
97
+ if (m[1] === "steps") {
98
+ const [stepId, ...rest] = segs;
99
+ if (!stepId || !(stepId in b.steps)) return false;
100
+ v = b.steps[stepId];
101
+ segs.length = 0;
102
+ segs.push(...rest);
103
+ } else {
104
+ v = m[1] === "input" ? b.input : b.item;
105
+ }
106
+ for (const seg of segs) {
107
+ if (v === null || typeof v !== "object") return false;
108
+ const o = v;
109
+ if (!(seg in o)) return false;
110
+ v = o[seg];
111
+ }
112
+ return true;
50
113
  }
51
- function stepJournalKey(cacheKey, step) {
52
- return `${cacheKey}\0${step.id}\0${step.kind}`;
114
+ function resolveValueStrict(node, b, label) {
115
+ if (typeof node === "string") {
116
+ if (node.startsWith("$$")) return node.slice(1);
117
+ if (node.startsWith("$")) {
118
+ const v = resolveRef(node, b);
119
+ if (v === void 0 && !refPathExists(node, b)) {
120
+ throw new WorkflowCompileError(
121
+ `${label}: reference '${node}' resolves to nothing \u2014 the referenced field does not exist`
122
+ );
123
+ }
124
+ return v;
125
+ }
126
+ return node;
127
+ }
128
+ if (Array.isArray(node)) return node.map((n) => resolveValueStrict(n, b, label));
129
+ if (node && typeof node === "object") {
130
+ const out = {};
131
+ for (const [k, v] of Object.entries(node))
132
+ out[k] = resolveValueStrict(v, b, label);
133
+ return out;
134
+ }
135
+ return node;
53
136
  }
54
- function isCacheEnabled(ctx, step) {
55
- return step.cacheable === true && ctx.cache !== void 0 && ctx.cacheKey !== void 0;
137
+ var COMPARATORS = {
138
+ "<": (a, c) => a < c,
139
+ "<=": (a, c) => a <= c,
140
+ ">": (a, c) => a > c,
141
+ ">=": (a, c) => a >= c
142
+ };
143
+ function literal(token, b) {
144
+ if (token.startsWith("$")) return resolveRef(token, b);
145
+ if (token === "true") return true;
146
+ if (token === "false") return false;
147
+ if (token === "null") return null;
148
+ if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
149
+ return token.replace(/^['"]|['"]$/g, "");
56
150
  }
57
- async function readStepCache(ctx, step, resolvedInputs) {
58
- const key = stepJournalKey(ctx.cacheKey, step);
59
- const hash = hashResolvedInputs(step.kind, resolvedInputs);
60
- const entry = await ctx.cache.get(key);
61
- if (entry !== void 0 && entry.resolvedInputHash === hash) {
62
- return { hit: true, output: entry.output };
151
+ function evalPredicate(expr, b) {
152
+ const m = expr.match(/^(.+?)\s*(==|!=|<=|>=|<|>)\s*(.+)$/);
153
+ if (!m) return Boolean(literal(expr.trim(), b));
154
+ const left = literal(m[1].trim(), b);
155
+ const right = literal(m[3].trim(), b);
156
+ switch (m[2]) {
157
+ case "==":
158
+ return left === right;
159
+ case "!=":
160
+ return left !== right;
161
+ default: {
162
+ const cmp = COMPARATORS[m[2]];
163
+ return cmp(Number(left), Number(right));
164
+ }
63
165
  }
64
- return { hit: false, key, hash };
65
166
  }
66
- function errorMessage(err) {
67
- return err instanceof Error ? err.message : String(err);
167
+ function normalizeToolId(ref) {
168
+ let s = ref.trim();
169
+ if (s.endsWith("/TOOL.md")) s = s.slice(0, -"/TOOL.md".length);
170
+ const slash = s.lastIndexOf("/");
171
+ return slash === -1 ? s : s.slice(slash + 1);
68
172
  }
69
- function formatZodError(err) {
70
- return err.issues.map((i) => `${i.path.length > 0 ? i.path.join(".") + ": " : ""}${i.message}`).join(", ");
173
+ var f = (step, key) => step[key];
174
+ function assertLinear(steps) {
175
+ steps.forEach((s, i) => {
176
+ const next = s.next;
177
+ const followingId = steps[i + 1]?.id;
178
+ if (next && next !== followingId) {
179
+ throw new WorkflowCompileError(
180
+ `step '${s.id}' has a non-linear next='${next}' \u2014 the structured-subset compiler only runs steps in document order. Hand-author a BranchStep or nest structurally.`
181
+ );
182
+ }
183
+ });
71
184
  }
72
- async function runSequence(steps, ctx, item, index) {
73
- let last;
185
+ function collectStepIds(steps, ids = /* @__PURE__ */ new Set()) {
74
186
  for (const s of steps) {
75
- const out = await execStep(s, ctx, item, index);
76
- ctx.state.steps[s.id] = out;
77
- ctx.onStepComplete?.(s.id, out);
78
- last = out;
187
+ if (typeof s?.id === "string") ids.add(s.id);
188
+ if (Array.isArray(s?.steps)) collectStepIds(s.steps, ids);
189
+ if (Array.isArray(s?.branches)) {
190
+ for (const br of s.branches) {
191
+ if (Array.isArray(br?.steps)) collectStepIds(br.steps, ids);
192
+ }
193
+ }
79
194
  }
80
- return last;
195
+ return ids;
81
196
  }
82
- async function execAgentStep(step, ctx, b) {
83
- ctx.onStepStart?.(step.id);
84
- if (step.adapter && ctx.state.maxTotalCostUsd !== void 0 && spentUsd(ctx.state) >= ctx.state.maxTotalCostUsd) {
85
- throw new Error(
86
- `step '${step.id}': budget_exceeded \u2014 run spend $${spentUsd(ctx.state).toFixed(4)} >= cap $${ctx.state.maxTotalCostUsd}`
197
+ function compileWorkflow(handle, opts) {
198
+ const steps = handle.steps;
199
+ if (handle.start && handle.start !== steps[0]?.id) {
200
+ throw new WorkflowCompileError(
201
+ `start='${handle.start}' is not the first step \u2014 structured-subset runs in order`
87
202
  );
88
203
  }
89
- const cwd = step.cwd ? resolveSel(step.cwd, b) : ctx.cwd;
90
- const sandbox = typeof step.sandbox === "function" ? step.sandbox(b) : step.sandbox;
91
- const sessionId = step.adapter ? await ctx.agents.spawn(resolveSel(step.adapter, b), {
92
- cwd,
93
- workspaceSlug: ctx.workspaceSlug,
94
- stepId: step.id,
95
- ...sandbox !== void 0 ? { sandbox } : {},
96
- ...step.options !== void 0 ? { options: step.options } : {}
97
- }) : ctx.agents.resolveByLabel(step.sessionRef);
98
- if (!sessionId) throw new Error(`step '${step.id}': no session (adapter and sessionRef both unresolved)`);
99
- await ctx.agents.sendPromptAndWait(sessionId, step.prompt(b));
100
- if (step.policy && ctx.agents.onAwaitingInput) {
101
- await ctx.agents.onAwaitingInput(sessionId, step.policy);
102
- }
103
- if (ctx.agents.readCostUsd) {
104
- ctx.state.costBySession.set(sessionId, await ctx.agents.readCostUsd(sessionId));
204
+ const ctx = { opts, knownStepIds: collectStepIds(steps) };
205
+ const result = handle.result;
206
+ const output = result !== void 0 ? (b) => resolveValue(result, b) : void 0;
207
+ return {
208
+ id: handle.id,
209
+ description: handle.description,
210
+ steps: compileSiblingsToSteps(steps, ctx),
211
+ ...output ? { output } : {}
212
+ };
213
+ }
214
+ function compileSiblingsToSteps(steps, ctx) {
215
+ assertLinear(steps);
216
+ const out = [];
217
+ for (let i = 0; i < steps.length; i++) {
218
+ const s = steps[i];
219
+ if (s.kind === "branch") {
220
+ out.push(compileBranchChain(steps, i, ctx));
221
+ return out;
222
+ }
223
+ out.push(compileStep(s, ctx));
105
224
  }
106
- if (!step.outputSchema) {
107
- let text;
108
- if (ctx.agents.readFinalMessage) {
109
- try {
110
- text = await ctx.agents.readFinalMessage(sessionId);
111
- } catch {
112
- }
225
+ return out;
226
+ }
227
+ function compileStepList(steps, id, ctx) {
228
+ const compiled = compileSiblingsToSteps(steps, ctx);
229
+ if (compiled.length === 1) return compiled[0];
230
+ return { kind: "group", id, steps: compiled };
231
+ }
232
+ function compileBranchChain(steps, i, ctx) {
233
+ const branchStep = steps[i];
234
+ const branches = f(branchStep, "branches");
235
+ const defaultTarget = f(branchStep, "default");
236
+ const resolveTarget = (targetId, label) => {
237
+ const idx = steps.findIndex((s) => s.id === targetId);
238
+ if (idx === -1 || idx <= i) {
239
+ throw new WorkflowCompileError(
240
+ `branch '${branchStep.id}' ${label} targets '${targetId}', which is not a sibling later in this step list \u2014 the forward-only branch compiler only supports jumping to a later sibling in the SAME list (not backward, and not into a nested map/loop/parallel body). Hand-author a 'loop' step for retry-style control flow instead.`
241
+ );
113
242
  }
114
- return { sessionId, ...text !== void 0 ? { text } : {} };
243
+ return idx;
244
+ };
245
+ const defaultIdx = defaultTarget !== void 0 ? resolveTarget(defaultTarget, "default") : i + 1;
246
+ let otherwise = compileSiblingsToSteps(steps.slice(defaultIdx), ctx);
247
+ for (let k = branches.length - 1; k >= 0; k--) {
248
+ const br = branches[k];
249
+ const targetIdx = resolveTarget(br.next, `branches[${k}].next`);
250
+ const thenSteps = compileSiblingsToSteps(steps.slice(targetIdx), ctx);
251
+ const condExpr = br.when;
252
+ const nodeId = k === 0 ? branchStep.id : `${branchStep.id}__branch${k}`;
253
+ otherwise = [
254
+ {
255
+ kind: "branch",
256
+ id: nodeId,
257
+ cond: (b) => evalPredicate(condExpr, b),
258
+ then: thenSteps,
259
+ ...otherwise.length > 0 ? { otherwise } : {}
260
+ }
261
+ ];
115
262
  }
116
- if (!ctx.agents.readFinalMessage) {
117
- throw new Error(`step '${step.id}': outputSchema requires a host with readFinalMessage`);
263
+ return otherwise[0];
264
+ }
265
+ function compileAgentStep(step, id, ctx) {
266
+ const opts = ctx.opts;
267
+ const prompt = f(step, "prompt");
268
+ if (typeof prompt !== "string" || prompt.length === 0) {
269
+ throw new WorkflowCompileError(`agent step '${id}' needs a non-empty 'prompt'`);
118
270
  }
119
- const maxRetries = step.maxRetries ?? 2;
120
- let lastErr = "";
121
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
122
- const raw = await ctx.agents.readFinalMessage(sessionId);
123
- const candidate = extractJsonCandidate(raw);
124
- let value;
125
- try {
126
- value = JSON.parse(candidate);
127
- } catch {
128
- lastErr = "not valid JSON";
129
- if (attempt < maxRetries) {
130
- await ctx.agents.sendPromptAndWait(
131
- sessionId,
132
- `Your previous reply did not match the required schema: ${lastErr}. Reply again with ONLY a JSON object that matches. No prose, no code fence needed.`
133
- );
134
- }
135
- continue;
271
+ let adapter = typeof step.adapter === "string" ? step.adapter : void 0;
272
+ let options = step.options !== void 0 ? step.options : void 0;
273
+ const agentRef = step.agent?.ref;
274
+ if (agentRef !== void 0) {
275
+ if (typeof agentRef !== "string" || agentRef.trim().length === 0) {
276
+ throw new WorkflowCompileError(`agent step '${id}' has an empty 'agent.ref'`);
136
277
  }
137
- const res = step.outputSchema.safeParse(value);
138
- if (res.success) return { sessionId, output: res.data };
139
- lastErr = formatZodError(res.error);
140
- if (attempt < maxRetries) {
141
- await ctx.agents.sendPromptAndWait(
142
- sessionId,
143
- `Your previous reply did not match the required schema: ${lastErr}. Reply again with ONLY a JSON object that matches. No prose, no code fence needed.`
278
+ const resolved = opts.agentRefs ? get(opts.agentRefs, agentRef) : void 0;
279
+ if (!resolved) {
280
+ const available = opts.agentRefs ? opts.agentRefs instanceof Map ? [...opts.agentRefs.keys()] : Object.keys(opts.agentRefs) : [];
281
+ throw new WorkflowCompileError(
282
+ `agent step '${id}' references unknown agent ref '${agentRef}'` + (available.length > 0 ? ` \u2014 available refs: ${available.join(", ")}` : " \u2014 no agent refs are configured for this compile (not running in an app context?)")
144
283
  );
145
284
  }
285
+ adapter = resolved.adapter;
286
+ options = resolved.options;
146
287
  }
147
- throw new Error(`step '${step.id}': output_invalid \u2014 final message never matched outputSchema (${lastErr})`);
288
+ return buildAgentStep(id, {
289
+ prompt: (b) => String(resolveValue(prompt, b)),
290
+ ...adapter !== void 0 ? { adapter } : {},
291
+ ...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
292
+ ...step.model !== void 0 ? { model: step.model } : {},
293
+ ...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
294
+ ...step.cacheable ? { cacheable: true } : {},
295
+ ...options !== void 0 ? { options } : {},
296
+ policy: step.policy,
297
+ ...step.outputSchema !== void 0 ? { outputSchema: step.outputSchema } : {},
298
+ ...step.maxRetries !== void 0 ? { maxRetries: step.maxRetries } : {},
299
+ ...step.harness !== void 0 ? { harness: step.harness } : {}
300
+ });
148
301
  }
149
- async function execStep(step, ctx, item, index) {
150
- const { state, signal } = ctx;
151
- const b = view(state, item, index);
152
- if (step.kind !== "agent") {
153
- ctx.onStepStart?.(step.id);
302
+ function compileGateStep(step, id) {
303
+ const command = f(step, "command");
304
+ if (typeof command !== "string" || command.trim().length === 0) {
305
+ throw new WorkflowCompileError(`gate step '${id}' needs a non-empty 'command'`);
154
306
  }
307
+ const args = f(step, "args");
308
+ const cwd = f(step, "cwd");
309
+ const reportPath = f(step, "report");
310
+ const timeoutMs = f(step, "timeout_ms");
311
+ const retry = f(step, "retry");
312
+ const onFail = f(
313
+ step,
314
+ "on_fail"
315
+ );
316
+ return {
317
+ kind: "gate",
318
+ id,
319
+ command,
320
+ ...args !== void 0 ? { args } : {},
321
+ ...cwd !== void 0 ? { cwd } : {},
322
+ ...reportPath !== void 0 ? { reportPath } : {},
323
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
324
+ ...retry !== void 0 ? {
325
+ retry: {
326
+ maxAttempts: retry.max_attempts,
327
+ backoff: retry.backoff,
328
+ ...retry.initial_ms !== void 0 ? { initialMs: retry.initial_ms } : {}
329
+ }
330
+ } : {},
331
+ ...onFail !== void 0 ? { onFail: { reprompt: onFail.reprompt, ...onFail.with !== void 0 ? { with: onFail.with } : {} } } : {}
332
+ };
333
+ }
334
+ function compileStep(step, ctx) {
335
+ const opts = ctx.opts;
336
+ const id = step.id;
155
337
  switch (step.kind) {
156
338
  case "tool": {
157
- const input = step.input(b);
158
- const runIt = () => runTool({
159
- tool: step.tool,
160
- candidates: step.candidates,
161
- input,
162
- context: step.context ? step.context(b) : void 0,
163
- resolverContext: step.resolverContext,
164
- secrets: step.secrets,
165
- signal
339
+ const ref = typeof step.tool === "string" ? step.tool : step.tool.entry;
340
+ const toolId = normalizeToolId(ref);
341
+ const tool = get(opts.tools, toolId);
342
+ if (!tool)
343
+ throw new WorkflowCompileError(`no tool registered for '${toolId}'`);
344
+ const inputs = f(step, "inputs") ?? {};
345
+ assertKnownStepRefs(inputs, ctx.knownStepIds, `tool step '${id}' inputs`, {
346
+ makeError: (message) => new WorkflowCompileError(message)
166
347
  });
167
- if (!isCacheEnabled(ctx, step)) return runIt();
168
- const c = await readStepCache(ctx, step, input);
169
- if (c.hit) return c.output;
170
- const out = await runIt();
171
- await ctx.cache.set(c.key, { output: out, resolvedInputHash: c.hash });
172
- return out;
173
- }
174
- case "transform":
175
- return step.compute(b);
176
- case "map": {
177
- const arr = [...step.over(b)];
178
- const parallelism = Math.max(1, step.parallelism ?? 1);
179
- const tolerant = step.onError === "collect";
180
- const results = new Array(arr.length);
181
- for (let i = 0; i < arr.length; i += parallelism) {
182
- const chunk = arr.slice(i, i + parallelism);
183
- if (!tolerant) {
184
- const outs = await Promise.all(
185
- chunk.map((el, j) => {
186
- const idx = i + j;
187
- const inner = step.body(el, idx, view(state, el, idx));
188
- return execStep(inner, ctx, el, idx);
189
- })
190
- );
191
- for (let j = 0; j < outs.length; j++) results[i + j] = outs[j];
192
- continue;
193
- }
194
- const settled = await Promise.allSettled(
195
- chunk.map((el, j) => {
196
- const idx = i + j;
197
- const inner = step.body(el, idx, view(state, el, idx));
198
- return execStep(inner, ctx, el, idx);
199
- })
200
- );
201
- settled.forEach((s, j) => {
202
- const idx = i + j;
203
- results[idx] = s.status === "fulfilled" ? { status: "fulfilled", index: idx, value: s.value } : { status: "rejected", index: idx, item: chunk[j], error: errorMessage(s.reason) };
204
- });
205
- }
206
- if (!tolerant) return results;
207
- const outcomes = results;
208
348
  return {
209
- results: outcomes,
210
- succeeded: outcomes.filter((r) => r.status === "fulfilled").length,
211
- failed: outcomes.filter((r) => r.status === "rejected").length
349
+ kind: "tool",
350
+ id,
351
+ tool,
352
+ candidates: opts.candidates,
353
+ input: (b) => resolveValue(inputs, b),
354
+ context: opts.contextFor ? (b) => opts.contextFor(toolId, b) : void 0
212
355
  };
213
356
  }
214
- case "pipeline": {
215
- const items = [...step.over(b)];
216
- const tolerant = step.onError === "collect";
217
- if (items.length === 0) return tolerant ? { results: [], succeeded: 0, failed: 0 } : [];
218
- const cap = Math.max(1, step.concurrency ?? items.length);
219
- const results = new Array(items.length);
220
- let next = 0;
221
- const runItem = async (idx) => {
222
- let prev = void 0;
223
- try {
224
- for (const stage of step.stages) {
225
- prev = await execStep(stage(items[idx], idx, prev, view(state, items[idx], idx)), ctx, items[idx], idx);
226
- }
227
- results[idx] = tolerant ? { status: "fulfilled", index: idx, value: prev } : prev;
228
- } catch (err) {
229
- if (!tolerant) throw err;
230
- results[idx] = { status: "rejected", index: idx, item: items[idx], error: errorMessage(err) };
231
- }
232
- };
233
- const worker = async () => {
234
- while (next < items.length) {
235
- const idx = next++;
236
- await runItem(idx);
237
- }
238
- };
239
- await Promise.all(Array.from({ length: Math.min(cap, items.length) }, () => worker()));
240
- if (!tolerant) return results;
241
- const outcomes = results;
357
+ case "map": {
358
+ const over = f(step, "over");
359
+ const inner = compileStepList(f(step, "steps"), `${id}__body`, ctx);
360
+ const onError = f(step, "onError");
242
361
  return {
243
- results: outcomes,
244
- succeeded: outcomes.filter((r) => r.status === "fulfilled").length,
245
- failed: outcomes.filter((r) => r.status === "rejected").length
362
+ kind: "map",
363
+ id,
364
+ parallelism: f(step, "parallelism"),
365
+ over: (b) => resolveRef(over, b),
366
+ body: () => inner,
367
+ ...onError !== void 0 ? { onError } : {}
246
368
  };
247
369
  }
248
- case "branch": {
249
- const chosen = step.cond(b) ? step.then : step.otherwise ?? [];
250
- return runSequence(chosen, ctx, item, index);
251
- }
252
370
  case "loop": {
253
- let iterations = 0;
254
- let last;
255
- while (iterations < step.maxIterations && step.while(view(state, item, index))) {
256
- last = await runSequence(step.body, ctx, item, index);
257
- iterations++;
258
- }
259
- return last;
260
- }
261
- case "parallel": {
262
- const outs = await Promise.all(
263
- step.branches.map(
264
- (br) => runSequence(br.steps, ctx, item, index).then((last) => ({
265
- id: br.id,
266
- last
267
- }))
268
- )
269
- );
270
- const record = {};
271
- for (const { id, last } of outs) record[id] = last;
272
- return record;
371
+ const whileExpr = f(step, "while");
372
+ return {
373
+ kind: "loop",
374
+ id,
375
+ maxIterations: f(step, "max_iterations"),
376
+ while: (b) => evalPredicate(whileExpr, b),
377
+ body: [compileStepList(f(step, "steps"), `${id}__body`, ctx)]
378
+ };
379
+ }
380
+ case "parallel": {
381
+ const branches = f(step, "branches");
382
+ return {
383
+ kind: "parallel",
384
+ id,
385
+ branches: branches.map((br) => ({
386
+ id: br.id,
387
+ steps: [compileStepList(br.steps, `${id}__${br.id}`, ctx)]
388
+ }))
389
+ };
273
390
  }
274
391
  case "approval": {
275
- const prompt = step.prompt(b);
276
- const approvers = step.approvers ?? [];
277
- const approved = ctx.approve ? await ctx.approve({ stepId: step.id, prompt, approvers }) : true;
278
- const followups = approved ? step.onApprove ?? [] : step.onReject ?? [];
279
- await runSequence(followups, ctx, item, index);
280
- return { approved };
392
+ const prompt = f(step, "prompt");
393
+ const approvers = (f(step, "approvers") ?? []).map(
394
+ (a) => a.role ?? a.userId ?? String(a)
395
+ );
396
+ const artifacts = f(step, "artifacts");
397
+ const timeoutMs = f(step, "timeout_ms");
398
+ return {
399
+ kind: "approval",
400
+ id,
401
+ approvers,
402
+ ...artifacts !== void 0 ? { artifacts } : {},
403
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
404
+ prompt: (b) => String(resolveValue(prompt, b))
405
+ };
281
406
  }
282
407
  case "suspend": {
283
- if (!ctx.resume) throw new WorkflowSuspendedError(step.id, step.on);
284
- return ctx.resume({ stepId: step.id, on: step.on });
408
+ const resume = f(step, "resume");
409
+ return { kind: "suspend", id, on: resume.on, timeoutMs: resume.timeout_ms };
285
410
  }
286
- case "group":
287
- return runSequence(step.steps, ctx, item, index);
288
411
  case "subworkflow": {
289
- const childInput = step.input ? step.input(b) : state.input;
290
- const child = await runWorkflowInner(step.workflow, childInput, {
291
- approve: ctx.approve,
292
- resume: ctx.resume,
293
- signal,
294
- agents: ctx.agents,
295
- cwd: ctx.cwd,
296
- workspaceSlug: ctx.workspaceSlug,
297
- cache: ctx.cache,
298
- cacheKey: ctx.cacheKey
412
+ const wfId = f(step, "workflow");
413
+ const child = opts.workflows ? get(opts.workflows, wfId) : void 0;
414
+ if (!child)
415
+ throw new WorkflowCompileError(
416
+ `subworkflow '${wfId}' not found \u2014 pass it in opts.workflows`
417
+ );
418
+ const compiledChild = compileWorkflow(child, opts);
419
+ const inputs = f(step, "inputs");
420
+ if (inputs === void 0) {
421
+ return { kind: "subworkflow", id, workflow: compiledChild };
422
+ }
423
+ assertKnownStepRefs(inputs, ctx.knownStepIds, `subworkflow step '${id}' inputs`, {
424
+ makeError: (message) => new WorkflowCompileError(message)
299
425
  });
300
- return child.output;
426
+ const entries = Object.entries(inputs);
427
+ return {
428
+ kind: "subworkflow",
429
+ id,
430
+ workflow: compiledChild,
431
+ // Each top-level key resolves strictly: a missing referenced field
432
+ // throws at run time naming this step + key (no silent `undefined`).
433
+ input: (b) => {
434
+ const out = {};
435
+ for (const [k, v] of entries)
436
+ out[k] = resolveValueStrict(v, b, `subworkflow step '${id}' input key '${k}'`);
437
+ return out;
438
+ }
439
+ };
301
440
  }
302
441
  case "agent": {
303
- if (!ctx.agents) throw new Error(`step '${step.id}': AgentStep requires a host agents implementation`);
304
- if (!isCacheEnabled(ctx, step)) return execAgentStep(step, ctx, b);
305
- const resolved = {
306
- prompt: step.prompt(b),
307
- adapter: step.adapter ? resolveSel(step.adapter, b) : void 0,
308
- sessionRef: step.sessionRef
309
- };
310
- const c = await readStepCache(ctx, step, resolved);
311
- if (c.hit) return c.output;
312
- const out = await execAgentStep(step, ctx, b);
313
- await ctx.cache.set(c.key, { output: out, resolvedInputHash: c.hash });
314
- return out;
442
+ if (typeof step.prompt === "function") {
443
+ const passthrough = step;
444
+ return passthrough;
445
+ }
446
+ return compileAgentStep(step, id, ctx);
447
+ }
448
+ case "gate":
449
+ return compileGateStep(step, id);
450
+ case "transform": {
451
+ const passthrough = step;
452
+ return passthrough;
315
453
  }
454
+ case "branch":
455
+ throw new WorkflowCompileError(
456
+ `internal: branch step '${id}' reached compileStep directly \u2014 branch steps must be compiled via the sibling-list walker`
457
+ );
458
+ default:
459
+ throw new WorkflowCompileError(
460
+ `unsupported step kind '${step.kind}' (step '${id}')`
461
+ );
316
462
  }
317
463
  }
318
- async function runWorkflowInner(workflow, input, hooks, maxTotalCostUsd) {
319
- const state = { input, steps: {}, costBySession: /* @__PURE__ */ new Map(), maxTotalCostUsd };
320
- const ctx = { state, ...hooks };
321
- let lastId;
322
- for (const step of workflow.steps) {
323
- const out = await execStep(step, ctx, void 0, void 0);
324
- state.steps[step.id] = out;
325
- ctx.onStepComplete?.(step.id, out);
326
- lastId = step.id;
464
+
465
+ // src/ref-string.ts
466
+ function resolveRefString(stepId, field, value, b, onUnmatched = "passthrough") {
467
+ if (value.startsWith("$$")) return value.slice(1);
468
+ let hit;
469
+ try {
470
+ hit = resolveRefPrefixed(value, b);
471
+ } catch (err) {
472
+ throw new Error(
473
+ `step '${stepId}': ${field} '${value}' is not a valid reference \u2014 ${err instanceof Error ? err.message : String(err)}`
474
+ );
327
475
  }
328
- const bindings = view(state);
329
- const output = workflow.output ? workflow.output(bindings) : lastId !== void 0 ? state.steps[lastId] : void 0;
330
- return { output, bindings };
476
+ if (!hit) {
477
+ if (onUnmatched === "error" && value.startsWith("$")) {
478
+ throw new Error(
479
+ `step '${stepId}': ${field} '${value}' is not a valid reference \u2014 a leading '$' must open a $input/$item/$steps.<id>/$index reference ('$$' escapes a literal '$')`
480
+ );
481
+ }
482
+ return value;
483
+ }
484
+ const token = value.slice(0, value.length - hit.rest.length);
485
+ if (hit.resolved === void 0) {
486
+ throw new Error(
487
+ `step '${stepId}': ${field} '${token}' resolves to nothing \u2014 the referenced field does not exist`
488
+ );
489
+ }
490
+ return String(hit.resolved) + hit.rest;
331
491
  }
332
- async function runWorkflow(args) {
333
- return runWorkflowInner(args.workflow, args.input, {
334
- approve: args.approve,
335
- resume: args.resume,
336
- signal: args.signal,
337
- agents: args.agents,
338
- cwd: args.cwd,
339
- workspaceSlug: args.workspaceSlug,
340
- cache: args.cache,
341
- cacheKey: args.cacheKey,
342
- onStepStart: args.onStepStart,
343
- onStepComplete: args.onStepComplete
344
- }, args.maxTotalCostUsd);
492
+ var NodeFsPort = class {
493
+ root;
494
+ constructor(root) {
495
+ this.root = root;
496
+ }
497
+ resolve(p) {
498
+ const normalized = p.replace(/^\/+/, "");
499
+ if (normalized.startsWith("..")) {
500
+ throw new Error(`NodeFsPort: path "${p}" escapes the workspace root "${this.root}"`);
501
+ }
502
+ return join(this.root, normalized);
503
+ }
504
+ async exists(p) {
505
+ try {
506
+ await stat(this.resolve(p));
507
+ return true;
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ async readFile(p) {
513
+ return readFile(this.resolve(p), "utf8");
514
+ }
515
+ async writeFile(p, content) {
516
+ const target = this.resolve(p);
517
+ await mkdir(dirname(target), { recursive: true });
518
+ await writeFile(target, content, "utf8");
519
+ }
520
+ async appendFile(p, content) {
521
+ const target = this.resolve(p);
522
+ await mkdir(dirname(target), { recursive: true });
523
+ await appendFile(target, content, "utf8");
524
+ }
525
+ async readdir(p) {
526
+ return readdir(this.resolve(p));
527
+ }
528
+ async walk(p) {
529
+ const out = [];
530
+ const visit = async (dir) => {
531
+ let entries = [];
532
+ try {
533
+ entries = await readdir(dir, { withFileTypes: true });
534
+ } catch {
535
+ return;
536
+ }
537
+ for (const ent of entries) {
538
+ if (ent.name.startsWith(".")) continue;
539
+ const full = join(dir, ent.name);
540
+ if (ent.isDirectory()) {
541
+ await visit(full);
542
+ } else if (ent.isFile()) {
543
+ out.push(full);
544
+ }
545
+ }
546
+ };
547
+ await visit(this.resolve(p));
548
+ const prefix = this.resolve(p);
549
+ return out.map((f2) => f2.slice(prefix.length).replace(/^\//, ""));
550
+ }
551
+ async stat(p) {
552
+ try {
553
+ const s = await stat(this.resolve(p));
554
+ return {
555
+ kind: s.isDirectory() ? "directory" : "file",
556
+ ...s.isFile() ? { bytes: s.size } : {},
557
+ modifiedAt: s.mtime
558
+ };
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+ async lock() {
564
+ return { release: async () => {
565
+ } };
566
+ }
567
+ };
568
+
569
+ // src/knowledge.ts
570
+ var DEFAULT_MAX_ENTRIES = 50;
571
+ function resolveStringArray(stepId, index, field, values, b) {
572
+ if (values === void 0) return void 0;
573
+ return values.map(
574
+ (v) => resolveRefString(stepId, `harness.knowledge[${index}].${field}`, v, b)
575
+ );
576
+ }
577
+ function resolveKnowledgeSelectors(stepId, selectors, b) {
578
+ return selectors.map((sel, index) => {
579
+ const deferred = sel.deferred === true;
580
+ const workspace = deferred ? resolveRefString(stepId, `harness.knowledge[${index}].workspace`, sel.workspace, b) : sel.workspace;
581
+ return {
582
+ ...sel,
583
+ deferred: void 0,
584
+ workspace,
585
+ ...sel.anyOf !== void 0 ? { anyOf: deferred ? resolveStringArray(stepId, index, "anyOf", sel.anyOf, b) : [...sel.anyOf] } : {},
586
+ ...sel.allOf !== void 0 ? { allOf: deferred ? resolveStringArray(stepId, index, "allOf", sel.allOf, b) : [...sel.allOf] } : {},
587
+ ...sel.kinds !== void 0 ? { kinds: deferred ? resolveStringArray(stepId, index, "kinds", sel.kinds, b) : [...sel.kinds] } : {}
588
+ };
589
+ });
590
+ }
591
+ function workspaceDirName(workspace) {
592
+ const trimmed = workspace.replace(/\/+$/, "");
593
+ return basename(trimmed) || trimmed;
345
594
  }
595
+ function safeSlug(slug) {
596
+ return slug.replace(/[^a-zA-Z0-9._-]+/g, "-");
597
+ }
598
+ async function materializeKnowledge(stepId, selectors, stepCwd) {
599
+ const knowledgeDir = join(stepCwd, ".knowledge");
600
+ const records = [];
601
+ const warnings = [];
602
+ const indexSections = [];
603
+ let written = 0;
604
+ for (const [i, sel] of selectors.entries()) {
605
+ if (sel.mode !== void 0 && sel.mode !== "files") {
606
+ throw new Error(
607
+ `step '${stepId}': harness.knowledge[${i}].mode must be "files" (v1 supports no other mode)`
608
+ );
609
+ }
610
+ const workspace = sel.workspace;
611
+ if (typeof workspace !== "string" || workspace.length === 0) {
612
+ throw new Error(
613
+ `step '${stepId}': harness.knowledge[${i}] needs a non-empty 'workspace'`
614
+ );
615
+ }
616
+ if (!isAbsolute(workspace)) {
617
+ throw new Error(
618
+ `step '${stepId}': harness.knowledge[${i}].workspace '${workspace}' must be absolute by run time \u2014 the loader resolves relative paths; did you bypass loadWorkflowHandle?`
619
+ );
620
+ }
621
+ const fs = new NodeFsPort(workspace);
622
+ const rootStat = await fs.stat(".");
623
+ if (rootStat === null || rootStat.kind !== "directory") {
624
+ const warning = `knowledge-workspace-missing: harness.knowledge[${i}] (workspace '${workspace}') does not name an existing directory`;
625
+ warnings.push(warning);
626
+ records.push({ workspace, matched: 0, written: 0 });
627
+ continue;
628
+ }
629
+ const kinds = (sel.kinds ?? []).filter((k) => isRefinedKind(k));
630
+ const resolved = filterEntriesByAllOf(
631
+ await resolveKnowledge({
632
+ fs,
633
+ query: {
634
+ ...sel.anyOf && sel.anyOf.length > 0 ? { tags: [...sel.anyOf] } : {},
635
+ ...kinds.length > 0 ? { kinds } : {}
636
+ }
637
+ }),
638
+ sel.allOf
639
+ );
640
+ const sorted = [...resolved].sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
641
+ const cap = sel.maxEntries ?? DEFAULT_MAX_ENTRIES;
642
+ const picked = sorted.slice(0, cap);
643
+ const dirName = workspaceDirName(workspace);
644
+ const lines = [];
645
+ for (const entry of picked) {
646
+ const rel = join(dirName, `${safeSlug(entry.slug)}.md`);
647
+ const raw = await fs.readFile(entry.path);
648
+ await mkdir(join(knowledgeDir, dirName), { recursive: true });
649
+ await writeFile(join(knowledgeDir, rel), raw, "utf8");
650
+ lines.push(
651
+ `- [${entry.title || entry.slug}](${rel.split("\\").join("/")}) \u2014 ${entry.kind}, ${entry.tags.join(", ")}`
652
+ );
653
+ }
654
+ written += picked.length;
655
+ if (lines.length > 0) {
656
+ indexSections.push(`## ${dirName}
346
657
 
347
- // src/build-agent-step.ts
348
- function buildAgentStep(id, fields) {
349
- const prompt = fields.prompt;
350
- return {
351
- kind: "agent",
352
- id,
353
- ...fields.adapter !== void 0 ? { adapter: fields.adapter } : {},
354
- ...fields.sessionRef !== void 0 ? { sessionRef: fields.sessionRef } : {},
355
- ...fields.sandbox !== void 0 ? { sandbox: fields.sandbox } : {},
356
- ...fields.cacheable ? { cacheable: true } : {},
357
- ...fields.options !== void 0 ? { options: fields.options } : {},
358
- prompt: typeof prompt === "function" ? prompt : ((_b) => prompt),
359
- policy: fields.policy ?? { awaiting: "fail" },
360
- ...fields.outputSchema !== void 0 ? { outputSchema: fields.outputSchema } : {},
361
- ...fields.maxRetries !== void 0 ? { maxRetries: fields.maxRetries } : {}
362
- };
658
+ ${lines.join("\n")}`);
659
+ }
660
+ records.push({ workspace, matched: resolved.length, written: picked.length });
661
+ if (resolved.length === 0) {
662
+ warnings.push(
663
+ `knowledge-empty: harness.knowledge[${i}] (workspace '${workspace}') matched 0 entries`
664
+ );
665
+ }
666
+ }
667
+ const index = "# Knowledge index\n\n" + (indexSections.length > 0 ? indexSections.join("\n\n") : "_No entries matched._") + "\n";
668
+ await mkdir(knowledgeDir, { recursive: true });
669
+ await writeFile(join(knowledgeDir, "INDEX.md"), index, "utf8");
670
+ return { records, written, warnings };
363
671
  }
364
672
 
365
- // src/compile-workflow.ts
366
- var WorkflowCompileError = class extends Error {
367
- constructor(message) {
368
- super(`compileWorkflow: ${message}`);
369
- this.name = "WorkflowCompileError";
673
+ // src/run-workflow.ts
674
+ var WorkflowSuspendedError = class extends Error {
675
+ constructor(stepId, on) {
676
+ super(
677
+ `workflow suspended at step '${stepId}' awaiting [${on.join(", ")}] \u2014 no resume hook supplied`
678
+ );
679
+ this.stepId = stepId;
680
+ this.on = on;
681
+ this.name = "WorkflowSuspendedError";
370
682
  }
683
+ stepId;
684
+ on;
371
685
  };
372
- var get = (m, key) => m instanceof Map ? m.get(key) : m[key];
373
- function dig(value, path) {
374
- let v = value;
375
- for (const seg of path) {
376
- if (v == null) return void 0;
377
- v = v[seg];
378
- }
379
- return v;
686
+ function view(state, item, index) {
687
+ return { input: state.input, steps: state.steps, item, index };
380
688
  }
381
- function resolveRef(ref, b) {
382
- if (ref === "$index") return b.index;
383
- const m = ref.match(/^\$(input|item|steps)((?:\.[^.]+)*)$/);
384
- if (!m) throw new WorkflowCompileError(`bad reference '${ref}'`);
385
- const segs = m[2] ? m[2].slice(1).split(".") : [];
386
- switch (m[1]) {
387
- case "input":
388
- return dig(b.input, segs);
389
- case "item":
390
- return dig(b.item, segs);
391
- case "steps": {
392
- const [stepId, ...rest] = segs;
393
- if (!stepId) throw new WorkflowCompileError(`'$steps' needs a step id`);
394
- return dig(b.steps[stepId], rest);
395
- }
396
- }
689
+ function resolveSel(sel, b) {
690
+ return typeof sel === "function" ? sel(b) : sel;
397
691
  }
398
- function resolveValue(node, b) {
399
- if (typeof node === "string") {
400
- if (node.startsWith("$$")) return node.slice(1);
401
- if (node.startsWith("$")) return resolveRef(node, b);
402
- return node;
403
- }
404
- if (Array.isArray(node)) return node.map((n) => resolveValue(n, b));
405
- if (node && typeof node === "object") {
406
- const out = {};
407
- for (const [k, v] of Object.entries(node))
408
- out[k] = resolveValue(v, b);
409
- return out;
410
- }
411
- return node;
692
+ function extractJsonCandidate(raw) {
693
+ const jsonFence = /```json\s*([\s\S]*?)```/g;
694
+ const lastJson = lastMatch(jsonFence, raw);
695
+ if (lastJson !== void 0) return lastJson;
696
+ const anyFence = /```\s*([\s\S]*?)```/g;
697
+ const lastAny = lastMatch(anyFence, raw);
698
+ if (lastAny !== void 0) return lastAny;
699
+ return raw.trim();
700
+ }
701
+ function lastMatch(re, s) {
702
+ let m;
703
+ let last = null;
704
+ while ((m = re.exec(s)) !== null) last = m;
705
+ return last ? last[1]?.trim() : void 0;
706
+ }
707
+ function spentUsd(state) {
708
+ let total = 0;
709
+ for (const c of state.costBySession.values()) total += c;
710
+ return total;
412
711
  }
413
- var COMPARATORS = {
414
- "<": (a, c) => a < c,
415
- "<=": (a, c) => a <= c,
416
- ">": (a, c) => a > c,
417
- ">=": (a, c) => a >= c
418
- };
419
- function literal(token, b) {
420
- if (token.startsWith("$")) return resolveRef(token, b);
421
- if (token === "true") return true;
422
- if (token === "false") return false;
423
- if (token === "null") return null;
424
- if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
425
- return token.replace(/^['"]|['"]$/g, "");
712
+ function hashResolvedInputs(kind, resolved) {
713
+ return createHash("sha256").update(`${kind}\0${JSON.stringify(resolved) ?? "undefined"}`).digest("hex");
426
714
  }
427
- function evalPredicate(expr, b) {
428
- const m = expr.match(/^(.+?)\s*(==|!=|<=|>=|<|>)\s*(.+)$/);
429
- if (!m) return Boolean(literal(expr.trim(), b));
430
- const left = literal(m[1].trim(), b);
431
- const right = literal(m[3].trim(), b);
432
- switch (m[2]) {
433
- case "==":
434
- return left === right;
435
- case "!=":
436
- return left !== right;
437
- default: {
438
- const cmp = COMPARATORS[m[2]];
439
- return cmp(Number(left), Number(right));
440
- }
715
+ function stepJournalKey(cacheKey, step) {
716
+ return `${cacheKey}\0${step.id}\0${step.kind}`;
717
+ }
718
+ function isCacheEnabled(ctx, step) {
719
+ return step.cacheable === true && ctx.cache !== void 0 && ctx.cacheKey !== void 0;
720
+ }
721
+ async function readStepCache(ctx, step, resolvedInputs) {
722
+ const key = stepJournalKey(ctx.cacheKey, step);
723
+ const hash = hashResolvedInputs(step.kind, resolvedInputs);
724
+ const entry = await ctx.cache.get(key);
725
+ if (entry !== void 0 && entry.resolvedInputHash === hash) {
726
+ return { hit: true, output: entry.output };
441
727
  }
728
+ return { hit: false, key, hash };
442
729
  }
443
- function normalizeToolId(ref) {
444
- let s = ref.trim();
445
- if (s.endsWith("/TOOL.md")) s = s.slice(0, -"/TOOL.md".length);
446
- const slash = s.lastIndexOf("/");
447
- return slash === -1 ? s : s.slice(slash + 1);
730
+ function errorMessage(err) {
731
+ return err instanceof Error ? err.message : String(err);
448
732
  }
449
- var f = (step, key) => step[key];
450
- function assertLinear(steps) {
451
- steps.forEach((s, i) => {
452
- const next = s.next;
453
- const followingId = steps[i + 1]?.id;
454
- if (next && next !== followingId) {
455
- throw new WorkflowCompileError(
456
- `step '${s.id}' has a non-linear next='${next}' \u2014 the structured-subset compiler only runs steps in document order. Hand-author a BranchStep or nest structurally.`
457
- );
458
- }
459
- });
733
+ function formatZodError(err) {
734
+ return err.issues.map((i) => `${i.path.length > 0 ? i.path.join(".") + ": " : ""}${i.message}`).join(", ");
460
735
  }
461
- function collectStepIds(steps, ids = /* @__PURE__ */ new Set()) {
736
+ async function runSequence(steps, ctx, item, index) {
737
+ let last;
462
738
  for (const s of steps) {
463
- if (typeof s?.id === "string") ids.add(s.id);
464
- if (Array.isArray(s?.steps)) collectStepIds(s.steps, ids);
465
- if (Array.isArray(s?.branches)) {
466
- for (const br of s.branches) {
467
- if (Array.isArray(br?.steps)) collectStepIds(br.steps, ids);
468
- }
469
- }
739
+ const out = await execStep(s, ctx, item, index);
740
+ ctx.state.steps[s.id] = out;
741
+ ctx.onStepComplete?.(s.id, out);
742
+ last = out;
470
743
  }
471
- return ids;
744
+ return last;
472
745
  }
473
- function assertKnownStepRefs(node, knownStepIds, label) {
474
- if (typeof node === "string") {
475
- if (node.startsWith("$$")) return;
476
- const m = node.match(/^\$steps\.([^.]+)/);
477
- if (m && !knownStepIds.has(m[1])) {
478
- throw new WorkflowCompileError(
479
- `${label} references unknown step '${m[1]}' via '${node}' \u2014 no step with that id exists in this workflow`
746
+ async function execAgentStep(step, ctx, b) {
747
+ ctx.onStepStart?.(step.id);
748
+ if (step.adapter && ctx.state.maxTotalCostUsd !== void 0 && spentUsd(ctx.state) >= ctx.state.maxTotalCostUsd) {
749
+ throw new Error(
750
+ `step '${step.id}': budget_exceeded \u2014 run spend $${spentUsd(ctx.state).toFixed(4)} >= cap $${ctx.state.maxTotalCostUsd}`
751
+ );
752
+ }
753
+ const cwd = step.harness?.cwd ?? (step.cwd ? resolveSel(step.cwd, b) : ctx.cwd);
754
+ let knowledgeOut;
755
+ let knowledgeWarnings = [];
756
+ if (step.harness?.knowledge && step.harness.knowledge.length > 0) {
757
+ if (cwd === void 0) {
758
+ throw new Error(
759
+ `step '${step.id}': harness.knowledge requires a resolvable working directory (set step cwd, harness.cwd, or the run cwd)`
480
760
  );
481
761
  }
482
- return;
762
+ const knowledgeSelectors = resolveKnowledgeSelectors(step.id, step.harness.knowledge, b).map(
763
+ (sel) => ({
764
+ ...sel,
765
+ workspace: isAbsolute(sel.workspace) ? sel.workspace : join(cwd, sel.workspace)
766
+ })
767
+ );
768
+ const materialized = await materializeKnowledge(step.id, knowledgeSelectors, cwd);
769
+ knowledgeOut = materialized.records;
770
+ knowledgeWarnings = materialized.warnings;
771
+ if (materialized.written > 0) {
772
+ const note = `Knowledge for this step is materialized under .knowledge/ (see .knowledge/INDEX.md, ${materialized.written} entries).`;
773
+ const inner = step.prompt;
774
+ step = { ...step, prompt: (b2) => `${note}
775
+
776
+ ${inner(b2)}` };
777
+ }
483
778
  }
484
- if (Array.isArray(node)) {
485
- node.forEach((n) => assertKnownStepRefs(n, knownStepIds, label));
486
- return;
779
+ const sandbox = typeof step.sandbox === "function" ? step.sandbox(b) : step.sandbox;
780
+ const model = step.model !== void 0 ? resolveSel(step.model, b) : void 0;
781
+ const harness = step.harness !== void 0 || model !== void 0 ? {
782
+ ...step.harness ?? {},
783
+ ...model !== void 0 && step.harness?.model === void 0 ? { model } : {}
784
+ } : void 0;
785
+ const sessionId = step.adapter ? await ctx.agents.spawn(resolveSel(step.adapter, b), {
786
+ cwd,
787
+ workspaceSlug: ctx.workspaceSlug,
788
+ stepId: step.id,
789
+ ...sandbox !== void 0 ? { sandbox } : {},
790
+ ...step.options !== void 0 ? { options: step.options } : {},
791
+ ...harness !== void 0 ? { harness } : {}
792
+ }) : ctx.agents.resolveByLabel(step.sessionRef);
793
+ if (!sessionId) throw new Error(`step '${step.id}': no session (adapter and sessionRef both unresolved)`);
794
+ if (knowledgeWarnings.length > 0 && ctx.agents.emitHarnessWarning) {
795
+ ctx.agents.emitHarnessWarning({
796
+ sessionId,
797
+ warnings: knowledgeWarnings,
798
+ label: step.id
799
+ });
487
800
  }
488
- if (node && typeof node === "object") {
489
- for (const v of Object.values(node)) assertKnownStepRefs(v, knownStepIds, label);
801
+ const harnessOut = harness !== void 0 ? {
802
+ ...harness,
803
+ ...harness.tools && harness.tools.length > 0 ? { toolsApplied: false } : {}
804
+ } : void 0;
805
+ await ctx.agents.sendPromptAndWait(sessionId, step.prompt(b));
806
+ if (step.policy && ctx.agents.onAwaitingInput) {
807
+ await ctx.agents.onAwaitingInput(sessionId, step.policy);
490
808
  }
491
- }
492
- function compileWorkflow(handle, opts) {
493
- const steps = handle.steps;
494
- if (handle.start && handle.start !== steps[0]?.id) {
495
- throw new WorkflowCompileError(
496
- `start='${handle.start}' is not the first step \u2014 structured-subset runs in order`
497
- );
809
+ if (ctx.agents.readCostUsd) {
810
+ ctx.state.costBySession.set(sessionId, await ctx.agents.readCostUsd(sessionId));
498
811
  }
499
- const ctx = { opts, knownStepIds: collectStepIds(steps) };
500
- const result = handle.result;
501
- const output = result !== void 0 ? (b) => resolveValue(result, b) : void 0;
502
- return {
503
- id: handle.id,
504
- description: handle.description,
505
- steps: compileSiblingsToSteps(steps, ctx),
506
- ...output ? { output } : {}
507
- };
508
- }
509
- function compileSiblingsToSteps(steps, ctx) {
510
- assertLinear(steps);
511
- const out = [];
512
- for (let i = 0; i < steps.length; i++) {
513
- const s = steps[i];
514
- if (s.kind === "branch") {
515
- out.push(compileBranchChain(steps, i, ctx));
516
- return out;
812
+ if (!step.outputSchema) {
813
+ let text;
814
+ if (ctx.agents.readFinalMessage) {
815
+ try {
816
+ text = await ctx.agents.readFinalMessage(sessionId);
817
+ } catch {
818
+ }
517
819
  }
518
- out.push(compileStep(s, ctx));
820
+ return { sessionId, ...text !== void 0 ? { text } : {}, ...harnessOut ? { harness: harnessOut } : {}, ...knowledgeOut ? { knowledgeApplied: knowledgeOut } : {} };
519
821
  }
520
- return out;
521
- }
522
- function compileStepList(steps, id, ctx) {
523
- const compiled = compileSiblingsToSteps(steps, ctx);
524
- if (compiled.length === 1) return compiled[0];
525
- return { kind: "group", id, steps: compiled };
526
- }
527
- function compileBranchChain(steps, i, ctx) {
528
- const branchStep = steps[i];
529
- const branches = f(branchStep, "branches");
530
- const defaultTarget = f(branchStep, "default");
531
- const resolveTarget = (targetId, label) => {
532
- const idx = steps.findIndex((s) => s.id === targetId);
533
- if (idx === -1 || idx <= i) {
534
- throw new WorkflowCompileError(
535
- `branch '${branchStep.id}' ${label} targets '${targetId}', which is not a sibling later in this step list \u2014 the forward-only branch compiler only supports jumping to a later sibling in the SAME list (not backward, and not into a nested map/loop/parallel body). Hand-author a 'loop' step for retry-style control flow instead.`
822
+ if (!ctx.agents.readFinalMessage) {
823
+ throw new Error(`step '${step.id}': outputSchema requires a host with readFinalMessage`);
824
+ }
825
+ const maxRetries = step.maxRetries ?? 2;
826
+ let lastErr = "";
827
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
828
+ const raw = await ctx.agents.readFinalMessage(sessionId);
829
+ const candidate = extractJsonCandidate(raw);
830
+ let value;
831
+ try {
832
+ value = JSON.parse(candidate);
833
+ } catch {
834
+ lastErr = "not valid JSON";
835
+ if (attempt < maxRetries) {
836
+ await ctx.agents.sendPromptAndWait(
837
+ sessionId,
838
+ `Your previous reply did not match the required schema: ${lastErr}. Reply again with ONLY a JSON object that matches. No prose, no code fence needed.`
839
+ );
840
+ }
841
+ continue;
842
+ }
843
+ const res = step.outputSchema.safeParse(value);
844
+ if (res.success) return { sessionId, output: res.data, ...harnessOut ? { harness: harnessOut } : {}, ...knowledgeOut ? { knowledgeApplied: knowledgeOut } : {} };
845
+ lastErr = formatZodError(res.error);
846
+ if (attempt < maxRetries) {
847
+ await ctx.agents.sendPromptAndWait(
848
+ sessionId,
849
+ `Your previous reply did not match the required schema: ${lastErr}. Reply again with ONLY a JSON object that matches. No prose, no code fence needed.`
536
850
  );
537
851
  }
538
- return idx;
539
- };
540
- const defaultIdx = defaultTarget !== void 0 ? resolveTarget(defaultTarget, "default") : i + 1;
541
- let otherwise = compileSiblingsToSteps(steps.slice(defaultIdx), ctx);
542
- for (let k = branches.length - 1; k >= 0; k--) {
543
- const br = branches[k];
544
- const targetIdx = resolveTarget(br.next, `branches[${k}].next`);
545
- const thenSteps = compileSiblingsToSteps(steps.slice(targetIdx), ctx);
546
- const condExpr = br.when;
547
- const nodeId = k === 0 ? branchStep.id : `${branchStep.id}__branch${k}`;
548
- otherwise = [
549
- {
550
- kind: "branch",
551
- id: nodeId,
552
- cond: (b) => evalPredicate(condExpr, b),
553
- then: thenSteps,
554
- ...otherwise.length > 0 ? { otherwise } : {}
555
- }
556
- ];
557
852
  }
558
- return otherwise[0];
853
+ throw new Error(`step '${step.id}': output_invalid \u2014 final message never matched outputSchema (${lastErr})`);
559
854
  }
560
- function compileAgentStep(step, id, ctx) {
561
- const opts = ctx.opts;
562
- const prompt = f(step, "prompt");
563
- if (typeof prompt !== "string" || prompt.length === 0) {
564
- throw new WorkflowCompileError(`agent step '${id}' needs a non-empty 'prompt'`);
855
+ function tryParseJson(text) {
856
+ const trimmed = text.trim();
857
+ if (!trimmed) return void 0;
858
+ try {
859
+ return JSON.parse(trimmed);
860
+ } catch {
861
+ return void 0;
565
862
  }
566
- let adapter = typeof step.adapter === "string" ? step.adapter : void 0;
567
- let options = step.options !== void 0 ? step.options : void 0;
568
- const agentRef = step.agent?.ref;
569
- if (agentRef !== void 0) {
570
- if (typeof agentRef !== "string" || agentRef.trim().length === 0) {
571
- throw new WorkflowCompileError(`agent step '${id}' has an empty 'agent.ref'`);
572
- }
573
- const resolved = opts.agentRefs ? get(opts.agentRefs, agentRef) : void 0;
574
- if (!resolved) {
575
- const available = opts.agentRefs ? opts.agentRefs instanceof Map ? [...opts.agentRefs.keys()] : Object.keys(opts.agentRefs) : [];
576
- throw new WorkflowCompileError(
577
- `agent step '${id}' references unknown agent ref '${agentRef}'` + (available.length > 0 ? ` \u2014 available refs: ${available.join(", ")}` : " \u2014 no agent refs are configured for this compile (not running in an app context?)")
863
+ }
864
+ function defaultRunGateCommand(spec) {
865
+ return new Promise((resolve2) => {
866
+ execFile(
867
+ spec.command,
868
+ [...spec.args],
869
+ { cwd: spec.cwd, timeout: spec.timeoutMs, maxBuffer: 10 * 1024 * 1024 },
870
+ (err, stdout, stderr) => {
871
+ if (!err) {
872
+ resolve2({ exitCode: 0, stdout, stderr });
873
+ return;
874
+ }
875
+ const nodeErr = err;
876
+ const timedOut = nodeErr.killed === true && nodeErr.signal !== void 0 && spec.timeoutMs !== void 0;
877
+ const exitCode = typeof nodeErr.code === "number" ? nodeErr.code : 1;
878
+ resolve2({ exitCode, stdout, stderr, ...timedOut ? { timedOut: true } : {} });
879
+ }
880
+ );
881
+ });
882
+ }
883
+ async function resolveGateReport(cmd, cwd, reportPath) {
884
+ const fromStdout = tryParseJson(cmd.stdout);
885
+ if (fromStdout !== void 0) return fromStdout;
886
+ if (!reportPath) return void 0;
887
+ try {
888
+ const abs = isAbsolute(reportPath) ? reportPath : join(cwd, reportPath);
889
+ const raw = await readFile(abs, "utf8");
890
+ return tryParseJson(raw);
891
+ } catch {
892
+ return void 0;
893
+ }
894
+ }
895
+ function resolveGateCwd(step, ctx, b) {
896
+ const fallback = ctx.cwd ?? process.cwd();
897
+ if (!step.cwd) return fallback;
898
+ const resolved = resolveRefString(step.id, "cwd", resolveSel(step.cwd, b), b, "error");
899
+ return isAbsolute(resolved) ? resolved : resolve(fallback, resolved);
900
+ }
901
+ async function execGateStep(step, ctx, b) {
902
+ const cwd = resolveGateCwd(step, ctx, b);
903
+ const args = (step.args ?? []).map(
904
+ (arg, index) => resolveRefString(step.id, `args[${index}]`, resolveSel(arg, b), b, "error")
905
+ );
906
+ const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
907
+ let last;
908
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
909
+ if (attempt > 1 && step.onFail?.reprompt) {
910
+ if (!ctx.agents) {
911
+ throw new Error(`step '${step.id}': on_fail.reprompt requires a host agents implementation`);
912
+ }
913
+ const targetSessionId = ctx.agents.resolveByLabel(step.onFail.reprompt);
914
+ if (!targetSessionId) {
915
+ throw new Error(
916
+ `step '${step.id}': on_fail.reprompt targets unknown step '${step.onFail.reprompt}' \u2014 no session found`
917
+ );
918
+ }
919
+ const reportText = JSON.stringify(last?.report ?? null, null, 2);
920
+ const extra = step.onFail.with ? `
921
+
922
+ Additional context:
923
+ ${JSON.stringify(step.onFail.with, null, 2)}` : "";
924
+ await ctx.agents.sendPromptAndWait(
925
+ targetSessionId,
926
+ `Gate '${step.id}' failed (exit code ${last?.exitCode}). Report:
927
+ ${reportText}${extra}
928
+
929
+ Please address the findings above, then reply when done.`
578
930
  );
579
931
  }
580
- adapter = resolved.adapter;
581
- options = resolved.options;
932
+ if (attempt > 1 && step.retry?.initialMs) {
933
+ const delayMs = step.retry.backoff === "exponential" ? step.retry.initialMs * 2 ** (attempt - 2) : step.retry.initialMs;
934
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
935
+ }
936
+ const cmdResult = await (ctx.runGateCommand ?? defaultRunGateCommand)({
937
+ command: step.command,
938
+ args,
939
+ cwd,
940
+ timeoutMs: step.timeoutMs
941
+ });
942
+ const report = await resolveGateReport(cmdResult, cwd, step.reportPath);
943
+ const ok = cmdResult.exitCode === 0;
944
+ last = { ok, exitCode: cmdResult.exitCode, report };
945
+ ctx.onGateReport?.({ stepId: step.id, ok, exitCode: cmdResult.exitCode, report, attempt });
946
+ if (ok) break;
947
+ }
948
+ if (!last.ok) {
949
+ const err = new Error(
950
+ `step '${step.id}': gate failed after ${maxAttempts} attempt(s) \u2014 exit code ${last.exitCode}`
951
+ );
952
+ err.exitCode = last.exitCode;
953
+ err.report = last.report;
954
+ throw err;
955
+ }
956
+ return last;
957
+ }
958
+ async function execStep(step, ctx, item, index) {
959
+ const { state, signal } = ctx;
960
+ const b = view(state, item, index);
961
+ if (step.kind !== "agent") {
962
+ ctx.onStepStart?.(step.id);
582
963
  }
583
- return buildAgentStep(id, {
584
- prompt: (b) => String(resolveValue(prompt, b)),
585
- ...adapter !== void 0 ? { adapter } : {},
586
- ...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
587
- ...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
588
- ...step.cacheable ? { cacheable: true } : {},
589
- ...options !== void 0 ? { options } : {},
590
- policy: step.policy,
591
- ...step.outputSchema !== void 0 ? { outputSchema: step.outputSchema } : {},
592
- ...step.maxRetries !== void 0 ? { maxRetries: step.maxRetries } : {}
593
- });
594
- }
595
- function compileStep(step, ctx) {
596
- const opts = ctx.opts;
597
- const id = step.id;
598
964
  switch (step.kind) {
599
965
  case "tool": {
600
- const ref = typeof step.tool === "string" ? step.tool : step.tool.entry;
601
- const toolId = normalizeToolId(ref);
602
- const tool = get(opts.tools, toolId);
603
- if (!tool)
604
- throw new WorkflowCompileError(`no tool registered for '${toolId}'`);
605
- const inputs = f(step, "inputs") ?? {};
606
- assertKnownStepRefs(inputs, ctx.knownStepIds, `tool step '${id}' inputs`);
607
- return {
608
- kind: "tool",
609
- id,
610
- tool,
611
- candidates: opts.candidates,
612
- input: (b) => resolveValue(inputs, b),
613
- context: opts.contextFor ? (b) => opts.contextFor(toolId, b) : void 0
614
- };
966
+ const input = step.input(b);
967
+ const runIt = () => runTool({
968
+ tool: step.tool,
969
+ candidates: step.candidates,
970
+ input,
971
+ context: step.context ? step.context(b) : void 0,
972
+ resolverContext: step.resolverContext,
973
+ secrets: step.secrets,
974
+ signal
975
+ });
976
+ if (!isCacheEnabled(ctx, step)) return runIt();
977
+ const c = await readStepCache(ctx, step, input);
978
+ if (c.hit) return c.output;
979
+ const out = await runIt();
980
+ await ctx.cache.set(c.key, { output: out, resolvedInputHash: c.hash });
981
+ return out;
615
982
  }
983
+ case "transform":
984
+ return step.compute(b);
616
985
  case "map": {
617
- const over = f(step, "over");
618
- const inner = compileStepList(f(step, "steps"), `${id}__body`, ctx);
619
- const onError = f(step, "onError");
986
+ const arr = [...step.over(b)];
987
+ const parallelism = Math.max(1, step.parallelism ?? 1);
988
+ const tolerant = step.onError === "collect";
989
+ const results = new Array(arr.length);
990
+ for (let i = 0; i < arr.length; i += parallelism) {
991
+ const chunk = arr.slice(i, i + parallelism);
992
+ if (!tolerant) {
993
+ const outs = await Promise.all(
994
+ chunk.map((el, j) => {
995
+ const idx = i + j;
996
+ const inner = step.body(el, idx, view(state, el, idx));
997
+ return execStep(inner, ctx, el, idx);
998
+ })
999
+ );
1000
+ for (let j = 0; j < outs.length; j++) results[i + j] = outs[j];
1001
+ continue;
1002
+ }
1003
+ const settled = await Promise.allSettled(
1004
+ chunk.map((el, j) => {
1005
+ const idx = i + j;
1006
+ const inner = step.body(el, idx, view(state, el, idx));
1007
+ return execStep(inner, ctx, el, idx);
1008
+ })
1009
+ );
1010
+ settled.forEach((s, j) => {
1011
+ const idx = i + j;
1012
+ results[idx] = s.status === "fulfilled" ? { status: "fulfilled", index: idx, value: s.value } : { status: "rejected", index: idx, item: chunk[j], error: errorMessage(s.reason) };
1013
+ });
1014
+ }
1015
+ if (!tolerant) return results;
1016
+ const outcomes = results;
620
1017
  return {
621
- kind: "map",
622
- id,
623
- parallelism: f(step, "parallelism"),
624
- over: (b) => resolveRef(over, b),
625
- body: () => inner,
626
- ...onError !== void 0 ? { onError } : {}
1018
+ results: outcomes,
1019
+ succeeded: outcomes.filter((r) => r.status === "fulfilled").length,
1020
+ failed: outcomes.filter((r) => r.status === "rejected").length
627
1021
  };
628
1022
  }
629
- case "loop": {
630
- const whileExpr = f(step, "while");
1023
+ case "pipeline": {
1024
+ const items = [...step.over(b)];
1025
+ const tolerant = step.onError === "collect";
1026
+ if (items.length === 0) return tolerant ? { results: [], succeeded: 0, failed: 0 } : [];
1027
+ const cap = Math.max(1, step.concurrency ?? items.length);
1028
+ const results = new Array(items.length);
1029
+ let next = 0;
1030
+ const runItem = async (idx) => {
1031
+ let prev = void 0;
1032
+ try {
1033
+ for (const stage of step.stages) {
1034
+ prev = await execStep(stage(items[idx], idx, prev, view(state, items[idx], idx)), ctx, items[idx], idx);
1035
+ }
1036
+ results[idx] = tolerant ? { status: "fulfilled", index: idx, value: prev } : prev;
1037
+ } catch (err) {
1038
+ if (!tolerant) throw err;
1039
+ results[idx] = { status: "rejected", index: idx, item: items[idx], error: errorMessage(err) };
1040
+ }
1041
+ };
1042
+ const worker = async () => {
1043
+ while (next < items.length) {
1044
+ const idx = next++;
1045
+ await runItem(idx);
1046
+ }
1047
+ };
1048
+ await Promise.all(Array.from({ length: Math.min(cap, items.length) }, () => worker()));
1049
+ if (!tolerant) return results;
1050
+ const outcomes = results;
631
1051
  return {
632
- kind: "loop",
633
- id,
634
- maxIterations: f(step, "max_iterations"),
635
- while: (b) => evalPredicate(whileExpr, b),
636
- body: [compileStepList(f(step, "steps"), `${id}__body`, ctx)]
1052
+ results: outcomes,
1053
+ succeeded: outcomes.filter((r) => r.status === "fulfilled").length,
1054
+ failed: outcomes.filter((r) => r.status === "rejected").length
637
1055
  };
638
1056
  }
1057
+ case "branch": {
1058
+ const chosen = step.cond(b) ? step.then : step.otherwise ?? [];
1059
+ return runSequence(chosen, ctx, item, index);
1060
+ }
1061
+ case "loop": {
1062
+ let iterations = 0;
1063
+ let last;
1064
+ while (iterations < step.maxIterations && step.while(view(state, item, index))) {
1065
+ last = await runSequence(step.body, ctx, item, index);
1066
+ iterations++;
1067
+ }
1068
+ return last;
1069
+ }
639
1070
  case "parallel": {
640
- const branches = f(step, "branches");
641
- return {
642
- kind: "parallel",
643
- id,
644
- branches: branches.map((br) => ({
645
- id: br.id,
646
- steps: [compileStepList(br.steps, `${id}__${br.id}`, ctx)]
647
- }))
648
- };
1071
+ const outs = await Promise.all(
1072
+ step.branches.map(
1073
+ (br) => runSequence(br.steps, ctx, item, index).then((last) => ({
1074
+ id: br.id,
1075
+ last
1076
+ }))
1077
+ )
1078
+ );
1079
+ const record = {};
1080
+ for (const { id, last } of outs) record[id] = last;
1081
+ return record;
649
1082
  }
650
1083
  case "approval": {
651
- const prompt = f(step, "prompt");
652
- const approvers = (f(step, "approvers") ?? []).map(
653
- (a) => a.role ?? a.userId ?? String(a)
654
- );
655
- return {
656
- kind: "approval",
657
- id,
1084
+ const prompt = step.prompt(b);
1085
+ const approvers = step.approvers ?? [];
1086
+ const raw = ctx.approve ? await ctx.approve({
1087
+ stepId: step.id,
1088
+ prompt,
658
1089
  approvers,
659
- prompt: (b) => String(resolveValue(prompt, b))
660
- };
1090
+ ...step.artifacts !== void 0 ? { artifacts: step.artifacts } : {},
1091
+ ...step.timeoutMs !== void 0 ? { timeoutMs: step.timeoutMs } : {}
1092
+ }) : true;
1093
+ const decision = typeof raw === "boolean" ? { approved: raw, who: "host" } : raw;
1094
+ const approved = decision.approved;
1095
+ const followups = approved ? step.onApprove ?? [] : step.onReject ?? [];
1096
+ await runSequence(followups, ctx, item, index);
1097
+ return { approved, who: decision.who, ...decision.note !== void 0 ? { note: decision.note } : {} };
661
1098
  }
662
1099
  case "suspend": {
663
- const resume = f(step, "resume");
664
- return { kind: "suspend", id, on: resume.on, timeoutMs: resume.timeout_ms };
1100
+ if (!ctx.resume) throw new WorkflowSuspendedError(step.id, step.on);
1101
+ return ctx.resume({ stepId: step.id, on: step.on });
665
1102
  }
1103
+ case "group":
1104
+ return runSequence(step.steps, ctx, item, index);
666
1105
  case "subworkflow": {
667
- const wfId = f(step, "workflow");
668
- const child = opts.workflows ? get(opts.workflows, wfId) : void 0;
669
- if (!child)
670
- throw new WorkflowCompileError(
671
- `subworkflow '${wfId}' not found \u2014 pass it in opts.workflows`
672
- );
673
- const compiledChild = compileWorkflow(child, opts);
674
- const inputs = f(step, "inputs");
675
- if (inputs === void 0) {
676
- return { kind: "subworkflow", id, workflow: compiledChild };
677
- }
678
- assertKnownStepRefs(inputs, ctx.knownStepIds, `subworkflow step '${id}' inputs`);
679
- return {
680
- kind: "subworkflow",
681
- id,
682
- workflow: compiledChild,
683
- input: (b) => resolveValue(inputs, b)
684
- };
1106
+ const childInput = step.input ? step.input(b) : state.input;
1107
+ const child = await runWorkflowInner(step.workflow, childInput, {
1108
+ approve: ctx.approve,
1109
+ resume: ctx.resume,
1110
+ signal,
1111
+ agents: ctx.agents,
1112
+ cwd: ctx.cwd,
1113
+ workspaceSlug: ctx.workspaceSlug,
1114
+ cache: ctx.cache,
1115
+ cacheKey: ctx.cacheKey,
1116
+ runGateCommand: ctx.runGateCommand,
1117
+ onGateReport: ctx.onGateReport
1118
+ });
1119
+ return child.output;
685
1120
  }
686
1121
  case "agent": {
687
- if (typeof step.prompt === "function") {
688
- const passthrough = step;
689
- return passthrough;
690
- }
691
- return compileAgentStep(step, id, ctx);
692
- }
693
- case "transform": {
694
- const passthrough = step;
695
- return passthrough;
1122
+ if (!ctx.agents) throw new Error(`step '${step.id}': AgentStep requires a host agents implementation`);
1123
+ if (!isCacheEnabled(ctx, step)) return execAgentStep(step, ctx, b);
1124
+ const resolved = {
1125
+ prompt: step.prompt(b),
1126
+ adapter: step.adapter ? resolveSel(step.adapter, b) : void 0,
1127
+ model: step.model ? resolveSel(step.model, b) : void 0,
1128
+ sessionRef: step.sessionRef
1129
+ };
1130
+ const c = await readStepCache(ctx, step, resolved);
1131
+ if (c.hit) return c.output;
1132
+ const out = await execAgentStep(step, ctx, b);
1133
+ await ctx.cache.set(c.key, { output: out, resolvedInputHash: c.hash });
1134
+ return out;
696
1135
  }
697
- case "branch":
698
- throw new WorkflowCompileError(
699
- `internal: branch step '${id}' reached compileStep directly \u2014 branch steps must be compiled via the sibling-list walker`
700
- );
701
- default:
702
- throw new WorkflowCompileError(
703
- `unsupported step kind '${step.kind}' (step '${id}')`
704
- );
1136
+ case "gate":
1137
+ return execGateStep(step, ctx, b);
1138
+ }
1139
+ }
1140
+ async function runWorkflowInner(workflow, input, hooks, maxTotalCostUsd) {
1141
+ const state = { input, steps: {}, costBySession: /* @__PURE__ */ new Map(), maxTotalCostUsd };
1142
+ const ctx = { state, ...hooks };
1143
+ let lastId;
1144
+ for (const step of workflow.steps) {
1145
+ const out = await execStep(step, ctx, void 0, void 0);
1146
+ state.steps[step.id] = out;
1147
+ ctx.onStepComplete?.(step.id, out);
1148
+ lastId = step.id;
705
1149
  }
1150
+ const bindings = view(state);
1151
+ const output = workflow.output ? workflow.output(bindings) : lastId !== void 0 ? state.steps[lastId] : void 0;
1152
+ return { output, bindings };
1153
+ }
1154
+ async function runWorkflow(args) {
1155
+ return runWorkflowInner(args.workflow, args.input, {
1156
+ approve: args.approve,
1157
+ resume: args.resume,
1158
+ signal: args.signal,
1159
+ agents: args.agents,
1160
+ cwd: args.cwd,
1161
+ workspaceSlug: args.workspaceSlug,
1162
+ cache: args.cache,
1163
+ cacheKey: args.cacheKey,
1164
+ onStepStart: args.onStepStart,
1165
+ onStepComplete: args.onStepComplete,
1166
+ runGateCommand: args.runGateCommand,
1167
+ onGateReport: args.onGateReport
1168
+ }, args.maxTotalCostUsd);
706
1169
  }
707
1170
  function compileWorkflowManifest(source, opts) {
708
1171
  const manifest = parseWorkflowManifest(source);
@@ -710,6 +1173,6 @@ function compileWorkflowManifest(source, opts) {
710
1173
  return compileWorkflow(handle, opts);
711
1174
  }
712
1175
 
713
- export { WorkflowCompileError, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, resolveRef, resolveValue, runWorkflow };
1176
+ export { NodeFsPort, WorkflowCompileError, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, materializeKnowledge, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow };
714
1177
  //# sourceMappingURL=index.mjs.map
715
1178
  //# sourceMappingURL=index.mjs.map