@agentproto/workflow-runtime 0.13.0 → 0.14.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 +51 -14
- package/dist/index.d.ts +60 -14
- package/dist/index.mjs +144 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -301,9 +301,13 @@ try {
|
|
|
301
301
|
|
|
302
302
|
This is a manual retry, not a resumable run object — `runWorkflow` has no
|
|
303
303
|
notion of "the run that failed"; the cacheKey is just a namespace the caller
|
|
304
|
-
re-supplies. A first-class `run.retry
|
|
305
|
-
|
|
306
|
-
|
|
304
|
+
re-supplies. A first-class `run.retry` verb that resumes a named run without
|
|
305
|
+
the caller re-threading `workflow`/`input`/`cacheKey` by hand — AIP-58 P5 —
|
|
306
|
+
is a HOST concern, not something this transport-agnostic package implements
|
|
307
|
+
itself: see `@agentproto/runtime`'s `WorkflowRunner.retry()` / the
|
|
308
|
+
`workflow_retry` MCP tool, which owns runId allocation and an always-on
|
|
309
|
+
internal journal (so it works even when the original run never passed a
|
|
310
|
+
`cacheKey` at all — every step it runs is journaled internally either way).
|
|
307
311
|
|
|
308
312
|
### Run workspace (AIP-58 §4)
|
|
309
313
|
|
|
@@ -347,12 +351,18 @@ steps:
|
|
|
347
351
|
|
|
348
352
|
`path` is read relative to `$run.workspace` (absolute paths, and any path
|
|
349
353
|
that would resolve OUTSIDE the workspace, throw). The step hashes
|
|
350
|
-
(`sha256`) and sizes the file, copies it to `artifactsDir/<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
(`sha256`) and sizes the file, copies it to `artifactsDir/<basename of path,
|
|
355
|
+
sanitized>` — `path: "briefs/latest.md"` above lands at
|
|
356
|
+
`artifacts/latest.md`, keeping the extension, NOT the bare key
|
|
357
|
+
(`artifacts/brief`) — and binds/reports an `ArtifactEntry` — `{ key, path:
|
|
358
|
+
"artifacts/<basename>", sha256, size, stepId, contentType? }` (`path` here is
|
|
359
|
+
relative to the RUN WORKSPACE ROOT, the parent of `$run.workspace` itself —
|
|
360
|
+
not the source location; always read it back rather than assuming a name).
|
|
361
|
+
If two keys' files share a basename, the second one claimed is prefixed with
|
|
362
|
+
its own sanitized key (`artifacts/<key>-<basename>`) instead of silently
|
|
363
|
+
overwriting the first — deterministic, same result run to run. Pass
|
|
364
|
+
`onArtifact` to `runWorkflow` to observe every one recorded, cache hit or
|
|
365
|
+
fresh.
|
|
356
366
|
|
|
357
367
|
A declarative WORKFLOW.md manifest may instead declare **`outputsFiles`**
|
|
358
368
|
(AIP-16, amended by AIP-58 §4 with `required`) at the top level — checked
|
|
@@ -400,11 +410,38 @@ relocates (copies) the file from the ORIGINAL run's `artifactsDir` into the
|
|
|
400
410
|
CURRENT run's own before returning — the two runs' workspaces stay fully
|
|
401
411
|
disjoint on disk; only the *bytes* are reused, exactly the same way
|
|
402
412
|
`run.replay`'s journal-sourced step reuse is specified to work (AIP-58 §6).
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
|
|
414
|
+
A plain cacheable `tool`/`agent` step gets the same treatment, not a narrower
|
|
415
|
+
one (this used to be a documented gap — see "Cache key vs. the run
|
|
416
|
+
workspace" below for why it had to stop being one):
|
|
417
|
+
|
|
418
|
+
- **Hashing ignores the workspace's identity.** `hashResolvedInputs`
|
|
419
|
+
replaces every occurrence of `ctx.workspace` inside the serialized
|
|
420
|
+
resolved input/prompt (including trailing subpaths, e.g.
|
|
421
|
+
`<workspace>/cleaned/out.txt`) with a stable placeholder before hashing.
|
|
422
|
+
Two runs of the same logical step under the same `cacheKey` hash
|
|
423
|
+
identically even though AIP-58 §4 gives each one a fresh, disjoint
|
|
424
|
+
workspace directory — without this, ANY step whose input/prompt names
|
|
425
|
+
`$run.workspace` / `{{run.workspace}}` / `_workflowFsRoot` could never hit
|
|
426
|
+
the journal at all.
|
|
427
|
+
- **A hit relocates forward.** On a hit, every workspace-relative file/
|
|
428
|
+
directory the entry recorded (`StepCacheEntry.workspaceFiles` — collected,
|
|
429
|
+
best-effort, from every string in the step's output that resolved to a
|
|
430
|
+
real path under the ORIGINAL run's workspace) is copied into the matching
|
|
431
|
+
path under the CURRENT run's own; the recorded output's path strings are
|
|
432
|
+
then rewritten from the original workspace onto the current one. A
|
|
433
|
+
downstream step whose input reads one of those paths (`$steps.<id>.path`)
|
|
434
|
+
finds the bytes there, not just the (by-then-gone) original run's.
|
|
435
|
+
Best-effort, same posture as the `artifact` step's own relocation: a
|
|
436
|
+
source already swept by a host's `scratch/` retention policy is not this
|
|
437
|
+
run's problem to recover.
|
|
438
|
+
- **Still no cross-step content hashing.** This closes the *workspace-path*
|
|
439
|
+
gap only — the journal still hashes each step's own resolved inputs, not
|
|
440
|
+
a transitive hash of everything upstream. A step whose resolved input is
|
|
441
|
+
an opaque reference that stays textually identical across runs (a fixed
|
|
442
|
+
relative filename, an id) replays from cache on that basis alone,
|
|
443
|
+
regardless of whether the value behind that reference changed upstream —
|
|
444
|
+
same limitation any resolved-input-hash cache has, workspace or not.
|
|
408
445
|
|
|
409
446
|
### Step lifecycle callbacks
|
|
410
447
|
|
package/dist/index.d.ts
CHANGED
|
@@ -74,12 +74,14 @@ type FanOutOutcome<T = unknown> = {
|
|
|
74
74
|
/** `err.message` if the throw was an `Error`, else `String(err)`. */
|
|
75
75
|
readonly error: string;
|
|
76
76
|
} | {
|
|
77
|
-
/** Never started: the fan-out's spawn circuit breaker opened
|
|
78
|
-
* (see {@link MapStep.maxConsecutiveSpawnFailures})
|
|
77
|
+
/** Never started: either the fan-out's spawn circuit breaker opened
|
|
78
|
+
* first (see {@link MapStep.maxConsecutiveSpawnFailures}), or the run
|
|
79
|
+
* was cancelled while earlier items were still in flight. */
|
|
79
80
|
readonly status: "skipped";
|
|
80
81
|
readonly index: number;
|
|
81
82
|
readonly item: unknown;
|
|
82
|
-
/** `circuit-open: <first error of the failure streak
|
|
83
|
+
/** `circuit-open: <first error of the failure streak>`, or
|
|
84
|
+
* `run-cancelled: run cancelled`. */
|
|
83
85
|
readonly reason: string;
|
|
84
86
|
};
|
|
85
87
|
/**
|
|
@@ -431,7 +433,11 @@ interface AgentStep {
|
|
|
431
433
|
interface ArtifactStep {
|
|
432
434
|
kind: "artifact";
|
|
433
435
|
id: string;
|
|
434
|
-
/** Artifact key —
|
|
436
|
+
/** Artifact key — identifies this artifact for `workflow_artifact_get`/
|
|
437
|
+
* `workflow_publish`. Its on-disk filename under `artifactsDir` is
|
|
438
|
+
* `path`'s own basename (sanitized), not the key — see {@link
|
|
439
|
+
* ArtifactEntry.path} — unless another key's file shares the same
|
|
440
|
+
* basename, in which case the key disambiguates it (F42). */
|
|
435
441
|
key: Selector<string> | string;
|
|
436
442
|
/** Path to the source file. Relative to `$run.workspace`; MUST resolve
|
|
437
443
|
* inside it (an absolute path or a `..`-escaping relative one throws). */
|
|
@@ -440,10 +446,15 @@ interface ArtifactStep {
|
|
|
440
446
|
}
|
|
441
447
|
/**
|
|
442
448
|
* AIP-58 §4/§1 `ArtifactEntry` — one run-scoped copy of a declared output
|
|
443
|
-
* file. `path` is
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
449
|
+
* file. `path` is `"artifacts/<basename>"` (F42: the declared source file's
|
|
450
|
+
* OWN basename, sanitized — e.g. `outputsFiles.pdf: {path: transcript.pdf}`
|
|
451
|
+
* ⇒ `artifacts/transcript.pdf` — keeping the extension, unlike the bare key),
|
|
452
|
+
* relative to the RUN WORKSPACE ROOT (`<runsRoot>/<runId>/`, the parent of
|
|
453
|
+
* `$run.workspace` itself) — never the original in-workspace location the
|
|
454
|
+
* file was read from. Two keys whose files share a basename get the SECOND
|
|
455
|
+
* one's name prefixed with its own sanitized key instead (deterministic,
|
|
456
|
+
* never a silent overwrite) — always read `path` back rather than assuming
|
|
457
|
+
* `artifacts/<key>` or `artifacts/<basename(path)>`.
|
|
447
458
|
*/
|
|
448
459
|
interface ArtifactEntry {
|
|
449
460
|
key: string;
|
|
@@ -516,6 +527,12 @@ type GateCommandRunner = (spec: {
|
|
|
516
527
|
args: readonly string[];
|
|
517
528
|
cwd: string;
|
|
518
529
|
timeoutMs?: number;
|
|
530
|
+
/** The run's cancel signal — a cancelled run kills an in-flight gate
|
|
531
|
+
* command's child process the same way it kills an agent step's session.
|
|
532
|
+
* A host-injected runner should pass this through to its own subprocess
|
|
533
|
+
* spawn (e.g. `child_process.execFile`'s `signal` option) so the process
|
|
534
|
+
* is signalled, not merely orphaned. */
|
|
535
|
+
signal?: AbortSignal;
|
|
519
536
|
}) => Promise<GateCommandResult>;
|
|
520
537
|
/** One `kind: "gate"` command attempt's outcome, reported as it happens
|
|
521
538
|
* (every attempt, not just the last) — the `gate-report` lifecycle event. */
|
|
@@ -691,6 +708,20 @@ interface StepCacheEntry {
|
|
|
691
708
|
* §4 forbids two runs sharing a workspace) copies the file forward from
|
|
692
709
|
* here into the new run's own `artifactsDir` instead of re-declaring it. */
|
|
693
710
|
artifactsDirAtCache?: string;
|
|
711
|
+
/** Set only for a cacheable `tool`/`agent` step (see `hashResolvedInputs`/
|
|
712
|
+
* `buildCacheEntry` in `run-workflow.ts`) whenever the host wires a run
|
|
713
|
+
* workspace: the absolute `$run.workspace` / `_workflowFsRoot` path this
|
|
714
|
+
* entry was cached under. A hit in a LATER run (a different workspace —
|
|
715
|
+
* AIP-58 §4 forbids two runs sharing one) rewrites `output`'s path
|
|
716
|
+
* strings from here onto the new run's own, and relocates
|
|
717
|
+
* {@link workspaceFiles} the same way. */
|
|
718
|
+
workspaceAtCache?: string;
|
|
719
|
+
/** Workspace-relative files/directories (under {@link workspaceAtCache})
|
|
720
|
+
* this entry's `output` pointed at and that existed on disk when the
|
|
721
|
+
* entry was written — copied forward into a later cache-hit run's own
|
|
722
|
+
* workspace so a downstream step reading one of these paths finds the
|
|
723
|
+
* bytes there too, not just in the original (by-then-gone) run's. */
|
|
724
|
+
workspaceFiles?: readonly string[];
|
|
694
725
|
}
|
|
695
726
|
/** Opt-in journal for cacheable steps. Host-injected; file-backed in the runtime. */
|
|
696
727
|
interface StepCache {
|
|
@@ -710,13 +741,16 @@ interface StepSkippedInfo {
|
|
|
710
741
|
/** `"branch-not-taken"`: the step sits in an untaken `branch` arm.
|
|
711
742
|
* `"circuit-open"`: a `map`/`pipeline` item never started because the
|
|
712
743
|
* fan-out's spawn circuit breaker opened
|
|
713
|
-
* ({@link MapStep.maxConsecutiveSpawnFailures}).
|
|
714
|
-
|
|
744
|
+
* ({@link MapStep.maxConsecutiveSpawnFailures}). `"run-cancelled"`: a
|
|
745
|
+
* `map`/`pipeline` item never started because the run was cancelled
|
|
746
|
+
* (`RunWorkflowArgs.signal` aborted) while earlier items were still in
|
|
747
|
+
* flight. */
|
|
748
|
+
reason: "branch-not-taken" | "circuit-open" | "run-cancelled";
|
|
715
749
|
/** Id of the authored step whose decision skipped the step — the `branch`
|
|
716
|
-
* step, or (circuit-open) the `map`/`pipeline` step. */
|
|
750
|
+
* step, or (circuit-open/run-cancelled) the `map`/`pipeline` step. */
|
|
717
751
|
branchId: string;
|
|
718
|
-
/** `"circuit-open"
|
|
719
|
-
*
|
|
752
|
+
/** `"circuit-open"`: the first error of the spawn-failure streak that
|
|
753
|
+
* tripped the breaker. `"run-cancelled"`: a fixed "run cancelled" message. */
|
|
720
754
|
message?: string;
|
|
721
755
|
}
|
|
722
756
|
/** What `onStepFailed` reports. */
|
|
@@ -875,6 +909,18 @@ declare class AgentSpawnError extends Error {
|
|
|
875
909
|
readonly cause: unknown;
|
|
876
910
|
constructor(stepId: string, cause: unknown);
|
|
877
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Thrown instead of dispatching a step whose run was cancelled
|
|
914
|
+
* (`ctx.signal.aborted`) — checked BEFORE any work for the step begins (see
|
|
915
|
+
* {@link execStepBody}'s entry guard), so a cancel stops the run from
|
|
916
|
+
* starting any further step, including a sibling within the SAME stage/
|
|
917
|
+
* fan-out, not just a later one. Never journaled: it's thrown before a
|
|
918
|
+
* cacheable step's `cache.set` call, so `workflow_retry` re-executes it.
|
|
919
|
+
*/
|
|
920
|
+
declare class WorkflowCancelledError extends Error {
|
|
921
|
+
readonly stepId: string;
|
|
922
|
+
constructor(stepId: string);
|
|
923
|
+
}
|
|
878
924
|
declare function runWorkflow(args: RunWorkflowArgs): Promise<WorkflowRunResult>;
|
|
879
925
|
|
|
880
926
|
/**
|
|
@@ -1157,4 +1203,4 @@ declare class NodeFsPort implements FsPort {
|
|
|
1157
1203
|
lock(): Promise<FsLockHandle>;
|
|
1158
1204
|
}
|
|
1159
1205
|
|
|
1160
|
-
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 };
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runTool } from '@agentproto/driver';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
|
-
import { stat, readFile, mkdir, writeFile, appendFile, readdir, copyFile } from 'fs/promises';
|
|
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';
|
|
7
7
|
import { assertKnownStepRefs } from '@agentproto/workflow';
|
|
@@ -1014,6 +1014,14 @@ var AgentSpawnError = class extends Error {
|
|
|
1014
1014
|
stepId;
|
|
1015
1015
|
cause;
|
|
1016
1016
|
};
|
|
1017
|
+
var WorkflowCancelledError = class extends Error {
|
|
1018
|
+
constructor(stepId) {
|
|
1019
|
+
super(`step '${stepId}': run cancelled \u2014 not started`);
|
|
1020
|
+
this.stepId = stepId;
|
|
1021
|
+
this.name = "WorkflowCancelledError";
|
|
1022
|
+
}
|
|
1023
|
+
stepId;
|
|
1024
|
+
};
|
|
1017
1025
|
function withReleaseScope(ctx) {
|
|
1018
1026
|
return { ...ctx, spawned: [] };
|
|
1019
1027
|
}
|
|
@@ -1079,9 +1087,9 @@ function spawnBreaker(threshold) {
|
|
|
1079
1087
|
}
|
|
1080
1088
|
};
|
|
1081
1089
|
}
|
|
1082
|
-
function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, reason, results) {
|
|
1090
|
+
function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, kind, reason, results) {
|
|
1083
1091
|
for (let idx = from; idx < items.length; idx++) {
|
|
1084
|
-
results[idx] = { status: "skipped", index: idx, item: items[idx], reason:
|
|
1092
|
+
results[idx] = { status: "skipped", index: idx, item: items[idx], reason: `${kind}: ${reason}` };
|
|
1085
1093
|
const itemCtx = withIndexedHooks(ctx, idx);
|
|
1086
1094
|
if (!itemCtx.onStepSkipped) continue;
|
|
1087
1095
|
let bodies;
|
|
@@ -1091,7 +1099,7 @@ function skipUnstartedItems(ctx, fanOutId, from, items, bodiesOf, reason, result
|
|
|
1091
1099
|
continue;
|
|
1092
1100
|
}
|
|
1093
1101
|
for (const id of new Set(skippableStepIds(bodies))) {
|
|
1094
|
-
itemCtx.onStepSkipped(id, { reason:
|
|
1102
|
+
itemCtx.onStepSkipped(id, { reason: kind, branchId: fanOutId, message: reason });
|
|
1095
1103
|
}
|
|
1096
1104
|
}
|
|
1097
1105
|
}
|
|
@@ -1131,8 +1139,74 @@ function spentUsd(state) {
|
|
|
1131
1139
|
for (const c of state.costBySession.values()) total += c;
|
|
1132
1140
|
return total;
|
|
1133
1141
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1142
|
+
var WORKSPACE_HASH_PLACEHOLDER = "\0$run.workspace\0";
|
|
1143
|
+
function hashResolvedInputs(kind, resolved, workspace) {
|
|
1144
|
+
const serialized = `${kind}\0${JSON.stringify(resolved) ?? "undefined"}`;
|
|
1145
|
+
const normalized = workspace ? serialized.split(workspace).join(WORKSPACE_HASH_PLACEHOLDER) : serialized;
|
|
1146
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
1147
|
+
}
|
|
1148
|
+
function rewriteWorkspacePaths(value, from, to) {
|
|
1149
|
+
if (typeof value === "string") return value.split(from).join(to);
|
|
1150
|
+
if (Array.isArray(value)) return value.map((v) => rewriteWorkspacePaths(v, from, to));
|
|
1151
|
+
if (value !== null && typeof value === "object" && value.constructor === Object) {
|
|
1152
|
+
const out = {};
|
|
1153
|
+
for (const [k, v] of Object.entries(value)) out[k] = rewriteWorkspacePaths(v, from, to);
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
return value;
|
|
1157
|
+
}
|
|
1158
|
+
function collectWorkspaceCandidates(value, workspace, out) {
|
|
1159
|
+
if (typeof value === "string") {
|
|
1160
|
+
if (value === workspace || value.startsWith(`${workspace}/`)) {
|
|
1161
|
+
const rel = relative(workspace, value);
|
|
1162
|
+
if (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)) out.add(rel);
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (Array.isArray(value)) {
|
|
1167
|
+
for (const v of value) collectWorkspaceCandidates(v, workspace, out);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (value !== null && typeof value === "object" && value.constructor === Object) {
|
|
1171
|
+
for (const v of Object.values(value)) collectWorkspaceCandidates(v, workspace, out);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async function collectWorkspaceFiles(out, workspace) {
|
|
1175
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1176
|
+
collectWorkspaceCandidates(out, workspace, candidates);
|
|
1177
|
+
const files = [];
|
|
1178
|
+
for (const rel of candidates) {
|
|
1179
|
+
try {
|
|
1180
|
+
await stat(join(workspace, rel));
|
|
1181
|
+
files.push(rel);
|
|
1182
|
+
} catch {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return files;
|
|
1186
|
+
}
|
|
1187
|
+
async function buildCacheEntry(ctx, out, hash) {
|
|
1188
|
+
if (ctx.workspace === void 0) return { output: out, resolvedInputHash: hash };
|
|
1189
|
+
const workspaceFiles = await collectWorkspaceFiles(out, ctx.workspace);
|
|
1190
|
+
return {
|
|
1191
|
+
output: out,
|
|
1192
|
+
resolvedInputHash: hash,
|
|
1193
|
+
workspaceAtCache: ctx.workspace,
|
|
1194
|
+
...workspaceFiles.length > 0 ? { workspaceFiles } : {}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
async function relocateCachedOutput(ctx, entry) {
|
|
1198
|
+
const from = entry.workspaceAtCache;
|
|
1199
|
+
const to = ctx.workspace;
|
|
1200
|
+
if (from === void 0 || to === void 0 || from === to) return entry.output;
|
|
1201
|
+
for (const rel of entry.workspaceFiles ?? []) {
|
|
1202
|
+
const dest = join(to, rel);
|
|
1203
|
+
try {
|
|
1204
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
1205
|
+
await cp(join(from, rel), dest, { recursive: true });
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return rewriteWorkspacePaths(entry.output, from, to);
|
|
1136
1210
|
}
|
|
1137
1211
|
function stepJournalKey(ctx, step) {
|
|
1138
1212
|
return `${ctx.cacheKey}\0${step.id}\0${step.kind}${ctx.cacheKeySuffix ?? ""}`;
|
|
@@ -1149,10 +1223,10 @@ function completeStep(ctx, stepId, out) {
|
|
|
1149
1223
|
}
|
|
1150
1224
|
async function readStepCache(ctx, step, resolvedInputs) {
|
|
1151
1225
|
const key = stepJournalKey(ctx, step);
|
|
1152
|
-
const hash = hashResolvedInputs(step.kind, resolvedInputs);
|
|
1226
|
+
const hash = hashResolvedInputs(step.kind, resolvedInputs, ctx.workspace);
|
|
1153
1227
|
const entry = await ctx.cache.get(key);
|
|
1154
1228
|
if (entry !== void 0 && entry.resolvedInputHash === hash) {
|
|
1155
|
-
return { hit: true, output: entry
|
|
1229
|
+
return { hit: true, output: await relocateCachedOutput(ctx, entry) };
|
|
1156
1230
|
}
|
|
1157
1231
|
return { hit: false, key, hash };
|
|
1158
1232
|
}
|
|
@@ -1267,7 +1341,7 @@ ${inner(b2)}` };
|
|
|
1267
1341
|
} : void 0;
|
|
1268
1342
|
let sessionId;
|
|
1269
1343
|
if (step.adapter) {
|
|
1270
|
-
if (ctx.signal?.aborted) throw new
|
|
1344
|
+
if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
|
|
1271
1345
|
try {
|
|
1272
1346
|
sessionId = await ctx.agents.spawn(resolveSel(step.adapter, b), {
|
|
1273
1347
|
cwd,
|
|
@@ -1379,7 +1453,7 @@ function defaultRunGateCommand(spec) {
|
|
|
1379
1453
|
execFile(
|
|
1380
1454
|
spec.command,
|
|
1381
1455
|
[...spec.args],
|
|
1382
|
-
{ cwd: spec.cwd, timeout: spec.timeoutMs, maxBuffer: 10 * 1024 * 1024 },
|
|
1456
|
+
{ cwd: spec.cwd, timeout: spec.timeoutMs, maxBuffer: 10 * 1024 * 1024, signal: spec.signal },
|
|
1383
1457
|
(err, stdout, stderr) => {
|
|
1384
1458
|
if (!err) {
|
|
1385
1459
|
resolve2({ exitCode: 0, stdout, stderr });
|
|
@@ -1419,6 +1493,7 @@ async function execGateStep(step, ctx, b) {
|
|
|
1419
1493
|
const maxAttempts = Math.max(1, step.retry?.maxAttempts ?? 1);
|
|
1420
1494
|
let last;
|
|
1421
1495
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1496
|
+
if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
|
|
1422
1497
|
if (attempt > 1 && step.onFail?.reprompt) {
|
|
1423
1498
|
if (!ctx.agents) {
|
|
1424
1499
|
throw new Error(`step '${step.id}': on_fail.reprompt requires a host agents implementation`);
|
|
@@ -1450,8 +1525,10 @@ Please address the findings above, then reply when done.`
|
|
|
1450
1525
|
command: step.command,
|
|
1451
1526
|
args,
|
|
1452
1527
|
cwd,
|
|
1453
|
-
timeoutMs: step.timeoutMs
|
|
1528
|
+
timeoutMs: step.timeoutMs,
|
|
1529
|
+
signal: ctx.signal
|
|
1454
1530
|
});
|
|
1531
|
+
if (ctx.signal?.aborted) throw new WorkflowCancelledError(step.id);
|
|
1455
1532
|
const report = await resolveGateReport(cmdResult, cwd, step.reportPath);
|
|
1456
1533
|
const ok = cmdResult.exitCode === 0;
|
|
1457
1534
|
last = { ok, exitCode: cmdResult.exitCode, report };
|
|
@@ -1485,6 +1562,7 @@ async function execStep(step, ctx, item, index) {
|
|
|
1485
1562
|
}
|
|
1486
1563
|
async function execStepBody(step, ctx, item, index) {
|
|
1487
1564
|
const { state, signal } = ctx;
|
|
1565
|
+
if (signal?.aborted) throw new WorkflowCancelledError(step.id);
|
|
1488
1566
|
const b = view(ctx, item, index);
|
|
1489
1567
|
if (step.kind !== "agent" && step.kind !== "tool" && step.kind !== "artifact") {
|
|
1490
1568
|
ctx.onStepStart?.(step.id);
|
|
@@ -1509,7 +1587,7 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1509
1587
|
if (c.hit) return cacheHit(ctx, step, c.output);
|
|
1510
1588
|
ctx.onStepStart?.(step.id);
|
|
1511
1589
|
const out = await runIt();
|
|
1512
|
-
await ctx.cache.set(c.key,
|
|
1590
|
+
await ctx.cache.set(c.key, await buildCacheEntry(ctx, out, c.hash));
|
|
1513
1591
|
return out;
|
|
1514
1592
|
}
|
|
1515
1593
|
case "transform":
|
|
@@ -1544,15 +1622,20 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1544
1622
|
}
|
|
1545
1623
|
};
|
|
1546
1624
|
const worker = async () => {
|
|
1547
|
-
while (!failed && breaker.open === void 0 && next < arr.length) {
|
|
1625
|
+
while (!failed && breaker.open === void 0 && signal?.aborted !== true && next < arr.length) {
|
|
1548
1626
|
const idx = next++;
|
|
1549
1627
|
await runItem(idx);
|
|
1550
1628
|
}
|
|
1551
1629
|
};
|
|
1552
1630
|
await Promise.all(Array.from({ length: Math.min(parallelism, arr.length) }, () => worker()));
|
|
1631
|
+
if (signal?.aborted === true && next < arr.length) {
|
|
1632
|
+
if (!tolerant) throw new WorkflowCancelledError(step.id);
|
|
1633
|
+
skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], "run-cancelled", "run cancelled", results);
|
|
1634
|
+
return tolerantResult(results, breaker.open);
|
|
1635
|
+
}
|
|
1553
1636
|
if (!tolerant) return results;
|
|
1554
1637
|
if (breaker.open !== void 0) {
|
|
1555
|
-
skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], breaker.open, results);
|
|
1638
|
+
skipUnstartedItems(ctx, step.id, next, arr, (el, idx) => [step.body(el, idx, view(ctx, el, idx))], "circuit-open", breaker.open, results);
|
|
1556
1639
|
}
|
|
1557
1640
|
return tolerantResult(results, breaker.open);
|
|
1558
1641
|
}
|
|
@@ -1587,12 +1670,26 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1587
1670
|
}
|
|
1588
1671
|
};
|
|
1589
1672
|
const worker = async () => {
|
|
1590
|
-
while ((!tolerant || breaker.open === void 0) && next < items.length) {
|
|
1673
|
+
while ((!tolerant || breaker.open === void 0) && signal?.aborted !== true && next < items.length) {
|
|
1591
1674
|
const idx = next++;
|
|
1592
1675
|
await runItem(idx);
|
|
1593
1676
|
}
|
|
1594
1677
|
};
|
|
1595
1678
|
await Promise.all(Array.from({ length: Math.min(cap, items.length) }, () => worker()));
|
|
1679
|
+
if (signal?.aborted === true && next < items.length) {
|
|
1680
|
+
if (!tolerant) throw new WorkflowCancelledError(step.id);
|
|
1681
|
+
skipUnstartedItems(
|
|
1682
|
+
ctx,
|
|
1683
|
+
step.id,
|
|
1684
|
+
next,
|
|
1685
|
+
items,
|
|
1686
|
+
(el, idx) => step.stages.map((stage) => stage(el, idx, void 0, view(ctx, el, idx))),
|
|
1687
|
+
"run-cancelled",
|
|
1688
|
+
"run cancelled",
|
|
1689
|
+
results
|
|
1690
|
+
);
|
|
1691
|
+
return tolerantResult(results, breaker.open);
|
|
1692
|
+
}
|
|
1596
1693
|
if (!tolerant) return results;
|
|
1597
1694
|
if (breaker.open !== void 0) {
|
|
1598
1695
|
skipUnstartedItems(
|
|
@@ -1601,6 +1698,7 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1601
1698
|
next,
|
|
1602
1699
|
items,
|
|
1603
1700
|
(el, idx) => step.stages.map((stage) => stage(el, idx, void 0, view(ctx, el, idx))),
|
|
1701
|
+
"circuit-open",
|
|
1604
1702
|
breaker.open,
|
|
1605
1703
|
results
|
|
1606
1704
|
);
|
|
@@ -1681,7 +1779,8 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1681
1779
|
cacheKey: ctx.cacheKey,
|
|
1682
1780
|
runGateCommand: ctx.runGateCommand,
|
|
1683
1781
|
onGateReport: ctx.onGateReport,
|
|
1684
|
-
spawned: ctx.spawned
|
|
1782
|
+
spawned: ctx.spawned,
|
|
1783
|
+
usedArtifactNames: ctx.usedArtifactNames
|
|
1685
1784
|
});
|
|
1686
1785
|
return child.output;
|
|
1687
1786
|
}
|
|
@@ -1697,7 +1796,7 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1697
1796
|
const c = await readStepCache(ctx, step, resolved);
|
|
1698
1797
|
if (c.hit) return cacheHit(ctx, step, c.output);
|
|
1699
1798
|
const out = await execAgentStep(step, ctx, b);
|
|
1700
|
-
await ctx.cache.set(c.key,
|
|
1799
|
+
await ctx.cache.set(c.key, await buildCacheEntry(ctx, out, c.hash));
|
|
1701
1800
|
return out;
|
|
1702
1801
|
}
|
|
1703
1802
|
case "gate":
|
|
@@ -1706,11 +1805,24 @@ async function execStepBody(step, ctx, item, index) {
|
|
|
1706
1805
|
return execArtifactStep(step, ctx, b);
|
|
1707
1806
|
}
|
|
1708
1807
|
}
|
|
1709
|
-
function
|
|
1710
|
-
const cleaned =
|
|
1808
|
+
function sanitizeArtifactFilename(name) {
|
|
1809
|
+
const cleaned = name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1711
1810
|
return cleaned.length > 0 ? cleaned : "artifact";
|
|
1712
1811
|
}
|
|
1713
|
-
|
|
1812
|
+
function reserveArtifactDestName(rawPath, key, usedNames) {
|
|
1813
|
+
const base = sanitizeArtifactFilename(basename(rawPath));
|
|
1814
|
+
let candidate = base;
|
|
1815
|
+
if (usedNames.has(candidate)) {
|
|
1816
|
+
const keyPart = sanitizeArtifactFilename(key);
|
|
1817
|
+
candidate = `${keyPart}-${base}`;
|
|
1818
|
+
for (let n = 2; usedNames.has(candidate); n++) {
|
|
1819
|
+
candidate = `${keyPart}-${n}-${base}`;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
usedNames.add(candidate);
|
|
1823
|
+
return candidate;
|
|
1824
|
+
}
|
|
1825
|
+
async function copyIntoArtifacts(stepId, key, rawPath, workspace, artifactsDir, contentType, usedNames) {
|
|
1714
1826
|
const abs = isAbsolute(rawPath) ? resolve(rawPath) : resolve(workspace, rawPath);
|
|
1715
1827
|
const rel = relative(workspace, abs);
|
|
1716
1828
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
@@ -1718,7 +1830,7 @@ async function copyIntoArtifacts(stepId, key, rawPath, workspace, artifactsDir,
|
|
|
1718
1830
|
}
|
|
1719
1831
|
const buf = await readFile(abs);
|
|
1720
1832
|
const sha256 = createHash("sha256").update(buf).digest("hex");
|
|
1721
|
-
const destName =
|
|
1833
|
+
const destName = reserveArtifactDestName(rawPath, key, usedNames);
|
|
1722
1834
|
await mkdir(artifactsDir, { recursive: true });
|
|
1723
1835
|
await writeFile(join(artifactsDir, destName), buf);
|
|
1724
1836
|
return {
|
|
@@ -1742,12 +1854,13 @@ async function execArtifactStep(step, ctx, b) {
|
|
|
1742
1854
|
const resolvedInputs = { key, path: rawPath };
|
|
1743
1855
|
const cacheOn = ctx.cache !== void 0 && ctx.cacheKey !== void 0;
|
|
1744
1856
|
const journalKey = cacheOn ? stepJournalKey(ctx, step) : void 0;
|
|
1745
|
-
const hash = cacheOn ? hashResolvedInputs(step.kind, resolvedInputs) : void 0;
|
|
1857
|
+
const hash = cacheOn ? hashResolvedInputs(step.kind, resolvedInputs, ctx.workspace) : void 0;
|
|
1746
1858
|
if (cacheOn) {
|
|
1747
1859
|
const entry = await ctx.cache.get(journalKey);
|
|
1748
1860
|
if (entry !== void 0 && entry.resolvedInputHash === hash) {
|
|
1749
1861
|
const out2 = entry.output;
|
|
1750
|
-
const destName =
|
|
1862
|
+
const destName = out2.path.slice("artifacts/".length);
|
|
1863
|
+
ctx.usedArtifactNames?.add(destName);
|
|
1751
1864
|
const destAbs = join(artifactsDir, destName);
|
|
1752
1865
|
const srcAbs = join(entry.artifactsDirAtCache ?? artifactsDir, destName);
|
|
1753
1866
|
if (srcAbs !== destAbs) {
|
|
@@ -1762,7 +1875,7 @@ async function execArtifactStep(step, ctx, b) {
|
|
|
1762
1875
|
}
|
|
1763
1876
|
}
|
|
1764
1877
|
ctx.onStepStart?.(step.id);
|
|
1765
|
-
const out = await copyIntoArtifacts(step.id, key, rawPath, workspace, artifactsDir, contentType);
|
|
1878
|
+
const out = await copyIntoArtifacts(step.id, key, rawPath, workspace, artifactsDir, contentType, ctx.usedArtifactNames);
|
|
1766
1879
|
if (cacheOn) {
|
|
1767
1880
|
await ctx.cache.set(journalKey, { output: out, resolvedInputHash: hash, artifactsDirAtCache: artifactsDir });
|
|
1768
1881
|
}
|
|
@@ -1780,7 +1893,7 @@ async function checkOutputsFiles(outputsFiles, ctx, workflowId, lastStepId) {
|
|
|
1780
1893
|
const abs = isAbsolute(rawPath) ? rawPath : resolve(ctx.workspace, rawPath);
|
|
1781
1894
|
let out;
|
|
1782
1895
|
try {
|
|
1783
|
-
out = await copyIntoArtifacts(lastStepId ?? workflowId, key, abs, ctx.workspace, ctx.artifactsDir, contract.contentType);
|
|
1896
|
+
out = await copyIntoArtifacts(lastStepId ?? workflowId, key, abs, ctx.workspace, ctx.artifactsDir, contract.contentType, ctx.usedArtifactNames);
|
|
1784
1897
|
} catch {
|
|
1785
1898
|
if (contract.required === true) {
|
|
1786
1899
|
throw new MissingArtifactError(key, lastStepId);
|
|
@@ -1811,7 +1924,11 @@ async function runFinally(steps, ctx, bodyFailed) {
|
|
|
1811
1924
|
async function runWorkflowInner(workflow, input, hooks, maxTotalCostUsd) {
|
|
1812
1925
|
const state = { input, steps: {}, costBySession: /* @__PURE__ */ new Map(), maxTotalCostUsd, cachedHits: /* @__PURE__ */ new Set() };
|
|
1813
1926
|
const ownsScope = hooks.spawned === void 0;
|
|
1814
|
-
const ctx = {
|
|
1927
|
+
const ctx = {
|
|
1928
|
+
state,
|
|
1929
|
+
...hooks,
|
|
1930
|
+
...ownsScope ? { spawned: [], usedArtifactNames: /* @__PURE__ */ new Set() } : {}
|
|
1931
|
+
};
|
|
1815
1932
|
let lastId;
|
|
1816
1933
|
let bodyFailed = false;
|
|
1817
1934
|
try {
|
|
@@ -1865,6 +1982,6 @@ function compileWorkflowManifest(source, opts) {
|
|
|
1865
1982
|
return compileWorkflow(handle, opts);
|
|
1866
1983
|
}
|
|
1867
1984
|
|
|
1868
|
-
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 };
|
|
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 };
|
|
1869
1986
|
//# sourceMappingURL=index.mjs.map
|
|
1870
1987
|
//# sourceMappingURL=index.mjs.map
|