@automatalabs/workflows 0.20.2 → 0.21.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 +101 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +154 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/validate.d.ts +84 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +326 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -263,6 +263,47 @@ testable without a live agent — pass a stub runner.
|
|
|
263
263
|
|
|
264
264
|
---
|
|
265
265
|
|
|
266
|
+
## Loading workflows from folders — `openWorkflowDir`
|
|
267
|
+
|
|
268
|
+
Integrators who keep a versioned folder of workflow scripts don't need to hand-roll
|
|
269
|
+
`readFileSync` plumbing or a `loadSavedWorkflow` resolver:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { openWorkflowDir, runDynamicWorkflow } from "@automatalabs/workflows";
|
|
273
|
+
|
|
274
|
+
const flows = openWorkflowDir("./workflows"); // or ["./workflows", teamDir] — first hit wins
|
|
275
|
+
|
|
276
|
+
flows.list(); // [{ name, file, meta }] — meta parsed per call, browsable by a UI
|
|
277
|
+
flows.read("review-pr"); // name → script string; throws with searched dirs + did-you-mean
|
|
278
|
+
flows.resolve; // (name) => string | undefined — IS a loadSavedWorkflow resolver
|
|
279
|
+
|
|
280
|
+
const run = await runDynamicWorkflow("review-pr", { // a NAME works when `workflows` is set
|
|
281
|
+
workflows: flows, // also accepts "./workflows" or [dir, dir]
|
|
282
|
+
args: { pr: 42 },
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Semantics worth knowing:
|
|
287
|
+
|
|
288
|
+
- **Construction does no I/O** — nothing is created, nothing is scanned or cached. Every method
|
|
289
|
+
reads the filesystem at call time, so a long-lived view always reflects the current working
|
|
290
|
+
tree (a cached scan would serve stale scripts after a `git checkout`/pull/save). Missing
|
|
291
|
+
directories simply contribute nothing.
|
|
292
|
+
- **The filename stem is the name** (`review-pr.workflow.js` or `review-pr.js` ⇒ `review-pr`),
|
|
293
|
+
mirroring the agentType registry convention; across dirs the first hit wins, within a dir
|
|
294
|
+
`.workflow.js` beats `.js`.
|
|
295
|
+
- **`workflows` also wires nested calls**: with the option set, `workflow("<name>")` inside the
|
|
296
|
+
script resolves from the same view. (Without it, `runDynamicWorkflow` has no saved-workflow
|
|
297
|
+
resolver and nested names cannot resolve.) A top-level string containing `export const meta`
|
|
298
|
+
is always treated as a verbatim script, never a name.
|
|
299
|
+
- **Versioning is git's job.** A run persists its script content, so `resume()` replays the exact
|
|
300
|
+
script that started the run even if the file changed since; an edited file simply cache-misses
|
|
301
|
+
from the first changed call on the next fresh run.
|
|
302
|
+
- **`resolve()` validates name shape strictly** (one flat path segment) — inline nested scripts
|
|
303
|
+
fall through to verbatim parsing, and path traversal out of the configured dirs is impossible.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
266
307
|
## Listening in on the live ACP stream (events)
|
|
267
308
|
|
|
268
309
|
`createAcpRunner()` returns an `AcpAgentRunner` with a **typed event bus**. Subscribe with
|
|
@@ -353,6 +394,42 @@ promises — `parallel([() => agent("a"), () => agent("b")])`, not `parallel([ag
|
|
|
353
394
|
|
|
354
395
|
---
|
|
355
396
|
|
|
397
|
+
## Validating scripts — `agentprism-workflows validate`
|
|
398
|
+
|
|
399
|
+
The package ships a bin that validates a workflow script **without spending tokens or spawning
|
|
400
|
+
any agent process** — no backend auth needed:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
npx @automatalabs/workflows validate my-workflow.js --args '{"target":"src/"}'
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Two passes: a **static parse** (the `meta` literal, syntax, the determinism blocklist), then a
|
|
407
|
+
**dry run** — the script executes in the real engine realm while every `agent()` call is served
|
|
408
|
+
by an in-process mock `AgentRunner` that fabricates schema-conforming results. The dry run
|
|
409
|
+
catches what a parse can't: thunk-vs-promise mistakes, reference errors, broken plumbing between
|
|
410
|
+
calls. Checkpoints take their headless defaults; script-declared `meta.backends` are treated as
|
|
411
|
+
approved (with a warning that real runs require approval). The report lists every agent call with
|
|
412
|
+
its backend attribution, every checkpoint, and warnings; exit codes are `0` valid, `1` parse
|
|
413
|
+
failure, `2` dry-run failure, `3` usage error.
|
|
414
|
+
|
|
415
|
+
Flags: `--args <json>` / `--args-file <path>`, `--workflows-dir <dir>` (repeatable — validate by
|
|
416
|
+
NAME and resolve nested `workflow("<name>")` calls from your folder), `--parse-only`,
|
|
417
|
+
`--cwd <dir>`, `--token-budget <n>` (exercise `budget`-guarded paths; the mock reports 1000
|
|
418
|
+
tokens per call), `--max-agents <n>`, `--timeout-ms <n>`, `--json`.
|
|
419
|
+
|
|
420
|
+
The same check is available programmatically — it never throws for an invalid script:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { validateWorkflowScript } from "@automatalabs/workflows";
|
|
424
|
+
|
|
425
|
+
const report = await validateWorkflowScript(script, { args: { target: "src/" } });
|
|
426
|
+
report.ok; // parse ok AND dry run completed
|
|
427
|
+
report.dryRun?.agentCalls; // [{ label, phase, model, backend, schema }, …]
|
|
428
|
+
report.warnings; // approval reminders, phase mismatches, headless-abort checkpoints, …
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
356
433
|
## Structured output
|
|
357
434
|
|
|
358
435
|
Pass a JSON Schema to `agent({ schema })` (in a script) or `runner.run(prompt, { schema })` (direct)
|
|
@@ -422,6 +499,10 @@ Within a provider, the model spec selects the concrete model through ACP session
|
|
|
422
499
|
runDynamicWorkflow, // (script, { args?, runner?, exec? }) => Promise<WorkflowRunResult>
|
|
423
500
|
runWorkflow, // the bare engine run (no status trio)
|
|
424
501
|
parseWorkflowScript, // parse a script's meta + body
|
|
502
|
+
validateWorkflowScript, // token-free parse + mock-runner dry run (the `validate` CLI's core)
|
|
503
|
+
fabricateFromSchema, // the dry run's JSON-Schema value fabricator
|
|
504
|
+
formatValidateReport, // render a ValidateWorkflowReport as CLI text
|
|
505
|
+
openWorkflowDir, // read-only view over folders of workflow scripts (name = filename stem)
|
|
425
506
|
WorkflowManager, // stateful / resumable run manager
|
|
426
507
|
|
|
427
508
|
// ── ACP backend ──
|
|
@@ -442,6 +523,7 @@ AGENTPRISM_PERSISTENCE_ROOT_ENV,
|
|
|
442
523
|
|
|
443
524
|
// ── Types ──
|
|
444
525
|
RunDynamicWorkflowOptions, WorkflowRunOptions, AgentOptions, ExecOptions,
|
|
526
|
+
ValidateWorkflowOptions, ValidateWorkflowReport, ValidatedAgentCall, ValidatedCheckpoint,
|
|
445
527
|
WorkflowManagerOptions, CheckpointOptions, WorkflowRunResult, WorkflowSnapshot,
|
|
446
528
|
WorkflowPathOptions, RunPersistence, RunPersistenceOptions,
|
|
447
529
|
AcpPoolOptions, AgentRunner, RunOptions, AgentResult, AgentUsage, JournalEntry,
|
|
@@ -459,6 +541,25 @@ globals documented by the ambient `dsl.d.ts`.)
|
|
|
459
541
|
|
|
460
542
|
---
|
|
461
543
|
|
|
544
|
+
## Skill for AI agents that write workflows
|
|
545
|
+
|
|
546
|
+
The repository publishes an **agent skill** — a self-contained, backend-agnostic authoring guide
|
|
547
|
+
in the standard `SKILL.md` format — at
|
|
548
|
+
[`skills/agentprism-workflow-authoring/`](https://github.com/VikashLoomba/agentprism-workflows/tree/main/skills/agentprism-workflow-authoring).
|
|
549
|
+
Install it into whatever coding agent you use (Claude Code, Codex, Cursor, OpenCode, …) with the
|
|
550
|
+
[skills](https://skills.sh) CLI:
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
npx skills add VikashLoomba/agentprism-workflows
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
It teaches the full script DSL: routing each `agent()` call to a different ACP backend inside one
|
|
557
|
+
script, structured outputs across all backends, `checkpoint()` gates, budgets, worktree
|
|
558
|
+
isolation, and the determinism rules that make runs resumable. `reference.md` alongside it holds
|
|
559
|
+
the exhaustive option tables.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
462
563
|
## See also
|
|
463
564
|
|
|
464
565
|
- **[`@automatalabs/mcp-server`](https://www.npmjs.com/package/@automatalabs/mcp-server)** — the
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The @automatalabs/workflows bin (`agentprism-workflows`). One subcommand:
|
|
3
|
+
//
|
|
4
|
+
// agentprism-workflows validate <workflow-file> [options]
|
|
5
|
+
//
|
|
6
|
+
// Validates a workflow script without spending tokens: static parse (meta literal,
|
|
7
|
+
// syntax, determinism blocklist), then a dry run over an in-process mock AgentRunner
|
|
8
|
+
// that fabricates schema-conforming results — no ACP process is spawned. See
|
|
9
|
+
// ./validate.ts for the programmatic API (`validateWorkflowScript`).
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { openWorkflowDir } from "@automatalabs/workflow-engine";
|
|
13
|
+
import { validateWorkflowScript, formatValidateReport } from "./validate.js";
|
|
14
|
+
const USAGE = `Usage: agentprism-workflows validate <workflow-file-or-name> [options]
|
|
15
|
+
|
|
16
|
+
Validates an AgentPrism workflow script without spending tokens:
|
|
17
|
+
1. static parse — the meta literal, syntax, and the determinism blocklist
|
|
18
|
+
2. dry run — the script executes against a mock agent backend that fabricates
|
|
19
|
+
schema-conforming results; no ACP process is spawned, no tokens are spent,
|
|
20
|
+
and checkpoints resolve to their headless defaults
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--args <json> the script's \`args\` global for the dry run (a JSON value)
|
|
24
|
+
--args-file <path> read the args JSON from a file instead
|
|
25
|
+
--workflows-dir <dir> a directory of workflow scripts (repeatable; precedence in
|
|
26
|
+
the order given). Enables validating by NAME (filename stem)
|
|
27
|
+
and resolves nested workflow("<name>") calls in the dry run
|
|
28
|
+
--parse-only static parse only; skip the dry run
|
|
29
|
+
--cwd <dir> base cwd for the dry run (default: a throwaway temp dir;
|
|
30
|
+
point it at a real repo only if you want worktree isolation
|
|
31
|
+
to create — and clean up — real git worktrees)
|
|
32
|
+
--token-budget <n> set budget.total so budget-guarded paths execute
|
|
33
|
+
(the mock backend reports 1000 tokens per agent call)
|
|
34
|
+
--max-agents <n> cap dry-run agent calls
|
|
35
|
+
--timeout-ms <n> dry-run wall-clock limit (default 30000)
|
|
36
|
+
--json print the machine-readable report to stdout
|
|
37
|
+
-h, --help show this help
|
|
38
|
+
|
|
39
|
+
Notes:
|
|
40
|
+
- without --workflows-dir, nested workflow("<saved-name>") calls fail in the dry
|
|
41
|
+
run (no saved-workflow resolver); nested INLINE script strings always validate
|
|
42
|
+
- script-declared meta.backends are treated as approved for the dry run, but the
|
|
43
|
+
report reminds you that real runs require explicit approval
|
|
44
|
+
|
|
45
|
+
Exit codes: 0 valid · 1 parse/static failure · 2 dry-run failure · 3 usage error`;
|
|
46
|
+
function fail(message) {
|
|
47
|
+
process.stderr.write(`${message}\n\nRun \`agentprism-workflows validate --help\` for usage.\n`);
|
|
48
|
+
process.exit(3);
|
|
49
|
+
}
|
|
50
|
+
function parseIntFlag(name, raw) {
|
|
51
|
+
const value = Number(raw);
|
|
52
|
+
if (raw === undefined || !Number.isFinite(value) || value <= 0)
|
|
53
|
+
fail(`${name} expects a positive number`);
|
|
54
|
+
return Math.floor(value);
|
|
55
|
+
}
|
|
56
|
+
async function main(argv) {
|
|
57
|
+
const [command, ...rest] = argv;
|
|
58
|
+
if (command === undefined || command === "-h" || command === "--help") {
|
|
59
|
+
process.stdout.write(`${USAGE}\n`);
|
|
60
|
+
process.exit(command === undefined ? 3 : 0);
|
|
61
|
+
}
|
|
62
|
+
if (command !== "validate")
|
|
63
|
+
fail(`unknown command "${command}" — the only command is: validate`);
|
|
64
|
+
let file;
|
|
65
|
+
let json = false;
|
|
66
|
+
const workflowDirs = [];
|
|
67
|
+
const options = {};
|
|
68
|
+
for (let i = 0; i < rest.length; i++) {
|
|
69
|
+
const arg = rest[i];
|
|
70
|
+
switch (arg) {
|
|
71
|
+
case "-h":
|
|
72
|
+
case "--help":
|
|
73
|
+
process.stdout.write(`${USAGE}\n`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
break;
|
|
76
|
+
case "--json":
|
|
77
|
+
json = true;
|
|
78
|
+
break;
|
|
79
|
+
case "--parse-only":
|
|
80
|
+
options.dryRun = false;
|
|
81
|
+
break;
|
|
82
|
+
case "--args":
|
|
83
|
+
try {
|
|
84
|
+
options.args = JSON.parse(rest[++i] ?? "");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
fail("--args expects a JSON value (quote it for your shell)");
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case "--args-file":
|
|
91
|
+
try {
|
|
92
|
+
options.args = JSON.parse(readFileSync(resolve(rest[++i] ?? ""), "utf8"));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
fail(`--args-file: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case "--workflows-dir":
|
|
99
|
+
workflowDirs.push(resolve(rest[++i] ?? fail("--workflows-dir expects a directory")));
|
|
100
|
+
break;
|
|
101
|
+
case "--cwd":
|
|
102
|
+
options.cwd = resolve(rest[++i] ?? fail("--cwd expects a directory"));
|
|
103
|
+
break;
|
|
104
|
+
case "--token-budget":
|
|
105
|
+
options.tokenBudget = parseIntFlag("--token-budget", rest[++i]);
|
|
106
|
+
break;
|
|
107
|
+
case "--max-agents":
|
|
108
|
+
options.maxAgents = parseIntFlag("--max-agents", rest[++i]);
|
|
109
|
+
break;
|
|
110
|
+
case "--timeout-ms":
|
|
111
|
+
options.timeoutMs = parseIntFlag("--timeout-ms", rest[++i]);
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
if (arg.startsWith("-"))
|
|
115
|
+
fail(`unknown option "${arg}"`);
|
|
116
|
+
if (file !== undefined)
|
|
117
|
+
fail("exactly one workflow file expected");
|
|
118
|
+
file = arg;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (file === undefined)
|
|
122
|
+
fail("missing <workflow-file-or-name>");
|
|
123
|
+
const flows = workflowDirs.length > 0 ? openWorkflowDir(workflowDirs) : undefined;
|
|
124
|
+
if (flows)
|
|
125
|
+
options.workflows = flows;
|
|
126
|
+
// The positional is a file path first; with --workflows-dir it may also be a NAME.
|
|
127
|
+
let script;
|
|
128
|
+
if (existsSync(resolve(file))) {
|
|
129
|
+
try {
|
|
130
|
+
script = readFileSync(resolve(file), "utf8");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
fail(`cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (flows) {
|
|
137
|
+
try {
|
|
138
|
+
script = flows.read(file); // throws with searched dirs + closest matches
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
fail(`cannot read ${file}: no such file (pass --workflows-dir to validate by name)`);
|
|
146
|
+
}
|
|
147
|
+
const report = await validateWorkflowScript(script, options);
|
|
148
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : `${formatValidateReport(report)}\n`);
|
|
149
|
+
process.exit(report.exitCode);
|
|
150
|
+
}
|
|
151
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
152
|
+
process.stderr.write(`validate crashed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
153
|
+
process.exit(3);
|
|
154
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { WorkflowManager as EngineWorkflowManager } from "@automatalabs/workflow-engine";
|
|
2
2
|
import type { AcpEventName, AcpRunnerEventMap } from "@automatalabs/acp-agents";
|
|
3
|
-
import type { ExecOptions, WorkflowManagerOptions } from "@automatalabs/workflow-engine";
|
|
3
|
+
import type { ExecOptions, WorkflowDir, WorkflowManagerOptions } from "@automatalabs/workflow-engine";
|
|
4
4
|
import type { AgentRunner, WorkflowBackendConfig, WorkflowRunResult } from "@automatalabs/shared-types";
|
|
5
5
|
export { runWorkflow, parseWorkflowScript } from "@automatalabs/workflow-engine";
|
|
6
|
+
export { openWorkflowDir, type WorkflowDir, type WorkflowDirEntry, type OpenWorkflowDirOptions, } from "@automatalabs/workflow-engine";
|
|
7
|
+
export { validateWorkflowScript, fabricateFromSchema, formatValidateReport, MOCK_TOKENS_PER_AGENT } from "./validate.js";
|
|
8
|
+
export type { ValidateWorkflowOptions, ValidateWorkflowReport, ValidatedAgentCall, ValidatedCheckpoint, } from "./validate.js";
|
|
6
9
|
export type { WorkflowRunOptions, AgentOptions, ExecOptions, WorkflowManagerOptions, CheckpointOptions, WorkflowRunResult, WorkflowSnapshot, WorkflowPathOptions, RunPersistence, RunPersistenceOptions, } from "@automatalabs/workflow-engine";
|
|
7
10
|
export { AGENTPRISM_PERSISTENCE_ROOT_ENV, WorkflowError, WorkflowErrorCode, isWorkflowError, isProviderUsageLimit, } from "@automatalabs/workflow-engine";
|
|
8
11
|
export { createAcpRunner, AcpAgentRunner, InteractiveSession, selectBackend, ClaudeBackend, CodexBackend, CustomAcpBackend, AGENT_METHODS, CLIENT_METHODS, AGENT_METHOD_COVERAGE, CLIENT_METHOD_COVERAGE, ACP_AUTH_REQUIRED_ERROR_CODE, clientCapabilitiesFor, adaptPromptContent, resolveBackendRegistry, BACKENDS_ENV, toJsonSchema, toStrictJsonSchema, } from "@automatalabs/acp-agents";
|
|
@@ -89,6 +92,14 @@ export interface RunDynamicWorkflowOptions {
|
|
|
89
92
|
exec?: ExecOptions;
|
|
90
93
|
/** Approval policy for script-declared `meta.backends` (see {@link ScriptBackendApproval}). */
|
|
91
94
|
allowScriptBackends?: ScriptBackendApproval;
|
|
95
|
+
/**
|
|
96
|
+
* A workflow directory view (or dir path(s) to open one over) serving saved workflows
|
|
97
|
+
* by name. When set, the first argument may be a workflow NAME instead of a script
|
|
98
|
+
* (resolver first, verbatim-script fallback — the engine's own nested-workflow rule),
|
|
99
|
+
* and nested `workflow("<name>")` calls resolve from the same view (it is wired into
|
|
100
|
+
* the run's `loadSavedWorkflow`).
|
|
101
|
+
*/
|
|
102
|
+
workflows?: string | string[] | WorkflowDir;
|
|
92
103
|
}
|
|
93
104
|
/**
|
|
94
105
|
* Run a dynamic workflow script to a TERMINAL result, with the AgentRunner seam
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
// vm-realm globals, NOT importable symbols; they are documented for author IntelliSense
|
|
11
11
|
// in ./dsl.d.ts (referenced above), not exported here.
|
|
12
12
|
import { ACP_CROSS_CUTTING_EVENT_NAMES, createAcpRunner } from "@automatalabs/acp-agents";
|
|
13
|
-
import { parseWorkflowScript, WorkflowError, WorkflowErrorCode, WorkflowManager as EngineWorkflowManager, } from "@automatalabs/workflow-engine";
|
|
13
|
+
import { openWorkflowDir, parseWorkflowScript, WorkflowError, WorkflowErrorCode, WorkflowManager as EngineWorkflowManager, } from "@automatalabs/workflow-engine";
|
|
14
14
|
// ── Engine: run entry, script parsing, the managed-run lifecycle, and the
|
|
15
15
|
// option/result + error types the host composes against. ──
|
|
16
16
|
export { runWorkflow, parseWorkflowScript } from "@automatalabs/workflow-engine";
|
|
17
|
+
// ── Workflow directory view: openWorkflowDir("./workflows") binds a read-only,
|
|
18
|
+
// per-call-fresh view over folders of versioned workflow scripts (name = filename
|
|
19
|
+
// stem). `view.resolve` IS a loadSavedWorkflow resolver; runDynamicWorkflow accepts
|
|
20
|
+
// the view (or dir paths) via `workflows` to serve top-level names AND nested
|
|
21
|
+
// workflow("<name>") calls. ──
|
|
22
|
+
export { openWorkflowDir, } from "@automatalabs/workflow-engine";
|
|
23
|
+
// ── Token-free script validation: static parse + mock-runner dry run. Also the core of
|
|
24
|
+
// the `agentprism-workflows validate` CLI (./cli.ts). ──
|
|
25
|
+
export { validateWorkflowScript, fabricateFromSchema, formatValidateReport, MOCK_TOKENS_PER_AGENT } from "./validate.js";
|
|
17
26
|
export { AGENTPRISM_PERSISTENCE_ROOT_ENV, WorkflowError, WorkflowErrorCode, isWorkflowError, isProviderUsageLimit, } from "@automatalabs/workflow-engine";
|
|
18
27
|
// ── ACP backend: the default AgentRunner implementation, interactive sessions, backend
|
|
19
28
|
// selection, the concrete backends (built-in + custom registry), the pool/runner options,
|
|
@@ -158,12 +167,22 @@ function toAgentEventPayload(name, event) {
|
|
|
158
167
|
* for an ordinary pause/fail — so the caller can read `result.status` directly.
|
|
159
168
|
*/
|
|
160
169
|
export async function runDynamicWorkflow(script, opts = {}) {
|
|
170
|
+
// Saved-workflow view: `script` may be a workflow NAME when `workflows` is set. A real
|
|
171
|
+
// script always contains the mandatory `export const meta` head, so anything without it
|
|
172
|
+
// is treated as a name and resolved via read() — which throws a diagnosable error
|
|
173
|
+
// (searched dirs + closest matches) instead of the engine's parse error on a bare name.
|
|
174
|
+
const flows = opts.workflows === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: typeof opts.workflows === "string" || Array.isArray(opts.workflows)
|
|
177
|
+
? openWorkflowDir(opts.workflows, { cwd: opts.cwd })
|
|
178
|
+
: opts.workflows;
|
|
179
|
+
const resolvedScript = flows !== undefined && !script.includes("export const meta") ? flows.read(script) : script;
|
|
161
180
|
// Script-declared backends need explicit approval BEFORE the run. A malformed script is
|
|
162
181
|
// deliberately not diagnosed here — runSync re-parses and throws the engine's own parse
|
|
163
182
|
// error (its pre-existing contract), so the approval gate never masks a parse message.
|
|
164
183
|
let declared;
|
|
165
184
|
try {
|
|
166
|
-
declared = parseWorkflowScript(
|
|
185
|
+
declared = parseWorkflowScript(resolvedScript).meta.backends;
|
|
167
186
|
}
|
|
168
187
|
catch {
|
|
169
188
|
declared = undefined;
|
|
@@ -174,9 +193,9 @@ export async function runDynamicWorkflow(script, opts = {}) {
|
|
|
174
193
|
}
|
|
175
194
|
const owned = opts.runner === undefined;
|
|
176
195
|
const runner = opts.runner ?? createAcpRunner();
|
|
177
|
-
const manager = new WorkflowManager({ agent: runner, cwd: opts.cwd });
|
|
196
|
+
const manager = new WorkflowManager({ agent: runner, cwd: opts.cwd, loadSavedWorkflow: flows?.resolve });
|
|
178
197
|
try {
|
|
179
|
-
return await manager.runSync(
|
|
198
|
+
return await manager.runSync(resolvedScript, opts.args, exec);
|
|
180
199
|
}
|
|
181
200
|
finally {
|
|
182
201
|
manager.dispose();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { WorkflowDir } from "@automatalabs/workflow-engine";
|
|
2
|
+
import type { WorkflowMeta } from "@automatalabs/shared-types";
|
|
3
|
+
export interface ValidateWorkflowOptions {
|
|
4
|
+
/** The `args` global handed to the script during the dry run. */
|
|
5
|
+
args?: unknown;
|
|
6
|
+
/** A workflow directory view (or dir path(s)) serving saved workflows by name, so
|
|
7
|
+
* nested `workflow("<name>")` calls resolve during the dry run instead of failing. */
|
|
8
|
+
workflows?: string | string[] | WorkflowDir;
|
|
9
|
+
/** Base cwd for the dry run. Default: a throwaway temp dir (so `isolation: "worktree"`
|
|
10
|
+
* degrades to a no-op instead of creating real worktrees in a repo). */
|
|
11
|
+
cwd?: string;
|
|
12
|
+
/** false => static parse only, no dry run. Default true. */
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
/** Set budget.total for the dry run so budget-guarded paths execute. The mock runner
|
|
15
|
+
* reports 1000 tokens per agent call. */
|
|
16
|
+
tokenBudget?: number;
|
|
17
|
+
/** Cap on dry-run agent calls (defaults to the engine's own cap). */
|
|
18
|
+
maxAgents?: number;
|
|
19
|
+
/** Dry-run wall-clock limit. Default 30_000 ms. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/** One agent() call observed during the dry run, with its backend attribution. */
|
|
23
|
+
export interface ValidatedAgentCall {
|
|
24
|
+
label: string;
|
|
25
|
+
phase?: string;
|
|
26
|
+
/** The model spec the call requested (undefined = the run/session default). */
|
|
27
|
+
model?: string;
|
|
28
|
+
tier?: string;
|
|
29
|
+
mode?: string;
|
|
30
|
+
/** Which backend the spec routes to: "claude" | "codex" | "opencode" | a custom backend
|
|
31
|
+
* name (suffixed " (script-declared)" when it comes from meta.backends) | "default". */
|
|
32
|
+
backend: string;
|
|
33
|
+
/** True when the call requested structured output. */
|
|
34
|
+
schema: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ValidatedCheckpoint {
|
|
37
|
+
prompt: string;
|
|
38
|
+
kind: string;
|
|
39
|
+
/** The reply the dry run took (the checkpoint's headless default). */
|
|
40
|
+
reply: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface ValidateWorkflowReport {
|
|
43
|
+
/** True when the parse succeeded AND the dry run (if performed) completed. */
|
|
44
|
+
ok: boolean;
|
|
45
|
+
/** 0 = valid; 1 = parse/static failure; 2 = dry-run failure. */
|
|
46
|
+
exitCode: 0 | 1 | 2;
|
|
47
|
+
parse: {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
error?: string;
|
|
50
|
+
meta?: WorkflowMeta;
|
|
51
|
+
};
|
|
52
|
+
dryRun?: {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
status: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
/** True when the run was cut off by ValidateWorkflowOptions.timeoutMs. */
|
|
57
|
+
timedOut: boolean;
|
|
58
|
+
agentCalls: ValidatedAgentCall[];
|
|
59
|
+
checkpoints: ValidatedCheckpoint[];
|
|
60
|
+
phasesVisited: string[];
|
|
61
|
+
logs: string[];
|
|
62
|
+
durationMs: number;
|
|
63
|
+
/** The script's return value, composed from fabricated agent results. */
|
|
64
|
+
result?: unknown;
|
|
65
|
+
};
|
|
66
|
+
warnings: string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fabricate a value that structurally satisfies a JSON Schema — the dry run's stand-in
|
|
70
|
+
* for a real agent's structured output. Deterministic and intentionally simple: first
|
|
71
|
+
* enum/anyOf variant, `true` booleans (so ok-gates terminate), `mock-<field>` strings.
|
|
72
|
+
*/
|
|
73
|
+
export declare function fabricateFromSchema(schema: unknown, hint?: string, depth?: number): unknown;
|
|
74
|
+
/** Tokens the mock runner reports per agent call, so `--token-budget` exercises
|
|
75
|
+
* budget-guarded script paths deterministically. */
|
|
76
|
+
export declare const MOCK_TOKENS_PER_AGENT = 1000;
|
|
77
|
+
/**
|
|
78
|
+
* Validate a workflow script: parse it, then (by default) dry-run it against a mock
|
|
79
|
+
* AgentRunner. Never throws for an invalid script — read `report.ok` / `report.exitCode`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function validateWorkflowScript(script: string, options?: ValidateWorkflowOptions): Promise<ValidateWorkflowReport>;
|
|
82
|
+
/** Render a ValidateWorkflowReport as the human-readable CLI output. */
|
|
83
|
+
export declare function formatValidateReport(report: ValidateWorkflowReport): string;
|
|
84
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAA2B,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAExF,MAAM,WAAW,uBAAuB;IACtC,iEAAiE;IACjE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;2FACuF;IACvF,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,WAAW,CAAC;IAC5C;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;8CAC0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;6FACyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,EAAE,EAAE,OAAO,CAAC;IACZ,gEAAgE;IAChE,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,KAAK,EAAE;QACL,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,YAAY,CAAC;KACrB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,OAAO,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,0EAA0E;QAC1E,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,EAAE,kBAAkB,EAAE,CAAC;QACjC,WAAW,EAAE,mBAAmB,EAAE,CAAC;QACnC,aAAa,EAAE,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,yEAAyE;QACzE,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,SAAU,EAAE,KAAK,SAAI,GAAG,OAAO,CA2DvF;AAcD;qDACqD;AACrD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAqB1C;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,sBAAsB,CAAC,CAwLjC;AAMD,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAgC3E"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Token-free validation for workflow scripts: a static parse (meta literal, syntax,
|
|
2
|
+
// determinism blocklist) followed by an optional DRY RUN — the script executes for real
|
|
3
|
+
// in the engine's deterministic realm, but every agent() call is served by an in-process
|
|
4
|
+
// mock AgentRunner that fabricates schema-conforming results. No ACP process is spawned,
|
|
5
|
+
// no tokens are spent, checkpoints resolve to their headless defaults, and run state is
|
|
6
|
+
// journaled nowhere (journaling off + a throwaway persistence root for the run lease).
|
|
7
|
+
//
|
|
8
|
+
// This is the programmatic core behind `agentprism-workflows validate` (see ./cli.ts).
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { openWorkflowDir, WorkflowManager, parseWorkflowScript } from "@automatalabs/workflow-engine";
|
|
13
|
+
import { resolveBackendRegistry, selectBackend } from "@automatalabs/acp-agents";
|
|
14
|
+
/**
|
|
15
|
+
* Fabricate a value that structurally satisfies a JSON Schema — the dry run's stand-in
|
|
16
|
+
* for a real agent's structured output. Deterministic and intentionally simple: first
|
|
17
|
+
* enum/anyOf variant, `true` booleans (so ok-gates terminate), `mock-<field>` strings.
|
|
18
|
+
*/
|
|
19
|
+
export function fabricateFromSchema(schema, hint = "value", depth = 0) {
|
|
20
|
+
if (depth > 16)
|
|
21
|
+
return null;
|
|
22
|
+
if (!schema || typeof schema !== "object")
|
|
23
|
+
return `mock-${hint}`;
|
|
24
|
+
const s = schema;
|
|
25
|
+
if (s.const !== undefined)
|
|
26
|
+
return s.const;
|
|
27
|
+
if (Array.isArray(s.enum) && s.enum.length > 0)
|
|
28
|
+
return s.enum[0];
|
|
29
|
+
if (s.default !== undefined)
|
|
30
|
+
return s.default;
|
|
31
|
+
const variants = s.anyOf ?? s.oneOf ?? s.allOf;
|
|
32
|
+
if (Array.isArray(variants) && variants.length > 0)
|
|
33
|
+
return fabricateFromSchema(variants[0], hint, depth + 1);
|
|
34
|
+
let type = s.type;
|
|
35
|
+
if (Array.isArray(type))
|
|
36
|
+
type = type[0];
|
|
37
|
+
if (type === undefined) {
|
|
38
|
+
if (s.properties)
|
|
39
|
+
type = "object";
|
|
40
|
+
else if (s.items)
|
|
41
|
+
type = "array";
|
|
42
|
+
else
|
|
43
|
+
return `mock-${hint}`;
|
|
44
|
+
}
|
|
45
|
+
switch (type) {
|
|
46
|
+
case "object": {
|
|
47
|
+
const out = {};
|
|
48
|
+
const props = (s.properties ?? {});
|
|
49
|
+
for (const [name, sub] of Object.entries(props))
|
|
50
|
+
out[name] = fabricateFromSchema(sub, name, depth + 1);
|
|
51
|
+
for (const name of Array.isArray(s.required) ? s.required : []) {
|
|
52
|
+
if (!(name in out))
|
|
53
|
+
out[name] = `mock-${name}`;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
case "array": {
|
|
58
|
+
const min = typeof s.minItems === "number" ? s.minItems : 1;
|
|
59
|
+
const count = Math.min(Math.max(min, 1), 3);
|
|
60
|
+
return Array.from({ length: count }, (_x, i) => fabricateFromSchema(s.items, `${hint}-${i + 1}`, depth + 1));
|
|
61
|
+
}
|
|
62
|
+
case "string": {
|
|
63
|
+
if (s.format === "uri" || s.format === "url")
|
|
64
|
+
return "https://example.invalid/mock";
|
|
65
|
+
if (s.format === "date-time")
|
|
66
|
+
return "2024-01-01T00:00:00Z";
|
|
67
|
+
if (s.format === "date")
|
|
68
|
+
return "2024-01-01";
|
|
69
|
+
let value = `mock-${hint}`;
|
|
70
|
+
if (typeof s.minLength === "number" && value.length < s.minLength) {
|
|
71
|
+
value = value.padEnd(s.minLength, "x");
|
|
72
|
+
}
|
|
73
|
+
if (typeof s.maxLength === "number" && value.length > s.maxLength) {
|
|
74
|
+
value = value.slice(0, s.maxLength);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
case "integer":
|
|
79
|
+
case "number": {
|
|
80
|
+
if (typeof s.minimum === "number")
|
|
81
|
+
return s.minimum;
|
|
82
|
+
if (typeof s.maximum === "number" && s.maximum < 1)
|
|
83
|
+
return s.maximum;
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
case "boolean":
|
|
87
|
+
return true;
|
|
88
|
+
case "null":
|
|
89
|
+
return null;
|
|
90
|
+
default:
|
|
91
|
+
return `mock-${hint}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Tokens the mock runner reports per agent call, so `--token-budget` exercises
|
|
95
|
+
* budget-guarded script paths deterministically. */
|
|
96
|
+
export const MOCK_TOKENS_PER_AGENT = 1000;
|
|
97
|
+
function attributeBackend(model, tier, declared) {
|
|
98
|
+
const spec = model ?? tier;
|
|
99
|
+
if (!spec)
|
|
100
|
+
return "default";
|
|
101
|
+
const head = spec.split("/")[0].replace(/\[[^\]]*\]\s*$/, "").trim().toLowerCase();
|
|
102
|
+
if (declared && Object.keys(declared).some((name) => name.toLowerCase() === head)) {
|
|
103
|
+
return `${head} (script-declared)`;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const registry = resolveBackendRegistry(declared);
|
|
107
|
+
return selectBackend({ model, tier }, registry).id;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "default";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate a workflow script: parse it, then (by default) dry-run it against a mock
|
|
115
|
+
* AgentRunner. Never throws for an invalid script — read `report.ok` / `report.exitCode`.
|
|
116
|
+
*/
|
|
117
|
+
export async function validateWorkflowScript(script, options = {}) {
|
|
118
|
+
const warnings = [];
|
|
119
|
+
let meta;
|
|
120
|
+
try {
|
|
121
|
+
meta = parseWorkflowScript(script).meta;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
parse: { ok: false, error: error instanceof Error ? error.message : String(error) },
|
|
128
|
+
warnings,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const declaredBackends = meta.backends && Object.keys(meta.backends).length > 0 ? meta.backends : undefined;
|
|
132
|
+
if (declaredBackends) {
|
|
133
|
+
warnings.push(`script declares custom backends (${Object.keys(declaredBackends).join(", ")}) — real runs must approve them ` +
|
|
134
|
+
`(allowScriptBackends / exec.scriptBackends / AGENTPRISM_ALLOW_SCRIPT_BACKENDS=1); the dry run treats them as approved`);
|
|
135
|
+
}
|
|
136
|
+
if (options.dryRun === false) {
|
|
137
|
+
return { ok: true, exitCode: 0, parse: { ok: true, meta }, warnings };
|
|
138
|
+
}
|
|
139
|
+
// Throwaway directories: the run cwd (unless the caller pins one) so worktree isolation
|
|
140
|
+
// no-ops, and a private persistence root so the run lease never touches the real store.
|
|
141
|
+
const ownedCwd = options.cwd === undefined;
|
|
142
|
+
const baseCwd = options.cwd ?? mkdtempSync(join(tmpdir(), "agentprism-validate-"));
|
|
143
|
+
const persistenceRoot = mkdtempSync(join(tmpdir(), "agentprism-validate-state-"));
|
|
144
|
+
const mockMeta = new Map();
|
|
145
|
+
const runner = {
|
|
146
|
+
async run(_prompt, runOptions = {}) {
|
|
147
|
+
const label = runOptions.label ?? "";
|
|
148
|
+
mockMeta.set(label, {
|
|
149
|
+
tier: runOptions.tier,
|
|
150
|
+
mode: runOptions.mode,
|
|
151
|
+
schema: runOptions.schema !== undefined,
|
|
152
|
+
});
|
|
153
|
+
runOptions.onUsage?.({
|
|
154
|
+
input: MOCK_TOKENS_PER_AGENT - 250,
|
|
155
|
+
output: 250,
|
|
156
|
+
cacheRead: 0,
|
|
157
|
+
cacheWrite: 0,
|
|
158
|
+
total: MOCK_TOKENS_PER_AGENT,
|
|
159
|
+
cost: 0,
|
|
160
|
+
});
|
|
161
|
+
if (runOptions.schema !== undefined)
|
|
162
|
+
return fabricateFromSchema(runOptions.schema);
|
|
163
|
+
return `[dry-run] mock output for ${runOptions.label ?? "agent"}`;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const agentCalls = [];
|
|
167
|
+
const checkpoints = [];
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
let timedOut = false;
|
|
170
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
timedOut = true;
|
|
173
|
+
controller.abort();
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
timer.unref?.();
|
|
176
|
+
const flows = options.workflows === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: typeof options.workflows === "string" || Array.isArray(options.workflows)
|
|
179
|
+
? openWorkflowDir(options.workflows)
|
|
180
|
+
: options.workflows;
|
|
181
|
+
const manager = new WorkflowManager({
|
|
182
|
+
agent: runner,
|
|
183
|
+
cwd: baseCwd,
|
|
184
|
+
journaling: false,
|
|
185
|
+
persistenceRoot,
|
|
186
|
+
loadSavedWorkflow: flows?.resolve,
|
|
187
|
+
});
|
|
188
|
+
manager.on("agentStart", (event) => {
|
|
189
|
+
const extra = mockMeta.get(event.label) ?? mockMeta.get("") ?? { schema: false };
|
|
190
|
+
agentCalls.push({
|
|
191
|
+
label: event.label,
|
|
192
|
+
phase: event.phase,
|
|
193
|
+
model: event.model,
|
|
194
|
+
tier: extra.tier,
|
|
195
|
+
mode: extra.mode,
|
|
196
|
+
backend: attributeBackend(event.model, extra.tier, declaredBackends),
|
|
197
|
+
schema: extra.schema,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
const run = await manager.runSync(script, options.args, {
|
|
202
|
+
journaling: false,
|
|
203
|
+
signal: controller.signal,
|
|
204
|
+
tokenBudget: options.tokenBudget,
|
|
205
|
+
maxAgents: options.maxAgents,
|
|
206
|
+
scriptBackends: declaredBackends,
|
|
207
|
+
confirm: async (promptText, checkpointOptions) => {
|
|
208
|
+
const opts = (checkpointOptions ?? {});
|
|
209
|
+
if (opts.headless === "abort") {
|
|
210
|
+
warnings.push(`checkpoint "${truncate(promptText, 60)}" sets headless: "abort" — unattended runs will fail at it`);
|
|
211
|
+
}
|
|
212
|
+
// Mirror the engine's headless resolution exactly: the declared default, else true.
|
|
213
|
+
const reply = opts.default ?? true;
|
|
214
|
+
checkpoints.push({ prompt: promptText, kind: opts.kind ?? "confirm", reply });
|
|
215
|
+
return reply;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
// agentStart fires BEFORE the mock records its options, so backfill attribution for
|
|
219
|
+
// any call whose mock metadata arrived after the event (same tick ordering).
|
|
220
|
+
for (const call of agentCalls) {
|
|
221
|
+
const extra = mockMeta.get(call.label);
|
|
222
|
+
if (extra) {
|
|
223
|
+
call.tier = extra.tier;
|
|
224
|
+
call.mode = extra.mode;
|
|
225
|
+
call.schema = extra.schema;
|
|
226
|
+
call.backend = attributeBackend(call.model, extra.tier, declaredBackends);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const ok = run.status === "completed";
|
|
230
|
+
if (!ok && flows === undefined && run.reason?.includes("must be the first statement") && /\bworkflow\s*\(/.test(script)) {
|
|
231
|
+
warnings.push('the failure looks like a nested workflow("<name>") call on a bare name — provide workflow dirs ' +
|
|
232
|
+
"(ValidateWorkflowOptions.workflows / --workflows-dir) so names resolve during the dry run");
|
|
233
|
+
}
|
|
234
|
+
if (ok) {
|
|
235
|
+
if (agentCalls.length === 0 && checkpoints.length === 0) {
|
|
236
|
+
warnings.push("the script completed without a single agent() or checkpoint() call");
|
|
237
|
+
}
|
|
238
|
+
const declaredPhases = (meta.phases ?? []).map((p) => p.title);
|
|
239
|
+
// A phase counts as used via phase() OR via a per-call agent({ phase }) assignment.
|
|
240
|
+
const visited = new Set([...(run.phases ?? []), ...agentCalls.flatMap((c) => (c.phase ? [c.phase] : []))]);
|
|
241
|
+
for (const title of declaredPhases) {
|
|
242
|
+
if (!visited.has(title))
|
|
243
|
+
warnings.push(`meta.phases declares "${title}" but no phase("${title}") or agent({ phase }) used it`);
|
|
244
|
+
}
|
|
245
|
+
if (declaredPhases.length > 0) {
|
|
246
|
+
for (const title of visited) {
|
|
247
|
+
if (!declaredPhases.includes(title))
|
|
248
|
+
warnings.push(`phase "${title}" is used but meta.phases does not declare it`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok,
|
|
254
|
+
exitCode: ok ? 0 : 2,
|
|
255
|
+
parse: { ok: true, meta },
|
|
256
|
+
dryRun: {
|
|
257
|
+
ok,
|
|
258
|
+
status: run.status,
|
|
259
|
+
reason: timedOut ? `dry run exceeded ${timeoutMs}ms and was aborted` : run.reason,
|
|
260
|
+
timedOut,
|
|
261
|
+
agentCalls,
|
|
262
|
+
checkpoints,
|
|
263
|
+
phasesVisited: run.phases ?? [],
|
|
264
|
+
logs: run.logs ?? [],
|
|
265
|
+
durationMs: run.durationMs,
|
|
266
|
+
result: run.result,
|
|
267
|
+
},
|
|
268
|
+
warnings,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
try {
|
|
274
|
+
rmSync(persistenceRoot, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* best-effort cleanup */
|
|
278
|
+
}
|
|
279
|
+
if (ownedCwd) {
|
|
280
|
+
try {
|
|
281
|
+
rmSync(baseCwd, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
/* best-effort cleanup */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function truncate(text, max) {
|
|
290
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
291
|
+
}
|
|
292
|
+
/** Render a ValidateWorkflowReport as the human-readable CLI output. */
|
|
293
|
+
export function formatValidateReport(report) {
|
|
294
|
+
const lines = [];
|
|
295
|
+
if (report.parse.ok) {
|
|
296
|
+
const meta = report.parse.meta;
|
|
297
|
+
const phases = meta?.phases?.length ? `${meta.phases.length} declared phase(s)` : "no declared phases";
|
|
298
|
+
const backends = meta?.backends ? `, ${Object.keys(meta.backends).length} script-declared backend(s)` : "";
|
|
299
|
+
lines.push(`✓ parse "${meta?.name}" — ${phases}${backends}`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
lines.push(`✗ parse ${report.parse.error}`);
|
|
303
|
+
}
|
|
304
|
+
const dry = report.dryRun;
|
|
305
|
+
if (dry) {
|
|
306
|
+
const summary = `${dry.agentCalls.length} agent call(s), ${dry.checkpoints.length} checkpoint(s), ${dry.durationMs}ms`;
|
|
307
|
+
lines.push(dry.ok ? `✓ dry run completed — ${summary}` : `✗ dry run ${dry.status} — ${dry.reason ?? "unknown failure"} (${summary})`);
|
|
308
|
+
for (const call of dry.agentCalls) {
|
|
309
|
+
const spec = call.model ?? (call.tier ? `tier=${call.tier}` : "(default model)");
|
|
310
|
+
const bits = [call.phase ? `[${call.phase}]` : undefined, spec, `→ ${call.backend}`, call.schema ? "(schema)" : undefined, call.mode ? `mode=${call.mode}` : undefined]
|
|
311
|
+
.filter(Boolean)
|
|
312
|
+
.join(" ");
|
|
313
|
+
lines.push(` • ${call.label} ${bits}`);
|
|
314
|
+
}
|
|
315
|
+
for (const cp of dry.checkpoints) {
|
|
316
|
+
lines.push(` ◆ checkpoint [${cp.kind}] "${truncate(cp.prompt, 60)}" → ${JSON.stringify(cp.reply)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (report.parse.ok) {
|
|
320
|
+
lines.push("- dry run skipped (--parse-only)");
|
|
321
|
+
}
|
|
322
|
+
for (const warning of report.warnings)
|
|
323
|
+
lines.push(` ! ${warning}`);
|
|
324
|
+
lines.push(report.ok ? "result: valid" : "result: INVALID");
|
|
325
|
+
return lines.join("\n");
|
|
326
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automatalabs/workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"agentprism-workflows": "./dist/cli.js"
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
17
20
|
".": {
|
|
18
21
|
"types": "./dist/index.d.ts",
|
|
@@ -27,9 +30,9 @@
|
|
|
27
30
|
"access": "public"
|
|
28
31
|
},
|
|
29
32
|
"dependencies": {
|
|
33
|
+
"@automatalabs/workflow-engine": "0.10.0",
|
|
30
34
|
"@automatalabs/shared-types": "0.12.1",
|
|
31
|
-
"@automatalabs/
|
|
32
|
-
"@automatalabs/acp-agents": "0.20.2"
|
|
35
|
+
"@automatalabs/acp-agents": "0.20.4"
|
|
33
36
|
},
|
|
34
37
|
"scripts": {
|
|
35
38
|
"build": "tsc -b",
|