@automatalabs/workflows 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,14 +11,15 @@ or a registered custom ACP agent — driving the actual subprocess to completion
11
11
 
12
12
  This package is the **canonical SDK** that the stdio MCP server
13
13
  [`@automatalabs/mcp-server`](https://www.npmjs.com/package/@automatalabs/mcp-server) is built on.
14
- If you want to expose a `workflow` tool to an MCP host (Claude Code, Zed, …), use that package; if
15
- you want to embed the runner in your own program, use this one.
14
+ Its CLI can also delegate to a build-time embedded copy of that server with the `mcp` subcommand,
15
+ so an MCP host can expose the `workflow` tool without a separate package install. The standalone
16
+ MCP server package remains independently published, while programs embedding the runner continue
17
+ to use this package's workflow/runner APIs.
16
18
 
17
- It is a **programmatic library**, not an MCP stdio server. It is a thin facade over the engine +
18
- ACP packages and adds ACP-defaulted helpers for ordinary runs (`runDynamicWorkflow`) and
19
- substitution tests (`runIsolation`). The ACP layer does use `@modelcontextprotocol/sdk` internally
20
- when it hosts the optional StructuredOutput tool for eligible agents; consumers still interact
21
- through this SDK's workflow/runner APIs rather than MCP server schemas.
19
+ The SDK itself remains a thin programmatic facade over the engine + ACP packages, with
20
+ ACP-defaulted helpers for ordinary runs (`runDynamicWorkflow`) and substitution tests
21
+ (`runIsolation`). The ACP layer also uses `@modelcontextprotocol/sdk` internally when it hosts the
22
+ optional StructuredOutput tool for eligible agents.
22
23
 
23
24
  ---
24
25
 
@@ -117,21 +118,21 @@ Every script **must** begin with `export const meta = { name, description, phase
117
118
  statement, and must be **deterministic** — `Date.now()`, `Math.random()`, and `new Date()` are
118
119
  unavailable inside the realm (they would break journal replay on resume).
119
120
 
120
- Replay-safe read-only/worktree fan-out is explicit:
121
+ Completed reader/worktree calls both use ordinary journal replay:
121
122
 
122
123
  ```js
123
124
  const [audit, experiment] = await parallel([
124
125
  () => agent("Audit src/api without changing files.", {
125
- label: "audit:api", resume: { filesystem: "read-only" },
126
+ label: "audit:api",
126
127
  }),
127
128
  () => agent("Try the worker fix in isolation; return a unified diff.", {
128
- label: "try:worker", isolation: "worktree", resume: { filesystem: "read-only" },
129
+ label: "try:worker", isolation: "worktree",
129
130
  }),
130
131
  ]);
131
132
  ```
132
133
 
133
- The worktree and edits are discarded; return the diff as data. The declaration is required for
134
- content-addressed reuse in both arms—worktree isolation alone is not a safety proof. See the
134
+ The worktree and edits are discarded; return the diff as data. Replay requires matching journal
135
+ identity/input facts, not a filesystem-safety declaration. See the
135
136
  [incremental resume API](../../docs/api.md#content-addressed-incremental-resume).
136
137
 
137
138
  ### Substitution testing (isolation mode)
@@ -303,7 +304,7 @@ boolean).
303
304
 
304
305
  Resume guarantees journal/script replay integrity and checkpoint-reply targeting only. It never
305
306
  assumes or guarantees that the filesystem, external systems, agent output semantics, or any other
306
- part of the world stayed fresh between runs. If an unsafe prefix must run live, a supplied reply is
307
+ part of the world stayed fresh between runs. If a prior correspondence miss runs live, a supplied reply is
307
308
  injected only when execution reaches the exact recorded checkpoint call site with the same
308
309
  checkpoint identity and inputs; content-only matching is insufficient after that live prefix. A
309
310
  reply that is not applied is reported in `resumeReport` and terminal summaries. Authors who want a
@@ -352,15 +353,15 @@ Run results expose `effectiveLimits`; inspect status exposes the same values as
352
353
  agent rows carry their resolved `timeoutMs` plus `errorCode`.
353
354
 
354
355
  `exec.resumeFromRunId` asks the manager to admit a terminal source, persist a self-contained seed
355
- under a new run ID, and match safe calls by exact path/hash or unique hash+input fingerprint.
356
+ under a new run ID, and match completed calls by exact path/hash or unique hash+input fingerprint.
356
357
  Every allocated call receives a terminal manifest row even when the run halts around it. A
357
358
  non-result occurrence remains live on resume and stays in the identity seed as an ambiguity
358
- blocker until reached, so completed siblings on either side can replay safely. Uncertain,
359
- ambiguous, or unsafe calls run live. Current-environment and Node/V8 differences are
359
+ blocker until reached, so completed siblings on either side can replay. Uncertain,
360
+ ambiguous, or mismatched calls run live. Filesystem/current-environment and Node/V8 differences are
360
361
  reported as provenance without changing admission or matching. Identity hits preserve
361
362
  script-visible logical budget debit while adding zero current provider usage; replayed session
362
363
  records are rebound to the current call index/label/phase. `resumePolicy: "positional"` requests
363
- index/prefix matching but cannot bypass new-format format/metadata/manifest/input/safety checks. The distinct
364
+ index/prefix matching but cannot bypass new-format format/metadata/manifest/input checks. The distinct
364
365
  same-ID `resume()` and low-level `resumeJournal` paths remain permanently legacy positional and
365
366
  emit no `resumeReport`. See the [full contract](../../docs/api.md#content-addressed-incremental-resume).
366
367
  Operational limits are resolved from the new execution's `exec` options and manager defaults, not
@@ -369,9 +370,8 @@ copied from the source run; pass the desired timeout/retry/concurrency values ag
369
370
  neither replay identity nor the execution-input fingerprint and may change without invalidating
370
371
  completed calls or interrupted-turn continuation.
371
372
 
372
- Crash snapshots reconciled to `paused` / `interrupted` without a terminal environment use the
373
- `crash-residue` positional bridge with legacy hash-stable prefix eligibility. Normally settled
374
- sources with an input-fingerprint format below 2 use the
373
+ Current-format crash snapshots reconciled to `paused` / `interrupted` use identity correspondence
374
+ even without terminal-environment capture. Sources with an input-fingerprint format below 2 use the
375
375
  `inputs-format-legacy` positional bridge. That bridge also accepts ancestor-scoped rows carried by a
376
376
  ≤0.23 resume hop when the ancestor run still exists in the same persistence directory; nested and deleted-run scopes stay live. Engine
377
377
  package versions are persisted and surfaced as diagnostics but never gate replay. Every new-run
@@ -544,7 +544,7 @@ Semantics worth knowing:
544
544
  is always treated as a verbatim script, never a name.
545
545
  - **Versioning is git's job.** Same-ID `resume()` reloads the exact persisted script. A new
546
546
  `resumeFromRunId` execution can use an edited script: changed calls run live, while uniquely
547
- matching safety-marked calls may move and replay after admission.
547
+ matching completed calls may move and replay after admission.
548
548
  - **`resolve()` validates name shape strictly** (one flat path segment) — inline nested scripts
549
549
  fall through to verbatim parsing, and path traversal out of the configured dirs is impossible.
550
550
 
@@ -621,7 +621,7 @@ ships no runtime code).
621
621
 
622
622
  | global | what it does |
623
623
  |--------|--------------|
624
- | `agent(prompt, options?)` | Run ONE subagent to completion; returns its result (text, or the validated object with `options.schema`). `options.resume: { filesystem: "read-only" }` is the author safety declaration for content-addressed replay. |
624
+ | `agent(prompt, options?)` | Run ONE subagent to completion; returns its result (text, or the validated object with `options.schema`). The legacy `options.resume` annotation is accepted but replay-neutral. |
625
625
  | `parallel(thunks)` | Run an array of **thunks** (`() => Promise`) concurrently; resolves in input order. |
626
626
  | `pipeline(items, ...stages)` | Map `items` through sequential async stages, concurrently across items. |
627
627
  | `workflow(nameOrScript, args?)` | Run a saved (or inline) workflow nested in this run, sharing its limiter/budget. |
@@ -779,6 +779,36 @@ formatHarnessConfigReport(report); // the CLI's human table
779
779
 
780
780
  ---
781
781
 
782
+ ## Launching the MCP server — `agentprism-workflows mcp`
783
+
784
+ Register the bundled stdio server directly from the workflows package; no separate
785
+ `@automatalabs/mcp-server` installation is required:
786
+
787
+ ```json
788
+ {
789
+ "mcpServers": {
790
+ "agentprism-workflow": {
791
+ "command": "npx",
792
+ "args": ["-y", "@automatalabs/workflows", "mcp"]
793
+ }
794
+ }
795
+ }
796
+ ```
797
+
798
+ For the source inner loop, build workflows before launching its compiled CLI:
799
+
800
+ ```bash
801
+ pnpm --filter @automatalabs/workflows build
802
+ node packages/workflows/dist/cli.js mcp
803
+ ```
804
+
805
+ If the embedded bundle is absent in a monorepo checkout, the command falls back to the built
806
+ `packages/mcp-server/dist/cli.js`. A root `pnpm build` therefore also supports development before
807
+ running `node packages/workflows/dist/cli.js mcp`. The independently published
808
+ `@automatalabs/mcp-server` package and `agentprism-workflow` bin remain available.
809
+
810
+ ---
811
+
782
812
  ## Structured output
783
813
 
784
814
  Pass a JSON Schema to `agent({ schema })` (in a script) or `runner.run(prompt, { schema })` (direct)
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // The @automatalabs/workflows bin (`agentprism-workflows`). Two subcommands:
2
+ // The @automatalabs/workflows bin (`agentprism-workflows`). Three subcommands:
3
3
  //
4
4
  // agentprism-workflows validate <workflow-file> [options]
5
5
  // agentprism-workflows config [harness ...] [options]
6
+ // agentprism-workflows mcp
6
7
  //
7
8
  // validate checks a workflow script without spending tokens: static parse (meta literal,
8
9
  // syntax, direct nondeterministic call expressions), then a dry run over an in-process
@@ -13,7 +14,12 @@
13
14
  // config runs that same no-prompt probe standalone — no script needed — and prints each
14
15
  // requested harness's advertised config-option catalog (model ids, effort levels, modes,
15
16
  // …). See ./config.ts for the programmatic API (`probeHarnessConfig`).
17
+ //
18
+ // mcp delegates stdio unchanged to the MCP server embedded at build time. In a source
19
+ // checkout without that bundle, it falls back to the separately built mcp-server entry.
20
+ import { spawn } from "node:child_process";
16
21
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
22
+ import { constants as osConstants } from "node:os";
17
23
  import { resolve } from "node:path";
18
24
  import { openWorkflowDir } from "@automatalabs/workflow-engine";
19
25
  import { validateWorkflowScript, formatValidateReport } from "./validate.js";
@@ -27,6 +33,7 @@ Commands:
27
33
  catalog (model ids, effort levels, modes, …) so
28
34
  model/configOptions values come from the live
29
35
  catalog, not guesswork
36
+ mcp launch the embedded AgentPrism stdio MCP server
30
37
 
31
38
  Run \`agentprism-workflows <command> --help\` for that command's options.`;
32
39
  const USAGE = `Usage: agentprism-workflows validate <workflow-file-or-name> [options]
@@ -90,6 +97,13 @@ Options:
90
97
  -h, --help show this help
91
98
 
92
99
  Exit codes: 0 all probed · 1 at least one probe failed · 3 usage error`;
100
+ const MCP_USAGE = `Usage: agentprism-workflows mcp
101
+
102
+ Launches the embedded AgentPrism MCP server over stdio. stdin and stdout are reserved
103
+ for JSON-RPC framing and are inherited unchanged by the server process.
104
+
105
+ Options:
106
+ -h, --help show this help`;
93
107
  let activeCommand = "";
94
108
  function fail(message) {
95
109
  const hint = activeCommand === "" ? "agentprism-workflows --help" : `agentprism-workflows ${activeCommand} --help`;
@@ -143,6 +157,86 @@ async function mainConfig(rest) {
143
157
  writeFileSync(process.stdout.fd, json ? `${JSON.stringify(report, null, 2)}\n` : `${formatHarnessConfigReport(report)}\n`);
144
158
  process.exitCode = report.exitCode;
145
159
  }
160
+ async function mainMcp(rest) {
161
+ if (rest.length === 1 && (rest[0] === "-h" || rest[0] === "--help")) {
162
+ process.stdout.write(`${MCP_USAGE}\n`);
163
+ process.exit(0);
164
+ }
165
+ if (rest.length > 0)
166
+ fail(`mcp does not accept arguments (received: ${rest.join(" ")})`);
167
+ const bundlePath = resolve(import.meta.dirname, "mcp-server.js");
168
+ const monorepoFallbackPath = resolve(import.meta.dirname, "../../mcp-server/dist/cli.js");
169
+ const serverPath = existsSync(bundlePath)
170
+ ? bundlePath
171
+ : existsSync(monorepoFallbackPath)
172
+ ? monorepoFallbackPath
173
+ : undefined;
174
+ if (serverPath === undefined) {
175
+ fail(`MCP server bundle not found at ${bundlePath}, and the monorepo fallback is not built. ` +
176
+ "Run `pnpm --filter @automatalabs/workflows build` to create the bundle, or run `pnpm build` at the repository root.");
177
+ }
178
+ await new Promise((resolvePromise) => {
179
+ const child = spawn(process.execPath, [serverPath], { stdio: "inherit" });
180
+ const forwardedSignals = ["SIGINT", "SIGTERM"];
181
+ let settled = false;
182
+ let cleaned = false;
183
+ const forwarders = new Map();
184
+ const cleanup = () => {
185
+ if (cleaned)
186
+ return;
187
+ cleaned = true;
188
+ for (const [signal, forward] of forwarders)
189
+ process.off(signal, forward);
190
+ child.off("error", onError);
191
+ child.off("exit", onExit);
192
+ };
193
+ const finish = (action) => {
194
+ if (settled)
195
+ return;
196
+ settled = true;
197
+ cleanup();
198
+ action();
199
+ resolvePromise();
200
+ };
201
+ const onError = (error) => {
202
+ finish(() => {
203
+ process.stderr.write(`mcp server failed to start: ${error.message}\n`);
204
+ process.exitCode = 1;
205
+ });
206
+ };
207
+ const onExit = (code, signal) => {
208
+ finish(() => {
209
+ if (signal === null) {
210
+ process.exitCode = code ?? 1;
211
+ return;
212
+ }
213
+ // Preserve signal termination for callers (shells, npx, and MCP hosts). Set a
214
+ // conventional nonzero fallback first in case re-raising is unsupported here.
215
+ process.exitCode = 128 + (osConstants.signals[signal] ?? 1);
216
+ try {
217
+ process.kill(process.pid, signal);
218
+ }
219
+ catch (error) {
220
+ process.stderr.write(`mcp server exited on ${signal}, but the parent could not re-raise it: ${error instanceof Error ? error.message : String(error)}\n`);
221
+ }
222
+ });
223
+ };
224
+ for (const signal of forwardedSignals) {
225
+ const forward = () => {
226
+ try {
227
+ child.kill(signal);
228
+ }
229
+ catch (error) {
230
+ process.stderr.write(`could not forward ${signal} to the mcp server: ${error instanceof Error ? error.message : String(error)}\n`);
231
+ }
232
+ };
233
+ forwarders.set(signal, forward);
234
+ process.on(signal, forward);
235
+ }
236
+ child.once("error", onError);
237
+ child.once("exit", onExit);
238
+ });
239
+ }
146
240
  async function main(argv) {
147
241
  const [command, ...rest] = argv;
148
242
  if (command === undefined || command === "-h" || command === "--help") {
@@ -153,8 +247,12 @@ async function main(argv) {
153
247
  activeCommand = "config";
154
248
  return mainConfig(rest);
155
249
  }
250
+ if (command === "mcp") {
251
+ activeCommand = "mcp";
252
+ return mainMcp(rest);
253
+ }
156
254
  if (command !== "validate")
157
- fail(`unknown command "${command}" — the commands are: validate, config`);
255
+ fail(`unknown command "${command}" — the commands are: validate, config, mcp`);
158
256
  activeCommand = "validate";
159
257
  let file;
160
258
  let json = false;