@agentproto/workflow-runtime 0.14.0 → 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 +32 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.mjs +125 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
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 {
|
|
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 =
|
|
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
|
-
|
|
1454
|
-
spec.
|
|
1455
|
-
[
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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 = () =>
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|