@agentproto/workflow-runtime 0.14.0 → 0.16.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 {
@@ -104,6 +110,10 @@ interface TolerantFanOutResult<T = unknown> {
104
110
  /** Default for {@link MapStep.maxConsecutiveSpawnFailures} /
105
111
  * {@link PipelineStep.maxConsecutiveSpawnFailures}. */
106
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;
107
117
  /**
108
118
  * Run a sub-step once per element of an array, optionally with bounded
109
119
  * concurrency. The element + index are exposed to the body via `bindings.item`
@@ -494,7 +504,9 @@ interface GateStep {
494
504
  /** Path (relative to `cwd`), of a JSON report file, consulted when stdout
495
505
  * doesn't itself parse as JSON. */
496
506
  reportPath?: string;
497
- /** 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). */
498
510
  timeoutMs?: number;
499
511
  /** Re-run on a failing exit code, up to `maxAttempts` (default 1 = no
500
512
  * retry — a single attempt, fail immediately on a non-zero exit). */
@@ -1203,4 +1215,4 @@ declare class NodeFsPort implements FsPort {
1203
1215
  lock(): Promise<FsLockHandle>;
1204
1216
  }
1205
1217
 
1206
- 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, WorkflowCancelledError, 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) {
@@ -1448,23 +1460,83 @@ function tryParseJson(text) {
1448
1460
  return void 0;
1449
1461
  }
1450
1462
  }
1463
+ var GATE_STDIO_DRAIN_MS = 2e3;
1464
+ var GATE_KILL_GRACE_MS = 2e3;
1451
1465
  function defaultRunGateCommand(spec) {
1466
+ const timeoutMs = spec.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
1452
1467
  return new Promise((resolve2) => {
1453
- execFile(
1454
- spec.command,
1455
- [...spec.args],
1456
- { cwd: spec.cwd, timeout: spec.timeoutMs, maxBuffer: 10 * 1024 * 1024, signal: spec.signal },
1457
- (err, stdout, stderr) => {
1458
- if (!err) {
1459
- resolve2({ exitCode: 0, stdout, stderr });
1460
- return;
1461
- }
1462
- const nodeErr = err;
1463
- const timedOut = nodeErr.killed === true && nodeErr.signal !== void 0 && spec.timeoutMs !== void 0;
1464
- const exitCode = typeof nodeErr.code === "number" ? nodeErr.code : 1;
1465
- 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 {
1466
1487
  }
1467
- );
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
+ });
1468
1540
  });
1469
1541
  }
1470
1542
  async function resolveGateReport(cmd, cwd, reportPath) {
@@ -1545,6 +1617,27 @@ Please address the findings above, then reply when done.`
1545
1617
  }
1546
1618
  return last;
1547
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
+ }
1548
1641
  function cacheHit(ctx, step, output) {
1549
1642
  ctx.state.cachedHits.add(cachedHitKey(ctx, step.id));
1550
1643
  ctx.onStepStart?.(step.id, { cached: true });
@@ -1570,15 +1663,20 @@ async function execStepBody(step, ctx, item, index) {
1570
1663
  switch (step.kind) {
1571
1664
  case "tool": {
1572
1665
  const input = step.input(b);
1573
- const runIt = () => runTool({
1574
- tool: step.tool,
1575
- candidates: step.candidates,
1576
- input,
1577
- context: step.context ? step.context(b) : void 0,
1578
- resolverContext: step.resolverContext,
1579
- secrets: step.secrets,
1580
- signal
1581
- });
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
+ );
1582
1680
  if (!isCacheEnabled(ctx, step)) {
1583
1681
  ctx.onStepStart?.(step.id);
1584
1682
  return runIt();
@@ -1982,6 +2080,6 @@ function compileWorkflowManifest(source, opts) {
1982
2080
  return compileWorkflow(handle, opts);
1983
2081
  }
1984
2082
 
1985
- export { AgentInputRequiredError, AgentSpawnError, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, MissingArtifactError, NodeFsPort, StepOutcomeError, WorkflowCancelledError, 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 };
1986
2084
  //# sourceMappingURL=index.mjs.map
1987
2085
  //# sourceMappingURL=index.mjs.map