@agentproto/workflow-runtime 0.13.1 → 0.15.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/README.md CHANGED
@@ -76,6 +76,38 @@ whenever the host wires `RunWorkflowArgs.workspace` — see **Run workspace
76
76
  | `group` | Run a list of steps as one unit; output is the last step's. |
77
77
  | `subworkflow` | Run a nested workflow with its own isolated bindings. |
78
78
 
79
+ ### Step timeouts (`tool` / `gate`)
80
+
81
+ A `tool` or `gate` step's dispatch is bounded by `timeout_ms` (`timeoutMs` on
82
+ the compiled `ToolStep`/`GateStep`) — default `DEFAULT_STEP_TIMEOUT_MS` (10
83
+ minutes) when the step declares none. On expiry the step fails with `step
84
+ '<id>': timed out after <n>ms`; the underlying subprocess (a `kind: cli`
85
+ driver's spawn, or a `gate` step's command) is killed by its WHOLE process
86
+ group, not just the direct child, so a subprocess that spawned its own
87
+ children (e.g. headless Chrome) can't leave orphans running past the step.
88
+
89
+ This exists because completion for both used to depend on the child's stdio
90
+ pipes closing (`close`), not the direct child's own `exit` — a grandchild
91
+ that inherited an fd (inherited/piped stdout or stderr) and outlived its
92
+ parent (e.g. reparented to pid 1) could hold that pipe open forever even
93
+ after the tool's actual work was done, hanging the step indefinitely with no
94
+ timeout to bound it. Both the CLI driver's `runSubprocess` and this
95
+ package's `defaultRunGateCommand` now settle on `exit` (with a short drain
96
+ window for any already-in-flight stdio) instead.
97
+
98
+ ```yaml
99
+ steps:
100
+ - id: pdf-render
101
+ kind: tool
102
+ tool: pdf.render
103
+ timeout_ms: 120000 # override the 10-minute default
104
+ - id: lint
105
+ kind: gate
106
+ command: pnpm
107
+ args: ["lint"]
108
+ timeout_ms: 60000
109
+ ```
110
+
79
111
  ### WORKFLOW.md `kind: branch` — exclusive arms + join
80
112
 
81
113
  `compileWorkflow` compiles a declarative AIP-15 `kind: branch` step (goto-style
package/dist/index.d.ts CHANGED
@@ -51,6 +51,12 @@ interface ToolStep<TInput = unknown, TOutput = unknown, TContext extends ToolCon
51
51
  /** Cache this step's output under the run's cacheKey; only its `input` is hashed
52
52
  * (not `context`/secrets). Default false. */
53
53
  cacheable?: boolean;
54
+ /** Hard wall-clock cap for this step's dispatch (driver resolve + execute),
55
+ * in ms. Bounds a driver that never settles its own promise (e.g. a `kind:
56
+ * cli` subprocess whose stdio pipe is held open by an orphaned grandchild
57
+ * process — F45) rather than relying on the driver to self-timeout.
58
+ * Default {@link DEFAULT_STEP_TIMEOUT_MS} when unset. */
59
+ timeoutMs?: number;
54
60
  }
55
61
  /** A pure in-run computation (combine / filter / shape) — no tool dispatch. */
56
62
  interface TransformStep {
@@ -74,12 +80,14 @@ type FanOutOutcome<T = unknown> = {
74
80
  /** `err.message` if the throw was an `Error`, else `String(err)`. */
75
81
  readonly error: string;
76
82
  } | {
77
- /** Never started: the fan-out's spawn circuit breaker opened first
78
- * (see {@link MapStep.maxConsecutiveSpawnFailures}). */
83
+ /** Never started: either the fan-out's spawn circuit breaker opened
84
+ * first (see {@link MapStep.maxConsecutiveSpawnFailures}), or the run
85
+ * was cancelled while earlier items were still in flight. */
79
86
  readonly status: "skipped";
80
87
  readonly index: number;
81
88
  readonly item: unknown;
82
- /** `circuit-open: <first error of the failure streak>`. */
89
+ /** `circuit-open: <first error of the failure streak>`, or
90
+ * `run-cancelled: run cancelled`. */
83
91
  readonly reason: string;
84
92
  };
85
93
  /**
@@ -102,6 +110,10 @@ interface TolerantFanOutResult<T = unknown> {
102
110
  /** Default for {@link MapStep.maxConsecutiveSpawnFailures} /
103
111
  * {@link PipelineStep.maxConsecutiveSpawnFailures}. */
104
112
  declare const DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES = 3;
113
+ /** Default for {@link ToolStep.timeoutMs} / {@link GateStep.timeoutMs} when
114
+ * the step declares none — 10 minutes. Bounds an otherwise-unbounded `tool`
115
+ * or `gate` step dispatch (F45). */
116
+ declare const DEFAULT_STEP_TIMEOUT_MS: number;
105
117
  /**
106
118
  * Run a sub-step once per element of an array, optionally with bounded
107
119
  * concurrency. The element + index are exposed to the body via `bindings.item`
@@ -492,7 +504,9 @@ interface GateStep {
492
504
  /** Path (relative to `cwd`), of a JSON report file, consulted when stdout
493
505
  * doesn't itself parse as JSON. */
494
506
  reportPath?: string;
495
- /** Hard wall-clock cap for a single command invocation, in ms. */
507
+ /** Hard wall-clock cap for a single command invocation, in ms. Default
508
+ * {@link DEFAULT_STEP_TIMEOUT_MS} when unset — a gate's subprocess is
509
+ * never left to run unbounded (F45). */
496
510
  timeoutMs?: number;
497
511
  /** Re-run on a failing exit code, up to `maxAttempts` (default 1 = no
498
512
  * retry — a single attempt, fail immediately on a non-zero exit). */
@@ -525,6 +539,12 @@ type GateCommandRunner = (spec: {
525
539
  args: readonly string[];
526
540
  cwd: string;
527
541
  timeoutMs?: number;
542
+ /** The run's cancel signal — a cancelled run kills an in-flight gate
543
+ * command's child process the same way it kills an agent step's session.
544
+ * A host-injected runner should pass this through to its own subprocess
545
+ * spawn (e.g. `child_process.execFile`'s `signal` option) so the process
546
+ * is signalled, not merely orphaned. */
547
+ signal?: AbortSignal;
528
548
  }) => Promise<GateCommandResult>;
529
549
  /** One `kind: "gate"` command attempt's outcome, reported as it happens
530
550
  * (every attempt, not just the last) — the `gate-report` lifecycle event. */
@@ -733,13 +753,16 @@ interface StepSkippedInfo {
733
753
  /** `"branch-not-taken"`: the step sits in an untaken `branch` arm.
734
754
  * `"circuit-open"`: a `map`/`pipeline` item never started because the
735
755
  * fan-out's spawn circuit breaker opened
736
- * ({@link MapStep.maxConsecutiveSpawnFailures}). */
737
- reason: "branch-not-taken" | "circuit-open";
756
+ * ({@link MapStep.maxConsecutiveSpawnFailures}). `"run-cancelled"`: a
757
+ * `map`/`pipeline` item never started because the run was cancelled
758
+ * (`RunWorkflowArgs.signal` aborted) while earlier items were still in
759
+ * flight. */
760
+ reason: "branch-not-taken" | "circuit-open" | "run-cancelled";
738
761
  /** Id of the authored step whose decision skipped the step — the `branch`
739
- * step, or (circuit-open) the `map`/`pipeline` step. */
762
+ * step, or (circuit-open/run-cancelled) the `map`/`pipeline` step. */
740
763
  branchId: string;
741
- /** `"circuit-open"` only: the first error of the spawn-failure streak
742
- * that tripped the breaker. */
764
+ /** `"circuit-open"`: the first error of the spawn-failure streak that
765
+ * tripped the breaker. `"run-cancelled"`: a fixed "run cancelled" message. */
743
766
  message?: string;
744
767
  }
745
768
  /** What `onStepFailed` reports. */
@@ -898,6 +921,18 @@ declare class AgentSpawnError extends Error {
898
921
  readonly cause: unknown;
899
922
  constructor(stepId: string, cause: unknown);
900
923
  }
924
+ /**
925
+ * Thrown instead of dispatching a step whose run was cancelled
926
+ * (`ctx.signal.aborted`) — checked BEFORE any work for the step begins (see
927
+ * {@link execStepBody}'s entry guard), so a cancel stops the run from
928
+ * starting any further step, including a sibling within the SAME stage/
929
+ * fan-out, not just a later one. Never journaled: it's thrown before a
930
+ * cacheable step's `cache.set` call, so `workflow_retry` re-executes it.
931
+ */
932
+ declare class WorkflowCancelledError extends Error {
933
+ readonly stepId: string;
934
+ constructor(stepId: string);
935
+ }
901
936
  declare function runWorkflow(args: RunWorkflowArgs): Promise<WorkflowRunResult>;
902
937
 
903
938
  /**
@@ -1180,4 +1215,4 @@ declare class NodeFsPort implements FsPort {
1180
1215
  lock(): Promise<FsLockHandle>;
1181
1216
  }
1182
1217
 
1183
- export { type AgentHarness, AgentInputRequiredError, type AgentRefResolution, type AgentSandboxRef, type AgentSessionHost, AgentSpawnError, type AgentStep, type AgentStepFields, type ApprovalDecision, type ApprovalRequest, type ApprovalStep, type ArtifactEntry, type ArtifactStep, type Bindings, type BranchStep, type CompileWorkflowOptions, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, type FanOutOutcome, type GateCommandResult, type GateCommandRunner, type GateReportEvent, type GateStep, type GroupStep, type HarnessKnowledgeSelector, type InputRequiredRequest, type KnowledgeAppliedRecord, type LoopStep, type MapStep, type MaterializedKnowledge, MissingArtifactError, NodeFsPort, type OutputSchemaLike, type OutputsFileContract, type ParallelStep, type PipelineStep, type ResumeRequest, type RunStep, type RunWorkflowArgs, type RuntimeWorkflow, type SchemaValidationIssue, type Selector, type StepCache, type StepCacheEntry, type StepFailedInfo, type StepHookInfo, StepOutcomeError, type StepSkippedInfo, type SubworkflowStep, type SuspendStep, type TolerantFanOutResult, type ToolStep, type TransformStep, WorkflowCompileError, type WorkflowInputValidation, type WorkflowRunResult, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, isCompilableJsonSchema, materializeKnowledge, normalizeWorkflowInputsSchema, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow, validateAgainstJsonSchema, validateWorkflowInput };
1218
+ export { type AgentHarness, AgentInputRequiredError, type AgentRefResolution, type AgentSandboxRef, type AgentSessionHost, AgentSpawnError, type AgentStep, type AgentStepFields, type ApprovalDecision, type ApprovalRequest, type ApprovalStep, type ArtifactEntry, type ArtifactStep, type Bindings, type BranchStep, type CompileWorkflowOptions, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, DEFAULT_STEP_TIMEOUT_MS, type FanOutOutcome, type GateCommandResult, type GateCommandRunner, type GateReportEvent, type GateStep, type GroupStep, type HarnessKnowledgeSelector, type InputRequiredRequest, type KnowledgeAppliedRecord, type LoopStep, type MapStep, type MaterializedKnowledge, MissingArtifactError, NodeFsPort, type OutputSchemaLike, type OutputsFileContract, type ParallelStep, type PipelineStep, type ResumeRequest, type RunStep, type RunWorkflowArgs, type RuntimeWorkflow, type SchemaValidationIssue, type Selector, type StepCache, type StepCacheEntry, type StepFailedInfo, type StepHookInfo, StepOutcomeError, type StepSkippedInfo, type SubworkflowStep, type SuspendStep, type TolerantFanOutResult, type ToolStep, type TransformStep, WorkflowCancelledError, WorkflowCompileError, type WorkflowInputValidation, type WorkflowRunResult, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, isCompilableJsonSchema, materializeKnowledge, normalizeWorkflowInputsSchema, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow, validateAgainstJsonSchema, validateWorkflowInput };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { runTool } from '@agentproto/driver';
2
2
  import { createHash } from 'crypto';
3
- import { execFile } from 'child_process';
3
+ import { spawn } from 'child_process';
4
4
  import { stat, readFile, mkdir, writeFile, appendFile, readdir, copyFile, cp } from 'fs/promises';
5
5
  import { join, dirname, isAbsolute, basename, resolve, relative } from 'path';
6
6
  import { z } from 'zod';
@@ -323,6 +323,15 @@ function normalizeToolId(ref) {
323
323
  return slash === -1 ? s : s.slice(slash + 1);
324
324
  }
325
325
  var f = (step, key) => step[key];
326
+ function assertStepTimeoutMs(id, kind, value) {
327
+ if (value === void 0) return void 0;
328
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
329
+ throw new WorkflowCompileError(
330
+ `${kind} step '${id}' has an invalid 'timeout_ms' (${JSON.stringify(value)}) \u2014 must be a number >= 1`
331
+ );
332
+ }
333
+ return value;
334
+ }
326
335
  function assertLinear(steps) {
327
336
  steps.forEach((s, i) => {
328
337
  const next = s.next;
@@ -563,7 +572,7 @@ function compileGateStep(step, id) {
563
572
  const args = f(step, "args");
564
573
  const cwd = f(step, "cwd");
565
574
  const reportPath = f(step, "report");
566
- const timeoutMs = f(step, "timeout_ms");
575
+ const timeoutMs = assertStepTimeoutMs(id, "gate", step.timeout_ms);
567
576
  const retry = f(step, "retry");
568
577
  const onFail = f(
569
578
  step,
@@ -601,6 +610,7 @@ function compileStep(step, ctx) {
601
610
  assertKnownStepRefs(inputs, ctx.knownStepIds, `tool step '${id}' inputs`, {
602
611
  makeError: (message) => new WorkflowCompileError(message)
603
612
  });
613
+ const timeoutMs = assertStepTimeoutMs(id, "tool", step.timeout_ms);
604
614
  return {
605
615
  kind: "tool",
606
616
  id,
@@ -608,7 +618,8 @@ function compileStep(step, ctx) {
608
618
  candidates: opts.candidates,
609
619
  input: (b) => resolveValue(inputs, b),
610
620
  context: opts.contextFor ? (b) => opts.contextFor(toolId, b) : void 0,
611
- ...step.cacheable ? { cacheable: true } : {}
621
+ ...step.cacheable ? { cacheable: true } : {},
622
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
612
623
  };
613
624
  }
614
625
  case "map": {
@@ -773,6 +784,7 @@ function resolveRefString(stepId, field, value, b, onUnmatched = "passthrough")
773
784
 
774
785
  // src/types.ts
775
786
  var DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES = 3;
787
+ var DEFAULT_STEP_TIMEOUT_MS = 10 * 60 * 1e3;
776
788
  var NodeFsPort = class {
777
789
  root;
778
790
  constructor(root) {
@@ -1014,6 +1026,14 @@ var AgentSpawnError = class extends Error {
1014
1026
  stepId;
1015
1027
  cause;
1016
1028
  };
1029
+ var WorkflowCancelledError = class extends Error {
1030
+ constructor(stepId) {
1031
+ super(`step '${stepId}': run cancelled \u2014 not started`);
1032
+ this.stepId = stepId;
1033
+ this.name = "WorkflowCancelledError";
1034
+ }
1035
+ stepId;
1036
+ };
1017
1037
  function withReleaseScope(ctx) {
1018
1038
  return { ...ctx, spawned: [] };
1019
1039
  }
@@ -1079,9 +1099,9 @@ function spawnBreaker(threshold) {
1079
1099
  }
1080
1100
  };
1081
1101
  }
1082
- function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, reason, results) {
1102
+ function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, kind, reason, results) {
1083
1103
  for (let idx = from; idx < items.length; idx++) {
1084
- results[idx] = { status: "skipped", index: idx, item: items[idx], reason: `circuit-open: ${reason}` };
1104
+ results[idx] = { status: "skipped", index: idx, item: items[idx], reason: `${kind}: ${reason}` };
1085
1105
  const itemCtx = withIndexedHooks(ctx, idx);
1086
1106
  if (!itemCtx.onStepSkipped) continue;
1087
1107
  let bodies;
@@ -1091,7 +1111,7 @@ function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, reason, result
1091
1111
  continue;
1092
1112
  }
1093
1113
  for (const id of new Set(skippableStepIds(bodies))) {
1094
- itemCtx.onStepSkipped(id, { reason: "circuit-open", branchId: fanOutId, message: reason });
1114
+ itemCtx.onStepSkipped(id, { reason: kind, branchId: fanOutId, message: reason });
1095
1115
  }
1096
1116
  }
1097
1117
  }
@@ -1333,7 +1353,7 @@ ${inner(b2)}` };
1333
1353
  } : void 0;
1334
1354
  let sessionId;
1335
1355
  if (step.adapter) {
1336
- if (ctx.signal?.aborted) throw new Error(`step '${step.id}': run cancelled \u2014 not spawning`);
1356
+ if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
1337
1357
  try {
1338
1358
  sessionId = await ctx.agents.spawn(resolveSel(step.adapter, b), {
1339
1359
  cwd,
@@ -1440,23 +1460,83 @@ function tryParseJson(text) {
1440
1460
  return void 0;
1441
1461
  }
1442
1462
  }
1463
+ var GATE_STDIO_DRAIN_MS = 2e3;
1464
+ var GATE_KILL_GRACE_MS = 2e3;
1443
1465
  function defaultRunGateCommand(spec) {
1466
+ const timeoutMs = spec.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
1444
1467
  return new Promise((resolve2) => {
1445
- execFile(
1446
- spec.command,
1447
- [...spec.args],
1448
- { cwd: spec.cwd, timeout: spec.timeoutMs, maxBuffer: 10 * 1024 * 1024 },
1449
- (err, stdout, stderr) => {
1450
- if (!err) {
1451
- resolve2({ exitCode: 0, stdout, stderr });
1452
- return;
1453
- }
1454
- const nodeErr = err;
1455
- const timedOut = nodeErr.killed === true && nodeErr.signal !== void 0 && spec.timeoutMs !== void 0;
1456
- const exitCode = typeof nodeErr.code === "number" ? nodeErr.code : 1;
1457
- resolve2({ exitCode, stdout, stderr, ...timedOut ? { timedOut: true } : {} });
1468
+ const child = spawn(spec.command, [...spec.args], {
1469
+ cwd: spec.cwd,
1470
+ stdio: ["ignore", "pipe", "pipe"],
1471
+ detached: process.platform !== "win32"
1472
+ });
1473
+ let stdout = "";
1474
+ let stderr = "";
1475
+ let settled = false;
1476
+ let timedOut = false;
1477
+ let stdoutEnded = !child.stdout;
1478
+ let stderrEnded = !child.stderr;
1479
+ let exitCode;
1480
+ let drainTimer;
1481
+ const killGroup = (sig) => {
1482
+ if (typeof child.pid !== "number") return;
1483
+ try {
1484
+ if (process.platform !== "win32") process.kill(-child.pid, sig);
1485
+ else child.kill(sig);
1486
+ } catch {
1458
1487
  }
1459
- );
1488
+ };
1489
+ let killTimer;
1490
+ const kill = () => {
1491
+ killGroup("SIGTERM");
1492
+ killTimer = setTimeout(() => killGroup("SIGKILL"), GATE_KILL_GRACE_MS).unref();
1493
+ };
1494
+ const onAbort = () => kill();
1495
+ if (spec.signal?.aborted) kill();
1496
+ else spec.signal?.addEventListener("abort", onAbort, { once: true });
1497
+ const timeoutTimer = setTimeout(() => {
1498
+ timedOut = true;
1499
+ kill();
1500
+ }, timeoutMs).unref();
1501
+ const finish = (code) => {
1502
+ if (settled) return;
1503
+ settled = true;
1504
+ clearTimeout(killTimer);
1505
+ clearTimeout(timeoutTimer);
1506
+ clearTimeout(drainTimer);
1507
+ spec.signal?.removeEventListener("abort", onAbort);
1508
+ child.stdout?.destroy();
1509
+ child.stderr?.destroy();
1510
+ resolve2({ exitCode: code, stdout, stderr, ...timedOut ? { timedOut: true } : {} });
1511
+ };
1512
+ const maybeFinish = () => {
1513
+ if (exitCode !== void 0 && stdoutEnded && stderrEnded) finish(exitCode);
1514
+ };
1515
+ child.stdout?.on("data", (chunk) => {
1516
+ stdout += chunk.toString();
1517
+ });
1518
+ child.stdout?.on("end", () => {
1519
+ stdoutEnded = true;
1520
+ maybeFinish();
1521
+ });
1522
+ child.stderr?.on("data", (chunk) => {
1523
+ stderr += chunk.toString();
1524
+ });
1525
+ child.stderr?.on("end", () => {
1526
+ stderrEnded = true;
1527
+ maybeFinish();
1528
+ });
1529
+ child.on("error", () => {
1530
+ clearTimeout(timeoutTimer);
1531
+ exitCode = 1;
1532
+ drainTimer = setTimeout(() => finish(1), GATE_STDIO_DRAIN_MS).unref();
1533
+ maybeFinish();
1534
+ });
1535
+ child.on("exit", (code, signal) => {
1536
+ exitCode = code ?? (signal ? 1 : 0);
1537
+ drainTimer = setTimeout(() => finish(exitCode), GATE_STDIO_DRAIN_MS).unref();
1538
+ maybeFinish();
1539
+ });
1460
1540
  });
1461
1541
  }
1462
1542
  async function resolveGateReport(cmd, cwd, reportPath) {
@@ -1485,6 +1565,7 @@ async function execGateStep(step, ctx, b) {
1485
1565
  const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
1486
1566
  let last;
1487
1567
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1568
+ if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
1488
1569
  if (attempt > 1 && step.onFail?.reprompt) {
1489
1570
  if (!ctx.agents) {
1490
1571
  throw new Error(`step '${step.id}': on_fail.reprompt requires a host agents implementation`);
@@ -1516,8 +1597,10 @@ Please address the findings above, then reply when done.`
1516
1597
  command: step.command,
1517
1598
  args,
1518
1599
  cwd,
1519
- timeoutMs: step.timeoutMs
1600
+ timeoutMs: step.timeoutMs,
1601
+ signal: ctx.signal
1520
1602
  });
1603
+ if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
1521
1604
  const report = await resolveGateReport(cmdResult, cwd, step.reportPath);
1522
1605
  const ok = cmdResult.exitCode === 0;
1523
1606
  last = { ok, exitCode: cmdResult.exitCode, report };
@@ -1534,6 +1617,27 @@ Please address the findings above, then reply when done.`
1534
1617
  }
1535
1618
  return last;
1536
1619
  }
1620
+ async function withStepTimeout(stepId, timeoutMs, parentSignal, fn) {
1621
+ const controller = new AbortController();
1622
+ const onParentAbort = (sig) => controller.abort(sig.reason);
1623
+ const onParentAbortEvent = () => onParentAbort(parentSignal);
1624
+ if (parentSignal?.aborted) onParentAbort(parentSignal);
1625
+ else parentSignal?.addEventListener("abort", onParentAbortEvent, { once: true });
1626
+ let timedOut = false;
1627
+ const timer = setTimeout(() => {
1628
+ timedOut = true;
1629
+ controller.abort(new Error(`step '${stepId}': timed out after ${timeoutMs}ms`));
1630
+ }, timeoutMs);
1631
+ try {
1632
+ return await fn(controller.signal);
1633
+ } catch (err) {
1634
+ if (timedOut) throw new Error(`step '${stepId}': timed out after ${timeoutMs}ms`);
1635
+ throw err;
1636
+ } finally {
1637
+ clearTimeout(timer);
1638
+ parentSignal?.removeEventListener("abort", onParentAbortEvent);
1639
+ }
1640
+ }
1537
1641
  function cacheHit(ctx, step, output) {
1538
1642
  ctx.state.cachedHits.add(cachedHitKey(ctx, step.id));
1539
1643
  ctx.onStepStart?.(step.id, { cached: true });
@@ -1551,6 +1655,7 @@ async function execStep(step, ctx, item, index) {
1551
1655
  }
1552
1656
  async function execStepBody(step, ctx, item, index) {
1553
1657
  const { state, signal } = ctx;
1658
+ if (signal?.aborted) throw new WorkflowCancelledError(step.id);
1554
1659
  const b = view(ctx, item, index);
1555
1660
  if (step.kind !== "agent" && step.kind !== "tool" && step.kind !== "artifact") {
1556
1661
  ctx.onStepStart?.(step.id);
@@ -1558,15 +1663,20 @@ async function execStepBody(step, ctx, item, index) {
1558
1663
  switch (step.kind) {
1559
1664
  case "tool": {
1560
1665
  const input = step.input(b);
1561
- const runIt = () => runTool({
1562
- tool: step.tool,
1563
- candidates: step.candidates,
1564
- input,
1565
- context: step.context ? step.context(b) : void 0,
1566
- resolverContext: step.resolverContext,
1567
- secrets: step.secrets,
1568
- signal
1569
- });
1666
+ const runIt = () => withStepTimeout(
1667
+ step.id,
1668
+ step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS,
1669
+ signal,
1670
+ (sig) => runTool({
1671
+ tool: step.tool,
1672
+ candidates: step.candidates,
1673
+ input,
1674
+ context: step.context ? step.context(b) : void 0,
1675
+ resolverContext: step.resolverContext,
1676
+ secrets: step.secrets,
1677
+ signal: sig
1678
+ })
1679
+ );
1570
1680
  if (!isCacheEnabled(ctx, step)) {
1571
1681
  ctx.onStepStart?.(step.id);
1572
1682
  return runIt();
@@ -1610,15 +1720,20 @@ async function execStepBody(step, ctx, item, index) {
1610
1720
  }
1611
1721
  };
1612
1722
  const worker = async () => {
1613
- while (!failed && breaker.open === void 0 && next < arr.length) {
1723
+ while (!failed && breaker.open === void 0 && signal?.aborted !== true && next < arr.length) {
1614
1724
  const idx = next++;
1615
1725
  await runItem(idx);
1616
1726
  }
1617
1727
  };
1618
1728
  await Promise.all(Array.from({ length: Math.min(parallelism, arr.length) }, () => worker()));
1729
+ if (signal?.aborted === true && next < arr.length) {
1730
+ if (!tolerant) throw new WorkflowCancelledError(step.id);
1731
+ skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], "run-cancelled", "run cancelled", results);
1732
+ return tolerantResult(results, breaker.open);
1733
+ }
1619
1734
  if (!tolerant) return results;
1620
1735
  if (breaker.open !== void 0) {
1621
- skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], breaker.open, results);
1736
+ skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], "circuit-open", breaker.open, results);
1622
1737
  }
1623
1738
  return tolerantResult(results, breaker.open);
1624
1739
  }
@@ -1653,12 +1768,26 @@ async function execStepBody(step, ctx, item, index) {
1653
1768
  }
1654
1769
  };
1655
1770
  const worker = async () => {
1656
- while ((!tolerant || breaker.open === void 0) && next < items.length) {
1771
+ while ((!tolerant || breaker.open === void 0) && signal?.aborted !== true && next < items.length) {
1657
1772
  const idx = next++;
1658
1773
  await runItem(idx);
1659
1774
  }
1660
1775
  };
1661
1776
  await Promise.all(Array.from({ length: Math.min(cap, items.length) }, () => worker()));
1777
+ if (signal?.aborted === true && next < items.length) {
1778
+ if (!tolerant) throw new WorkflowCancelledError(step.id);
1779
+ skipUnstartedItems(
1780
+ ctx,
1781
+ step.id,
1782
+ next,
1783
+ items,
1784
+ (el, idx) => step.stages.map((stage) => stage(el, idx, void 0, view(ctx, el, idx))),
1785
+ "run-cancelled",
1786
+ "run cancelled",
1787
+ results
1788
+ );
1789
+ return tolerantResult(results, breaker.open);
1790
+ }
1662
1791
  if (!tolerant) return results;
1663
1792
  if (breaker.open !== void 0) {
1664
1793
  skipUnstartedItems(
@@ -1667,6 +1796,7 @@ async function execStepBody(step, ctx, item, index) {
1667
1796
  next,
1668
1797
  items,
1669
1798
  (el, idx) => step.stages.map((stage) => stage(el, idx, void 0, view(ctx, el, idx))),
1799
+ "circuit-open",
1670
1800
  breaker.open,
1671
1801
  results
1672
1802
  );
@@ -1950,6 +2080,6 @@ function compileWorkflowManifest(source, opts) {
1950
2080
  return compileWorkflow(handle, opts);
1951
2081
  }
1952
2082
 
1953
- export { AgentInputRequiredError, AgentSpawnError, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, MissingArtifactError, NodeFsPort, StepOutcomeError, WorkflowCompileError, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, isCompilableJsonSchema, materializeKnowledge, normalizeWorkflowInputsSchema, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow, validateAgainstJsonSchema, validateWorkflowInput };
2083
+ export { AgentInputRequiredError, AgentSpawnError, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, DEFAULT_STEP_TIMEOUT_MS, MissingArtifactError, NodeFsPort, StepOutcomeError, WorkflowCancelledError, WorkflowCompileError, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, isCompilableJsonSchema, materializeKnowledge, normalizeWorkflowInputsSchema, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow, validateAgainstJsonSchema, validateWorkflowInput };
1954
2084
  //# sourceMappingURL=index.mjs.map
1955
2085
  //# sourceMappingURL=index.mjs.map