@automatalabs/workflows 0.52.1 → 0.53.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 +19 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -3
- package/dist/mcp-server.js +449 -24
- package/dist/validate.d.ts +5 -3
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +44 -9
- package/package.json +5 -5
package/dist/mcp-server.js
CHANGED
|
@@ -31875,7 +31875,7 @@ var workflowToolInputShape = {
|
|
|
31875
31875
|
),
|
|
31876
31876
|
harnesses: external_exports.array(external_exports.string().regex(/^[a-z][a-z0-9._-]*$/i, "invalid backend name")).min(1).max(16).optional().describe('With action="config", backend names to probe. Omit to probe every backend registered on this server.'),
|
|
31877
31877
|
modelSpecs: external_exports.array(external_exports.string().min(1).max(256)).min(1).max(16).optional().describe(
|
|
31878
|
-
'With action="config", exact routed model specs to select before reading their model-specific mode and config-option catalogs.'
|
|
31878
|
+
'With action="config", exact routed model specs to select before reading their model-specific mode and config-option catalogs. Use mode only when modes.availableModes explicitly lists its exact id; modes:null means unsupported.'
|
|
31879
31879
|
),
|
|
31880
31880
|
modelFilter: external_exports.string().min(1).max(128).optional().describe(
|
|
31881
31881
|
'With action="config", return model ids matching this case-insensitive substring or /regular expression/. Omit for bounded per-provider model summaries.'
|
|
@@ -32781,11 +32781,24 @@ var waitSchema = external_exports.object({
|
|
|
32781
32781
|
returnedBecause: external_exports.enum(["terminal", "timeout", "immediate"])
|
|
32782
32782
|
});
|
|
32783
32783
|
var diagnosticRecordSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
32784
|
+
var sessionModeStateSchema = external_exports.object({
|
|
32785
|
+
currentModeId: external_exports.string(),
|
|
32786
|
+
availableModes: external_exports.array(external_exports.object({
|
|
32787
|
+
id: external_exports.string(),
|
|
32788
|
+
name: external_exports.string(),
|
|
32789
|
+
description: external_exports.string().optional(),
|
|
32790
|
+
_meta: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
|
|
32791
|
+
})),
|
|
32792
|
+
_meta: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
|
|
32793
|
+
});
|
|
32784
32794
|
var harnessDiagnosticSchema = external_exports.object({
|
|
32785
32795
|
backendId: external_exports.string(),
|
|
32786
32796
|
model: external_exports.string().optional(),
|
|
32787
32797
|
probed: external_exports.boolean(),
|
|
32788
32798
|
error: external_exports.string().optional(),
|
|
32799
|
+
modes: sessionModeStateSchema.nullable().optional().describe(
|
|
32800
|
+
"Present on every probed:true row. Exact advertised mode domain; null means unsupported, so omit mode."
|
|
32801
|
+
),
|
|
32789
32802
|
options: external_exports.array(external_exports.unknown()),
|
|
32790
32803
|
omittedOptions: external_exports.number().int().nonnegative()
|
|
32791
32804
|
});
|
|
@@ -33223,34 +33236,30 @@ function callKey(scope, callIndex) {
|
|
|
33223
33236
|
return JSON.stringify([scope, callIndex]);
|
|
33224
33237
|
}
|
|
33225
33238
|
|
|
33226
|
-
// ../mcp-server/src/generated/authoring-prompt-content.ts
|
|
33227
|
-
var AUTHORING_PROMPT_CONTENT = '# Writing AgentPrism workflow scripts\n\nA workflow script is plain JavaScript, passed around as a **string**, not a module. The engine runs it in a deterministic sandboxed realm. Each `agent()` call opens a session on an [Agent Client Protocol](https://agentclientprotocol.com) (ACP) backend \u2014 Claude Code, OpenAI Codex, OpenCode, pi, or a custom ACP agent server. The backend runs its own tool loop to completion and returns final text or a schema-validated object. One script can mix backends per call.\n\nThe **Workflow script reference** section at the end of this document holds the exhaustive option tables, routing grammar, and error codes.\n\n## The guide, by task\n\nEvery section of the guide is inlined below, after the core: Running workflows (the MCP server and `workflow` tool), backends and structured output, composition and failure, quality helpers and checkpoints, the execution environment, determinism and resume, and worked examples with validation.\n\n## The mental model\n\n- **The script is the orchestrator; agents are workers.** All control flow \u2014 loops, fan-out, dedup, aggregation, conditionals \u2014 lives in script code. Agents cannot spawn agents and cannot see each other. Give each agent one self-contained task.\n- **Each `agent()` call opens a fresh session with no memory.** Interpolate everything a later call needs into its prompt. (Sole exception: resume can continue the same usage/auth-interrupted occurrence \u2014 see Determinism and resume.)\n- **Agents are real coding agents, not chat completions.** They have file access, shells, and tools, rooted at the run\'s working directory. "Read the failing test and fix it" is a valid prompt; the agent will edit files.\n- **The DSL primitives are realm globals, not imports.** There is nothing to `import` \u2014 `agent`, `parallel`, `pipeline`, `gate`, `checkpoint`, `args`, \u2026 are injected. Top-level `await` and a top-level `return` are valid. The script\'s return value becomes the run\'s `result`.\n- **Scripts are plain JavaScript, not TypeScript.** Type annotations fail to parse. The realm has no Node APIs (no `require`, `import`, `fs`, `fetch`, timers). All side effects happen through agents.\n- **Live observability needs no script annotations.** Journaling runs publish redacted progress and transcript upserts at `workflow://runs/{runId}/events`. Author labels for human correlation, not to enable this behavior.\n\n## Minimal script\n\n```js\nexport const meta = {\n name: "repo-summary",\n description: "Summarize what a repository does",\n};\n\nconst summary = await agent(\n `Read the README and the package manifests under ${args.path}, then ` +\n `summarize what this project does in five sentences.`,\n { label: "summarize" },\n);\nreturn { summary };\n```\n\nRun scripts through the MCP server\'s `workflow` tool \u2014 registration, the run/await/inspect/stop actions, and the `args`/`cwd` globals are covered in the **Running workflows** section below.\n\n## Pre-flight checklist\n\n- [ ] `export const meta = { name, description }` is the first statement, a pure literal.\n- [ ] No `Date.now()` / `Math.random()` / no-arg `new Date()` / `Date()`; no imports, no Node APIs. Timestamps and randomness come in through `args`.\n- [ ] Every `parallel` element is a **thunk**; results are `.filter(Boolean)`-ed or null-checked.\n- [ ] Every prompt is self-contained: prior results are interpolated in, and every file path a prompt references was written by an earlier call, supplied through `args`, or created by that prompt\'s own instructions.\n- [ ] Schemas: object root, `additionalProperties: false`, everything `required`, a `description` on every field.\n- [ ] When using MCP, pinned model ids, modes, effort values, and `configOptions` come from `workflow` action `config`, never from memory. `mode` only on calls with a pinned `model`.\n- [ ] Worktree-isolated agents return their work as data \u2014 their edits are discarded when the call ends.\n- [ ] Replay is intentional: completed calls with matching identity and input fingerprints replay. Change a hashed field (normally the prompt) when a completed call must run again.\n- [ ] Loops terminate on bounds the script controls; caps and drops are `log()`-ed, not silent.\n- [ ] `checkpoint()` guards irreversible actions, with a sane headless `default` or an intentional `headless: "pause"`.\n- [ ] `return` a compact, structured result \u2014 it is the run\'s `result`, not a transcript.\n- [ ] The MCP `workflow` run preflight succeeds with no surprising warnings before admission.\n\nFor the complete `agent()` option table, model-routing grammar, checkpoint options, error codes, `meta.backends` config fields, and the MCP tool input shapes, see the **Workflow script reference** section below.\n\n\n## Running workflows \u2014 the MCP `workflow` tool\n\nUse the connected `workflow` tool for deterministic batch orchestration (the server also registers a separate `repl` tool for interactive orchestration, out of scope here).\n\nThe stdio command the host spawns is a thin **shim**. It proxies to a shared per-user **workflow daemon** (Streamable HTTP on loopback, auto-started on first use). Runs execute in the daemon, so they survive session end, host restarts, and tool timeouts; only daemon exit can interrupt in-flight work. Any later session can await, inspect, or stop a run. Runs, journals, and logs persist under `~/.agentprism/workflows/` per project namespace.\n\nEvery `run` call names its project with the required `projectDir` argument \u2014 an absolute path, normally the workspace root. One registration serves every project. `inspect`/`await`/`stop` take only a `runId`; the runId locates its project store automatically. Add `--in-process` to the args for the pre-daemon single-process behavior (`projectDir` is then optional), or register the daemon\'s HTTP endpoint directly in HTTP-capable hosts (`agentprism-workflow daemon url` prints snippets). The command resolves at spawn time, so a reconnect (`/mcp` in Claude Code) picks up the latest published version.\n\n### The `workflow` tool, by action\n\n- **Config** (`{ action: "config", projectDir, harnesses?, modelSpecs?, modelFilter? }`): discover live model, mode, effort, and `configOptions` values from no-prompt backend sessions. Use `harnesses` plus `modelFilter` to find ids, then `modelSpecs` to select exact models and read their model-specific option domains. It starts no workflow and spends zero tokens. Use it only when pinning those values; an omitted model or backend-only model uses configured defaults without discovery.\n- **Run** (default, no `action`): supply exactly one of `script` (the raw source string, no Markdown fences) or `scriptPath` (an absolute path on the server\'s filesystem), plus `projectDir`. The tool automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded `status:"rejected"` diagnostics with no run ID, background slot, or token spend. A path is read once at admission and its content snapshotted; later edits affect only a new run. `args` arrives in the script as the `args` global; the run\'s base directory is the `cwd` global. Some hosts hand `args` through as a JSON **string** \u2014 tolerate both shapes (`typeof args === "string" ? JSON.parse(args) : args`). Foreground streams progress but is bound to the request and its timeout. Pass `background: true` for anything that may outlive one request; it acknowledges after durable admission with a `runId`.\n- **Await** (`{ action: "await", runId, waitMs }`): bounded collection for background runs. A timeout is progress, not failure \u2014 call again (`waitMs: 20000` is typical). At terminal status the response adds `outcome`: the authored result or pause context, plus `replayEligibility`, `resumeReport`, `fallbacks`, and `checkpointsTaken`.\n- **Inspect** (`{ action: "inspect", runId, lastN, labelGlob, logLines }`): a bounded snapshot \u2014 the latest matching calls with compact result previews plus the newest log lines. Use a narrow `labelGlob` to diagnose before deciding whether to resume, edit, or stop. Inspection never executes or resumes a script.\n- **Stop**: `{ action: "stop", runId }` durably aborts the whole run and returns its final snapshot; stopping a terminal run is a successful no-op. `{ action: "stop", runId, callIndex }` cancels exactly that in-flight agent: its slot settles to `null` with `AGENT_CANCELLED` and the run stays live. `labelGlob` only filters the returned snapshot; it never selects what to cancel.\n- **Resume**: a NEW run with `resumeFromRunId` plus the script content re-sent (the same `script` or `scriptPath`) and the desired `args` (+ `checkpointReplies` when answering a durable checkpoint). Read the returned `replayEligibility` for the predicted and observed replay prefix; never assume a prefix hit. Full semantics: **Determinism and resume**.\n\n### Operating rules\n\n- **Always retain the returned `runId`.** A paused, failed, or aborted response carries a redacted final-20 `logTail`. Read it before you change anything. Every admitted script is also an immutable resource at `workflow://runs/{runId}/script`, so a later session can recover a lost inline script.\n- **Two fingerprints control replay.** The identity hash covers the prompt, the resolved model, `mode` when set, non-empty sorted `configOptions`, `tier`, `phase`, `agentType`, the resolved agent definition, and the schema. The input fingerprint covers the resolved label, per-call `cwd` and isolation, `keepSession`, images, MCP servers, session/prompt metadata, and the approved script-backend digest.\n- **Operational bounds are not replay inputs.** Host `concurrency`, `agentRetries`, and `agentTimeoutMs`, plus per-call `timeoutMs` and `retries`, enter neither fingerprint. A resume does not inherit them from its source run; pass the values you want on every run. `agentTimeoutMs` caps the wall-clock time of each attempt; it is not an idle timer. A per-call `timeoutMs` can tighten that ceiling but cannot escape it. Each retry gets a fresh clock, so the envelope is `(resolved retries + 1) \xD7 resolved timeout`, with retries clamped to 3.\n- **Old journals stay usable.** Input formats below 2 replay positionally with `fallbackReason: "inputs-format-legacy"`. A current-format crash snapshot uses identity matching even without terminal-environment capture. Ancestor-scoped rows carried from \u22640.23 resume chains replay only while that ancestor run is still persisted. Journals resume across filesystem, environment, engine, Node, and V8 changes; `replayEligibility` reports those differences as diagnostics, never as gates.\n- **A background start returns immediately.** It sends no progress after it returns; collect progress with later bounded awaits. Background runs have no live checkpoint channel, so authored `headless` checkpoint modes apply. When a run\'s owner process dies, cold preflights reconcile stale `pending`/`running` state to `paused` with `pauseReason: "interrupted"`; a live owner is left alone.\n- A run paused with `reason: "auth_required"` resumes as a new run after you log in the backend\'s own CLI out of band.\n\n### Execution logs \u2014 the events resource\n\nEvery journaling run publishes an MCP resource at `workflow://runs/{runId}/events`. Subscribe to the canonical URI for advisory `resources/updated` hints, then read and paginate with `after`, `limit`, and `streamId`. Progress is coarse and redacted: `agentTranscript` rows are assistant/tool upserts partitioned by `(scope, callIndex, executionStartSeq)` and reduced by greatest revision per entry index. The durable cursor is authoritative when hints coalesce or a subscriber falls behind.\n\nEmbedding hosts can drive the same contract with `runDynamicWorkflow` / `WorkflowManager` from `@automatalabs/workflows`; the script contract is identical either way.\n\n## Choosing the agent for each call\n\nThe backend is selected **per `agent()` call** from its effective `model` string. One script can plan on one vendor\'s agent, implement on another\'s, and review on a third\'s, handing structured results between them.\n\nThe built-in names (`claude`, `codex`, `opencode`, `pi`) come from the runtime backend registry. Registered custom names extend that set.\n\n- **Omit `model` entirely** for maximum portability \u2014 the call runs on whatever default backend the host configured (`AGENTPRISM_DEFAULT_BACKEND`, or the host\'s session model). A script with no model specs anywhere runs unchanged on any backend.\n- **Route by one registered first segment.** Split on the first `/`; ASCII-case-insensitive `claude`, `codex`, `opencode`, `pi`, or a registered custom backend name selects that harness and is stripped exactly once. A custom registration wins on a built-in-name collision.\n- **Use a backend name alone** (`claude`, `codex`, `opencode`, `pi`, or a custom name) to preserve the harness\'s configured default model. No model config call is made.\n- **Everything else goes intact to the default backend.** `anthropic/\u2026`, `openai/\u2026`, bare `opus`, and bare `gpt-\u2026` are not routing aliases. When an id remains after routing, it is sent byte-for-byte: no catalog matching, case folding, bracket parsing, effort/Fast option driving, retry, or fallback. Harness rejection is an agent error.\n- **`tier`** (`"small" | "medium" | "big"`) is a coarse alternative resolved from the host\'s tier config \u2014 use it for "a cheap model" without naming a vendor.\n\nThe published examples use ids verified against live harness catalogs: `claude/opus[1m]`, `codex/gpt-5.6-sol`, and `opencode/zai/glm-5.2`. For Pi, `pi/openrouter/vendor/model-id` strips only `pi/`; Pi then splits provider `openrouter` from model id `vendor/model-id`. Prefer backend-only forms when the desired model is configured inside the harness.\n\nNever guess model ids, effort values, or option names from memory. With MCP, call the `workflow` tool using `action:"config"` and optional `harnesses` / `modelFilter`; it returns the live catalog without starting a workflow.\n\nOne no-prompt session per harness, zero tokens: the table lists every negotiable session option \u2014 model ids (including bracket variants like `opus[1m]`), effort levels, modes \u2014 exactly as the installed harness advertises them. One caveat: the bare `config` probe reads each harness with its **default model** selected, and option domains are **model-specific**. An option can appear only after a particular model is selected. Ceilings differ per model. Provider-served variants of the same model can advertise different domains. The authoritative per-model probe is the validator run on your real script: it selects each authored `{ backend, model }` pair first and echoes that pair\'s advertised table. Confirm every pinned model against its own echoed table; do not read package internals to discover options.\n\n```js\nconst plan = await agent(PLAN_PROMPT, { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN });\nconst impl = await agent(implPrompt(plan), { label: "implement", model: "codex/gpt-5.6-sol" });\nconst review = await agent(reviewPrompt(impl), { label: "review", model: "claude/opus[1m]", schema: REVIEW });\n```\n\nUse `configOptions` only for exact ACP session options advertised by that routed harness. With MCP, read the selected harness\'s `action:"config"` result before choosing ids or select values; catalogs vary by harness version, login, and machine.\n\n```js\nconst impl = await agent(implPrompt(plan), {\n label: "implement",\n model: "codex",\n configOptions: { "fast-mode": true, reasoning_effort: "high" },\n});\n```\n\nIds and string/boolean values pass through verbatim in ascending id order, after model selection and before the prompt. There are no aliases, coercion, client-side vocabulary, defaults, or cached catalogs. Copy option ids character-for-character from the catalog, punctuation included \u2014 `"fast-mode"`, not `fast_mode` \u2014 and quote ids that are not valid identifiers. Never put `"model"` in `configOptions`; use the dedicated `model` field. A harness rejection follows the ordinary agent-error path.\n\nPi\'s thought-level option is named `thinkingLevel`, and its choices depend on the exact model in the same call:\n\n```js\nconst review = await agent(REVIEW_PROMPT, {\n label: "pi-review",\n model: "pi/openrouter/vendor/model-id",\n configOptions: { thinkingLevel: "high" },\n});\n```\n\nValidation selects `openrouter/vendor/model-id` before reading Pi\'s choices. A listed value passes unchanged. A recognized value above an ordered model\'s ceiling, or in a model-specific gap, passes with a warning that names the effective clamp target. Pi advertises its SDK-derived domain directly. Claude and Codex are also ordered: when their options omit domain metadata, validation enumerates the advertised models and merges their per-model effort orders. A Claude model without an `effort` option does not support effort, and `default` never becomes a ceiling target. OpenCode and custom backends have no declared value order, so validation is exact-set. An unrecognized or unadvertised value fails with exit code `2`. Enumeration stops at 32 advertised models; a larger or inconsistently ordered catalog warns and falls back to exact advertised-value validation.\n\n**The harness is authoritative.** The client never substitutes a nearby model or silently falls back. A rejected id follows the existing agent-error path; a harness that accepts or ignores it determines the outcome. The public `fallbacks`/`onModelFallback` fields remain for compatibility but model resolution does not emit them.\n\n## Structured output\n\nPass `schema` \u2014 a **plain JSON Schema object literal** (no schema builders exist inside the realm) \u2014 and the call resolves to a **validated object** instead of text:\n\n```js\nconst FINDINGS = {\n type: "object",\n additionalProperties: false,\n required: ["findings"],\n properties: {\n findings: {\n type: "array",\n items: {\n type: "object",\n additionalProperties: false,\n required: ["file", "line", "summary"],\n properties: {\n file: { type: "string", description: "Repo-relative path \u2014 copy it exactly, never invent one" },\n line: { type: "number", description: "1-indexed line the finding anchors to" },\n summary: { type: "string", description: "One sentence stating the defect, grounded in code you actually read" },\n },\n },\n },\n },\n};\n\nconst report = await agent("Review the diff on this branch for correctness bugs.", {\n label: "review", schema: FINDINGS,\n});\nreport.findings.forEach((f) => log(`${f.file}:${f.line} ${f.summary}`));\n```\n\nThe same schema works on **every** backend; only the fulfillment channel differs, and the runner picks it for you: Claude uses its `outputFormat`, Codex its strict `outputSchema`, while Pi, OpenCode, and eligible custom ACP agents receive a client-hosted `StructuredOutput` MCP tool when they advertise HTTP MCP support. Pi accepts stdio, Streamable HTTP, and SSE MCP servers. If no valid tool capture exists, Pi retains the runner\'s common prompt-embedded schema and validated final-text JSON fallback. In every channel the runner validates the value client-side (with type coercion) and re-prompts a bounded number of times before failing the call with non-recoverable `SCHEMA_NONCOMPLIANCE`.\n\nSchema authoring rules that keep all channels healthy:\n\n- Root must be an object; set `additionalProperties: false` and list every property in `required`.\n- Put a `description` on every field \u2014 descriptions are the per-field prompt.\n- Keep schemas structurally simple. Exotic keywords (`oneOf`, `patternProperties`, unusual `format`s, backreference regexes) are normalized or stripped on the wire for some backends \u2014 validation still enforces them client-side, which shows up as re-prompt churn. Prefer `anyOf`, `enum`, and plain types.\n- Keep free-text fields small (tens of lines). An oversized structured output can exhaust schema repair and fail the call.\n- Validation checks structure, not truth. Check load-bearing values in script code (for example, reject findings whose `file` is not in a known file list) before spending more agents on them.\n\n## The `meta` header\n\nEvery script must **begin** with `export const meta = {...}` as a plain object literal (no computed values \u2014 it is parsed from the source text before anything runs):\n\n```js\nexport const meta = {\n name: "fix-flaky-tests", // required\n description: "Find flaky tests and fix them", // required\n phases: [ // optional; one { title, detail?, model? } entry\n { title: "Find", model: "opencode/zai/glm-5.2" }, // per phase() call, matched by exact title;\n { title: "Fix" }, // a phase model is that phase\'s default\n ],\n model: "claude/sonnet", // optional run-wide default model\n backends: { /* optional custom ACP agents \u2014 see "Custom ACP backends" */ },\n};\n```\n\nPer-agent model resolution order: explicit `agent({ model })` > `agent({ tier })` > the current phase\'s `model` > `meta.model` > the host session\'s default. So `meta.phases[].model` gives a whole phase a backend without repeating it on every call.\n\n## Fan-out: `parallel` and `pipeline`\n\n```js\n// parallel: an array of THUNKS (not promises!) run concurrently \u2014 a barrier that\n// resolves in input order. A failed slot resolves to null; filter before use.\nconst sweeps = (await parallel([\n () => agent("Audit error handling in src/server", { label: "sweep:errors", schema: FINDINGS }),\n () => agent("Audit input validation in src/api", { label: "sweep:input", schema: FINDINGS }),\n])).filter(Boolean);\n\n// pipeline: each item flows through the stages independently \u2014 NO barrier between\n// stages, so item A can be in stage 2 while item B is still in stage 1.\n// Stages receive (previousResult, originalItem, index).\nconst verified = (await pipeline(\n sweeps.flatMap((s) => s.findings),\n (f) => agent(`Adversarially verify this finding \u2014 try to refute it:\\n${JSON.stringify(f)}`,\n { label: `verify:${f.file}`, schema: VERDICT }),\n (verdict, f) => ({ ...f, real: verdict.real }),\n)).filter(Boolean).filter((f) => f.real);\n```\n\n**Default to `pipeline`** for multi-stage work. Add a `parallel` barrier only when the next stage needs *all* prior results at once: dedup across the full set, early-exit on a zero count, or prompts that compare "the other findings". The test is the **information dependency** \u2014 a barrier\'s cost is real, because the fastest worker idles for the slowest. All coordination lives in script code: agents cannot see each other, so never ask an agent to "check with the other reviewers" or "spawn helpers". Passing a promise instead of a thunk to `parallel` is a `TypeError` \u2014 wrap every call: `() => agent(...)`.\n\nFan-out also contends for the **working tree**, not just the concurrency limiter. Two agents running builds or test suites in the same checkout collide on build outputs, caches, and lockfiles, and concurrent `git fetch`es contend on the same `.git`. Give run-things agents `isolation: "worktree"` when the commits they must inspect are reachable from the run cwd\'s repository, or serialize them; fan out freely only the agents that just read.\n\nThe host caps concurrent agents per run (default 8); hand `parallel`/`pipeline` as many items as the task needs and let the limiter schedule them. The cap counts active agent attempts, not authored branches: queued branches begin as other attempts finish, and a branch that exhausts its timeout settles to `null` and frees its slot. `workflow(nameOrScript, args)` nests another workflow inline (one level deep, sharing this run\'s limiter) \u2014 inline script strings always work; saved names resolve when the host serves a workflows folder (see the reference section below).\n\n## Failure semantics \u2014 design for `null`\n\n- A **recoverable** failure (timeout, empty output, transient execution error) is retried per the call\'s `retries` (default 0), then the call **resolves to `null`** \u2014 inside `parallel`/`pipeline` *and* as a bare `await agent(...)`. Null-check anything load-bearing, and set `retries: 1\u20132` on steps you can\'t afford to lose.\n- A host can settle one runaway in-flight call with MCP `{ action: "stop", runId, callIndex }` or SDK `manager.cancelAgentCall(runId, callIndex)`. The call resolves to `null` with `AGENT_CANCELLED`, skips every configured retry, and does not abort the run or its siblings. Its failed call record is not cached as a journal result, so a later resume runs that occurrence live.\n- A **non-recoverable** failure (schema never validated, script bug) throws and fails the run. You *may* `try/catch` around an `agent()` call to degrade gracefully \u2014 rethrow anything you can\'t meaningfully handle. In particular, **always rethrow pause-class errors** (`err.code === "PROVIDER_USAGE_LIMIT"` or `"AUTH_REQUIRED"`): they must propagate out of the script so the engine can pause the run resumably \u2014 swallowing one converts that pause into a fake, lossy completion.\n- A **provider quota wall, missing backend authentication, or opted-in durable checkpoint pauses a managed run instead of failing it** \u2014 the journal checkpoints and the host can resume after the provider quota refills, authentication completes, or a checkpoint decision is supplied. Direct `runner.run()` calls still receive the `AUTH_REQUIRED` error because they have no manager lifecycle.\n- Per-call knobs: `timeoutMs` and `retries`. A finite `timeoutMs` may shorten the host\'s run-level `agentTimeoutMs` ceiling; `null` or omission is uncapped only when the host supplied no ceiling. The timeout is total wall-clock time per attempt, and every retry gets a fresh clock.\n\n## Phases\n\n```js\nphase("Explore"); // open a named phase: subsequent agents group under it\n\nconst found = [];\nwhile (found.length < 20) {\n const r = await agent("Find one more edge case not in: " + JSON.stringify(found.map((f) => f.name)),\n { label: `edge:${found.length}`, schema: EDGE });\n if (!r) break;\n found.push(r);\n}\n```\n\nTerminate every loop on a bound the script controls. The agent-count limit (`maxAgents`) is hard: once exhausted, further `agent()` calls throw `AGENT_LIMIT_EXCEEDED`. `phase()` groups agents in progress UIs and run logs; `log(msg)` (and `console.log`) append to the run log \u2014 narrate what matters, especially anything you drop.\n\n## Built-in quality loops\n\nThese helpers spawn their own subagents (on the default model \u2014 hand-roll with `parallel` + `agent` when you want panel members on specific backends). Full signatures in the reference section below.\n\n| helper | shape | use for |\n|---|---|---|\n| `gate(produce, validate, { attempts })` | produce \u2192 validate \u2192 feed `feedback` back; return `{ ok, value, verdict, attempts }` | produce-until-a-reviewer-approves loops that need the final review evidence |\n| `retry(thunk, { attempts, until })` | bounded retry until `until(result)` holds | flaky single steps |\n| `verify(item, { reviewers, threshold, lens })` | N adversarial reviewers vote `real`/not | killing plausible-but-wrong findings |\n| `judgePanel(attempts, { judges, rubric })` | score candidates 0\u20131 against a rubric, return the best | picking among independent solutions |\n| `loopUntilDry({ round, key, consecutiveEmpty, maxRounds })` | repeat a round, dedup by `key`, stop when dry | unknown-size discovery (bugs, edge cases) |\n| `completenessCheck(args, results)` | one critic lists what\'s still missing | a final "what did we not cover?" pass |\n\nThe `gate` pattern, spelled out \u2014 note how the producer thunk threads the validator\'s feedback into a *fresh* agent\'s prompt (sessions have no memory):\n\n```js\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement the fix described here:\\n${JSON.stringify(plan)}\\n` +\n (feedback ? `\\nA reviewer rejected attempt ${attempt}: ${feedback}\\nAddress every point.` : ""),\n { label: `fix:${attempt + 1}`, model: "codex/gpt-5.6-sol" },\n ),\n (result) => agent(\n `Run the test suite and review this change summary:\\n${result}\\n` +\n `Return ok=true only if tests pass and the fix is correct; include the reviewed commit SHA.`,\n { label: "gate-review", model: "claude/opus[1m]", schema: { type: "object", additionalProperties: false,\n required: ["ok"], properties: { ok: { type: "boolean" }, feedback: { type: "string" },\n commitSha: { type: "string" } } } },\n ),\n { attempts: 3 },\n);\nif (!outcome.ok) log(`reviewer never approved after ${outcome.attempts} attempts`);\nelse log(`reviewer approved commit ${outcome.verdict?.commitSha ?? "(unspecified)"}`);\n```\n\nFeedback is the producer\'s only context for the next attempt. Interpolate everything it needs, and name only files that provably exist.\n\n## Human gates: `checkpoint()`\n\n`checkpoint(promptText, options?)` is a zero-token, journaled human gate. With MCP elicitation (or a live SDK `confirm` callback) it waits for that reply; without a live channel, its default mode takes `default ?? true` immediately, so detached runs never hang.\n\n```js\nconst proceed = await checkpoint(`Apply this plan?\\n${JSON.stringify(plan, null, 2)}`, {\n kind: "confirm", // "confirm" | "input" | "select"\n default: false, // default headless mode takes this (or true)\n // headless: "abort", // abort when no live human is attached\n // headless: "pause", // or persist a resumable human-decision pause\n});\nif (!proceed) return { applied: false, plan };\n```\n\n`kind: "input"` resolves to free text, `kind: "select"` to one of `choices`. How the question reaches a human is the host\'s job (elicitation in the MCP server; `ExecOptions.confirm` in the SDK). With no live channel, `headless: "default"` (the default) takes `default ?? true`, `"abort"` aborts, and `"pause"` returns a managed run with `reason: "checkpoint_required"` plus non-secret `checkpointContext`. Resume the last mode with `checkpointReplies: { [context.callIndex]: decision }` or a live confirm. For `resumeFromRunId`, that key is the source context index; an unambiguous identity match may journal the injected answer at a shifted current index. Put a checkpoint before anything hard to reverse \u2014 applying diffs, pushing, publishing, or the first commit into a working copy the workflow did not create (`default: true` keeps detached runs moving).\n\n## Working directory, isolation, confinement\n\n- Every agent session runs in the run\'s base `cwd` unless the call narrows it: `agent({ cwd: "packages/api" })` (relative resolves against the base).\n- `isolation: "worktree"` runs the agent in a **throwaway git worktree** (`<repoRoot>/.agentprism/worktrees/\u2026`) so parallel agents can edit without colliding. The worktree and its branch are **always deleted when the call ends \u2014 an isolated agent\'s file edits are discarded**. Have isolated agents *return their work as data* (a unified diff, a file map, a report) and apply it in a later non-isolated step; use worktrees for experiments, builds, and verification, not for persistent edits. Outside a git repo, isolation degrades to the shared tree with a logged notice.\n- `resume: { filesystem: "read-only" }` is a deprecated compatibility annotation. It is not a runner mode and has no effect on replay; completed calls replay by journal correspondence whether they read or write. Use `mode`, tool policy, prompts, and worktrees when you actually need confinement.\n- `mode` requests an agent-advertised ACP session mode and is **strict** \u2014 an unsupported mode fails the call rather than running unconfined. Mode ids are backend-specific and drift with harness versions: read the advertised `mode` select from the MCP workflow tool\'s `action:"config"` result or an automatic run-preflight report (Codex-family examples: `read-only`, `agent`; Claude-family advertises permission modes such as `plan` and `acceptEdits`; OpenCode via its mode option; Pi advertises thinking-level config rather than modes). Only set `mode` on calls whose `model` you also pin. Use read-only/plan modes for reviewers and auditors that must not write.\n- `agentType: "<name>"` binds a reusable subagent definition \u2014 a Markdown file at `<cwd>/.agentprism/agents/<name>.md` (project) or `~/.agentprism/agents/<name>.md` (user; project wins) whose frontmatter sets tool allow/deny lists, a model, and isolation, and whose body is the role prompt. An unknown name logs a warning and degrades to defaults.\n\n## Where a mutating workflow runs\n\nThe run\'s base `cwd` is the USER\'S checkout \u2014 the working copy they launched the host from. Treat it as borrowed: committing onto whatever branch is checked out, switching branches, or resetting it are defects unless the user asked for exactly that. A script that commits should verify its target workspace in a preflight step, or create its own workspace idempotently, and refuse on a mismatch rather than adapt. `isolation: "worktree"` is NOT such a workspace \u2014 it is per-call and throwaway. Note also that a throwaway worktree branches from the run cwd\'s repository: an isolated agent sees another agent\'s commits only when they are reachable there.\n\n## Wiring tools and inputs into a call\n\n- `mcpServers: [{ name, command, args: [], env: [] }]` attaches MCP servers to that agent\'s session \u2014 the portable way to hand any backend a capability (image generation, a browser, a ticket system). The agent sees the server\'s tools natively. Note `env` is a list of `{ name, value }` pairs (ACP shape), not an object map; HTTP/SSE servers use `{ type: "http", name, url, headers: [] }`.\n- `images: [...]` appends base64 image blocks to the prompt (backends without image support receive a bracketed text note instead).\n- `meta` / `promptMeta` pass generic ACP `_meta` through to `session/new` / `session/prompt` \u2014 the escape hatch for driving a custom agent\'s extension surface.\n- `keepSession: true` keeps a successful agent\'s ACP session re-openable after the run: the re-attach record (sessionId, backend, effective pool identity, cwd, reopen capabilities) lands in `WorkflowRunResult.agentSessions`, and the HOST can continue that conversation later via `runner.loadSession()`. Usage/auth pause failures are kept open automatically so managed resume can continue the interrupted occurrence. Scripts themselves never request reattach.\n\n### Custom ACP backends\n\nAny process that speaks ACP over stdio can serve `agent()` calls \u2014 an in-house browser-QA agent, an image generator, a domain-specific executor. Two ways in:\n\n1. **Host-registered** (preferred): the embedder passes `createAcpRunner({ backends: { browser: { command: "/abs/browser-acp" } } })`; the script just routes with `model: "browser"`.\n2. **Script-declared**: the script itself declares the backend in `meta.backends` \u2014 but declarations are **inert until the host approves them** (an elicitation in the MCP server; `allowScriptBackends` in the SDK), because they spawn commands on the host machine. Don\'t rely on them silently working.\n\n```js\nexport const meta = {\n name: "checkout-qa",\n description: "Implement, then QA the checkout flow in a real browser",\n backends: {\n browser: { command: "browser-acp", args: ["--headless"] }, // requires host approval\n },\n};\n\nconst change = await agent("Implement the coupon-code field per the spec in docs/coupon.md.",\n { label: "implement" }); // default backend\nconst verdict = await agent(\n `Open the app, walk through checkout with coupon SAVE20, and verify the discount line. Change summary:\\n${change}`,\n { label: "qa", model: "browser", // the custom agent\n schema: { type: "object", additionalProperties: false, required: ["passed"],\n properties: { passed: { type: "boolean" }, notes: { type: "string" } } } },\n);\nreturn { change, qa: verdict };\n```\n\nStructured output works on custom backends through the same injected-tool/fallback ladder as OpenCode \u2014 no special-casing in the script.\n\n## Determinism and resume\n\nRuns are journaled: every `agent()` and `checkpoint()` result is recorded under a deterministic call index. A new run may reuse eligible results from a terminal source run. Uncertainty always means live execution.\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\n- Direct `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` calls fail static validation. The realm also blocks aliased or computed forms at runtime; `new Date(isoString)` is fine. Pass timestamps and random seeds through `args`.\n- The replay identity of an `agent()` call hashes: the prompt, the resolved `model`, `mode` when set, `configOptions` when non-empty (sorted keys), `tier`, `phase`, `agentType`, the resolved agent definition, and `schema`. The resolved agent definition includes its tool allowlist and denylist, model, isolation, and body prompt \u2014 editing a definition invalidates the calls that use it.\n- A separate input fingerprint hashes: the resolved label, per-call `cwd`, resolved isolation, `keepSession`, `images`, `mcpServers`, `meta`, `promptMeta`, and the approved script-backend digest.\n- Host `agentTimeoutMs`, `agentRetries`, and `concurrency`, plus per-call `timeoutMs` and `retries`, are operational bounds. They enter neither hash and may change freely on resume. A new run resolves them from its own request; it does not inherit the source values.\n- `args` is not hashed directly. New args that only raise a loop cap leave earlier identities unchanged, so those calls can replay. New args that change a prompt, model selection, phase, schema, call order, or runner-visible input make the affected calls run live. Unchanged independent calls may still replay.\n- Matching tries a unique exact `(kind, call path, identity hash)` row first (`"path-hash"`), then a unique `(kind, identity hash, input fingerprint)` row, so an unchanged call can replay as `"unique-hash"` after insertions or deletions. Source and current input fingerprints must be equal. Duplicate identities, duplicate content, consumed candidates, missing facts, and empty schema-less results run live. The engine never guesses by source order or occurrence.\n- Source admission requires: exact `cwd`, compatible call-path/input/checkpoint fingerprint formats, complete call/journal/allocation metadata, and a valid manifest and seed. Git HEAD and dirty digest, `environmentKey`, captured environment values, Node/V8, and producing engine version are diagnostics only. Environment differences may appear in `replayEligibility.provenanceChanges`; they never gate admission or matching.\n- A completed writer replays exactly like a reader. A live call, nested workflow, host checkpoint callback, or degraded worktree does not clear unrelated candidates. Nested child calls run live \u2014 they are outside the parent\'s journal \u2014 while matching root calls around them still replay. The engine does not reproduce file writes; a later live agent navigates the world it finds.\n- Replay costs zero current provider usage: a cached call returns its recorded result without spawning a session. Replayed session records keep their backend and session identity, rebound to the current call index, label, and phase.\n- A root call interrupted by `PROVIDER_USAGE_LIMIT` or `AUTH_REQUIRED` can continue its recorded session on either resume API. Continuation requires: the exact call index, identity hash, complete input fingerprint, non-worktree isolation, identical existing cwd, a coherent recorded session, and the runner\'s current backend/`poolKey`/reopen gates. A successful continuation finishes the unfinished turn and charges only its usage delta. Every failed gate runs fresh, and `fallbacks` records the reopen method or the exact skip reason. No script option controls this.\n- Completed checkpoint results replay when the identity and the `default`/`headless`/`timeoutMs` fingerprint match \u2014 headless results included. `checkpointReplies` keys always name the checkpoint index in the source run. A moved reply can follow intact prior correspondence; after a live divergence it must reach the exact recorded call site, so a different same-text branch cannot consume it.\n- `resumePolicy: "positional"` is a migration escape hatch for index/prefix matching. It cannot bypass format, metadata, manifest, cwd, or input checks. Marker-less, manual, and same-ID legacy journals keep historical hash-only positional behavior. Input formats below 2 use the `inputs-format-legacy` positional bridge and are rewritten under the current format on the next hop. A current-format crash snapshot with a valid identity manifest uses identity matching even without terminal-environment capture.\n- `label`, `cwd`, `mcpServers`, `images`, `meta`, `promptMeta`, and `keepSession` are not identity-hashed: changing one does not invalidate an ordinary replay. They are in the input fingerprint: changing one rejects continuation of an interrupted turn, and that occurrence runs fresh. To force a completed call to run again, change a hashed field \u2014 normally the prompt.\n- Keep call order deterministic. Derive iteration from `args` and prior agent results, never from ambient state.\n\nEvery `resumeFromRunId` result has a bounded `replayEligibility` summary. Background admission, foreground completion, both await shapes, and inspect expose the same fields: strategy, predicted replayable-prefix length, observed replayed prefix and counts, and the first non-replay when known. Active correspondence reasons include `strategy-live`, `positional-miss`, `positional-suffix`, `not-recorded`, `path-missing`, `inputs-missing`, `inputs-changed`, `ambiguous-identity`, `ambiguous-content`, `candidate-consumed`, `empty-output`, `worktree-degraded`, `seed-persistence-error`, and `resume-fatal-latch`. Older reason literals stay exported only so historical journals parse. Engine and input-format versions and environment provenance ride along as diagnostics.\n\nAn all-live outcome means correspondence could not be established \u2014 not that the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest or seed disable new-format replay. If any source row lacks a captured path or input fact (possible past the raw-frame cap, or with a non-strict-JSON `meta` value), the whole source is `"manifest-invalid"`: dropping the row could make an ambiguous sibling look unique.\n\n### Worked resume \u2014 raise a loop cap\n\nThe following workflow (shipped as `examples/resume-loop-cap.workflow.js`) requires eight reviews but lets the caller cap how many are attempted in one run:\n\n```js\nexport const meta = {\n name: "resume-loop-cap",\n description: "Run expensive review rounds up to an args-controlled cap",\n phases: [{ title: "Review" }],\n};\n\nconst input = args && typeof args === "object" && !Array.isArray(args) ? args : {};\nconst numericCap = Number(input.maxRounds);\nconst maxRounds = Number.isInteger(numericCap) && numericCap > 0 ? numericCap : 8;\n\nphase("Review");\nconst rounds = [];\nfor (let i = 0; i < maxRounds; i += 1) {\n rounds.push(\n await agent(\n `Review round ${i + 1}: inspect the repository and report unresolved release blockers.`,\n { label: `review:${i + 1}`, phase: "Review" },\n ),\n );\n}\n\nif (maxRounds < 8) throw new Error(`review cap ${maxRounds} reached before 8 rounds`);\nreturn { rounds };\n```\n\nRun it with `args: { "maxRounds": 6 }`. Then send the same content (via `script`, or the absolute `scriptPath` you edit) with `args: { "maxRounds": 8 }` and the first result\'s `runId` as `resumeFromRunId`. Rounds 1\u20136 replay for zero current provider tokens; only rounds 7\u20138 run live, because the cap controls call count but is not interpolated into the round prompt. If every round prompt included `maxRounds`, all eight identities would change and all would run live. Resume always states its content; a bare `resumeFromRunId` never silently reuses the old script.\n\nGive repeated calls stable, descriptive labels and narrate decisions with `log()` \u2014 inspection by `labelGlob` then turns a pause or failure into a diagnosis instead of a guess.\n\n### Kill, patch, resume\n\nStop the live run with `{ action: "stop", runId }`. The returned `aborted` snapshot is the durable acknowledgement: resume is safe immediately, and a further await adds nothing. Edit the file. Start a new run with its absolute `scriptPath` and `resumeFromRunId`. Every completed call whose recorded identity and input fingerprint correspond replays, regardless of filesystem or environment drift. Read `replayEligibility` and the full `resumeReport` for the per-call decisions. A repeated stop of a terminal run is a successful no-op.\n\nRegistration, the per-action contracts, background collection, and the events resource are covered in the **Running workflows** section above. Resume a durable checkpoint pause by re-sending the script with `resumeFromRunId` and `checkpointReplies` keyed by the source run\'s `checkpointContext.callIndex`.\n\n## Worked example \u2014 cross-vendor build with every major primitive\n\n```js\nexport const meta = {\n name: "feature-build",\n description: "Plan, gate on approval, implement, cross-vendor review, fix until green",\n phases: [{ title: "Plan" }, { title: "Implement" }, { title: "Review" }],\n};\n\nconst PLAN = { type: "object", additionalProperties: false, required: ["steps", "risks"],\n properties: {\n steps: { type: "array", items: { type: "string", description: "One concrete implementation step" } },\n risks: { type: "array", items: { type: "string" } } } };\nconst VERDICT = { type: "object", additionalProperties: false, required: ["ok"],\n properties: { ok: { type: "boolean" },\n feedback: { type: "string", description: "Required when ok=false: concretely what to change" } } };\n\nphase("Plan");\nconst plan = await agent(\n `Study this repo, then write an implementation plan for: ${args.feature}. Keep steps concrete.`,\n { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN },\n);\n\nconst approved = await checkpoint(\n `Implement "${args.feature}" with this plan?\\n- ${plan.steps.join("\\n- ")}\\nRisks: ${plan.risks.join("; ")}`,\n { kind: "confirm", default: true },\n);\nif (!approved) return { implemented: false, plan };\n\nphase("Implement");\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement: ${args.feature}\\nPlan:\\n- ${plan.steps.join("\\n- ")}\\n` +\n `Run the project\'s tests before finishing and report results.` +\n (feedback ? `\\n\\nReviewer feedback on attempt ${attempt}:\\n${feedback}\\nAddress every point.` : ""),\n { label: `implement:${attempt + 1}`, model: "codex/gpt-5.6-sol", retries: 1 },\n ),\n async (report) => {\n if (!report) return { ok: false, feedback: "implementation agent produced no result" };\n phase("Review");\n const reviews = (await parallel([ // two reviewers on different vendors\n () => agent(`Review the working-tree diff for correctness. Implementer\'s report:\\n${report}`,\n { label: "review:correctness", model: "claude/opus[1m]", schema: VERDICT }),\n () => agent(`Review the working-tree diff for regressions and missing tests. Report:\\n${report}`,\n { label: "review:coverage", model: "opencode/zai/glm-5.2", schema: VERDICT }),\n ])).filter(Boolean);\n const rejections = reviews.filter((r) => !r.ok);\n return rejections.length\n ? { ok: false, feedback: rejections.map((r) => r.feedback).join("\\n"), reviews }\n : { ok: true, reviews };\n },\n { attempts: 3 },\n);\n\nreturn { implemented: outcome.ok, attempts: outcome.attempts, reviewVerdict: outcome.verdict, plan };\n```\n\n(The planner would ideally run read-only, but mode ids are backend-specific \u2014 this call routes to OpenCode, so it leaves `mode` unset rather than guessing; a Claude-routed planner could safely say `mode: "plan"`.)\n\n## Worked example \u2014 fully backend-agnostic audit\n\nNo `model` anywhere: this script runs unchanged on whatever backend the host defaults to.\n\n```js\nexport const meta = {\n name: "edge-case-audit",\n description: "Exhaustively hunt edge-case bugs in a target dir, verify each, report gaps",\n phases: [{ title: "Hunt" }, { title: "Verify" }],\n};\n\nconst BUGS = { type: "object", additionalProperties: false, required: ["bugs"],\n properties: { bugs: { type: "array", items: { type: "object", additionalProperties: false,\n required: ["file", "scenario"], properties: {\n file: { type: "string", description: "Repo-relative path you actually opened" },\n scenario: { type: "string", description: "Concrete input/state \u2192 wrong behavior" } } } } } };\n\nphase("Hunt");\nconst seen = []; // what earlier rounds reported, threaded into each new prompt\nconst candidates = await loopUntilDry({\n round: async (i) => {\n const r = await agent(\n `Round ${i + 1}: find edge-case bugs in ${args.target} not already in this list:\\n` +\n JSON.stringify(seen) + `\\nOnly report what you can ground in code you read.`,\n { label: `hunt:${i + 1}`, schema: BUGS },\n );\n const bugs = r ? r.bugs : [];\n seen.push(...bugs);\n return bugs; // loopUntilDry dedups these by `key` across rounds\n },\n key: (b) => `${b.file}:${b.scenario}`,\n consecutiveEmpty: 2,\n maxRounds: 8,\n});\n\nphase("Verify");\nconst confirmed = (await pipeline(\n candidates,\n (bug) => verify(bug, { reviewers: 3, threshold: 0.66, lens: ["correctness", "reproducibility"] }),\n (v, bug) => (v.real ? bug : null),\n)).filter(Boolean);\n\nconst gaps = await completenessCheck(args, confirmed);\nlog(`${confirmed.length}/${candidates.length} confirmed; complete=${gaps.complete}`);\nreturn { confirmed, missing: gaps.missing ?? [] };\n```\n\n## Full-scale example scripts\n\nWhen the inline examples above aren\'t enough, study the complete, validated scripts that ship with the published authoring skill:\n\n- [`repo-triage.workflow.js`](https://github.com/agentprism/agentprism-workflows/blob/main/skills/agentprism-workflow-authoring/examples/repo-triage.workflow.js) \u2014 an autonomous cross-vendor repo triage and the broadest support-API tour: `pipeline` with no inter-stage barrier, a cross-vendor verification panel, `gate()` where writer and reviewer are different vendors, nesting a saved workflow by name, `completenessCheck()`, stage gating on tracked counters, string-form `args` hardening, path guards on schema outputs, and pause-class error rethrow.\n- `quick-wins.workflow.js` (included in full at the end of this document) \u2014 a small hunter that runs standalone *or* nested: `loopUntilDry()` with per-round vendor rotation, dedup threading via a `seen` list, and a tracked round bound (nested runs share the parent\'s limiter).\n- [`resume-loop-cap.workflow.js`](https://github.com/agentprism/agentprism-workflows/blob/main/skills/agentprism-workflow-authoring/examples/resume-loop-cap.workflow.js) \u2014 content-addressed replay: run with a low `maxRounds`, resume with a higher one; unchanged rounds replay for zero tokens (worked through in Determinism and resume).\n\n[`examples/README.md`](https://github.com/agentprism/agentprism-workflows/blob/main/skills/agentprism-workflow-authoring/examples/README.md) maps each script to what it teaches.\n\n## Automatic validation before admission\n\nThe workflow tool validates every `run` automatically before admission: static parse, mocked dry run, then no-prompt checks for every routed backend/model pair. Invalid scripts return `status:"rejected"` with bounded structured diagnostics, create no run ID, reserve no background slot, and spend no tokens. A successful preflight proceeds directly to the requested foreground or background run. Use `action:"config"` before authoring pinned values: `harnesses` / `modelFilter` discover model ids, then `modelSpecs` selects exact models and returns their model-specific mode and config-option domains. A green mocked dry run proves structure and reachable control flow, not the quality of prompts or schemas.\n\n---\n\n# Workflow script reference\n\nExhaustive tables for the AgentPrism workflow script DSL. The guide above covers authoring; this section is the lookup companion. Everything here is verified against `@automatalabs/workflow-engine` / `@automatalabs/acp-agents` as shipped with `@automatalabs/workflows`.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | ACP session mode id advertised by the selected backend. **Strict**: unsupported/unadvertised ids fail the call (never silently unconfined). Ids are backend-specific and drift with harness versions \u2014 read the advertised `mode` select from the config probe or a validator report (Codex-family examples: `read-only`, `agent`, `agent-full-access`; Claude-family advertises permission modes such as `plan`, `acceptEdits`, and `dontAsk`). Part of the resume hash when set. |\n| `configOptions` | `Record<string, string \\| boolean>` | Exact ACP session option ids and authored values. Applied in ascending id order after model and before the prompt, with no aliases or coercion. `"model"` is reserved for the dedicated `model` field. Part of the resume hash only when non-empty, with sorted keys. With MCP, read the advertised-options table from `workflow` action `config` before choosing values. |\n| `agentType` | `string` | Bind a named subagent definition (tools allow/deny, model, isolation, role prompt). See [agentType definitions](#agenttype-definitions). Part of the resume hash. |\n| `isolation` | `"worktree"` | Run in a throwaway git worktree branched from the run cwd. **Always removed (worktree + branch) when the call ends** \u2014 edits are discarded; return work as data. Degrades to the shared tree outside a git repo (logged). |\n| `resume` | `{ filesystem: "read-only" }` | Deprecated compatibility annotation. It is recorded as legacy diagnostic provenance, is not sent to the runner or hashed, and has no effect on replay. New scripts should omit it. |\n| `cwd` | `string` | Per-session working directory; relative resolves against the run\'s base cwd. Overridden by worktree isolation. Not hashed. |\n| `timeoutMs` | `number \\| null` | Total wall-clock cap for each attempt. A finite value may tighten a finite host `agentTimeoutMs` ceiling but cannot raise or disable it. With no host ceiling, a finite value applies and `null`/omitted is uncapped. |\n| `retries` | `number` | Retries after *recoverable* failures (default 0, host-overridable). Exhausted retries \u21D2 the call resolves `null`. |\n| `mcpServers` | `McpServerConfig[]` | MCP servers attached to this session. Stdio shape: `{ name, command, args: [], env: [{ name, value }] }` (`args`/`env` required, `env` is name/value pairs, not a map); `{ type: "http" \\| "sse", name, url, headers: [] }` also accepted. Not hashed. |\n| `images` | `PromptImage[]` | Base64 image blocks appended to the prompt; backends without image support get a bracketed text note. Not hashed. |\n| `meta` | `object` | ACP `_meta` merged into `session/new` \u2014 session-scoped extension passthrough (pairs with custom backends). Not hashed. |\n| `promptMeta` | `object` | ACP `_meta` merged into `session/prompt` \u2014 turn-scoped passthrough. Backend-computed keys win on conflict. Not hashed. |\n| `keepSession` | `boolean` | Skip release-time best-effort `session/close`; the non-secret re-attach record lands in `WorkflowRunResult.agentSessions` for host-side `loadSession()` / `resumeSession()`. Usage/auth pause failures are kept open automatically for managed continuation. Not identity-hashed; included in the input fingerprint. |\n\nThe timeout clock measures the whole attempt, including backend startup, model/config setup, tool\nwork, and streamed output; it is not an idle timer. Each retry starts a fresh clock, so the maximum\ntimeout envelope is `(retries + 1) \xD7 resolved timeoutMs` (retries are clamped to 3). An exhausted\ntimeout is recoverable `AGENT_TIMEOUT`: the call resolves to `null`, releases its concurrency slot,\nand asks the ACP session to cancel. A session that keeps running after the cancellation grace is\nclosed where supported and its pooled child is recycled.\n\nEvery new run, including one admitted with `resumeFromRunId`, resolves host limits from that run\'s\nrequest. It does not inherit `agentTimeoutMs`, retries, concurrency, or agent-count values from\nits source, so pass every operational bound the resumed execution should use.\n\n## Model specs & routing\n\nA `model` string is resolved solely from its first segment, then delegated to the harness:\n\n| spec shape | routes to | notes |\n|---|---|---|\n| *(omitted)* | host default backend | `AGENTPRISM_DEFAULT_BACKEND` (`claude` \\| `codex` \\| `opencode` \\| `pi` \\| custom name; default `claude`), session default model. Most portable. |\n| `claude`, `codex`, `opencode`, `pi`, or `<custom-name>` | that registered harness | Backend-only: no model config call; the harness default remains active. |\n| `claude/<id>`, `codex/<id>`, `opencode/<id>`, `pi/<id>`, or `<custom-name>/<id>` | that registered harness | Match the first segment ASCII-case-insensitively and strip exactly one segment. Custom names take priority on collision. The remaining `<id>` is sent verbatim, including further `/` characters. For Pi, that remainder is its `<provider>/<model-id>` and Pi preserves any further slashes in the model id. |\n| any other string, including `anthropic/\u2026`, `openai/\u2026`, bare `opus`, or bare `gpt-\u2026` | host default backend | The **entire** authored string is sent verbatim; these are not routing aliases. |\n\nSelection is a single `session/set_config_option` with `configId: "model"` and the exact remaining string. There is no catalog matching, case folding, normalization, bracket parsing, nearest-neighbor selection, sibling effort/Fast option driving, retry, or echo verification. Brackets, dots, and provider-style prefixes are ordinary model-id characters.\n\nWhatever the harness returns is the outcome. A rejection follows the existing agent-error path with no resolution-specific code or model fallback event. `onModelFallback` and `WorkflowRunResult.fallbacks` remain public compatibility surfaces; model resolution does not emit entries, while pause recovery emits `kind: "continuation"` reattach/skip notices.\n\n## Structured output channels\n\nOne author API (`schema`), four fulfillment paths \u2014 chosen automatically per backend:\n\n| backend | channel |\n|---|---|\n| Claude | native `outputFormat`, schema normalized to Anthropic\'s structured-outputs subset (e.g. `oneOf` \u2192 `anyOf`; unsupported keywords/formats stripped on the wire) |\n| Codex | native strict `outputSchema` (OpenAI strict subset normalization) |\n| Pi | a client-hosted `StructuredOutput` MCP tool injected when the agent advertises HTTP MCP support; common prompt-embedded schema and validated final-text JSON fallback |\n| OpenCode / custom ACP | a client-hosted **`StructuredOutput` MCP tool** injected into the session when the agent advertises HTTP MCP support (an agent may show it as `structured_output_StructuredOutput`); otherwise prompt-embedded schema + JSON parse of the final message. Custom backends can opt out of tool injection with `structuredOutputTool: false`. |\n\nPi accepts stdio, Streamable HTTP, and SSE MCP servers; ACP-transport MCP hosting remains client-side.\n\nIn every channel the runner coerces + validates client-side and re-prompts a bounded number of times; the final miss fails the call with non-recoverable `SCHEMA_NONCOMPLIANCE`. Constraints stripped from the wire are still enforced client-side \u2014 an exotic schema keyword shows up as re-prompt churn, so keep schemas simple.\n\n## DSL globals \u2014 complete signatures\n\n```\nagent(prompt, options?) \u2192 Promise<string | object | null>\nparallel(thunks) \u2192 Promise<results[]> // barrier; input order; failed slot = null\npipeline(items, ...stages) \u2192 Promise<results[]> // no inter-stage barrier; stage(prev, original, index); failed item = null\nworkflow(nameOrScript, args?) \u2192 Promise<unknown> // one nesting level; names resolve from the host\'s workflows folder, inline scripts always work\ngate(thunk, validator, { attempts = 3 }) \u2192 { ok, value, verdict, attempts }\n // thunk(feedback, attempt); validator(result) \u2192 { ok, feedback?, ... } | boolean | null (may be async / an agent call)\nretry(thunk, { attempts = 3, until? }) \u2192 last result // thunk(attempt); stops early when until(result)\nverify(item, { reviewers = 2, threshold = 0.5, lens? })\n \u2192 { real, realCount, total, votes: [{ real?, reason? }] }\n // N adversarial reviewers prompted to REFUTE; lens (string | string[]) rotates focus per reviewer\njudgePanel(attempts, { judges = 3, rubric = "overall quality and correctness" })\n \u2192 { index, attempt, score, judgments } // mean 0\u20131 score per candidate; stable tie-break by index\nloopUntilDry({ round, key = JSON.stringify, consecutiveEmpty = 2, maxRounds = 50 })\n \u2192 unique items[] // round(i) returns items; stops after N dry rounds; agent-limit exhaustion returns the partial result\ncompletenessCheck(taskArgs, results) \u2192 { complete, missing?: string[] }\ncheckpoint(promptText, options?) \u2192 Promise<reply> // journaled human gate; zero tokens\nphase(title) \u2192 void // open a named phase\nlog(message) \u2192 void // console.log/info/warn/error route here too\nargs // the host-provided input value, verbatim\ncwd // the run\'s base working directory (string); process.cwd() returns it too\n```\n\nFor `gate()`, `value` is the final producer result and `verdict` is the exact last completed\nvalidator return, including any extra structured fields. `{ ok: true }` and bare `true` pass;\n`{ ok: false, feedback? }`, bare `false`, and `null` reject. Only object feedback is threaded into\nthe next producer attempt. A producer result of `null` is still passed to the validator. Producer\nor validator exceptions propagate immediately, so no partial gate result is returned and no later\nattempt runs. An explicit unsupported `undefined` validator return is a rejection represented as\n`verdict: null`. If the script returns the gate result, its complete verdict is persisted and may\nreach the host; keep evidence concise and never put credentials or other secrets in verdict data.\n\n`verify`, `judgePanel`, and `completenessCheck` spawn their subagents on the run\'s default model \u2014 hand-roll with `parallel` + `agent` to pin panel members to specific backends.\n\n## `checkpoint()` options\n\n| option | type | meaning |\n|---|---|---|\n| `kind` | `"confirm" \\| "input" \\| "select"` | Reply shape: boolean-ish / free text / one of `choices`. Affects the journal hash and the host UI widget. |\n| `choices` | `string[]` | For `kind: "select"`. |\n| `default` | `unknown` | Reply taken in the default headless mode \u2014 journaled like a real reply. Defaults to `true`. |\n| `headless` | `"default" \\| "abort" \\| "pause"` | No live channel: `"default"` takes `default ?? true`, `"abort"` aborts, and `"pause"` creates a persisted `checkpoint_required` pause. Default `"default"`. |\n| `timeoutMs` | `number` | Deadline for the interactive prompt. |\n\nThe host supplies the live human channel (elicitation in the MCP server; `ExecOptions.confirm` in the SDK), and that channel wins even when `headless: "pause"` is declared. A durable pause carries non-secret `checkpointContext`; resume with `ExecOptions.checkpointReplies: { [context.callIndex]: decision }` or attach a live channel. On a new `resumeFromRunId` execution, reply keys always name indexes in the **source** recording; identity matching may inject that decision at a shifted current index. Completed host and headless checkpoint results both replay when identity and the checkpoint-options fingerprint over `default`, `headless`, and `timeoutMs` match. A changed option or ambiguous match runs fresh. Detached runs never pause for a checkpoint unless the author opts into `"pause"`.\n\n## Error codes (`WorkflowError.code`)\n\n| code | recoverable | engine behavior |\n|---|---|---|\n| `AGENT_TIMEOUT` | yes | Total wall-clock attempt cap exhausted. Every retry gets a fresh clock; after the final attempt the call resolves `null`, and ACP cancel escalates to close/recycle when the turn does not stop. |\n| `AGENT_CANCELLED` | yes | The host selected this in-flight call for cancellation. It resolves `null` immediately through an engine race, skips retries, leaves the run live, and is recorded as a failed call rather than a replayable journal result. |\n| `AGENT_EMPTY_OUTPUT` | yes | No assistant text on a schema-less call; same retry-then-`null`. |\n| `AGENT_EXECUTION_ERROR` | yes* | Generic agent failure (*refusal/truncation variants are non-recoverable). |\n| `SCHEMA_NONCOMPLIANCE` | no | Structured output never validated after the re-prompt ladder. Halts the run (catchable in-script). |\n| `PROVIDER_USAGE_LIMIT` | no | Quota/rate wall \u2014 the run **pauses** (journaled, resumable), with the provider\'s reset hint. |\n| `AGENT_LIMIT_EXCEEDED` | no | `maxAgents` cap hit. |\n| `AUTH_REQUIRED` | no | Backend needs authentication. `WorkflowManager` returns a resumable pause with `reason: "auth_required"` and redacted `authContext`; a direct runner throws. The host completes auth before resuming/retrying. |\n| `CHECKPOINT_REQUIRED` | no | `headless: "pause"` reached without a live channel. `WorkflowManager` returns `reason: "checkpoint_required"` plus non-secret `checkpointContext`; resume with `checkpointReplies` or live confirm. |\n| `SCRIPT_VALIDATION_ERROR` | no | Script failed parse/validation (bad meta, nondeterministic API, bad `meta.backends` shape). |\n| `SCRIPT_ERROR` | no | The script itself crashed (uncaught throw, floated rejection). |\n| `WORKFLOW_ABORTED` | \u2014 | Real cancellation (pause/stop/host signal) \u2014 never used for crashes. |\n\n`loopUntilDry` absorbs `AGENT_LIMIT_EXCEEDED` from its rounds and returns the partial result; everywhere else it propagates.\n\n## Determinism & the resume journal\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\nThe guide section **Determinism and resume** carries the full semantics: what each hash contains, matching, admission, continuation of interrupted calls, and checkpoint replay. Wire-level specifics for lookup:\n\n- Each `agent()` result is journaled under a monotonic call index and a SHA-256 identity hash. The canonical identity fields, in order, are `prompt`, resolved `model`, `mode` only when set, `configOptions` only when non-empty, `tier`, `phase`, `agentType`, resolved `agentDef`, and `schema`. Config-option keys are sorted before serialization. Missing fields other than `mode` and `configOptions` serialize as `null`; an unset `mode` and an unset/empty `configOptions` key are omitted for compatibility with older journals.\n- `agentDef` is the resolved definition\'s tools, disallowed tools, model, isolation, and body prompt. Changing a named definition therefore invalidates its call even when the `agentType` name is unchanged.\n- The legacy `resume: { filesystem: "read-only" }` annotation has no effect on admission or matching. Writers, readers, worktree calls, and unannotated calls follow the same journal rule.\n- `resumePolicy: "positional"` requests index/prefix correspondence but cannot bypass new-format format, metadata, manifest, cwd, or input checks. Marker-less journals and permanently marked manual/same-run legacy resumes retain historical hash-only positional behavior. Sources below input format 2 use `inputs-format-legacy`. Ancestor-scoped rows carried by a \u22640.23 resume hop replay only while that ancestor is still persisted; engine-minted nested scopes and deleted ancestor scopes stay live.\n- There is no `require`, `import`, Node API, or network API in the realm. `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` fail static validation; aliased or computed forms are blocked at runtime; `new Date(value)` works.\n\nEvery new-run resume exposes `replayEligibility` on admission, polling, inspection, and the terminal result. It reports strategy, predicted/observed replayable prefix and counts, first non-replay/reason/detail, engine/input-format diagnostics, non-gating runtime/environment `provenanceChanges`, and non-gating operational changes; `resumeReport` retains the complete terminal per-call correspondence.\n\nAn all-live outcome is expected when correspondence cannot be established, not when the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest/seed can disable reuse. A new-format source containing any result row without a captured call path/input fact\u2014possible with a call stack deeper than the raw-frame cap or a non-strict-JSON `meta` value\u2014is source-wide `"manifest-invalid"`; excluding the row could make an ambiguous sibling look unique. Format-1 bytes are never reinterpreted; they enter the positional bridge and replayed rows are recorded under format 2.\n\nAn args-controlled cap is the useful case: a cap that changes how many calls are reachable, but\ndoes not appear in an earlier call\'s prompt, lets those calls replay on resume. The worked example\nlives in the determinism-and-resume guide document and ships as\n`examples/resume-loop-cap.workflow.js`. This changed-args pattern is specific to new-run entry\npoints that accept current args with `resumeFromRunId`. The MCP `workflow` tool does, as does\n`WorkflowManager.runSync(script, newArgs, { resumeFromRunId })`. MCP resume always requires\nexplicit content; a bare `resumeFromRunId` is invalid. `WorkflowManager.resume(runId)` is a\ndifferent same-ID recovery API: it reloads the persisted original script/args and permanently uses\nlegacy positional replay semantics, while the independent default-on channel may still continue an\neligible usage/auth-interrupted live call.\n\n## <a name="custom-backends-metabackends"></a>Custom backends \u2014 `meta.backends`\n\n```js\nexport const meta = {\n name: "\u2026", description: "\u2026",\n backends: {\n browser: {\n command: "browser-acp", // required: executable (absolute or on PATH)\n args: ["--headless"], // default []\n env: { BROWSER_PROFILE: "qa" }, // merged OVER the child\'s inherited env \u2014 per-backend secrets go here\n sessionMeta: { viewport: "desktop" }, // static ACP _meta on every session/new (per-call `meta` merges over it)\n structuredOutputTool: true, // default true; false = keep this backend on the prompt/_meta schema fallback\n },\n },\n};\n```\n\nScript-declared backends are **trust-gated**: they spawn commands on the host machine, so they stay inert until the composition root approves them \u2014 elicitation approval in the MCP server, `allowScriptBackends: true` (or a per-backend callback) on `runDynamicWorkflow`, `ExecOptions.scriptBackends` on a manager, or `AGENTPRISM_ALLOW_SCRIPT_BACKENDS=1`. A *declined* backend aborts the run rather than silently rerouting its calls to the default backend. Host-registered names always win over script declarations. Prefer host registration (`createAcpRunner({ backends })` / `AGENTPRISM_BACKENDS` env JSON) when you control the host.\n\n## <a name="agenttype-definitions"></a>`agentType` definitions\n\nMarkdown files at `<runCwd>/.agentprism/agents/<name>.md` (project) and `~/.agentprism/agents/<name>.md` (user); project wins on name collision. Frontmatter + body:\n\n```markdown\n---\ndescription: Read-only security auditor\ntools: [read, grep, glob] # allowlist of tool names (omit = all)\ndisallowedTools: [bash] # denylist, applied after the allowlist\nmodel: claude/opus[1m] # verified id; agent({ model }) overrides it\nisolation: worktree # optional\n---\nYou are a security auditor. Report findings; never modify files.\n```\n\nThe body is prepended to the agent\'s task as role guidance. An unknown `agentType` logs a warning and runs with default tools/model (the name degrades to a prose hint).\n\n## How hosts run scripts (what authors can assume)\n\nThe connected MCP `workflow` tool is the canonical way an agent runs an authored script; the per-action contracts are in the Running workflows guide section. The tool is self-contained: `config` discovers live backend options and `run` validates automatically before admission. The `workflow` tool is the server\'s whole *workflow* surface: config/run/resume/inspect/await/stop\nare action branches, not separate tools, and this input does not resolve a saved workflow name.\n(The server also registers a second, separate model-facing tool, `repl`, for interactive REPL\norchestration \u2014 outside this authoring guide\'s scope.) A\nrun that pauses with `reason: "auth_required"` resumes via a new run after the backend\'s own CLI is\nlogged in out-of-band (see below). Prompt-capable MCP hosts (e.g. Claude Code, where it surfaces as\na slash command) also get this entire guide from the server itself as the **`author-workflow`**\nprompt, with an optional `task` argument.\n\nEnvironment knobs shared by the MCP server and the SDK: `AGENTPRISM_DEFAULT_BACKEND`,\n`AGENTPRISM_ACP_POOL_SIZE` (schema-run parallelism on OpenCode/custom backends scales with the\npool; one injected-tool registry per process), `AGENTPRISM_BACKENDS`,\n`AGENTPRISM_ALLOW_SCRIPT_BACKENDS`, `AGENTPRISM_PERSISTENCE_ROOT`, plus per-backend spawn\noverrides. Pi uses `AGENTPRISM_PI_ACP_CMD` with optional `AGENTPRISM_PI_ACP_ARGS`; otherwise the\ninstalled exact-pinned package bin is used before the `npx -y @automatalabs/pi-acp` fallback.\n\nEmbedding hosts drive the same contract directly through the SDK \u2014 `runDynamicWorkflow` /\n`WorkflowManager` from `@automatalabs/workflows`, with `exec` limits (`maxAgents`, `concurrency`,\n`agentTimeoutMs`, `agentRetries`), a live `confirm` checkpoint channel, and\n`exec.resumeFromRunId` for edited-script resume. See `docs/api.md` in the repository. The shapes\nbelow are the `workflow` tool\'s MCP surface, which is what script authors interact with.\n\nExact MCP tool input/output types:\n\n```ts\ninterface WorkflowConfigToolInput {\n action: "config";\n projectDir?: string;\n harnesses?: string[];\n modelSpecs?: string[];\n modelFilter?: string;\n probeTimeoutMs?: number;\n}\n\ninterface WorkflowExecuteToolInputBase {\n action?: "run";\n args?: unknown;\n maxAgents?: number;\n concurrency?: number;\n agentRetries?: number;\n agentTimeoutMs?: number | null;\n resumeFromRunId?: string;\n resumePolicy?: "auto" | "positional";\n checkpointReplies?: Record<number, unknown>;\n background?: boolean; // default false\n}\n\ntype WorkflowExecuteToolInput = WorkflowExecuteToolInputBase & (\n | { script: string; scriptPath?: never }\n | { script?: never; scriptPath: string } // absolute path on the server\n);\n// WorkflowExecuteToolInputBase also carries projectDir?: string \u2014 the absolute project\n// directory selecting the project-scoped run store and default execution cwd. REQUIRED for\n// config/run on the shared workflow daemon (one registration serves every project); optional on a\n// single-project (--in-process) server. inspect/await/stop never take it: a runId locates\n// its project store automatically.\n\ninterface WorkflowAwaitToolInput {\n action: "await";\n runId: string;\n waitMs?: number; // default 20_000; integer 0..25_000\n lastN?: number; // default 20; integer 1..50\n labelGlob?: string; // same whole-label glob as inspect\n logLines?: number; // default 20; integer 0..50\n}\n\ninterface WorkflowConfigToolResult {\n action: "config";\n ok: boolean;\n harnessOptions: Array<{ backendId: string; model?: string; probed: boolean; options?: unknown[]; error?: string }>;\n models: Array<{ backendId: string; hasModelOption: boolean; matches: string[] }>;\n}\n\ninterface WorkflowValidationRejected {\n action: "run";\n status: "rejected";\n validation: { ok: false; exitCode: 1 | 2; parse: object; dryRun?: object; warnings: string[] };\n}\n\ninterface WorkflowBackgroundAccepted {\n runId: string;\n status: "running";\n scriptSource: "inline" | "path";\n scriptUri: string;\n limits: WorkflowRunLimits;\n replayEligibility?: WorkflowReplayEligibility;\n}\n\ninterface WorkflowAwaitMetadata {\n requestedMs: number;\n elapsedMs: number;\n returnedBecause: "terminal" | "timeout" | "immediate";\n}\n\ninterface WorkflowRunAwaitResult<T = unknown> extends WorkflowRunStatus {\n wait: WorkflowAwaitMetadata;\n tokenUsage?: TokenUsage;\n outcome?: Omit<WorkflowExecutionToolResult<T>, "scriptSource">; // exactly when terminal\n scriptUri: string;\n lineage: Array<{ runId: string; uri: string; available: boolean }>;\n}\n\ninterface WorkflowStopToolInput {\n action: "stop";\n runId: string;\n callIndex?: number; // omitted = whole-run abort; present = cancel one in-flight agent\n lastN?: number;\n labelGlob?: string;\n logLines?: number;\n script?: never;\n scriptPath?: never;\n waitMs?: never;\n}\n```\n\nThe selected stop form requires a live, uniquely addressable agent attempt. Settled/unallocated\nindexes, checkpoints, duplicate scoped indexes, and terminal runs are errors that enumerate the\ncurrently in-flight call-index/label pairs. A successful selected cancellation returns the ordinary\nlive `WorkflowRunStatus`; whole-run stop returns the terminal `WorkflowStopResult`.\n\n`WorkflowRunResult.fallbacks?: WorkflowRunFallback[]` retains the compatibility shape\n`{ callIndex, label, phase?, requestedSpec, resolvedModel?, backendId?, kind, message, continuation? }`.\n`kind` is `model | modifier | continuation`; continuation details report either a reattached\n`resume | load` method or an exact skip reason. The model-resolution pipeline itself produces no entries.\n`WorkflowRunResult.checkpointsTaken?: WorkflowCheckpointTaken[]` records resolved checkpoints as\n`{ callIndex, kind, decision, source }`, where source is `live`, `headless-default`,\n`journal-replay`, or `injected`. A paused checkpoint is not resolved. Both fields are persisted and\nappear in foreground results plus terminal await `outcome`; neither appears on `WorkflowRunStatus`.\n\nAt most four background runs may be active or starting per server instance. Foreground, inspect,\nawait, and stop consume no slot; a durably stopped background run frees its slot immediately even\nwhile backend session wind-down remains. A timeout returns the freshest status and partial cumulative usage; replay\nhits cost/add zero. Terminal results have no MCP TTL and are reconstructed after restart while the\nproject run record remains readable. The inherited status fields stay redacted/bounded at 24,576\nstructured bytes and 8,192 text bytes. The full script lineage is never truncated; when lineage\nalone exceeds the status budget, `truncation.maxStructuredBytes` reports the larger actual envelope\nlimit. Terminal `outcome` preserves the raw authored result/full logs and has no new total cap, but\nit is never copied into text. It includes `scriptUri` but not the unpersisted admission-only\n`scriptSource`.\n\nThe background start has no enduring request signal, progress channel, or live checkpoint channel.\nIt returns immediately and emits no progress after returning, even if the initiating request\nsupplied a progress token. A later bounded `action:"await"` is a separate request; when that await\ncarries a progress token, it can stream coarse phase and distinct started/ended-call progress while\npending. The legacy/inconsistent-log polling fallback emits no progress notifications. A headless\ncheckpoint default continues; abort fails with `WORKFLOW_ABORTED`; pause returns\n`checkpoint_required` plus `outcome.checkpointContext`. Auth pauses return non-secret\n`outcome.authContext`; log the backend CLI in before resume. Background execution lives in the\nserving process (the daemon, or the single process under `--in-process`): that process\'s death can\ninterrupt an in-flight call, and stale durable `pending`/`running` state reconciles under its lease\nto `paused` / `interrupted`.\n\nEvery resumed background run durably seeds its inherited prefix (including a manager-owned\ncheckpoint injection) beneath its new run ID before acknowledgement, so later resume hops remain\nself-contained. The MCP layer never rewrites that seed. Await and inspect never execute or resume\nthe script; their cold preflight may only reconcile a dead owner\'s stale `pending`/`running` state\nto `paused` / `interrupted`.\n\nEvery admitted script is an immutable persistence-backed MCP resource at\n`workflow://runs/{runId}/script`. Run results link the new script; inspect/await link the full\nresume lineage oldest-to-newest as structured `{ runId, uri, available }` entries. Listing and\ncompletion include only the 50 newest runs, but a direct URI read works for any retained project\nrun. A path is never persisted or implicitly re-read, and the MCP layer retains no scripts, args,\nor synthetic lineage metadata in process memory.\n\n`action:"stop"` durably aborts a `running` or `paused` run live in the serving process: it cancels\nany pending agent/checkpoint request, appends `stopped`, releases the lease, and returns the final\ninspection projection with `stopped:true`. Only backend session wind-down can remain, observable\nthrough inspect\'s agent states. A repeated stop on a terminal run succeeds with `stopped:false,\nalreadyTerminal:true`. An in-flight stop may lack a quiescent terminal-environment proof, so the\nmanager can conservatively run the following resume live; inspect `replayEligibility` and\n`resumeReport` rather than assuming a prefix replay.\n\nRetain the run ID and inspect halted runs before guessing. The exact inspection input is:\n\n```ts\ninterface WorkflowInspectToolInput {\n action: "inspect";\n runId: string; // /^[a-z0-9]+-[a-z0-9]+$/, at most 128 characters\n lastN?: number; // default 20; integer 1..50\n labelGlob?: string; // non-empty; at most 128 Unicode code points\n logLines?: number; // default 20; integer 0..50\n script?: never;\n scriptPath?: never;\n}\n```\n\n`labelGlob` matches the whole raw agent label case-sensitively: `*` is zero or more Unicode code\npoints, `?` is exactly one, and backslash escapes the next character (a trailing backslash is\nliteral). Checkpoints and unknown legacy calls are excluded when a glob is present. Filtering\nhappens before `lastN`; selected calls return in ascending call-index order.\n\n```ts\ninterface WorkflowLogTail {\n lines: string[];\n totalLines: number;\n omittedLines: number;\n truncatedLines: number;\n redactedLines: number;\n}\n\ninterface WorkflowRunCallStatus {\n index: number;\n kind: "agent" | "checkpoint" | "unknown";\n label?: string;\n phase?: string;\n model?: string;\n backendId?: string;\n timeoutMs?: number | null;\n errorCode?: string;\n resultPreview: string;\n resultRedacted: boolean;\n resultTruncated: boolean;\n}\n\ninterface WorkflowRunStatus {\n runId: string;\n status: "pending" | "running" | "paused" | "completed" | "failed" | "aborted";\n workflowName: string;\n phases: string[];\n currentPhase?: string;\n reason?: string;\n errorCode?: string;\n limits?: WorkflowRunLimits; // absent only on legacy persisted records\n replayEligibility?: WorkflowReplayEligibility;\n logTail: WorkflowLogTail;\n calls: WorkflowRunCallStatus[];\n filter: { lastN: number; logLines: number; labelGlob?: string };\n truncation: {\n maxStructuredBytes: number;\n byteCapApplied: boolean;\n phases: { total: number; returned: number; shortened: number };\n logs: { total: number; returned: number; shortened: number; redacted: number };\n calls: {\n total: number;\n matched: number;\n returned: number;\n shortenedResults: number;\n redactedResults: number;\n };\n };\n}\n\ninterface WorkflowRunLimits {\n maxAgents: number;\n tokenBudget: null; // persisted-shape compatibility field; new runs always report null\n concurrency: number;\n agentRetries: number;\n agentTimeoutMs: number | null;\n}\n```\n\nInspection returns only this allowlisted projection: never raw script, args, prompts, histories,\nhashes, session IDs, cwd, checkpoint/auth details, or raw results. Credential-shaped data is\nredacted, results are structurally compacted, every outward text scalar/preview is capped at 512\nUTF-8 bytes, inherited status JSON at 24,576 bytes, and inspection text at 8,192 bytes. Full lineage\ncan raise the structured envelope limit as reported by `truncation.maxStructuredBytes`. An unknown ID is\na tool error with no structured content; reading an existing failed run succeeds and reports\n`status:"failed"`. Every paused, failed, or aborted execution result also carries a redacted\nfinal-20 `logTail` (present when empty) and renders it in the immediate terminal text. Completed\nexecution results omit that extra field while retaining their full `logs` array.\n\nBackend auth comes from the machine the host runs on: Claude via a logged-in Claude Code install or `ANTHROPIC_API_KEY`; Codex via `~/.codex/auth.json`; OpenCode via `opencode auth login` (its CLI must be installed \u2014 it is not bundled); Pi via one of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY`, or ambient credentials in `~/.pi/agent/auth.json`. A script only needs auth for the backends it actually routes to.\n\n## MCP-native validation and config discovery\n\n`action:"config"` probes the live server runner without prompting or starting a workflow. Omit `harnesses` to inspect every registered backend; pass `modelFilter` to retrieve bounded matching model ids, then pass `modelSpecs` to select exact models and inspect their model-specific option domains. `action:"run"` always performs static validation, a mocked dry run, and routed model/mode/config checks before admission. A rejected preflight returns structured diagnostics without a run ID.\n\n## Workflow folders\n\nHosts that keep versioned folders of workflow scripts serve them by name (the SDK\'s\n`openWorkflowDir` \u2014 see `docs/api.md`). The filename stem is the name (`review-pr.workflow.js` \u21D2\n`review-pr`; `.workflow.js` beats `.js`). For script AUTHORS the takeaway is simply:\n`workflow("<name>")` works when the host serves a folder; keep names equal to filename stems.\n\n---\n\n# Complete example \u2014 quick-wins.workflow.js\n\nA complete, validated script (`loopUntilDry()` with per-round vendor rotation, dedup threading via a `seen` list, and an args-controlled round cap; runs standalone or nested):\n\n```js\n// quick-wins \u2014 a small, self-contained hunter that repo-triage nests by name\n// (`workflow("quick-wins", {...})`) and that also runs standalone:\n//\n// npm start -- --workflow quick-wins\n// The workflow tool validates this script automatically before admission.\n//\n// Demonstrates loopUntilDry(): keep spawning hunt rounds \u2014 each on the next vendor\n// in the pool \u2014 until two consecutive rounds add nothing new (or the round cap\n// stops it first). Workflow scripts are self-contained strings with no imports, so\n// the vendor pool is repeated here rather than shared with repo-triage.\nexport const meta = {\n name: "quick-wins",\n description: "Hunt small, high-confidence quick wins across the repo until two consecutive rounds come up dry",\n phases: [{ title: "Hunt" }],\n};\n\n// args \u2014 every knob optional; hosts may hand args through as a JSON string.\nconst raw = typeof args === "string" ? (() => { try { return JSON.parse(args); } catch { return {}; } })() : args;\nconst opt = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};\nconst rounds = Number.isFinite(Number(opt.rounds)) && Number(opt.rounds) >= 1 ? Math.floor(Number(opt.rounds)) : 4;\nconst focus =\n typeof opt.focus === "string" && opt.focus.trim().length > 0\n ? opt.focus.trim()\n : "small, safe, high-confidence improvements";\nconst avoid = Array.isArray(opt.avoid) ? opt.avoid.filter((x) => typeof x === "string") : [];\n\n// These registered-prefix specs use ids verified against each live harness catalog.\nconst POOL = [\n { name: "claude", model: "claude/opus[1m]", mode: "plan" },\n { name: "codex", model: "codex/gpt-5.6-sol", mode: "read-only" },\n { name: "opencode", model: "opencode/zai/glm-5.2" },\n];\n\nconst WINS = {\n type: "object",\n additionalProperties: false,\n required: ["wins"],\n properties: {\n wins: {\n type: "array",\n items: {\n type: "object",\n additionalProperties: false,\n required: ["file", "summary", "action"],\n properties: {\n file: {\n type: "string",\n description: "Repo-relative path of a file you actually opened \u2014 copy it exactly, never invent one",\n },\n summary: { type: "string", description: "One sentence: the small problem or missed improvement" },\n action: { type: "string", description: "The concrete, low-risk change that fixes it, in one clause" },\n },\n },\n },\n },\n};\n\nphase("Hunt");\nconst seen = [];\nconst wins = await loopUntilDry({\n round: async (i) => {\n const v = POOL[i % POOL.length];\n const r = await agent(\n `Hunt round ${i + 1}: find up to 3 quick wins in this repository \u2014 ${focus}. ` +\n "A quick win is a small, safe, self-contained improvement (a missing guard, a stale doc line, an obvious dead branch), " +\n "not a refactor. Open files and ground every entry in code you actually read; never emit a placeholder.\\n" +\n `Already known \u2014 do NOT repeat anything on this list: ${JSON.stringify([...avoid, ...seen])}`,\n { label: `hunt:${i + 1}:${v.name}`, phase: "Hunt", schema: WINS, model: v.model, mode: v.mode },\n );\n const found = (r?.wins ?? []).filter((w) => typeof w.file === "string" && w.file.length > 0 && !w.file.startsWith("/"));\n seen.push(...found.map((w) => `${w.file}: ${w.summary}`));\n return found.map((w) => ({ ...w, foundBy: v.name }));\n },\n key: (w) => `${w.file}::${w.summary}`,\n consecutiveEmpty: 2,\n maxRounds: rounds,\n});\n\nlog(`quick-wins: ${wins.length} unique wins across the hunt`);\nreturn { wins };\n```\n';
|
|
33228
|
-
|
|
33229
33239
|
// ../mcp-server/src/authoring-prompt.ts
|
|
33230
33240
|
var AUTHORING_PROMPT_NAME = "author-workflow";
|
|
33231
33241
|
function buildAuthoringPromptText(task) {
|
|
33232
33242
|
const trimmed = task?.trim();
|
|
33233
|
-
const
|
|
33234
|
-
const closing = trimmed ? `## Your task
|
|
33235
|
-
|
|
33236
|
-
Author a workflow script that accomplishes the following, then run it with the \`workflow\` tool. ${discover}
|
|
33243
|
+
const taskSection = trimmed ? `## Your task
|
|
33237
33244
|
|
|
33238
|
-
${trimmed}` :
|
|
33239
|
-
|
|
33240
|
-
Author
|
|
33241
|
-
|
|
33242
|
-
|
|
33243
|
-
|
|
33244
|
-
|
|
33245
|
-
|
|
33246
|
-
|
|
33245
|
+
${trimmed}` : "## Next step\n\nAuthor the workflow script the user asks for, then run it with the `workflow` tool.";
|
|
33246
|
+
return [
|
|
33247
|
+
"# Author an AgentPrism workflow",
|
|
33248
|
+
"",
|
|
33249
|
+
"Use the connected `docs` tool for version-matched authoring guidance. Read topic `workflow/quickstart` first, then read only the related workflow topics needed for this task; do not load every topic. Workflow scripts and REPL evals have different `agent()` signatures, so use only `workflow/*` topics here.",
|
|
33250
|
+
"",
|
|
33251
|
+
'When the script pins a model, mode, or configOptions, call the `workflow` tool with `action:"config"` first; after choosing a model, use `modelSpecs` to read its exact option domain. Set mode only when that selected entry\'s `modes.availableModes` explicitly lists the exact id; `modes:null` means omit it, and never infer a generic `default`. The run action automatically performs static validation, a mocked dry run, and routed no-prompt config checks before admission. Correct any direct rejection diagnostic and re-run.',
|
|
33252
|
+
"",
|
|
33253
|
+
taskSection,
|
|
33254
|
+
""
|
|
33255
|
+
].join("\n");
|
|
33247
33256
|
}
|
|
33248
33257
|
function registerAuthoringPrompt(mcp) {
|
|
33249
33258
|
mcp.registerPrompt(
|
|
33250
33259
|
AUTHORING_PROMPT_NAME,
|
|
33251
33260
|
{
|
|
33252
33261
|
title: "Author an AgentPrism workflow script",
|
|
33253
|
-
description: "
|
|
33262
|
+
description: "Frame a workflow-authoring task and direct the assistant to select only the version-matched workflow documentation topics it needs through the `docs` tool.",
|
|
33254
33263
|
argsSchema: {
|
|
33255
33264
|
task: external_exports.string().optional().describe("What the workflow should accomplish (optional).")
|
|
33256
33265
|
}
|
|
@@ -33266,6 +33275,405 @@ function registerAuthoringPrompt(mcp) {
|
|
|
33266
33275
|
);
|
|
33267
33276
|
}
|
|
33268
33277
|
|
|
33278
|
+
// ../mcp-server/src/generated/authoring-docs-content.ts
|
|
33279
|
+
var AUTHORING_DOCS_SCHEMA_VERSION = 1;
|
|
33280
|
+
var AUTHORING_DOC_TOPIC_IDS = ["index", "workflow/quickstart", "workflow/run-lifecycle", "workflow/models-and-config", "workflow/composition-and-failure", "workflow/checkpoints-and-quality", "workflow/environment-and-tools", "workflow/determinism-and-resume", "workflow/api-agents", "workflow/api-control-flow", "workflow/api-resume-and-backends", "workflow/examples", "repl/quickstart", "repl/state-and-bindings", "repl/agent-handles", "repl/steering-queueing-and-cancellation", "repl/checkpoints-and-introspection", "repl/persistence-and-reset", "repl/api-reference", "repl/examples"];
|
|
33281
|
+
var AUTHORING_DOC_TOPICS = [
|
|
33282
|
+
{
|
|
33283
|
+
"id": "index",
|
|
33284
|
+
"title": "AgentPrism authoring documentation index",
|
|
33285
|
+
"description": "Bounded catalog of workflow-script and interactive-REPL documentation topics.",
|
|
33286
|
+
"uri": "agentprism://docs/index",
|
|
33287
|
+
"mimeType": "text/markdown",
|
|
33288
|
+
"relatedTopics": [
|
|
33289
|
+
"workflow/quickstart",
|
|
33290
|
+
"repl/quickstart"
|
|
33291
|
+
],
|
|
33292
|
+
"bytes": 3945,
|
|
33293
|
+
"sha256": "bdee47c01de4c5cb8b810629683f415ea246d7152b3fecea2f4ccc45e9c3aefd",
|
|
33294
|
+
"text": '# AgentPrism authoring documentation index\n\nRead one topic at a time with the `docs` tool. Workflow scripts and REPL evals have different `agent()` signatures and lifecycle semantics; choose the matching namespace.\n\nStart with `workflow/quickstart` for deterministic batch scripts or `repl/quickstart` for interactive persistent orchestration.\n\n## Workflow scripts\n\n- `workflow/quickstart` \u2014 **Workflow scripts: quickstart**: Minimal valid workflow, metadata shape, core sandbox rules, config discovery, and automatic validation.\n- `workflow/run-lifecycle` \u2014 **Workflow tool lifecycle**: Config, run, background admission, await, inspect, stop, resume, run resources, and events.\n- `workflow/models-and-config` \u2014 **Workflow models, routing, and configuration**: Backend/model routing, action:config discovery, modes, configOptions, schemas, and structured-output channels.\n- `workflow/composition-and-failure` \u2014 **Workflow composition and failure**: Exact meta header, phases, parallel and pipeline, nested workflows, null semantics, and bounded loops.\n- `workflow/checkpoints-and-quality` \u2014 **Workflow checkpoints and quality helpers**: gate, retry, verify, judgePanel, loopUntilDry, completenessCheck, and human checkpoints.\n- `workflow/environment-and-tools` \u2014 **Workflow execution environment and tools**: cwd, worktree isolation, tool access, MCP servers, images, custom backends, and agent definitions.\n- `workflow/determinism-and-resume` \u2014 **Workflow determinism and resume**: Identity/input fingerprints, content-addressed replay, eligibility diagnostics, checkpoints, and stop-patch-resume.\n- `workflow/api-agents` \u2014 **Workflow agent API reference**: Every agent() option, exact model grammar, timeout behavior, and structured-output semantics.\n- `workflow/api-control-flow` \u2014 **Workflow control-flow API reference**: Complete DSL global signatures, checkpoint options, gate verdicts, and workflow error taxonomy.\n- `workflow/api-resume-and-backends` \u2014 **Workflow resume and extension reference**: Journal matching details, replay diagnostics, script-declared backends, and agentType definitions.\n- `workflow/examples` \u2014 **Workflow composition examples**: Cross-vendor build, backend-agnostic audit, bounded loops, schemas, and automatic validation patterns.\n\n## Interactive REPL\n\n- `repl/quickstart` \u2014 **REPL orchestration: quickstart**: Persistent eval basics, correct agent signature, handles, polling, structured output, and interrupt semantics.\n- `repl/state-and-bindings` \u2014 **REPL state, bindings, and eval results**: Persistent lexical bindings, completion values, output rendering, soft-bound results, polling, and cleanup.\n- `repl/agent-handles` \u2014 **REPL agent calls and persistent handles**: Exact agent(modelSpec, task, options) API, routing, option vocabulary, handle identity, and failures.\n- `repl/steering-queueing-and-cancellation` \u2014 **REPL steering, queueing, and cancellation**: Strict active-turn steering, durable FIFO future turns, exact handle cancellation, ordering, and recovery.\n- `repl/checkpoints-and-introspection` \u2014 **REPL checkpoints and introspection**: Raising and answering durable checkpoints plus workspace(), agents(), and error diagnostics.\n- `repl/persistence-and-reset` \u2014 **REPL persistence, restore, and reset**: Snapshot boundaries, restart reconciliation, queue recovery, snapshot refusal, disconnect drain, and reset.\n- `repl/api-reference` \u2014 **REPL API reference**: Exact repl tool actions, every guest global, handle methods, combinator semantics, and environment limits.\n- `repl/examples` \u2014 **REPL orchestration examples**: Interactive steering, queue continuation, parallel reviews, checkpoints, polling, cancellation, and bounded loops.\n\nEach topic result includes exact related-topic ids. Model, mode, and config-option values remain live backend data: discover them with the `workflow` tool\'s `action:"config"` rather than documentation or memory.\n'
|
|
33295
|
+
},
|
|
33296
|
+
{
|
|
33297
|
+
"id": "workflow/quickstart",
|
|
33298
|
+
"title": "Workflow scripts: quickstart",
|
|
33299
|
+
"description": "Minimal valid workflow, metadata shape, core sandbox rules, config discovery, and automatic validation.",
|
|
33300
|
+
"uri": "agentprism://docs/workflow/quickstart",
|
|
33301
|
+
"mimeType": "text/markdown",
|
|
33302
|
+
"relatedTopics": [
|
|
33303
|
+
"workflow/composition-and-failure",
|
|
33304
|
+
"workflow/api-agents",
|
|
33305
|
+
"workflow/run-lifecycle",
|
|
33306
|
+
"workflow/examples"
|
|
33307
|
+
],
|
|
33308
|
+
"bytes": 3978,
|
|
33309
|
+
"sha256": "254e8a5612aa6dff27201accacce3d8dbe42af15b56ebdda165f5d23a7faac81",
|
|
33310
|
+
"text": '# Workflow scripts: quickstart\n\n**Context:** JavaScript passed to the MCP `workflow` tool. This is not REPL code: workflow scripts use `agent(prompt, options?)`, allow top-level `return`, and start from a required metadata export.\n\nA workflow script is a deterministic orchestrator. Script code owns loops, fan-out, conditionals, aggregation, and checkpoints; `agent()` workers perform repository or research tasks. Workers start fresh sessions and do not share memory, so interpolate every prior result a later worker needs into its prompt.\n\n## Minimal valid script\n\n```js\nexport const meta = {\n name: "review-target",\n description: "Review a target and return concrete findings",\n phases: [{ title: "Review" }],\n};\n\nphase("Review");\nconst report = await agent(\n `Review ${args.target}. Read the relevant files and report concrete findings.`,\n { label: "review" },\n);\nreturn { report };\n```\n\nThe metadata export must be the first statement and a pure object literal. `name` and `description` are required non-empty strings. `phases`, when present, is an array of objects shaped `{ title: string, detail?: string, model?: string }`, never strings.\n\nSubmit the source without Markdown fences using the `workflow` tool\'s run form, with an absolute `projectDir` on the shared daemon. `args` is the JSON value supplied by the tool call. Some hosts may carry caller data as a JSON string, so harden scripts that accept external input:\n\n```js\nconst raw = typeof args === "string" ? (() => {\n try { return JSON.parse(args); } catch { return {}; }\n})() : args;\nconst input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};\n```\n\n## Core rules\n\n- The DSL primitives are injected globals; do not import them.\n- Top-level `await` and top-level `return` are supported.\n- Scripts are JavaScript, not TypeScript.\n- No `require`, imports, filesystem API, network API, timers, `Date.now()`, `Math.random()`, or no-argument `Date` construction. Pass nondeterministic values through `args`.\n- Every `agent()` call should have a stable descriptive `label`.\n- A recoverable worker failure resolves to `null` after retries. Null-check load-bearing results.\n- `parallel()` takes thunks, not already-started promises:\n\n```js\nconst results = (await parallel([\n () => agent("Review correctness", { label: "review:correctness" }),\n () => agent("Review test coverage", { label: "review:coverage" }),\n])).filter(Boolean);\n```\n\n- Use a plain JSON Schema object in `schema` when script control flow depends on a worker result.\n- Return a compact JSON-serializable result; do not return a transcript.\n\n## Model selection\n\nOmit `model` for the server default, or use a backend-only value such as `"codex"` to retain that backend\'s configured default model. Before pinning a model id, `mode`, or `configOptions`, call `workflow` with `action:"config"`. After choosing a model, use `modelSpecs` to read that exact model\'s option domain. Set `mode` only when that selected harness entry\'s `modes.availableModes` explicitly lists the exact id; `modes:null` means the backend/model supports no modes, so omit `mode`. Never infer a generic `"default"` and never guess model or option ids.\n\n## Validation and execution\n\nEvery run is statically parsed, mock-executed, and checked against no-prompt backend configuration before admission. A rejection creates no run ID, reserves no background slot, and spends no tokens. Read the diagnostic, correct the script, and submit it again.\n\nUse foreground execution for short work. Use `background:true` for work that may outlive one tool request; retain the returned `runId`, then use bounded `await`, `inspect`, or `stop` calls.\n\n## What to read next\n\n- `workflow/composition-and-failure` \u2014 metadata, fan-out, phases, and null semantics.\n- `workflow/api-agents` \u2014 every `agent()` option and structured output.\n- `workflow/run-lifecycle` \u2014 config, run, await, inspect, stop, and resume.\n- `workflow/examples` \u2014 complete composition patterns.\n'
|
|
33311
|
+
},
|
|
33312
|
+
{
|
|
33313
|
+
"id": "workflow/run-lifecycle",
|
|
33314
|
+
"title": "Workflow tool lifecycle",
|
|
33315
|
+
"description": "Config, run, background admission, await, inspect, stop, resume, run resources, and events.",
|
|
33316
|
+
"uri": "agentprism://docs/workflow/run-lifecycle",
|
|
33317
|
+
"mimeType": "text/markdown",
|
|
33318
|
+
"relatedTopics": [
|
|
33319
|
+
"workflow/quickstart",
|
|
33320
|
+
"workflow/determinism-and-resume",
|
|
33321
|
+
"workflow/models-and-config"
|
|
33322
|
+
],
|
|
33323
|
+
"bytes": 6835,
|
|
33324
|
+
"sha256": "85a1d7894a0a36953511810bce9d073c29868bc0e1de89939d912c8f4356fa8f",
|
|
33325
|
+
"text": '## Running workflows \u2014 the MCP `workflow` tool\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nUse the connected `workflow` tool for deterministic batch orchestration. The shared server daemon owns execution, so admitted runs survive MCP client session churn and tool-request timeouts; only daemon exit can interrupt in-flight work. Any later session can await, inspect, or stop a run. Runs, journals, and logs persist per project namespace.\n\nEvery `config` and `run` call on the shared daemon names its project with the required `projectDir` argument \u2014 an absolute path, normally the workspace root. `inspect`/`await`/`stop` take only a `runId`; the run ID locates its project store automatically. In a single-project server, `projectDir` defaults to that server\'s project.\n\n### The `workflow` tool, by action\n\n- **Config** (`{ action: "config", projectDir, harnesses?, modelSpecs?, modelFilter? }`): discover live model, mode, effort, and `configOptions` values from no-prompt backend sessions. Use `harnesses` plus `modelFilter` to find ids, then `modelSpecs` to select exact models and read their model-specific option domains. Each successful entry reports `modes` explicitly: use only exact ids in `modes.availableModes`; `modes:null` means omit `mode`, never guess a default. It starts no workflow and spends zero tokens. Use it only when pinning those values; an omitted model or backend-only model uses configured defaults without discovery.\n- **Run** (default, no `action`): supply exactly one of `script` (the raw source string, no Markdown fences) or `scriptPath` (an absolute path on the server\'s filesystem), plus `projectDir`. The tool automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded `status:"rejected"` diagnostics with no run ID, background slot, or token spend. A path is read once at admission and its content snapshotted; later edits affect only a new run. `args` arrives in the script as the `args` global; the run\'s base directory is the `cwd` global. Some hosts hand `args` through as a JSON **string** \u2014 tolerate both shapes (`typeof args === "string" ? JSON.parse(args) : args`). Foreground streams progress but is bound to the request and its timeout. Pass `background: true` for anything that may outlive one request; it acknowledges after durable admission with a `runId`.\n- **Await** (`{ action: "await", runId, waitMs }`): bounded collection for background runs. A timeout is progress, not failure \u2014 call again (`waitMs: 20000` is typical). At terminal status the response adds `outcome`: the authored result or pause context, plus `replayEligibility`, `resumeReport`, `fallbacks`, and `checkpointsTaken`.\n- **Inspect** (`{ action: "inspect", runId, lastN, labelGlob, logLines }`): a bounded snapshot \u2014 the latest matching calls with compact result previews plus the newest log lines. Use a narrow `labelGlob` to diagnose before deciding whether to resume, edit, or stop. Inspection never executes or resumes a script.\n- **Stop**: `{ action: "stop", runId }` durably aborts the whole run and returns its final snapshot; stopping a terminal run is a successful no-op. `{ action: "stop", runId, callIndex }` cancels exactly that in-flight agent: its slot settles to `null` with `AGENT_CANCELLED` and the run stays live. `labelGlob` only filters the returned snapshot; it never selects what to cancel.\n- **Resume**: a NEW run with `resumeFromRunId` plus the script content re-sent (the same `script` or `scriptPath`) and the desired `args` (+ `checkpointReplies` when answering a durable checkpoint). Read the returned `replayEligibility` for the predicted and observed replay prefix; never assume a prefix hit. Full semantics: **Determinism and resume**.\n\n### Operating rules\n\n- **Always retain the returned `runId`.** A paused, failed, or aborted response carries a redacted final-20 `logTail`. Read it before you change anything. Every admitted script is also an immutable resource at `workflow://runs/{runId}/script`, so a later session can recover a lost inline script.\n- **Two fingerprints control replay.** The identity hash covers the prompt, the resolved model, `mode` when set, non-empty sorted `configOptions`, `tier`, `phase`, `agentType`, the resolved agent definition, and the schema. The input fingerprint covers the resolved label, per-call `cwd` and isolation, `keepSession`, images, MCP servers, session/prompt metadata, and the approved script-backend digest.\n- **Operational bounds are not replay inputs.** Host `concurrency`, `agentRetries`, and `agentTimeoutMs`, plus per-call `timeoutMs` and `retries`, enter neither fingerprint. A resume does not inherit them from its source run; pass the values you want on every run. `agentTimeoutMs` caps the wall-clock time of each attempt; it is not an idle timer. A per-call `timeoutMs` can tighten that ceiling but cannot escape it. Each retry gets a fresh clock, so the envelope is `(resolved retries + 1) \xD7 resolved timeout`, with retries clamped to 3.\n- **Old journals stay usable.** Input formats below 2 replay positionally with `fallbackReason: "inputs-format-legacy"`. A current-format crash snapshot uses identity matching even without terminal-environment capture. Ancestor-scoped rows carried from \u22640.23 resume chains replay only while that ancestor run is still persisted. Journals resume across filesystem, environment, engine, Node, and V8 changes; `replayEligibility` reports those differences as diagnostics, never as gates.\n- **A background start returns immediately.** It sends no progress after it returns; collect progress with later bounded awaits. Background runs have no live checkpoint channel, so authored `headless` checkpoint modes apply. When a run\'s owner process dies, cold preflights reconcile stale `pending`/`running` state to `paused` with `pauseReason: "interrupted"`; a live owner is left alone.\n- A run paused with `reason: "auth_required"` resumes as a new run after that backend\'s credentials are configured.\n\n### Execution logs \u2014 the events resource\n\nEvery journaling run publishes an MCP resource at `workflow://runs/{runId}/events`. Subscribe to the canonical URI for advisory `resources/updated` hints, then read and paginate with `after`, `limit`, and `streamId`. Progress is coarse and redacted: `agentTranscript` rows are assistant/tool upserts partitioned by `(scope, callIndex, executionStartSeq)` and reduced by greatest revision per entry index. The durable cursor is authoritative when hints coalesce or a subscriber falls behind.\n\nEmbedding hosts can drive the same contract with `runDynamicWorkflow` / `WorkflowManager` from `@automatalabs/workflows`; the script contract is identical either way.\n'
|
|
33326
|
+
},
|
|
33327
|
+
{
|
|
33328
|
+
"id": "workflow/models-and-config",
|
|
33329
|
+
"title": "Workflow models, routing, and configuration",
|
|
33330
|
+
"description": "Backend/model routing, action:config discovery, modes, configOptions, schemas, and structured-output channels.",
|
|
33331
|
+
"uri": "agentprism://docs/workflow/models-and-config",
|
|
33332
|
+
"mimeType": "text/markdown",
|
|
33333
|
+
"relatedTopics": [
|
|
33334
|
+
"workflow/api-agents",
|
|
33335
|
+
"workflow/environment-and-tools",
|
|
33336
|
+
"workflow/run-lifecycle"
|
|
33337
|
+
],
|
|
33338
|
+
"bytes": 8823,
|
|
33339
|
+
"sha256": "bf309ca0b8858a255b6fbd549b1badb994a9447f32752e3f25c55c565492e789",
|
|
33340
|
+
"text": '## Choosing the agent for each call\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nThe backend is selected **per `agent()` call** from its effective `model` string. One script can plan on one vendor\'s agent, implement on another\'s, and review on a third\'s, handing structured results between them.\n\nThe built-in names (`claude`, `codex`, `opencode`, `pi`) come from the runtime backend registry. Registered custom names extend that set.\n\n- **Omit `model` entirely** for maximum portability \u2014 the call runs on whatever default backend the host configured (`AGENTPRISM_DEFAULT_BACKEND`, or the host\'s session model). A script with no model specs anywhere runs unchanged on any backend.\n- **Route by one registered first segment.** Split on the first `/`; ASCII-case-insensitive `claude`, `codex`, `opencode`, `pi`, or a registered custom backend name selects that harness and is stripped exactly once. A custom registration wins on a built-in-name collision.\n- **Use a backend name alone** (`claude`, `codex`, `opencode`, `pi`, or a custom name) to preserve the harness\'s configured default model. No model config call is made.\n- **Everything else goes intact to the default backend.** `anthropic/\u2026`, `openai/\u2026`, bare `opus`, and bare `gpt-\u2026` are not routing aliases. When an id remains after routing, it is sent byte-for-byte: no catalog matching, case folding, bracket parsing, effort/Fast option driving, retry, or fallback. Harness rejection is an agent error.\n- **`tier`** (`"small" | "medium" | "big"`) is a coarse alternative resolved from the host\'s tier config \u2014 use it for "a cheap model" without naming a vendor.\n\nThe published examples use ids verified against live harness catalogs: `claude/opus[1m]`, `codex/gpt-5.6-sol`, and `opencode/zai/glm-5.2`. For Pi, `pi/openrouter/vendor/model-id` strips only `pi/`; Pi then splits provider `openrouter` from model id `vendor/model-id`. Prefer backend-only forms when the desired model is configured inside the harness.\n\nNever guess model ids, mode ids, effort values, or option names from memory. With MCP, call the `workflow` tool using `action:"config"` and optional `harnesses` / `modelFilter`; it returns the live catalog without starting a workflow.\n\nOne no-prompt session per harness, zero tokens: each successful harness entry contains `modes` plus its config-option catalog. A non-null `modes` object carries `currentModeId` and `availableModes`; only those exact advertised ids are valid. `modes:null` explicitly means that backend/model supports no ACP session modes, so omit `mode`\u2014absence never licenses an invented generic `"default"`. Config options list model ids (including bracket variants like `opus[1m]`), effort levels, and every other negotiable option exactly as the installed harness advertises them. One caveat: the bare `config` probe reads each harness with its **default model** selected, and option domains are **model-specific**. An option can appear only after a particular model is selected. Ceilings differ per model. Provider-served variants of the same model can advertise different domains. The authoritative per-model probe is the validator run on your real script: it selects each authored model spec first and echoes that pair\'s advertised modes and options. Confirm every pinned value against its own echoed entry; do not read package internals to discover options.\n\n```js\nconst plan = await agent(PLAN_PROMPT, { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN });\nconst impl = await agent(implPrompt(plan), { label: "implement", model: "codex/gpt-5.6-sol" });\nconst review = await agent(reviewPrompt(impl), { label: "review", model: "claude/opus[1m]", schema: REVIEW });\n```\n\nUse `configOptions` only for exact ACP session options advertised by that routed harness. With MCP, read the selected harness\'s `action:"config"` result before choosing ids or select values; catalogs vary by harness version, login, and machine.\n\n```js\nconst impl = await agent(implPrompt(plan), {\n label: "implement",\n model: "codex",\n configOptions: { "fast-mode": true, reasoning_effort: "high" },\n});\n```\n\nIds and string/boolean values pass through verbatim in ascending id order, after model selection and before the prompt. There are no aliases, coercion, client-side vocabulary, defaults, or cached catalogs. Copy option ids character-for-character from the catalog, punctuation included \u2014 `"fast-mode"`, not `fast_mode` \u2014 and quote ids that are not valid identifiers. Never put `"model"` in `configOptions`; use the dedicated `model` field. A harness rejection follows the ordinary agent-error path.\n\nPi\'s thought-level option is named `thinkingLevel`, and its choices depend on the exact model in the same call:\n\n```js\nconst review = await agent(REVIEW_PROMPT, {\n label: "pi-review",\n model: "pi/openrouter/vendor/model-id",\n configOptions: { thinkingLevel: "high" },\n});\n```\n\nValidation selects `openrouter/vendor/model-id` before reading Pi\'s choices. A listed value passes unchanged. A recognized value above an ordered model\'s ceiling, or in a model-specific gap, passes with a warning that names the effective clamp target. Pi advertises its SDK-derived domain directly. Claude and Codex are also ordered: when their options omit domain metadata, validation enumerates the advertised models and merges their per-model effort orders. A Claude model without an `effort` option does not support effort, and `default` never becomes a ceiling target. OpenCode and custom backends have no declared value order, so validation is exact-set. An unrecognized or unadvertised value fails with exit code `2`. Enumeration stops at 32 advertised models; a larger or inconsistently ordered catalog warns and falls back to exact advertised-value validation.\n\n**The harness is authoritative.** The client never substitutes a nearby model or silently falls back. A rejected id follows the existing agent-error path; a harness that accepts or ignores it determines the outcome. The public `fallbacks`/`onModelFallback` fields remain for compatibility but model resolution does not emit them.\n\n## Structured output\n\nPass `schema` \u2014 a **plain JSON Schema object literal** (no schema builders exist inside the realm) \u2014 and the call resolves to a **validated object** instead of text:\n\n```js\nconst FINDINGS = {\n type: "object",\n additionalProperties: false,\n required: ["findings"],\n properties: {\n findings: {\n type: "array",\n items: {\n type: "object",\n additionalProperties: false,\n required: ["file", "line", "summary"],\n properties: {\n file: { type: "string", description: "Repo-relative path \u2014 copy it exactly, never invent one" },\n line: { type: "number", description: "1-indexed line the finding anchors to" },\n summary: { type: "string", description: "One sentence stating the defect, grounded in code you actually read" },\n },\n },\n },\n },\n};\n\nconst report = await agent("Review the diff on this branch for correctness bugs.", {\n label: "review", schema: FINDINGS,\n});\nreport.findings.forEach((f) => log(`${f.file}:${f.line} ${f.summary}`));\n```\n\nThe same schema works on **every** backend; only the fulfillment channel differs, and the runner picks it for you: Claude uses its `outputFormat`, Codex its strict `outputSchema`, while Pi, OpenCode, and eligible custom ACP agents receive a client-hosted `StructuredOutput` MCP tool when they advertise HTTP MCP support. Pi accepts stdio, Streamable HTTP, and SSE MCP servers. If no valid tool capture exists, Pi retains the runner\'s common prompt-embedded schema and validated final-text JSON fallback. In every channel the runner validates the value client-side (with type coercion) and re-prompts a bounded number of times before failing the call with non-recoverable `SCHEMA_NONCOMPLIANCE`.\n\nSchema authoring rules that keep all channels healthy:\n\n- Root must be an object; set `additionalProperties: false` and list every property in `required`.\n- Put a `description` on every field \u2014 descriptions are the per-field prompt.\n- Keep schemas structurally simple. Exotic keywords (`oneOf`, `patternProperties`, unusual `format`s, backreference regexes) are normalized or stripped on the wire for some backends \u2014 validation still enforces them client-side, which shows up as re-prompt churn. Prefer `anyOf`, `enum`, and plain types.\n- Keep free-text fields small (tens of lines). An oversized structured output can exhaust schema repair and fail the call.\n- Validation checks structure, not truth. Check load-bearing values in script code (for example, reject findings whose `file` is not in a known file list) before spending more agents on them.\n'
|
|
33341
|
+
},
|
|
33342
|
+
{
|
|
33343
|
+
"id": "workflow/composition-and-failure",
|
|
33344
|
+
"title": "Workflow composition and failure",
|
|
33345
|
+
"description": "Exact meta header, phases, parallel and pipeline, nested workflows, null semantics, and bounded loops.",
|
|
33346
|
+
"uri": "agentprism://docs/workflow/composition-and-failure",
|
|
33347
|
+
"mimeType": "text/markdown",
|
|
33348
|
+
"relatedTopics": [
|
|
33349
|
+
"workflow/quickstart",
|
|
33350
|
+
"workflow/api-control-flow",
|
|
33351
|
+
"workflow/checkpoints-and-quality"
|
|
33352
|
+
],
|
|
33353
|
+
"bytes": 6402,
|
|
33354
|
+
"sha256": "630e7b4ced855ae4c592c87a38d35b6c77b6f2df288d0a9007659a3901447bad",
|
|
33355
|
+
"text": '## The `meta` header\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nEvery script must **begin** with `export const meta = {...}` as a plain object literal (no computed values \u2014 it is parsed from the source text before anything runs):\n\n```js\nexport const meta = {\n name: "fix-flaky-tests", // required\n description: "Find flaky tests and fix them", // required\n phases: [ // optional; one { title, detail?, model? } entry\n { title: "Find", model: "opencode/zai/glm-5.2" }, // per phase() call, matched by exact title;\n { title: "Fix" }, // a phase model is that phase\'s default\n ],\n model: "claude/sonnet", // optional run-wide default model\n backends: { /* optional custom ACP agents \u2014 see "Custom ACP backends" */ },\n};\n```\n\nPer-agent model resolution order: explicit `agent({ model })` > `agent({ tier })` > the current phase\'s `model` > `meta.model` > the host session\'s default. So `meta.phases[].model` gives a whole phase a backend without repeating it on every call.\n\n## Fan-out: `parallel` and `pipeline`\n\n```js\n// parallel: an array of THUNKS (not promises!) run concurrently \u2014 a barrier that\n// resolves in input order. A failed slot resolves to null; filter before use.\nconst sweeps = (await parallel([\n () => agent("Audit error handling in src/server", { label: "sweep:errors", schema: FINDINGS }),\n () => agent("Audit input validation in src/api", { label: "sweep:input", schema: FINDINGS }),\n])).filter(Boolean);\n\n// pipeline: each item flows through the stages independently \u2014 NO barrier between\n// stages, so item A can be in stage 2 while item B is still in stage 1.\n// Stages receive (previousResult, originalItem, index).\nconst verified = (await pipeline(\n sweeps.flatMap((s) => s.findings),\n (f) => agent(`Adversarially verify this finding \u2014 try to refute it:\\n${JSON.stringify(f)}`,\n { label: `verify:${f.file}`, schema: VERDICT }),\n (verdict, f) => ({ ...f, real: verdict.real }),\n)).filter(Boolean).filter((f) => f.real);\n```\n\n**Default to `pipeline`** for multi-stage work. Add a `parallel` barrier only when the next stage needs *all* prior results at once: dedup across the full set, early-exit on a zero count, or prompts that compare "the other findings". The test is the **information dependency** \u2014 a barrier\'s cost is real, because the fastest worker idles for the slowest. All coordination lives in script code: agents cannot see each other, so never ask an agent to "check with the other reviewers" or "spawn helpers". Passing a promise instead of a thunk to `parallel` is a `TypeError` \u2014 wrap every call: `() => agent(...)`.\n\nFan-out also contends for the **working tree**, not just the concurrency limiter. Two agents running builds or test suites in the same checkout collide on build outputs, caches, and lockfiles, and concurrent `git fetch`es contend on the same `.git`. Give run-things agents `isolation: "worktree"` when the commits they must inspect are reachable from the run cwd\'s repository, or serialize them; fan out freely only the agents that just read.\n\nThe host caps concurrent agents per run (default 8); hand `parallel`/`pipeline` as many items as the task needs and let the limiter schedule them. The cap counts active agent attempts, not authored branches: queued branches begin as other attempts finish, and a branch that exhausts its timeout settles to `null` and frees its slot. `workflow(nameOrScript, args)` nests another workflow inline (one level deep, sharing this run\'s limiter) \u2014 inline script strings always work; saved names resolve when the host serves a workflows folder.\n\n## Failure semantics \u2014 design for `null`\n\n- A **recoverable** failure (timeout, empty output, transient execution error) is retried per the call\'s `retries` (default 0), then the call **resolves to `null`** \u2014 inside `parallel`/`pipeline` *and* as a bare `await agent(...)`. Null-check anything load-bearing, and set `retries: 1\u20132` on steps you can\'t afford to lose.\n- A host can settle one runaway in-flight call with MCP `{ action: "stop", runId, callIndex }` or SDK `manager.cancelAgentCall(runId, callIndex)`. The call resolves to `null` with `AGENT_CANCELLED`, skips every configured retry, and does not abort the run or its siblings. Its failed call record is not cached as a journal result, so a later resume runs that occurrence live.\n- A **non-recoverable** failure (schema never validated, script bug) throws and fails the run. You *may* `try/catch` around an `agent()` call to degrade gracefully \u2014 rethrow anything you can\'t meaningfully handle. In particular, **always rethrow pause-class errors** (`err.code === "PROVIDER_USAGE_LIMIT"` or `"AUTH_REQUIRED"`): they must propagate out of the script so the engine can pause the run resumably \u2014 swallowing one converts that pause into a fake, lossy completion.\n- A **provider quota wall, missing backend authentication, or opted-in durable checkpoint pauses a managed run instead of failing it** \u2014 the journal checkpoints and the host can resume after the provider quota refills, authentication completes, or a checkpoint decision is supplied. Direct `runner.run()` calls still receive the `AUTH_REQUIRED` error because they have no manager lifecycle.\n- Per-call knobs: `timeoutMs` and `retries`. A finite `timeoutMs` may shorten the host\'s run-level `agentTimeoutMs` ceiling; `null` or omission is uncapped only when the host supplied no ceiling. The timeout is total wall-clock time per attempt, and every retry gets a fresh clock.\n\n## Phases\n\n```js\nphase("Explore"); // open a named phase: subsequent agents group under it\n\nconst found = [];\nwhile (found.length < 20) {\n const r = await agent("Find one more edge case not in: " + JSON.stringify(found.map((f) => f.name)),\n { label: `edge:${found.length}`, schema: EDGE });\n if (!r) break;\n found.push(r);\n}\n```\n\nTerminate every loop on a bound the script controls. The agent-count limit (`maxAgents`) is hard: once exhausted, further `agent()` calls throw `AGENT_LIMIT_EXCEEDED`. `phase()` groups agents in progress UIs and run logs; `log(msg)` (and `console.log`) append to the run log \u2014 narrate what matters, especially anything you drop.\n'
|
|
33356
|
+
},
|
|
33357
|
+
{
|
|
33358
|
+
"id": "workflow/checkpoints-and-quality",
|
|
33359
|
+
"title": "Workflow checkpoints and quality helpers",
|
|
33360
|
+
"description": "gate, retry, verify, judgePanel, loopUntilDry, completenessCheck, and human checkpoints.",
|
|
33361
|
+
"uri": "agentprism://docs/workflow/checkpoints-and-quality",
|
|
33362
|
+
"mimeType": "text/markdown",
|
|
33363
|
+
"relatedTopics": [
|
|
33364
|
+
"workflow/api-control-flow",
|
|
33365
|
+
"workflow/composition-and-failure",
|
|
33366
|
+
"workflow/examples"
|
|
33367
|
+
],
|
|
33368
|
+
"bytes": 4106,
|
|
33369
|
+
"sha256": "b904eded7f717110028fe853b05e4c5bef060186fdc0a7b763cb54a0e4f2ef67",
|
|
33370
|
+
"text": '## Built-in quality loops\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nThese helpers spawn their own subagents on the default model. Hand-roll with `parallel` + `agent` when you want panel members on specific backends. Full signatures are in `workflow/api-control-flow`.\n\n| helper | shape | use for |\n|---|---|---|\n| `gate(produce, validate, { attempts })` | produce \u2192 validate \u2192 feed `feedback` back; return `{ ok, value, verdict, attempts }` | produce-until-a-reviewer-approves loops that need the final review evidence |\n| `retry(thunk, { attempts, until })` | bounded retry until `until(result)` holds | flaky single steps |\n| `verify(item, { reviewers, threshold, lens })` | N adversarial reviewers vote `real`/not | killing plausible-but-wrong findings |\n| `judgePanel(attempts, { judges, rubric })` | score candidates 0\u20131 against a rubric, return the best | picking among independent solutions |\n| `loopUntilDry({ round, key, consecutiveEmpty, maxRounds })` | repeat a round, dedup by `key`, stop when dry | unknown-size discovery (bugs, edge cases) |\n| `completenessCheck(args, results)` | one critic lists what\'s still missing | a final "what did we not cover?" pass |\n\nThe `gate` pattern, spelled out \u2014 note how the producer thunk threads the validator\'s feedback into a *fresh* agent\'s prompt (sessions have no memory):\n\n```js\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement the fix described here:\\n${JSON.stringify(plan)}\\n` +\n (feedback ? `\\nA reviewer rejected attempt ${attempt}: ${feedback}\\nAddress every point.` : ""),\n { label: `fix:${attempt + 1}`, model: "codex/gpt-5.6-sol" },\n ),\n (result) => agent(\n `Run the test suite and review this change summary:\\n${result}\\n` +\n `Return ok=true only if tests pass and the fix is correct; include the reviewed commit SHA.`,\n { label: "gate-review", model: "claude/opus[1m]", schema: { type: "object", additionalProperties: false,\n required: ["ok"], properties: { ok: { type: "boolean" }, feedback: { type: "string" },\n commitSha: { type: "string" } } } },\n ),\n { attempts: 3 },\n);\nif (!outcome.ok) log(`reviewer never approved after ${outcome.attempts} attempts`);\nelse log(`reviewer approved commit ${outcome.verdict?.commitSha ?? "(unspecified)"}`);\n```\n\nFeedback is the producer\'s only context for the next attempt. Interpolate everything it needs, and name only files that provably exist.\n\n## Human gates: `checkpoint()`\n\n`checkpoint(promptText, options?)` is a zero-token, journaled human gate. With MCP elicitation (or a live SDK `confirm` callback) it waits for that reply; without a live channel, its default mode takes `default ?? true` immediately, so detached runs never hang.\n\n```js\nconst proceed = await checkpoint(`Apply this plan?\\n${JSON.stringify(plan, null, 2)}`, {\n kind: "confirm", // "confirm" | "input" | "select"\n default: false, // default headless mode takes this (or true)\n // headless: "abort", // abort when no live human is attached\n // headless: "pause", // or persist a resumable human-decision pause\n});\nif (!proceed) return { applied: false, plan };\n```\n\n`kind: "input"` resolves to free text, `kind: "select"` to one of `choices`. How the question reaches a human is the host\'s job (elicitation in the MCP server; `ExecOptions.confirm` in the SDK). With no live channel, `headless: "default"` (the default) takes `default ?? true`, `"abort"` aborts, and `"pause"` returns a managed run with `reason: "checkpoint_required"` plus non-secret `checkpointContext`. Resume the last mode with `checkpointReplies: { [context.callIndex]: decision }` or a live confirm. For `resumeFromRunId`, that key is the source context index; an unambiguous identity match may journal the injected answer at a shifted current index. Put a checkpoint before anything hard to reverse \u2014 applying diffs, pushing, publishing, or the first commit into a working copy the workflow did not create (`default: true` keeps detached runs moving).\n'
|
|
33371
|
+
},
|
|
33372
|
+
{
|
|
33373
|
+
"id": "workflow/environment-and-tools",
|
|
33374
|
+
"title": "Workflow execution environment and tools",
|
|
33375
|
+
"description": "cwd, worktree isolation, tool access, MCP servers, images, custom backends, and agent definitions.",
|
|
33376
|
+
"uri": "agentprism://docs/workflow/environment-and-tools",
|
|
33377
|
+
"mimeType": "text/markdown",
|
|
33378
|
+
"relatedTopics": [
|
|
33379
|
+
"workflow/api-agents",
|
|
33380
|
+
"workflow/api-resume-and-backends",
|
|
33381
|
+
"workflow/models-and-config"
|
|
33382
|
+
],
|
|
33383
|
+
"bytes": 5786,
|
|
33384
|
+
"sha256": "d8354d57084a8d3bfef2614058f5d6b595c1cc6d377e4483ee22c9c414ebb23b",
|
|
33385
|
+
"text": '## Working directory, isolation, confinement\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n- Every agent session runs in the run\'s base `cwd` unless the call narrows it: `agent({ cwd: "packages/api" })` (relative resolves against the base).\n- `isolation: "worktree"` runs the agent in a **throwaway git worktree** (`<repoRoot>/.agentprism/worktrees/\u2026`) so parallel agents can edit without colliding. The worktree and its branch are **always deleted when the call ends \u2014 an isolated agent\'s file edits are discarded**. Have isolated agents *return their work as data* (a unified diff, a file map, a report) and apply it in a later non-isolated step; use worktrees for experiments, builds, and verification, not for persistent edits. Outside a git repo, isolation degrades to the shared tree with a logged notice.\n- `resume: { filesystem: "read-only" }` is a deprecated compatibility annotation. It is not a runner mode and has no effect on replay; completed calls replay by journal correspondence whether they read or write. Use `mode`, tool policy, prompts, and worktrees when you actually need confinement.\n- `mode` requests an agent-advertised ACP session mode and is **strict** \u2014 an unsupported mode fails the call rather than running unconfined. Mode ids are backend/model-specific and drift with harness versions. Read the selected entry\'s `modes.availableModes` from `workflow` `action:"config"`; only copy an exact listed id. `modes:null` means no mode support, so omit `mode`; never infer `"default"` from a backend\'s ordinary behavior or from an absent mode value. Automatic preflight rejects unadvertised ids before admission. Only set `mode` on calls whose `model` you also pin. Use an explicitly advertised read-only/plan mode for reviewers and auditors that must not write.\n- `agentType: "<name>"` binds a reusable subagent definition \u2014 a Markdown file at `<cwd>/.agentprism/agents/<name>.md` (project) or `~/.agentprism/agents/<name>.md` (user; project wins) whose frontmatter sets tool allow/deny lists, a model, and isolation, and whose body is the role prompt. An unknown name logs a warning and degrades to defaults.\n\n## Where a mutating workflow runs\n\nThe run\'s base `cwd` is the USER\'S checkout \u2014 the working copy they launched the host from. Treat it as borrowed: committing onto whatever branch is checked out, switching branches, or resetting it are defects unless the user asked for exactly that. A script that commits should verify its target workspace in a preflight step, or create its own workspace idempotently, and refuse on a mismatch rather than adapt. `isolation: "worktree"` is NOT such a workspace \u2014 it is per-call and throwaway. Note also that a throwaway worktree branches from the run cwd\'s repository: an isolated agent sees another agent\'s commits only when they are reachable there.\n\n## Wiring tools and inputs into a call\n\n- `mcpServers: [{ name, command, args: [], env: [] }]` attaches MCP servers to that agent\'s session \u2014 the portable way to hand any backend a capability (image generation, a browser, a ticket system). The agent sees the server\'s tools natively. Note `env` is a list of `{ name, value }` pairs (ACP shape), not an object map; HTTP/SSE servers use `{ type: "http", name, url, headers: [] }`.\n- `images: [...]` appends base64 image blocks to the prompt (backends without image support receive a bracketed text note instead).\n- `meta` / `promptMeta` pass generic ACP `_meta` through to `session/new` / `session/prompt` \u2014 the escape hatch for driving a custom agent\'s extension surface.\n- `keepSession: true` keeps a successful agent\'s ACP session re-openable after the run: the re-attach record (sessionId, backend, effective pool identity, cwd, reopen capabilities) lands in `WorkflowRunResult.agentSessions`, and the HOST can continue that conversation later via `runner.loadSession()`. Usage/auth pause failures are kept open automatically so managed resume can continue the interrupted occurrence. Scripts themselves never request reattach.\n\n### Custom ACP backends\n\nAny process that speaks ACP over stdio can serve `agent()` calls \u2014 an in-house browser-QA agent, an image generator, a domain-specific executor. Two ways in:\n\n1. **Host-registered** (preferred): the embedder passes `createAcpRunner({ backends: { browser: { command: "/abs/browser-acp" } } })`; the script just routes with `model: "browser"`.\n2. **Script-declared**: the script itself declares the backend in `meta.backends` \u2014 but declarations are **inert until the host approves them** (an elicitation in the MCP server; `allowScriptBackends` in the SDK), because they spawn commands on the host machine. Don\'t rely on them silently working.\n\n```js\nexport const meta = {\n name: "checkout-qa",\n description: "Implement, then QA the checkout flow in a real browser",\n backends: {\n browser: { command: "browser-acp", args: ["--headless"] }, // requires host approval\n },\n};\n\nconst change = await agent("Implement the coupon-code field per the spec in docs/coupon.md.",\n { label: "implement" }); // default backend\nconst verdict = await agent(\n `Open the app, walk through checkout with coupon SAVE20, and verify the discount line. Change summary:\\n${change}`,\n { label: "qa", model: "browser", // the custom agent\n schema: { type: "object", additionalProperties: false, required: ["passed"],\n properties: { passed: { type: "boolean" }, notes: { type: "string" } } } },\n);\nreturn { change, qa: verdict };\n```\n\nStructured output works on custom backends through the same injected-tool/fallback ladder as OpenCode \u2014 no special-casing in the script.\n'
|
|
33386
|
+
},
|
|
33387
|
+
{
|
|
33388
|
+
"id": "workflow/determinism-and-resume",
|
|
33389
|
+
"title": "Workflow determinism and resume",
|
|
33390
|
+
"description": "Identity/input fingerprints, content-addressed replay, eligibility diagnostics, checkpoints, and stop-patch-resume.",
|
|
33391
|
+
"uri": "agentprism://docs/workflow/determinism-and-resume",
|
|
33392
|
+
"mimeType": "text/markdown",
|
|
33393
|
+
"relatedTopics": [
|
|
33394
|
+
"workflow/run-lifecycle",
|
|
33395
|
+
"workflow/api-resume-and-backends",
|
|
33396
|
+
"workflow/examples"
|
|
33397
|
+
],
|
|
33398
|
+
"bytes": 9234,
|
|
33399
|
+
"sha256": "78708c97d4da00a1a17831fbc325456bf62556f9abf2e79745f3ac4df1eb0883",
|
|
33400
|
+
"text": '## Determinism and resume\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nRuns are journaled: every `agent()` and `checkpoint()` result is recorded under a deterministic call index. A new run may reuse eligible results from a terminal source run. Uncertainty always means live execution.\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\n- Direct `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` calls fail static validation. The realm also blocks aliased or computed forms at runtime; `new Date(isoString)` is fine. Pass timestamps and random seeds through `args`.\n- The replay identity of an `agent()` call hashes: the prompt, the resolved `model`, `mode` when set, `configOptions` when non-empty (sorted keys), `tier`, `phase`, `agentType`, the resolved agent definition, and `schema`. The resolved agent definition includes its tool allowlist and denylist, model, isolation, and body prompt \u2014 editing a definition invalidates the calls that use it.\n- A separate input fingerprint hashes: the resolved label, per-call `cwd`, resolved isolation, `keepSession`, `images`, `mcpServers`, `meta`, `promptMeta`, and the approved script-backend digest.\n- Host `agentTimeoutMs`, `agentRetries`, and `concurrency`, plus per-call `timeoutMs` and `retries`, are operational bounds. They enter neither hash and may change freely on resume. A new run resolves them from its own request; it does not inherit the source values.\n- `args` is not hashed directly. New args that only raise a loop cap leave earlier identities unchanged, so those calls can replay. New args that change a prompt, model selection, phase, schema, call order, or runner-visible input make the affected calls run live. Unchanged independent calls may still replay.\n- Matching tries a unique exact `(kind, call path, identity hash)` row first (`"path-hash"`), then a unique `(kind, identity hash, input fingerprint)` row, so an unchanged call can replay as `"unique-hash"` after insertions or deletions. Source and current input fingerprints must be equal. Duplicate identities, duplicate content, consumed candidates, missing facts, and empty schema-less results run live. The engine never guesses by source order or occurrence.\n- Source admission requires: exact `cwd`, compatible call-path/input/checkpoint fingerprint formats, complete call/journal/allocation metadata, and a valid manifest and seed. Git HEAD and dirty digest, `environmentKey`, captured environment values, Node/V8, and producing engine version are diagnostics only. Environment differences may appear in `replayEligibility.provenanceChanges`; they never gate admission or matching.\n- A completed writer replays exactly like a reader. A live call, nested workflow, host checkpoint callback, or degraded worktree does not clear unrelated candidates. Nested child calls run live \u2014 they are outside the parent\'s journal \u2014 while matching root calls around them still replay. The engine does not reproduce file writes; a later live agent navigates the world it finds.\n- Replay costs zero current provider usage: a cached call returns its recorded result without spawning a session. Replayed session records keep their backend and session identity, rebound to the current call index, label, and phase.\n- A root call interrupted by `PROVIDER_USAGE_LIMIT` or `AUTH_REQUIRED` can continue its recorded session on either resume API. Continuation requires: the exact call index, identity hash, complete input fingerprint, non-worktree isolation, identical existing cwd, a coherent recorded session, and the runner\'s current backend/`poolKey`/reopen gates. A successful continuation finishes the unfinished turn and charges only its usage delta. Every failed gate runs fresh, and `fallbacks` records the reopen method or the exact skip reason. No script option controls this.\n- Completed checkpoint results replay when the identity and the `default`/`headless`/`timeoutMs` fingerprint match \u2014 headless results included. `checkpointReplies` keys always name the checkpoint index in the source run. A moved reply can follow intact prior correspondence; after a live divergence it must reach the exact recorded call site, so a different same-text branch cannot consume it.\n- `resumePolicy: "positional"` is a migration escape hatch for index/prefix matching. It cannot bypass format, metadata, manifest, cwd, or input checks. Marker-less, manual, and same-ID legacy journals keep historical hash-only positional behavior. Input formats below 2 use the `inputs-format-legacy` positional bridge and are rewritten under the current format on the next hop. A current-format crash snapshot with a valid identity manifest uses identity matching even without terminal-environment capture.\n- `label`, `cwd`, `mcpServers`, `images`, `meta`, `promptMeta`, and `keepSession` are not identity-hashed: changing one does not invalidate an ordinary replay. They are in the input fingerprint: changing one rejects continuation of an interrupted turn, and that occurrence runs fresh. To force a completed call to run again, change a hashed field \u2014 normally the prompt.\n- Keep call order deterministic. Derive iteration from `args` and prior agent results, never from ambient state.\n\nEvery `resumeFromRunId` result has a bounded `replayEligibility` summary. Background admission, foreground completion, both await shapes, and inspect expose the same fields: strategy, predicted replayable-prefix length, observed replayed prefix and counts, and the first non-replay when known. Active correspondence reasons include `strategy-live`, `positional-miss`, `positional-suffix`, `not-recorded`, `path-missing`, `inputs-missing`, `inputs-changed`, `ambiguous-identity`, `ambiguous-content`, `candidate-consumed`, `empty-output`, `worktree-degraded`, `seed-persistence-error`, and `resume-fatal-latch`. Older reason literals stay exported only so historical journals parse. Engine and input-format versions and environment provenance ride along as diagnostics.\n\nAn all-live outcome means correspondence could not be established \u2014 not that the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest or seed disable new-format replay. If any source row lacks a captured path or input fact (possible past the raw-frame cap, or with a non-strict-JSON `meta` value), the whole source is `"manifest-invalid"`: dropping the row could make an ambiguous sibling look unique.\n\n### Worked resume \u2014 raise a loop cap\n\nThe following workflow requires eight reviews but lets the caller cap how many are attempted in one run:\n\n```js\nexport const meta = {\n name: "resume-loop-cap",\n description: "Run expensive review rounds up to an args-controlled cap",\n phases: [{ title: "Review" }],\n};\n\nconst input = args && typeof args === "object" && !Array.isArray(args) ? args : {};\nconst numericCap = Number(input.maxRounds);\nconst maxRounds = Number.isInteger(numericCap) && numericCap > 0 ? numericCap : 8;\n\nphase("Review");\nconst rounds = [];\nfor (let i = 0; i < maxRounds; i += 1) {\n rounds.push(\n await agent(\n `Review round ${i + 1}: inspect the repository and report unresolved release blockers.`,\n { label: `review:${i + 1}`, phase: "Review" },\n ),\n );\n}\n\nif (maxRounds < 8) throw new Error(`review cap ${maxRounds} reached before 8 rounds`);\nreturn { rounds };\n```\n\nRun it with `args: { "maxRounds": 6 }`. Then send the same content (via `script`, or the absolute `scriptPath` you edit) with `args: { "maxRounds": 8 }` and the first result\'s `runId` as `resumeFromRunId`. Rounds 1\u20136 replay for zero current provider tokens; only rounds 7\u20138 run live, because the cap controls call count but is not interpolated into the round prompt. If every round prompt included `maxRounds`, all eight identities would change and all would run live. Resume always states its content; a bare `resumeFromRunId` never silently reuses the old script.\n\nGive repeated calls stable, descriptive labels and narrate decisions with `log()` \u2014 inspection by `labelGlob` then turns a pause or failure into a diagnosis instead of a guess.\n\n### Kill, patch, resume\n\nStop the live run with `{ action: "stop", runId }`. The returned `aborted` snapshot is the durable acknowledgement: resume is safe immediately, and a further await adds nothing. Edit the file. Start a new run with its absolute `scriptPath` and `resumeFromRunId`. Every completed call whose recorded identity and input fingerprint correspond replays, regardless of filesystem or environment drift. Read `replayEligibility` and the full `resumeReport` for the per-call decisions. A repeated stop of a terminal run is a successful no-op.\n\nRegistration, the per-action contracts, background collection, and the events resource are covered in **Running workflows** ([mcp-server-setup.md](mcp-server-setup.md)). Resume a durable checkpoint pause by re-sending the script with `resumeFromRunId` and `checkpointReplies` keyed by the source run\'s `checkpointContext.callIndex`.\n'
|
|
33401
|
+
},
|
|
33402
|
+
{
|
|
33403
|
+
"id": "workflow/api-agents",
|
|
33404
|
+
"title": "Workflow agent API reference",
|
|
33405
|
+
"description": "Every agent() option, exact model grammar, timeout behavior, and structured-output semantics.",
|
|
33406
|
+
"uri": "agentprism://docs/workflow/api-agents",
|
|
33407
|
+
"mimeType": "text/markdown",
|
|
33408
|
+
"relatedTopics": [
|
|
33409
|
+
"workflow/models-and-config",
|
|
33410
|
+
"workflow/environment-and-tools",
|
|
33411
|
+
"workflow/api-control-flow"
|
|
33412
|
+
],
|
|
33413
|
+
"bytes": 8375,
|
|
33414
|
+
"sha256": "e960a826b46677f5b80260749cf6543b293a83dbad0f21235464e8e1b0159d9f",
|
|
33415
|
+
"text": '# Workflow agent API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | ACP session mode id advertised by the selected backend/model. **Strict**: unsupported/unadvertised ids fail before prompting (and automatic workflow preflight rejects them before admission). Read the selected `action:"config"` entry\'s `modes.availableModes` and copy only an exact id; `modes:null` means omit this field. Never infer a generic `"default"`. Part of the resume hash when set. |\n| `configOptions` | `Record<string, string \\| boolean>` | Exact ACP session option ids and authored values. Applied in ascending id order after model and before the prompt, with no aliases or coercion. `"model"` is reserved for the dedicated `model` field. Part of the resume hash only when non-empty, with sorted keys. With MCP, read the advertised-options table from `workflow` action `config` before choosing values. |\n| `agentType` | `string` | Bind a named subagent definition (tools allow/deny, model, isolation, role prompt). See [agentType definitions](#agenttype-definitions). Part of the resume hash. |\n| `isolation` | `"worktree"` | Run in a throwaway git worktree branched from the run cwd. **Always removed (worktree + branch) when the call ends** \u2014 edits are discarded; return work as data. Degrades to the shared tree outside a git repo (logged). |\n| `resume` | `{ filesystem: "read-only" }` | Deprecated compatibility annotation. It is recorded as legacy diagnostic provenance, is not sent to the runner or hashed, and has no effect on replay. New scripts should omit it. |\n| `cwd` | `string` | Per-session working directory; relative resolves against the run\'s base cwd. Overridden by worktree isolation. Not hashed. |\n| `timeoutMs` | `number \\| null` | Total wall-clock cap for each attempt. A finite value may tighten a finite host `agentTimeoutMs` ceiling but cannot raise or disable it. With no host ceiling, a finite value applies and `null`/omitted is uncapped. |\n| `retries` | `number` | Retries after *recoverable* failures (default 0, host-overridable). Exhausted retries \u21D2 the call resolves `null`. |\n| `mcpServers` | `McpServerConfig[]` | MCP servers attached to this session. Stdio shape: `{ name, command, args: [], env: [{ name, value }] }` (`args`/`env` required, `env` is name/value pairs, not a map); `{ type: "http" \\| "sse", name, url, headers: [] }` also accepted. Not hashed. |\n| `images` | `PromptImage[]` | Base64 image blocks appended to the prompt; backends without image support get a bracketed text note. Not hashed. |\n| `meta` | `object` | ACP `_meta` merged into `session/new` \u2014 session-scoped extension passthrough (pairs with custom backends). Not hashed. |\n| `promptMeta` | `object` | ACP `_meta` merged into `session/prompt` \u2014 turn-scoped passthrough. Backend-computed keys win on conflict. Not hashed. |\n| `keepSession` | `boolean` | Skip release-time best-effort `session/close`; the non-secret re-attach record lands in `WorkflowRunResult.agentSessions` for host-side `loadSession()` / `resumeSession()`. Usage/auth pause failures are kept open automatically for managed continuation. Not identity-hashed; included in the input fingerprint. |\n\nThe timeout clock measures the whole attempt, including backend startup, model/config setup, tool\nwork, and streamed output; it is not an idle timer. Each retry starts a fresh clock, so the maximum\ntimeout envelope is `(retries + 1) \xD7 resolved timeoutMs` (retries are clamped to 3). An exhausted\ntimeout is recoverable `AGENT_TIMEOUT`: the call resolves to `null`, releases its concurrency slot,\nand asks the ACP session to cancel. A session that keeps running after the cancellation grace is\nclosed where supported and its pooled child is recycled.\n\nEvery new run, including one admitted with `resumeFromRunId`, resolves host limits from that run\'s\nrequest. It does not inherit `agentTimeoutMs`, retries, concurrency, or agent-count values from\nits source, so pass every operational bound the resumed execution should use.\n\n## Model specs & routing\n\nA `model` string is resolved solely from its first segment, then delegated to the harness:\n\n| spec shape | routes to | notes |\n|---|---|---|\n| *(omitted)* | host default backend | `AGENTPRISM_DEFAULT_BACKEND` (`claude` \\| `codex` \\| `opencode` \\| `pi` \\| custom name; default `claude`), session default model. Most portable. |\n| `claude`, `codex`, `opencode`, `pi`, or `<custom-name>` | that registered harness | Backend-only: no model config call; the harness default remains active. |\n| `claude/<id>`, `codex/<id>`, `opencode/<id>`, `pi/<id>`, or `<custom-name>/<id>` | that registered harness | Match the first segment ASCII-case-insensitively and strip exactly one segment. Custom names take priority on collision. The remaining `<id>` is sent verbatim, including further `/` characters. For Pi, that remainder is its `<provider>/<model-id>` and Pi preserves any further slashes in the model id. |\n| any other string, including `anthropic/\u2026`, `openai/\u2026`, bare `opus`, or bare `gpt-\u2026` | host default backend | The **entire** authored string is sent verbatim; these are not routing aliases. |\n\nSelection is a single `session/set_config_option` with `configId: "model"` and the exact remaining string. There is no catalog matching, case folding, normalization, bracket parsing, nearest-neighbor selection, sibling effort/Fast option driving, retry, or echo verification. Brackets, dots, and provider-style prefixes are ordinary model-id characters.\n\nWhatever the harness returns is the outcome. A rejection follows the existing agent-error path with no resolution-specific code or model fallback event. `onModelFallback` and `WorkflowRunResult.fallbacks` remain public compatibility surfaces; model resolution does not emit entries, while pause recovery emits `kind: "continuation"` reattach/skip notices.\n\n## Structured output channels\n\nOne author API (`schema`), four fulfillment paths \u2014 chosen automatically per backend:\n\n| backend | channel |\n|---|---|\n| Claude | native `outputFormat`, schema normalized to Anthropic\'s structured-outputs subset (e.g. `oneOf` \u2192 `anyOf`; unsupported keywords/formats stripped on the wire) |\n| Codex | native strict `outputSchema` (OpenAI strict subset normalization) |\n| Pi | a client-hosted `StructuredOutput` MCP tool injected when the agent advertises HTTP MCP support; common prompt-embedded schema and validated final-text JSON fallback |\n| OpenCode / custom ACP | a client-hosted **`StructuredOutput` MCP tool** injected into the session when the agent advertises HTTP MCP support (an agent may show it as `structured_output_StructuredOutput`); otherwise prompt-embedded schema + JSON parse of the final message. Custom backends can opt out of tool injection with `structuredOutputTool: false`. |\n\nPi accepts stdio, Streamable HTTP, and SSE MCP servers; ACP-transport MCP hosting remains client-side.\n\nIn every channel the runner coerces + validates client-side and re-prompts a bounded number of times; the final miss fails the call with non-recoverable `SCHEMA_NONCOMPLIANCE`. Constraints stripped from the wire are still enforced client-side \u2014 an exotic schema keyword shows up as re-prompt churn, so keep schemas simple.\n'
|
|
33416
|
+
},
|
|
33417
|
+
{
|
|
33418
|
+
"id": "workflow/api-control-flow",
|
|
33419
|
+
"title": "Workflow control-flow API reference",
|
|
33420
|
+
"description": "Complete DSL global signatures, checkpoint options, gate verdicts, and workflow error taxonomy.",
|
|
33421
|
+
"uri": "agentprism://docs/workflow/api-control-flow",
|
|
33422
|
+
"mimeType": "text/markdown",
|
|
33423
|
+
"relatedTopics": [
|
|
33424
|
+
"workflow/composition-and-failure",
|
|
33425
|
+
"workflow/checkpoints-and-quality",
|
|
33426
|
+
"workflow/api-agents"
|
|
33427
|
+
],
|
|
33428
|
+
"bytes": 6698,
|
|
33429
|
+
"sha256": "9e32ea004addd2a6779c12e31d298169c1310569413c9c8d2767c61397ffde30",
|
|
33430
|
+
"text": '# Workflow control-flow API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## DSL globals \u2014 complete signatures\n\n```\nagent(prompt, options?) \u2192 Promise<string | object | null>\nparallel(thunks) \u2192 Promise<results[]> // barrier; input order; failed slot = null\npipeline(items, ...stages) \u2192 Promise<results[]> // no inter-stage barrier; stage(prev, original, index); failed item = null\nworkflow(nameOrScript, args?) \u2192 Promise<unknown> // one nesting level; names resolve from the host\'s workflows folder, inline scripts always work\ngate(thunk, validator, { attempts = 3 }) \u2192 { ok, value, verdict, attempts }\n // thunk(feedback, attempt); validator(result) \u2192 { ok, feedback?, ... } | boolean | null (may be async / an agent call)\nretry(thunk, { attempts = 3, until? }) \u2192 last result // thunk(attempt); stops early when until(result)\nverify(item, { reviewers = 2, threshold = 0.5, lens? })\n \u2192 { real, realCount, total, votes: [{ real?, reason? }] }\n // N adversarial reviewers prompted to REFUTE; lens (string | string[]) rotates focus per reviewer\njudgePanel(attempts, { judges = 3, rubric = "overall quality and correctness" })\n \u2192 { index, attempt, score, judgments } // mean 0\u20131 score per candidate; stable tie-break by index\nloopUntilDry({ round, key = JSON.stringify, consecutiveEmpty = 2, maxRounds = 50 })\n \u2192 unique items[] // round(i) returns items; stops after N dry rounds; agent-limit exhaustion returns the partial result\ncompletenessCheck(taskArgs, results) \u2192 { complete, missing?: string[] }\ncheckpoint(promptText, options?) \u2192 Promise<reply> // journaled human gate; zero tokens\nphase(title) \u2192 void // open a named phase\nlog(message) \u2192 void // console.log/info/warn/error route here too\nargs // the host-provided input value, verbatim\ncwd // the run\'s base working directory (string); process.cwd() returns it too\n```\n\nFor `gate()`, `value` is the final producer result and `verdict` is the exact last completed\nvalidator return, including any extra structured fields. `{ ok: true }` and bare `true` pass;\n`{ ok: false, feedback? }`, bare `false`, and `null` reject. Only object feedback is threaded into\nthe next producer attempt. A producer result of `null` is still passed to the validator. Producer\nor validator exceptions propagate immediately, so no partial gate result is returned and no later\nattempt runs. An explicit unsupported `undefined` validator return is a rejection represented as\n`verdict: null`. If the script returns the gate result, its complete verdict is persisted and may\nreach the host; keep evidence concise and never put credentials or other secrets in verdict data.\n\n`verify`, `judgePanel`, and `completenessCheck` spawn their subagents on the run\'s default model \u2014 hand-roll with `parallel` + `agent` to pin panel members to specific backends.\n\n## `checkpoint()` options\n\n| option | type | meaning |\n|---|---|---|\n| `kind` | `"confirm" \\| "input" \\| "select"` | Reply shape: boolean-ish / free text / one of `choices`. Affects the journal hash and the host UI widget. |\n| `choices` | `string[]` | For `kind: "select"`. |\n| `default` | `unknown` | Reply taken in the default headless mode \u2014 journaled like a real reply. Defaults to `true`. |\n| `headless` | `"default" \\| "abort" \\| "pause"` | No live channel: `"default"` takes `default ?? true`, `"abort"` aborts, and `"pause"` creates a persisted `checkpoint_required` pause. Default `"default"`. |\n| `timeoutMs` | `number` | Deadline for the interactive prompt. |\n\nThe host supplies the live human channel (elicitation in the MCP server; `ExecOptions.confirm` in the SDK), and that channel wins even when `headless: "pause"` is declared. A durable pause carries non-secret `checkpointContext`; resume with `ExecOptions.checkpointReplies: { [context.callIndex]: decision }` or attach a live channel. On a new `resumeFromRunId` execution, reply keys always name indexes in the **source** recording; identity matching may inject that decision at a shifted current index. Completed host and headless checkpoint results both replay when identity and the checkpoint-options fingerprint over `default`, `headless`, and `timeoutMs` match. A changed option or ambiguous match runs fresh. Detached runs never pause for a checkpoint unless the author opts into `"pause"`.\n\n## Error codes (`WorkflowError.code`)\n\n| code | recoverable | engine behavior |\n|---|---|---|\n| `AGENT_TIMEOUT` | yes | Total wall-clock attempt cap exhausted. Every retry gets a fresh clock; after the final attempt the call resolves `null`, and ACP cancel escalates to close/recycle when the turn does not stop. |\n| `AGENT_CANCELLED` | yes | The host selected this in-flight call for cancellation. It resolves `null` immediately through an engine race, skips retries, leaves the run live, and is recorded as a failed call rather than a replayable journal result. |\n| `AGENT_EMPTY_OUTPUT` | yes | No assistant text on a schema-less call; same retry-then-`null`. |\n| `AGENT_EXECUTION_ERROR` | yes* | Generic agent failure (*refusal/truncation variants are non-recoverable). |\n| `SCHEMA_NONCOMPLIANCE` | no | Structured output never validated after the re-prompt ladder. Halts the run (catchable in-script). |\n| `PROVIDER_USAGE_LIMIT` | no | Quota/rate wall \u2014 the run **pauses** (journaled, resumable), with the provider\'s reset hint. |\n| `AGENT_LIMIT_EXCEEDED` | no | `maxAgents` cap hit. |\n| `AUTH_REQUIRED` | no | Backend needs authentication. `WorkflowManager` returns a resumable pause with `reason: "auth_required"` and redacted `authContext`; a direct runner throws. The host completes auth before resuming/retrying. |\n| `CHECKPOINT_REQUIRED` | no | `headless: "pause"` reached without a live channel. `WorkflowManager` returns `reason: "checkpoint_required"` plus non-secret `checkpointContext`; resume with `checkpointReplies` or live confirm. |\n| `SCRIPT_VALIDATION_ERROR` | no | Script failed parse/validation (bad meta, nondeterministic API, bad `meta.backends` shape). |\n| `SCRIPT_ERROR` | no | The script itself crashed (uncaught throw, floated rejection). |\n| `WORKFLOW_ABORTED` | \u2014 | Real cancellation (pause/stop/host signal) \u2014 never used for crashes. |\n\n`loopUntilDry` absorbs `AGENT_LIMIT_EXCEEDED` from its rounds and returns the partial result; everywhere else it propagates.\n'
|
|
33431
|
+
},
|
|
33432
|
+
{
|
|
33433
|
+
"id": "workflow/api-resume-and-backends",
|
|
33434
|
+
"title": "Workflow resume and extension reference",
|
|
33435
|
+
"description": "Journal matching details, replay diagnostics, script-declared backends, and agentType definitions.",
|
|
33436
|
+
"uri": "agentprism://docs/workflow/api-resume-and-backends",
|
|
33437
|
+
"mimeType": "text/markdown",
|
|
33438
|
+
"relatedTopics": [
|
|
33439
|
+
"workflow/determinism-and-resume",
|
|
33440
|
+
"workflow/environment-and-tools",
|
|
33441
|
+
"workflow/run-lifecycle"
|
|
33442
|
+
],
|
|
33443
|
+
"bytes": 6238,
|
|
33444
|
+
"sha256": "6ee7568438255bf6cc3e2e0fecda97860a89ae7dfe6f32f0641b681d7c6663f1",
|
|
33445
|
+
"text": '# Workflow resume and extension reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## Determinism & the resume journal\n\n> **Resume rule:** replay is content-addressed and fail-to-live on correspondence: a completed call replays when its identity and input fingerprint match uniquely. Filesystem or world state never gates replay.\n\nThe guide section **Determinism and resume** carries the full semantics: what each hash contains, matching, admission, continuation of interrupted calls, and checkpoint replay. Wire-level specifics for lookup:\n\n- Each `agent()` result is journaled under a monotonic call index and a SHA-256 identity hash. The canonical identity fields, in order, are `prompt`, resolved `model`, `mode` only when set, `configOptions` only when non-empty, `tier`, `phase`, `agentType`, resolved `agentDef`, and `schema`. Config-option keys are sorted before serialization. Missing fields other than `mode` and `configOptions` serialize as `null`; an unset `mode` and an unset/empty `configOptions` key are omitted for compatibility with older journals.\n- `agentDef` is the resolved definition\'s tools, disallowed tools, model, isolation, and body prompt. Changing a named definition therefore invalidates its call even when the `agentType` name is unchanged.\n- The legacy `resume: { filesystem: "read-only" }` annotation has no effect on admission or matching. Writers, readers, worktree calls, and unannotated calls follow the same journal rule.\n- `resumePolicy: "positional"` requests index/prefix correspondence but cannot bypass new-format format, metadata, manifest, cwd, or input checks. Marker-less journals and permanently marked manual/same-run legacy resumes retain historical hash-only positional behavior. Sources below input format 2 use `inputs-format-legacy`. Ancestor-scoped rows carried by a \u22640.23 resume hop replay only while that ancestor is still persisted; engine-minted nested scopes and deleted ancestor scopes stay live.\n- There is no `require`, `import`, Node API, or network API in the realm. `Date.now()`, `Math.random()`, and no-arg `new Date()` / `Date()` fail static validation; aliased or computed forms are blocked at runtime; `new Date(value)` works.\n\nEvery new-run resume exposes `replayEligibility` on admission, polling, inspection, and the terminal result. It reports strategy, predicted/observed replayable prefix and counts, first non-replay/reason/detail, engine/input-format diagnostics, non-gating runtime/environment `provenanceChanges`, and non-gating operational changes; `resumeReport` retains the complete terminal per-call correspondence.\n\nAn all-live outcome is expected when correspondence cannot be established, not when the world changed. Missing resume metadata, incompatible format literals, or an invalid manifest/seed can disable reuse. A new-format source containing any result row without a captured call path/input fact\u2014possible with a call stack deeper than the raw-frame cap or a non-strict-JSON `meta` value\u2014is source-wide `"manifest-invalid"`; excluding the row could make an ambiguous sibling look unique. Format-1 bytes are never reinterpreted; they enter the positional bridge and replayed rows are recorded under format 2.\n\nAn args-controlled cap is the useful case: a cap that changes how many calls are reachable, but\ndoes not appear in an earlier call\'s prompt, lets those calls replay on resume. The worked example lives in `workflow/determinism-and-resume`. This changed-args pattern is specific to new-run entry\npoints that accept current args with `resumeFromRunId`. The MCP `workflow` tool does, as does\n`WorkflowManager.runSync(script, newArgs, { resumeFromRunId })`. MCP resume always requires\nexplicit content; a bare `resumeFromRunId` is invalid. `WorkflowManager.resume(runId)` is a\ndifferent same-ID recovery API: it reloads the persisted original script/args and permanently uses\nlegacy positional replay semantics, while the independent default-on channel may still continue an\neligible usage/auth-interrupted live call.\n\n## <a name="custom-backends-metabackends"></a>Custom backends \u2014 `meta.backends`\n\n```js\nexport const meta = {\n name: "\u2026", description: "\u2026",\n backends: {\n browser: {\n command: "browser-acp", // required: executable (absolute or on PATH)\n args: ["--headless"], // default []\n env: { BROWSER_PROFILE: "qa" }, // merged OVER the child\'s inherited env \u2014 per-backend secrets go here\n sessionMeta: { viewport: "desktop" }, // static ACP _meta on every session/new (per-call `meta` merges over it)\n structuredOutputTool: true, // default true; false = keep this backend on the prompt/_meta schema fallback\n },\n },\n};\n```\n\nScript-declared backends are **trust-gated**: they spawn commands on the host machine, so they stay inert until the composition root approves them \u2014 elicitation approval in the MCP server, `allowScriptBackends: true` (or a per-backend callback) on `runDynamicWorkflow`, `ExecOptions.scriptBackends` on a manager, or `AGENTPRISM_ALLOW_SCRIPT_BACKENDS=1`. A *declined* backend aborts the run rather than silently rerouting its calls to the default backend. Host-registered names always win over script declarations. Prefer host registration (`createAcpRunner({ backends })` / `AGENTPRISM_BACKENDS` env JSON) when you control the host.\n\n## <a name="agenttype-definitions"></a>`agentType` definitions\n\nMarkdown files at `<runCwd>/.agentprism/agents/<name>.md` (project) and `~/.agentprism/agents/<name>.md` (user); project wins on name collision. Frontmatter + body:\n\n```markdown\n---\ndescription: Read-only security auditor\ntools: [read, grep, glob] # allowlist of tool names (omit = all)\ndisallowedTools: [bash] # denylist, applied after the allowlist\nmodel: claude/opus[1m] # verified id; agent({ model }) overrides it\nisolation: worktree # optional\n---\nYou are a security auditor. Report findings; never modify files.\n```\n\nThe body is prepended to the agent\'s task as role guidance. An unknown `agentType` logs a warning and runs with default tools/model (the name degrades to a prose hint).\n'
|
|
33446
|
+
},
|
|
33447
|
+
{
|
|
33448
|
+
"id": "workflow/examples",
|
|
33449
|
+
"title": "Workflow composition examples",
|
|
33450
|
+
"description": "Cross-vendor build, backend-agnostic audit, bounded loops, schemas, and automatic validation patterns.",
|
|
33451
|
+
"uri": "agentprism://docs/workflow/examples",
|
|
33452
|
+
"mimeType": "text/markdown",
|
|
33453
|
+
"relatedTopics": [
|
|
33454
|
+
"workflow/quickstart",
|
|
33455
|
+
"workflow/composition-and-failure",
|
|
33456
|
+
"workflow/checkpoints-and-quality"
|
|
33457
|
+
],
|
|
33458
|
+
"bytes": 6181,
|
|
33459
|
+
"sha256": "281d047d5def4a8a17660c5009e6b8fbf76197da768561e138418a3ba533b8c8",
|
|
33460
|
+
"text": '## Worked example \u2014 cross-vendor build with every major primitive\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n```js\nexport const meta = {\n name: "feature-build",\n description: "Plan, gate on approval, implement, cross-vendor review, fix until green",\n phases: [{ title: "Plan" }, { title: "Implement" }, { title: "Review" }],\n};\n\nconst PLAN = { type: "object", additionalProperties: false, required: ["steps", "risks"],\n properties: {\n steps: { type: "array", items: { type: "string", description: "One concrete implementation step" } },\n risks: { type: "array", items: { type: "string" } } } };\nconst VERDICT = { type: "object", additionalProperties: false, required: ["ok"],\n properties: { ok: { type: "boolean" },\n feedback: { type: "string", description: "Required when ok=false: concretely what to change" } } };\n\nphase("Plan");\nconst plan = await agent(\n `Study this repo, then write an implementation plan for: ${args.feature}. Keep steps concrete.`,\n { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN },\n);\n\nconst approved = await checkpoint(\n `Implement "${args.feature}" with this plan?\\n- ${plan.steps.join("\\n- ")}\\nRisks: ${plan.risks.join("; ")}`,\n { kind: "confirm", default: true },\n);\nif (!approved) return { implemented: false, plan };\n\nphase("Implement");\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement: ${args.feature}\\nPlan:\\n- ${plan.steps.join("\\n- ")}\\n` +\n `Run the project\'s tests before finishing and report results.` +\n (feedback ? `\\n\\nReviewer feedback on attempt ${attempt}:\\n${feedback}\\nAddress every point.` : ""),\n { label: `implement:${attempt + 1}`, model: "codex/gpt-5.6-sol", retries: 1 },\n ),\n async (report) => {\n if (!report) return { ok: false, feedback: "implementation agent produced no result" };\n phase("Review");\n const reviews = (await parallel([ // two reviewers on different vendors\n () => agent(`Review the working-tree diff for correctness. Implementer\'s report:\\n${report}`,\n { label: "review:correctness", model: "claude/opus[1m]", schema: VERDICT }),\n () => agent(`Review the working-tree diff for regressions and missing tests. Report:\\n${report}`,\n { label: "review:coverage", model: "opencode/zai/glm-5.2", schema: VERDICT }),\n ])).filter(Boolean);\n const rejections = reviews.filter((r) => !r.ok);\n return rejections.length\n ? { ok: false, feedback: rejections.map((r) => r.feedback).join("\\n"), reviews }\n : { ok: true, reviews };\n },\n { attempts: 3 },\n);\n\nreturn { implemented: outcome.ok, attempts: outcome.attempts, reviewVerdict: outcome.verdict, plan };\n```\n\n(The planner would ideally run read-only, but mode ids are backend/model-specific, so this call leaves `mode` unset rather than guessing. Add one only after `action:"config"` explicitly lists the exact id in `modes.availableModes`; `modes:null` means keep it omitted.)\n\n## Worked example \u2014 fully backend-agnostic audit\n\nNo `model` anywhere: this script runs unchanged on whatever backend the host defaults to.\n\n```js\nexport const meta = {\n name: "edge-case-audit",\n description: "Exhaustively hunt edge-case bugs in a target dir, verify each, report gaps",\n phases: [{ title: "Hunt" }, { title: "Verify" }],\n};\n\nconst BUGS = { type: "object", additionalProperties: false, required: ["bugs"],\n properties: { bugs: { type: "array", items: { type: "object", additionalProperties: false,\n required: ["file", "scenario"], properties: {\n file: { type: "string", description: "Repo-relative path you actually opened" },\n scenario: { type: "string", description: "Concrete input/state \u2192 wrong behavior" } } } } } };\n\nphase("Hunt");\nconst seen = []; // what earlier rounds reported, threaded into each new prompt\nconst candidates = await loopUntilDry({\n round: async (i) => {\n const r = await agent(\n `Round ${i + 1}: find edge-case bugs in ${args.target} not already in this list:\\n` +\n JSON.stringify(seen) + `\\nOnly report what you can ground in code you read.`,\n { label: `hunt:${i + 1}`, schema: BUGS },\n );\n const bugs = r ? r.bugs : [];\n seen.push(...bugs);\n return bugs; // loopUntilDry dedups these by `key` across rounds\n },\n key: (b) => `${b.file}:${b.scenario}`,\n consecutiveEmpty: 2,\n maxRounds: 8,\n});\n\nphase("Verify");\nconst confirmed = (await pipeline(\n candidates,\n (bug) => verify(bug, { reviewers: 3, threshold: 0.66, lens: ["correctness", "reproducibility"] }),\n (v, bug) => (v.real ? bug : null),\n)).filter(Boolean);\n\nconst gaps = await completenessCheck(args, confirmed);\nlog(`${confirmed.length}/${candidates.length} confirmed; complete=${gaps.complete}`);\nreturn { confirmed, missing: gaps.missing ?? [] };\n```\n\n## Automatic validation before admission\n\nThe MCP `workflow` tool validates every run automatically before admission: static parse, mocked dry run, then routed no-prompt config checks. Invalid scripts return `status:"rejected"` diagnostics without creating a run ID, reserving a background slot, or spending tokens. When pinning model, mode, or `configOptions`, use `action:"config"` first.\n\nThe mocked pass executes reachable script control flow with schema-conforming fabricated agent results. It can prove that syntax, metadata, helper calls, and reachable branches are structurally executable, but it cannot prove prompt quality, real-world judgment, or convergence through every branch. Keep loops bounded in script code and inspect validation warnings for declared phases that the default fabricated path did not reach.\n\nThe routed config pass probes each distinct backend/model pair without prompting. Unknown option ids, invalid select values, wrong value types, and the reserved `"model"` config key reject the script with direct alternatives. A backend that cannot be probed produces an explicit warning and leaves only that backend\'s option domain unverified.\n\nFor model/config details, read `workflow/models-and-config`. For edited-script replay patterns, read `workflow/determinism-and-resume`.\n'
|
|
33461
|
+
},
|
|
33462
|
+
{
|
|
33463
|
+
"id": "repl/quickstart",
|
|
33464
|
+
"title": "REPL orchestration: quickstart",
|
|
33465
|
+
"description": "Persistent eval basics, correct agent signature, handles, polling, structured output, and interrupt semantics.",
|
|
33466
|
+
"uri": "agentprism://docs/repl/quickstart",
|
|
33467
|
+
"mimeType": "text/markdown",
|
|
33468
|
+
"relatedTopics": [
|
|
33469
|
+
"repl/state-and-bindings",
|
|
33470
|
+
"repl/agent-handles",
|
|
33471
|
+
"repl/api-reference",
|
|
33472
|
+
"repl/examples"
|
|
33473
|
+
],
|
|
33474
|
+
"bytes": 3174,
|
|
33475
|
+
"sha256": "253f3e465fc3172b686ea7f1a4a9f989e8559c2dc9010fdc355418379271c968",
|
|
33476
|
+
"text": '# REPL orchestration: quickstart\n\n**Context:** JavaScript sent to the MCP `repl` tool. This is not workflow-script code: REPL `agent()` takes `(modelSpec, task, options?)`, top-level `return` is a syntax error, and named bindings persist between calls.\n\nThe REPL is one persistent QuickJS-in-WASM workspace per absolute `projectDir`. Use it when you want to inspect intermediate results and decide the next step interactively. Use `workflow` when the full orchestration is known up front and should be repeatable as one deterministic script.\n\n## First eval\n\n```js\nconst audit = agent("codex", "Inspect the parser for correctness bugs");\n```\n\nSend that code using:\n\n```json\n{ "action": "eval", "projectDir": "/absolute/project", "code": "..." }\n```\n\n`agent()` returns a persistent promise-handle immediately. Storing the handle before awaiting preserves its `id`, `queue()`, `steer()`, and `cancel()` methods for later evals.\n\nInspect or await it in another eval:\n\n```js\nagents()\n```\n\n```js\nconst report = await audit;\nreport\n```\n\nA completed eval returns `{ output, result? }`. If the soft hold bound expires while the eval remains suspended, the tool returns `{ output, running: [callIds] }`; execution continues server-side. Poll without running new code by evaluating the empty string:\n\n```json\n{ "action": "eval", "projectDir": "/absolute/project", "code": "" }\n```\n\n## Essential semantics\n\n- Top-level `await` works; top-level `return` does not.\n- `let`, `const`, `var`, functions, and classes remain available to later evals.\n- `_` is the previous eval\'s completion value.\n- Console output is returned as text but is not a persistent value. Assign values you need later.\n- There is no filesystem, network, import, or general timer API in the VM. Subagents perform external work; `sleep(ms)` is the one host-backed timer.\n- The default eval hold is 60 seconds and the per-call maximum is 120 seconds. This is a response hold, not cancellation.\n- Use `interrupt` with a call `id` to cancel one subagent/queued turn, or omit `id` to break the currently running eval.\n- `workspace()` shows bindings, in-flight calls, checkpoints, and diagnostics. `agents()` shows live agent lanes and queued turns.\n- `reset()` tears the workspace down after the current eval completes.\n\n## Structured output\n\n```js\nconst schema = {\n type: "object",\n additionalProperties: false,\n required: ["findings"],\n properties: {\n findings: { type: "array", items: { type: "string" } },\n },\n};\nconst review = agent("claude", "Review error handling", { schema });\nconst result = await review;\nresult.findings\n```\n\nAgent options are exactly `schema`, `cwd`, `configOptions`, and `mode`. Unknown keys reject. Model, mode, and option ids are backend-specific; use the `workflow` tool\'s zero-token `action:"config"` discovery before pinning them.\n\n## What to read next\n\n- `repl/state-and-bindings` \u2014 persistence, completion values, polling, and output.\n- `repl/agent-handles` \u2014 `agent()` options, failures, and handle identity.\n- `repl/steering-queueing-and-cancellation` \u2014 strict active-turn control and durable future turns.\n- `repl/api-reference` \u2014 every guest global and tool action.\n'
|
|
33477
|
+
},
|
|
33478
|
+
{
|
|
33479
|
+
"id": "repl/state-and-bindings",
|
|
33480
|
+
"title": "REPL state, bindings, and eval results",
|
|
33481
|
+
"description": "Persistent lexical bindings, completion values, output rendering, soft-bound results, polling, and cleanup.",
|
|
33482
|
+
"uri": "agentprism://docs/repl/state-and-bindings",
|
|
33483
|
+
"mimeType": "text/markdown",
|
|
33484
|
+
"relatedTopics": [
|
|
33485
|
+
"repl/quickstart",
|
|
33486
|
+
"repl/checkpoints-and-introspection",
|
|
33487
|
+
"repl/persistence-and-reset"
|
|
33488
|
+
],
|
|
33489
|
+
"bytes": 2837,
|
|
33490
|
+
"sha256": "02b4040fc3a76c9f07c234ce59c95a66b19b1b8eccf384362dece9642762115b",
|
|
33491
|
+
"text": '# REPL state, bindings, and eval results\n\nEvery project has one persistent JavaScript VM. A later `eval` sees the same global lexical environment and the same unresolved promise-handles as earlier evals.\n\n## Persist values explicitly\n\n```js\nconst target = "src/parser";\nconst findings = [];\n```\n\nBoth names remain available later. Console output does not create a value:\n\n```js\nconsole.log({ target }); // emits output only\n```\n\nThe special `_` binding contains the previous eval\'s completion value, similar to an interactive language shell:\n\n```js\n[1, 2, 3].map((x) => x * 2)\n// later:\n_.reduce((sum, x) => sum + x, 0)\n```\n\nAssign important results to descriptive names instead of depending on `_`, because each completed eval replaces it.\n\n## Top-level syntax\n\nTop-level `await` is supported:\n\n```js\nconst answer = await worker;\nanswer\n```\n\nTop-level `return` is a syntax error. The final expression becomes the eval completion value. Standard JavaScript control flow works, including async functions and `for await`.\n\n## Eval result shapes\n\nFinished:\n\n```js\n{ output: "zero or more console/error lines", result: "value repr when present" }\n```\n\nStill running after the soft hold bound:\n\n```js\n{ output: "lines emitted so far", running: ["c1", "c2"] }\n```\n\nThe eval continues after the second shape. Any later eval drains settlements first. An empty code string is the idempotent poll and runs no user code.\n\n`output` is newline-joined console, checkpoint, and uncaught-error rendering. Direct strings passed to console print in full. Objects and arrays use a depth-limited preview; evaluate a narrower property or slice to inspect more. Preserve large values in bindings rather than repeatedly printing them into model context.\n\n## Concurrency and serialization\n\nVM operations serialize, so concurrent clients cannot reorder individual eval operations. Subagents run concurrently under the workspace\'s host limit. The eval call pumps promise settlements until completion or its response bound; the bound does not terminate the underlying eval.\n\nUse `sleep(ms)` for a host-backed delay:\n\n```js\nawait sleep(250);\n```\n\nNo other timer API is available.\n\n## Introspection\n\n```js\nworkspace()\n```\n\nreturns a plain object containing `bindings`, `inFlight`, `checkpoints`, and `diagnostics`. Binding rows include the name, type/size preview, provenance, task/call id where relevant, and settlement status.\n\n```js\nagents()\n```\n\nreturns live agent/queued-turn rows with call ids, model specs, task previews, state, steering support, and queued-turn counts. Use these functions instead of guessing whether a handle is active.\n\n## Cleanup\n\n```js\nreset()\n```\n\nrequests teardown after the current eval completes. All bindings and pending work in that project workspace are discarded. It returns `undefined`; the next eval creates a fresh workspace.\n'
|
|
33492
|
+
},
|
|
33493
|
+
{
|
|
33494
|
+
"id": "repl/agent-handles",
|
|
33495
|
+
"title": "REPL agent calls and persistent handles",
|
|
33496
|
+
"description": "Exact agent(modelSpec, task, options) API, routing, option vocabulary, handle identity, and failures.",
|
|
33497
|
+
"uri": "agentprism://docs/repl/agent-handles",
|
|
33498
|
+
"mimeType": "text/markdown",
|
|
33499
|
+
"relatedTopics": [
|
|
33500
|
+
"repl/quickstart",
|
|
33501
|
+
"repl/steering-queueing-and-cancellation",
|
|
33502
|
+
"repl/api-reference"
|
|
33503
|
+
],
|
|
33504
|
+
"bytes": 3550,
|
|
33505
|
+
"sha256": "93acec40f5758f8f5317242ba2e9a871defc4b1a4f6c72285252d5a88999f4cd",
|
|
33506
|
+
"text": '# REPL agent calls and persistent handles\n\nREPL delegation uses:\n\n```js\nagent(modelSpec, task, options?) -> PromiseHandle\n```\n\nThis differs from workflow scripts, whose signature is `agent(prompt, options?)`.\n\n## Model routing\n\n`modelSpec` is required. Use a registered backend name alone to preserve its configured default model:\n\n```js\nconst worker = agent("codex", "Investigate the failing parser test");\n```\n\nUse `backend/model-id` only after model discovery:\n\n```js\nconst worker = agent("claude/verified-model-id", "Review the implementation");\n```\n\nThe known built-ins are Claude, Codex, OpenCode, and pi, plus host-registered custom backends. Unknown backend names reject and enumerate known backends. Use the `workflow` tool\'s `action:"config"` with `harnesses`/`modelFilter`, then `modelSpecs`, before pinning model, mode, or config-option values. Set `mode` only when the selected entry\'s `modes.availableModes` explicitly lists that exact id; `modes:null` means omit it, and a generic `"default"` must never be inferred.\n\n## Exact option vocabulary\n\n```js\nconst worker = agent("codex", "Inspect the parser", {\n schema: {\n type: "object",\n additionalProperties: false,\n required: ["summary"],\n properties: { summary: { type: "string" } },\n },\n cwd: "/absolute/path",\n mode: "advertised-mode-id",\n configOptions: { advertisedOptionId: "advertised-value" },\n});\n```\n\nOptions are exactly:\n\n- `schema`: plain JSON Schema object; the promise resolves to its validated object.\n- `cwd`: absolute worker session working directory.\n- `mode`: exact ACP mode explicitly listed in the selected backend/model\'s discovered `modes.availableModes`.\n- `configOptions`: exact string/boolean ACP option ids and values.\n\nUnknown option keys reject. Options must be JSON-serializable.\n\n## Preserve the handle\n\nThe returned promise is also the live handle:\n\n```js\nconst worker = agent("pi", "Research the issue");\nconst id = worker.id;\nconst answer = await worker;\n```\n\nDo not write this if you intend to reuse the session:\n\n```js\nconst worker = await agent("pi", "Research the issue");\n```\n\nThat variable stores only the answer and loses access to handle methods.\n\nThe founding handle exposes non-enumerable, immutable members:\n\n- `id`: stable call id such as `"c1"`.\n- `queue(prompt, options?)`: create a distinct durable FIFO future turn.\n- `steer(prompt, options?)`: attempt strict control of only the currently active turn.\n- `cancel()`: cancel the session\'s current public turn.\n\nA queued-turn handle exposes its own `id` and `cancel()`.\n\n## Settlement and failures\n\nWithout `schema`, the founding promise resolves to final assistant text. With `schema`, it resolves to the validated object.\n\nA rejected call carries an error with call/backend attribution where available. Errors whose `recoverable` field is not `false` are treated as recoverable by `parallel()` and `pipeline()` and become `null` slots. A non-recoverable error rejects the surrounding combinator/eval.\n\nDirect `await worker` propagates rejection; catch only errors you can handle meaningfully:\n\n```js\nlet answer;\ntry {\n answer = await worker;\n} catch (error) {\n console.error(error);\n answer = null;\n}\n```\n\n## Session continuity\n\nThe founding answer settling does not erase the handle binding. Queue later prompts on the founding handle to continue the same ACP session. Session continuity depends on the backend\'s continuation capability and the workspace\'s durable lane state. Never fabricate a new handle from a saved id; retain the actual promise-handle binding.\n'
|
|
33507
|
+
},
|
|
33508
|
+
{
|
|
33509
|
+
"id": "repl/steering-queueing-and-cancellation",
|
|
33510
|
+
"title": "REPL steering, queueing, and cancellation",
|
|
33511
|
+
"description": "Strict active-turn steering, durable FIFO future turns, exact handle cancellation, ordering, and recovery.",
|
|
33512
|
+
"uri": "agentprism://docs/repl/steering-queueing-and-cancellation",
|
|
33513
|
+
"mimeType": "text/markdown",
|
|
33514
|
+
"relatedTopics": [
|
|
33515
|
+
"repl/agent-handles",
|
|
33516
|
+
"repl/checkpoints-and-introspection",
|
|
33517
|
+
"repl/persistence-and-reset"
|
|
33518
|
+
],
|
|
33519
|
+
"bytes": 3681,
|
|
33520
|
+
"sha256": "c3af06afe4c969b5512da0ed7d4f7e1ef02dffc63b00444c55b8ef1b4c8b6ffb",
|
|
33521
|
+
"text": '# REPL steering, queueing, and exact cancellation\n\nA founding agent handle separates transient control of a running turn from durable future work.\n\n## Strict steering\n\n```js\nconst worker = agent("codex", "Investigate the parser failure");\n```\n\nOnly while `agents()` reports its active turn as running:\n\n```js\nconst outcome = await worker.steer("Focus on the parser state machine");\n```\n\nThe result is exactly:\n\n- `"injected"`: the instruction was delivered into the active turn.\n- `"idle"`: no turn was active; the instruction was not retained.\n- `"unsupported"`: the backend does not advertise strict steering.\n\nTransport or protocol failures reject. Steering never starts a new turn and never queues work. An `"idle"` result intentionally loses the instruction. Check `agents()` immediately before steering when timing matters.\n\nSteering support is based on the backend\'s raw ACP steering advertisement. Do not infer it from the backend name.\n\n## Durable queued turns\n\nAfter retaining the founding handle, queue future public turns:\n\n```js\nconst first = await worker;\nconst implement = worker.queue("Implement the fix");\nconst test = worker.queue("Run the focused tests");\nconsole.log(implement.id, test.id);\nconst implemented = await implement;\nconst tested = await test;\n```\n\nEach queue call synchronously returns a distinct promise-handle with its own stable id. Queued turns execute FIFO on the founding session using ordinary public prompts. Queueing is broker-owned and works on every backend that can continue the session; it does not depend on a backend-native queue API.\n\nQueued-turn options are exactly:\n\n```js\n{ promptMeta?: object }\n```\n\nThe prompt must be a string. Malformed queue requests still receive a durable call record and reject directly.\n\n## Cancellation\n\nCancel the founding session\'s currently active public turn:\n\n```js\nawait worker.cancel();\n```\n\nCancel one queued turn exactly:\n\n```js\nawait test.cancel();\n```\n\nOr cancel by stable call id outside the VM through the tool:\n\n```json\n{ "action": "interrupt", "projectDir": "/absolute/project", "id": "c4" }\n```\n\nA targeted cancellation rejects that call recoverably and leaves unrelated work live. Cancelling a queued handle does not cancel its founding turn or siblings. Cancellation settlement is first-wins; an already-settled target cannot be retroactively cancelled.\n\nOmit `id` only to break the currently running eval itself:\n\n```json\n{ "action": "interrupt", "projectDir": "/absolute/project" }\n```\n\nThat is eval control, not agent-call selection. It breaks an executing eval or terminates a suspended eval that cannot be safely resumed. It returns an honest idle refusal only when nothing is running.\n\n## Ordering and persistence\n\nQueue admission, handoff, settlement, and cancellation are persisted. Pending queue turns survive daemon restart and reattach lazily to their founding session when eligible. Queue delivery has the documented narrow at-least-once crash window around remote acceptance and local handoff persistence; prompts that cause external side effects should therefore be idempotent or guarded.\n\nA lane-fatal persistence/session failure rejects the active and queued turns on that lane. The broker never opens a blank replacement session and pretends context survived.\n\n## Recommended pattern\n\n```js\nconst worker = agent("claude", "Analyze the issue; do not edit yet");\n// While running, optionally steer after checking agents().\nconst analysis = await worker;\nconst fix = worker.queue("Implement the agreed fix");\nconst verify = worker.queue("Run tests and report the exact results");\nconst fixed = await fix;\nconst verified = await verify;\n({ analysis, fixed, verified })\n```\n'
|
|
33522
|
+
},
|
|
33523
|
+
{
|
|
33524
|
+
"id": "repl/checkpoints-and-introspection",
|
|
33525
|
+
"title": "REPL checkpoints and introspection",
|
|
33526
|
+
"description": "Raising and answering durable checkpoints plus workspace(), agents(), and error diagnostics.",
|
|
33527
|
+
"uri": "agentprism://docs/repl/checkpoints-and-introspection",
|
|
33528
|
+
"mimeType": "text/markdown",
|
|
33529
|
+
"relatedTopics": [
|
|
33530
|
+
"repl/state-and-bindings",
|
|
33531
|
+
"repl/steering-queueing-and-cancellation",
|
|
33532
|
+
"repl/api-reference"
|
|
33533
|
+
],
|
|
33534
|
+
"bytes": 2667,
|
|
33535
|
+
"sha256": "62c5c8a50ca429c0573e3ce227348a4fb8133ccabc197da455a63fd6ad0a9f7d",
|
|
33536
|
+
"text": '# REPL checkpoints and introspection\n\nREPL checkpoints park a promise until a later eval explicitly supplies the human answer.\n\n## Raise a checkpoint\n\n```js\nconst approval = checkpoint("Proceed with the destructive migration?", {\n choices: ["approve", "reject"],\n default: "reject",\n});\n```\n\nThe eval output includes a line such as:\n\n```text\ncheckpoint c3: Proceed with the destructive migration?\n```\n\nThe promise remains live in the workspace. The host does not infer an answer from conversation text; answer delivery is an explicit data-plane operation.\n\n## Inspect pending checkpoints\n\n```js\nworkspace().checkpoints\n```\n\nEach pending row identifies the call id, question, and available option metadata. Retain the promise binding or recover its call id through `workspace()`.\n\n## Answer from a later eval\n\n```js\ncheckpoint.answer("c3", "approve")\n```\n\nThis returns `true` if that checkpoint was pending and the answer settled it, or `false` if the id was unknown/already settled. Delivery is first-wins and idempotent. The answer must be JSON-serializable; `undefined` is normalized to `null`.\n\nThe original continuation resumes during the same eval\'s settlement drain:\n\n```js\nconst decision = await approval;\ndecision\n```\n\nCheckpoint promises and questions survive daemon restart through the workspace snapshot.\n\n## Workspace introspection\n\n```js\nconst state = workspace();\n```\n\nThe returned plain object contains:\n\n- `bindings`: persistent user bindings with bounded type/size/status previews and call provenance where applicable.\n- `inFlight`: all unsettled bridge calls, including agents, queue/steer/cancel controls, sleeps, and checkpoints.\n- `checkpoints`: currently pending human questions.\n- `diagnostics`: restore reconciliation notes and retained settlement-drain faults.\n\nInspect it narrowly to avoid unnecessary context:\n\n```js\nworkspace().bindings.map(({ name, type, status }) => ({ name, type, status }))\n```\n\n## Agent-lane introspection\n\n```js\nagents()\n```\n\nreturns only live agent/queued-turn entries, including call id, model spec, task preview, state, strict-steering support, and queued-turn count. This is the authority for deciding whether `handle.steer()` has an active target.\n\n## Error diagnosis\n\nUncaught errors render into eval output. Errors associated with an agent/queued call include its stable call id and resolved backend when known. A drain or reconciliation problem that did not lose state is retained under `workspace().diagnostics`; state loss additionally produces a notice in the next eval output.\n\nUse the empty eval to drain late settlements, then inspect `workspace()` and `agents()` before deciding to cancel or reset.\n'
|
|
33537
|
+
},
|
|
33538
|
+
{
|
|
33539
|
+
"id": "repl/persistence-and-reset",
|
|
33540
|
+
"title": "REPL persistence, restore, and reset",
|
|
33541
|
+
"description": "Snapshot boundaries, restart reconciliation, queue recovery, snapshot refusal, disconnect drain, and reset.",
|
|
33542
|
+
"uri": "agentprism://docs/repl/persistence-and-reset",
|
|
33543
|
+
"mimeType": "text/markdown",
|
|
33544
|
+
"relatedTopics": [
|
|
33545
|
+
"repl/state-and-bindings",
|
|
33546
|
+
"repl/steering-queueing-and-cancellation",
|
|
33547
|
+
"repl/checkpoints-and-introspection"
|
|
33548
|
+
],
|
|
33549
|
+
"bytes": 2619,
|
|
33550
|
+
"sha256": "47bacb8c2ce0c640d2167d369a5f5ae487d164e058e324f7e0ee9bdf1fdd56f8",
|
|
33551
|
+
"text": "# REPL persistence, restore, and reset\n\nThe REPL workspace is durable per project. Named bindings, pending agent and checkpoint promises, queue state, and call-id sequencing persist across MCP client disconnects and daemon restarts.\n\n## Snapshot boundaries\n\nEvery state-changing eval and settlement drain persists the workspace. On first touch after restart, the server restores the QuickJS snapshot, re-registers host callbacks, and reconciles outstanding calls. The guest library itself is not re-evaluated into a restored VM.\n\nA later call can therefore continue with prior bindings:\n\n```js\nworkspace().bindings\n```\n\nand await a handle created before restart:\n\n```js\nconst answer = await worker;\n```\n\n## Session and queued-turn recovery\n\nPending founding turns and queued turns are reconciled from durable records. Eligible queued turns reattach lazily to their founding ACP session. The broker never substitutes a blank session if continuity cannot be preserved; lane-fatal recovery faults reject the lane instead.\n\nStrict steering is transient. A pending steering control cannot be replayed as a future prompt after restart and rejects rather than changing semantics. Durable queue turns remain future work.\n\n## Snapshot refusal\n\nSnapshot compatibility is tied to the snapshot format and the exact QuickJS WASM binary hash. Corrupt, incompatible-format, or mismatched-binary snapshots are not restored silently.\n\nA refused snapshot is renamed aside with a `.refused-<timestamp>` suffix and the workspace automatically resets. The next successful eval output begins with a notice naming the file and refusal reason. The refused file is never silently deleted.\n\nReconciliation summaries and retained drain errors are available under:\n\n```js\nworkspace().diagnostics\n```\n\n## Client disconnect drain\n\nWhen the last MCP client for a project disconnects, the workspace drains in-flight subagent turns and closes idle children. Persistent workspace state remains. The next eligible queued turn reattaches its founding session lazily.\n\n## Explicit reset\n\n```js\nreset()\n```\n\nrequests teardown after the current eval completes. It discards bindings, pending calls, checkpoints, and the active snapshot for that workspace. The eval that calls `reset()` still completes normally; the next touch creates a fresh VM.\n\nPrefer targeted cleanup first:\n\n- cancel a queued handle with `queued.cancel()`;\n- cancel an active call with `handle.cancel()` or `repl` interrupt by id;\n- break only a runaway eval with interrupt and no id.\n\nUse `reset()` when the whole interactive state is intentionally disposable or no longer trustworthy.\n"
|
|
33552
|
+
},
|
|
33553
|
+
{
|
|
33554
|
+
"id": "repl/api-reference",
|
|
33555
|
+
"title": "REPL API reference",
|
|
33556
|
+
"description": "Exact repl tool actions, every guest global, handle methods, combinator semantics, and environment limits.",
|
|
33557
|
+
"uri": "agentprism://docs/repl/api-reference",
|
|
33558
|
+
"mimeType": "text/markdown",
|
|
33559
|
+
"relatedTopics": [
|
|
33560
|
+
"repl/quickstart",
|
|
33561
|
+
"repl/agent-handles",
|
|
33562
|
+
"repl/checkpoints-and-introspection"
|
|
33563
|
+
],
|
|
33564
|
+
"bytes": 4455,
|
|
33565
|
+
"sha256": "3a9342f8e7862d0ba53c830b9fe51f3606f6131f0386a8b2c9cf16044d565712",
|
|
33566
|
+
"text": '# REPL API reference\n\n**Context:** code evaluated by the MCP `repl` tool. Workflow scripts use a different `agent(prompt, options?)` signature and have additional run/journal APIs.\n\n## MCP tool actions\n\n```text\nrepl({ action: "eval", projectDir, code, timeoutMs? })\nrepl({ action: "interrupt", projectDir, id? })\n```\n\n`projectDir` is required on the shared daemon and optional in a single-project server. `eval` requires a code string; an empty string is the idempotent settlement poll. `timeoutMs` is an integer from 0 through 120000, default 60000, and bounds only how long the tool call pumps settlements. `interrupt` with `id` cancels exactly that live call; without `id` it breaks the running eval. Fields from the other action are rejected.\n\nFinished eval result:\n\n```ts\n{ output: string; result?: string }\n```\n\nStill-running eval result:\n\n```ts\n{ output: string; running: string[] }\n```\n\n## Guest globals\n\n```text\nagent(modelSpec, task, options?) -> PromiseHandle\ncheckpoint(question, options?) -> Promise\ncheckpoint.answer(callId, value) -> boolean\nparallel(thunks) -> Promise<results[]>\npipeline(items, ...stages) -> Promise<results[]>\nverify(item, { reviewers = 2, threshold = 0.5, lens? })\n -> { real, realCount, total, votes }\njudgePanel(attempts, { judges = 3, rubric = "overall quality and correctness" })\n -> { index, attempt, score, judgments }\ngate(thunk, validator, { attempts = 3 })\n -> { ok, value, verdict, attempts }\nretry(thunk, { attempts = 3, until? }) -> last result\nloopUntilDry({ round, key = JSON.stringify, consecutiveEmpty = 2, maxRounds = 50 })\n -> unique items[]\nsleep(ms) -> Promise<undefined>\nworkspace() -> { bindings, inFlight, checkpoints, diagnostics }\nagents() -> live agent/queued-turn rows\nreset() -> undefined\nconsole.log/info/warn/error/debug(...values) -> undefined\n_ -> previous eval completion value\n```\n\nTop-level `await` is accepted. Top-level `return` is a syntax error.\n\n## `agent()`\n\n```js\nagent(modelSpec, task, {\n schema?: object,\n cwd?: string,\n configOptions?: Record<string, string | boolean>,\n mode?: string,\n})\n```\n\nAll arguments except options are required strings. The option vocabulary is exact. Options cross the bridge as JSON. Without `schema`, the handle resolves to assistant text; with `schema`, to the validated object.\n\nFounding promise-handle members:\n\n```text\nhandle.id: string\nhandle.queue(prompt, { promptMeta?: object }?) -> QueuedPromiseHandle\nhandle.steer(prompt, { promptMeta?: object }?) -> Promise<"injected" | "idle" | "unsupported">\nhandle.cancel() -> Promise\n```\n\nQueued promise-handle members:\n\n```text\nqueued.id: string\nqueued.cancel() -> Promise\n```\n\n## Combinator semantics\n\n`parallel` requires functions, not promises, and preserves input order. A recoverable rejection becomes `null` in its slot; a non-recoverable rejection propagates.\n\n`pipeline` runs each item through each stage in order while items progress concurrently. A stage receives `(previousValue, originalItem, index)`. Recoverable item failure yields `null` for that item.\n\n`verify` uses the host\'s configured default backend for adversarial votes. Failed reviewers are dropped. `lens` may be one string or an array rotated across reviewers.\n\n`judgePanel` uses the host default backend to score every candidate from 0 to 1 and returns the highest mean; ties prefer the lower input index.\n\n`retry` calls `thunk(attempt)` up to the bound. Without `until`, the first result is accepted. With `until`, the last result is returned if no attempt passes.\n\n`gate` calls `thunk(feedback, attempt)`, then awaits `validator(result)`. The verdict may be boolean or `{ ok, feedback?, ... }`; object feedback enters the next producer attempt. The complete last verdict is returned.\n\n`loopUntilDry` repeatedly calls `round(index)`, deduplicates non-null items using `key`, and stops after the configured consecutive empty rounds or maximum rounds.\n\n## Checkpoints\n\n`checkpoint(question, options?)` accepts a string question and any JSON-serializable options object. The promise parks until a later eval calls `checkpoint.answer(id, value)`. Answer delivery is explicit, first-wins, and returns whether a pending checkpoint was settled.\n\n## Environment\n\nThe VM has ordinary deterministic JavaScript data/control APIs but no imports, `require`, filesystem, network, or general timers. `sleep(ms)` is host-backed. Console calls never throw and return output only. Bind values explicitly when they must persist.\n'
|
|
33567
|
+
},
|
|
33568
|
+
{
|
|
33569
|
+
"id": "repl/examples",
|
|
33570
|
+
"title": "REPL orchestration examples",
|
|
33571
|
+
"description": "Interactive steering, queue continuation, parallel reviews, checkpoints, polling, cancellation, and bounded loops.",
|
|
33572
|
+
"uri": "agentprism://docs/repl/examples",
|
|
33573
|
+
"mimeType": "text/markdown",
|
|
33574
|
+
"relatedTopics": [
|
|
33575
|
+
"repl/quickstart",
|
|
33576
|
+
"repl/agent-handles",
|
|
33577
|
+
"repl/steering-queueing-and-cancellation"
|
|
33578
|
+
],
|
|
33579
|
+
"bytes": 3176,
|
|
33580
|
+
"sha256": "dbae758165a803a32807952f0caa12bbd2ece22f86e1d9e65f9ead1ca3e3ba9f",
|
|
33581
|
+
"text": '# REPL orchestration examples\n\n## Interactive investigate, steer, implement, verify\n\nFirst eval\u2014retain the founding handle:\n\n```js\nconst parser = agent("codex", "Investigate the parser test failure. Analyze only; do not edit yet.");\n```\n\nWhile it is actually running:\n\n```js\nagents()\n```\n\n```js\nconst steering = await parser.steer("Focus on token recovery after malformed metadata");\nsteering\n```\n\nAfter the founding answer:\n\n```js\nconst analysis = await parser;\nconst fix = parser.queue("Implement the smallest correct fix and add focused tests");\nconst verify = parser.queue("Run the focused tests and report exact results");\nconst fixed = await fix;\nconst tested = await verify;\n({ analysis, fixed, tested })\n```\n\n## Parallel structured reviews\n\n```js\nconst verdictSchema = {\n type: "object",\n additionalProperties: false,\n required: ["ok", "reason"],\n properties: {\n ok: { type: "boolean" },\n reason: { type: "string" },\n },\n};\n\nconst reviews = (await parallel([\n () => agent("claude", "Review the current diff for correctness", { schema: verdictSchema }),\n () => agent("codex", "Review the current diff for regressions", { schema: verdictSchema }),\n () => agent("opencode", "Review whether tests cover the changed behavior", { schema: verdictSchema }),\n])).filter(Boolean);\nreviews\n```\n\n## Decide after inspection\n\n```js\nconst candidates = agent("pi", "Find up to five concrete reliability improvements", {\n schema: {\n type: "object",\n additionalProperties: false,\n required: ["items"],\n properties: {\n items: { type: "array", items: { type: "string" } },\n },\n },\n});\n```\n\nLater:\n\n```js\nconst found = await candidates;\nfound.items\n```\n\nChoose one after discussing it with the user, then continue the same session:\n\n```js\nconst chosen = found.items[0];\nconst implementation = candidates.queue(`Implement only this item: ${chosen}`);\nawait implementation\n```\n\n## Human checkpoint\n\n```js\nconst decision = checkpoint("Which candidate should be implemented?", {\n choices: found.items,\n});\n```\n\nAfter receiving the human response:\n\n```js\nworkspace().checkpoints\n```\n\n```js\ncheckpoint.answer("c4", found.items[1]);\nconst selected = await decision;\nselected\n```\n\n## Recover from a long-running eval\n\nIf a tool result reports `running`, poll without side effects:\n\n```json\n{ "action": "eval", "projectDir": "/absolute/project", "code": "" }\n```\n\nInspect state:\n\n```js\n({ agents: agents(), diagnostics: workspace().diagnostics })\n```\n\nCancel only one identified call through an out-of-band tool call:\n\n```json\n{ "action": "interrupt", "projectDir": "/absolute/project", "id": "c7" }\n```\n\nUse interrupt without an id only for a runaway eval, not as a substitute for targeted call cancellation.\n\n## Bounded hunt loop\n\n```js\nconst seen = [];\nconst findings = await loopUntilDry({\n round: async (i) => {\n const h = agent("claude", `Round ${i + 1}: find new bugs not in ${JSON.stringify(seen)}`);\n const text = await h;\n const rows = text ? [text] : [];\n seen.push(...rows);\n return rows;\n },\n consecutiveEmpty: 2,\n maxRounds: 5,\n});\nfindings\n```\n\nKeep interactive loops bounded even though the workspace persists indefinitely.\n'
|
|
33582
|
+
}
|
|
33583
|
+
];
|
|
33584
|
+
|
|
33585
|
+
// ../mcp-server/src/docs-tool.ts
|
|
33586
|
+
var DOCS_TOOL_NAME = "docs";
|
|
33587
|
+
var AUTHORING_DOC_MIME_TYPE = "text/markdown";
|
|
33588
|
+
var topicSchema = external_exports.enum(AUTHORING_DOC_TOPIC_IDS);
|
|
33589
|
+
var docsToolInputShape = {
|
|
33590
|
+
topic: topicSchema.optional().describe(
|
|
33591
|
+
'One version-matched documentation topic to read. Omit or use "index" for the bounded catalog; one call returns exactly one topic, never the whole documentation set.'
|
|
33592
|
+
)
|
|
33593
|
+
};
|
|
33594
|
+
var docsToolOutputShape = external_exports.object({
|
|
33595
|
+
topic: topicSchema,
|
|
33596
|
+
title: external_exports.string(),
|
|
33597
|
+
description: external_exports.string(),
|
|
33598
|
+
uri: external_exports.string(),
|
|
33599
|
+
mimeType: external_exports.literal(AUTHORING_DOC_MIME_TYPE),
|
|
33600
|
+
relatedTopics: external_exports.array(topicSchema),
|
|
33601
|
+
bytes: external_exports.number().int().nonnegative()
|
|
33602
|
+
}).strict();
|
|
33603
|
+
var topicsById = new Map(
|
|
33604
|
+
AUTHORING_DOC_TOPICS.map((topic) => [topic.id, topic])
|
|
33605
|
+
);
|
|
33606
|
+
function authoringDocTopic(topic) {
|
|
33607
|
+
const found = topicsById.get(topic);
|
|
33608
|
+
if (found === void 0) throw new Error(`Bundled authoring documentation topic is missing: ${topic}`);
|
|
33609
|
+
return found;
|
|
33610
|
+
}
|
|
33611
|
+
function authoringDocResource(topic) {
|
|
33612
|
+
return {
|
|
33613
|
+
contents: [{ uri: topic.uri, mimeType: AUTHORING_DOC_MIME_TYPE, text: topic.text }]
|
|
33614
|
+
};
|
|
33615
|
+
}
|
|
33616
|
+
function docsResult(topic) {
|
|
33617
|
+
return {
|
|
33618
|
+
topic: topic.id,
|
|
33619
|
+
title: topic.title,
|
|
33620
|
+
description: topic.description,
|
|
33621
|
+
uri: topic.uri,
|
|
33622
|
+
mimeType: AUTHORING_DOC_MIME_TYPE,
|
|
33623
|
+
relatedTopics: [...topic.relatedTopics],
|
|
33624
|
+
bytes: topic.bytes
|
|
33625
|
+
};
|
|
33626
|
+
}
|
|
33627
|
+
function registerAuthoringDocs(mcp, options) {
|
|
33628
|
+
for (const topic of AUTHORING_DOC_TOPICS) {
|
|
33629
|
+
const read = () => authoringDocResource(topic);
|
|
33630
|
+
mcp.registerResource(
|
|
33631
|
+
`agentprism-docs-${topic.id.replaceAll("/", "-")}`,
|
|
33632
|
+
topic.uri,
|
|
33633
|
+
{
|
|
33634
|
+
title: topic.title,
|
|
33635
|
+
description: topic.description,
|
|
33636
|
+
mimeType: AUTHORING_DOC_MIME_TYPE
|
|
33637
|
+
},
|
|
33638
|
+
read
|
|
33639
|
+
);
|
|
33640
|
+
options.registerResourceReader(topic.uri, read);
|
|
33641
|
+
}
|
|
33642
|
+
mcp.registerTool(
|
|
33643
|
+
DOCS_TOOL_NAME,
|
|
33644
|
+
{
|
|
33645
|
+
title: "Read AgentPrism workflow or REPL documentation",
|
|
33646
|
+
description: 'Read version-matched AgentPrism authoring documentation one bounded topic at a time. Omit topic or use "index" to see the catalog, then select only the workflow-script or REPL topic needed. Workflow and REPL agent() signatures differ, so use their separate namespaces. The result embeds the exact text/markdown MCP resource and lists related topic ids. This tool is read-only: it needs no projectDir, opens no backend session, runs no code, persists nothing, and spends no model tokens.',
|
|
33647
|
+
inputSchema: external_exports.object(docsToolInputShape).strict(),
|
|
33648
|
+
outputSchema: docsToolOutputShape,
|
|
33649
|
+
annotations: {
|
|
33650
|
+
readOnlyHint: true,
|
|
33651
|
+
destructiveHint: false,
|
|
33652
|
+
idempotentHint: true,
|
|
33653
|
+
openWorldHint: false
|
|
33654
|
+
}
|
|
33655
|
+
},
|
|
33656
|
+
({ topic = "index" }) => {
|
|
33657
|
+
const document2 = authoringDocTopic(topic);
|
|
33658
|
+
const structuredContent = docsResult(document2);
|
|
33659
|
+
return {
|
|
33660
|
+
structuredContent,
|
|
33661
|
+
content: [
|
|
33662
|
+
{
|
|
33663
|
+
type: "resource",
|
|
33664
|
+
resource: {
|
|
33665
|
+
uri: document2.uri,
|
|
33666
|
+
mimeType: AUTHORING_DOC_MIME_TYPE,
|
|
33667
|
+
text: document2.text
|
|
33668
|
+
}
|
|
33669
|
+
}
|
|
33670
|
+
],
|
|
33671
|
+
isError: false
|
|
33672
|
+
};
|
|
33673
|
+
}
|
|
33674
|
+
);
|
|
33675
|
+
}
|
|
33676
|
+
|
|
33269
33677
|
// ../mcp-server/src/repl-tool.ts
|
|
33270
33678
|
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
33271
33679
|
var DEFAULT_REPL_EVAL_BOUND_MS = 6e4;
|
|
@@ -33447,7 +33855,7 @@ function registerReplTool(mcp, options) {
|
|
|
33447
33855
|
mcp.registerTool(
|
|
33448
33856
|
"repl",
|
|
33449
33857
|
{
|
|
33450
|
-
description: 'A persistent QuickJS-in-WASM JavaScript VM you drive interactively to orchestrate subagents \u2014 one VM per projectDir, addressed by the same project model as the workflow tool. Two actions: eval runs code and holds the call open pumping settlements; interrupt cancels one subagent call (by id) or breaks the running eval (no id). Named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) PERSIST in the VM between calls \u2014 a later eval sees the same variables and awaits the same promises. Console logging produces output text only and creates no persistent value; nothing lives in the transcript. Inside code (JavaScript; top-level await is allowed, top-level return is a syntax error; console output is captured) the host bridge provides agent(modelSpec, task, opts?) \u2192 Promise: spawn an ACP subagent on a registry built-in (currently Claude, Codex, OpenCode, and pi) or a registered custom agent. The spec is "backend/model" (a bare "backend" runs its default model); an unknown backend rejects the call immediately, naming the known backends. The opts keys are schema (a structured-output JSON schema, validated per call), cwd, configOptions (backend-specific knobs, validated at admission), and mode
|
|
33858
|
+
description: 'A persistent QuickJS-in-WASM JavaScript VM you drive interactively to orchestrate subagents \u2014 one VM per projectDir, addressed by the same project model as the workflow tool. Two actions: eval runs code and holds the call open pumping settlements; interrupt cancels one subagent call (by id) or breaks the running eval (no id). Named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) PERSIST in the VM between calls \u2014 a later eval sees the same variables and awaits the same promises. Console logging produces output text only and creates no persistent value; nothing lives in the transcript. For deeper syntax and examples, read docs topic repl/quickstart and then one related repl/* topic. Inside code (JavaScript; top-level await is allowed, top-level return is a syntax error; console output is captured) the host bridge provides agent(modelSpec, task, opts?) \u2192 Promise: spawn an ACP subagent on a registry built-in (currently Claude, Codex, OpenCode, and pi) or a registered custom agent. The spec is "backend/model" (a bare "backend" runs its default model); an unknown backend rejects the call immediately, naming the known backends. The opts keys are schema (a structured-output JSON schema, validated per call), cwd, configOptions (backend-specific knobs, validated at admission), and mode. Before setting mode, use workflow action:"config" for that exact modelSpec and copy only an id explicitly listed in modes.availableModes; modes:null means omit mode, never infer "default". Unknown option keys reject synchronously. agent() returns a persistent promise-handle. Assign the handle before awaiting it: `const a = agent("codex", "inspect the failure"); const first = await a`. a.steer(text) targets only the currently running turn. It never starts or queues another turn and resolves "injected", "idle", or "unsupported"; transport and protocol failures reject. Steering while idle returns "idle" and loses the instruction by design. `const q = a.queue(text)` creates a distinct FIFO turn on the same session. q.id is available immediately, await q returns that turn\'s answer, and q.cancel() or an out-of-band interrupt of q.id cancels that exact turn. Queueing works on every backend that can continue the session; steering requires the ACP server\'s raw steering advertisement. Do not write `const a = await agent(...)` when you intend to reuse the handle, because that stores only the answer. Persistent-workspace example \u2014 first eval: `const a = agent("codex", "Investigate the parser failure")`; a later eval, only while agents() reports a\'s turn as running: `const steering = await a.steer("Focus on the parser state machine")`; after the founding answer settles: `const first = await a; const q1 = a.queue("Implement the fix"); const q2 = a.queue("Run the focused tests"); console.log(q1.id, q2.id, steering); const fixed = await q1; const tested = await q2`. checkpoint(question) parks a promise for a human answer, resolved by checkpoint.answer(id, value) in a later eval. parallel, pipeline, verify, judgePanel, gate, retry, loopUntilDry, and sleep(ms) round out the guest library. Introspection is in-band: workspace() returns { bindings, inFlight, checkpoints, diagnostics }; agents() lists live agents with their call ids and states; reset() tears the workspace down. `_` holds the previous eval\'s completion value. No fs, no net, no timers beyond sleep. Subagents (6 concurrent per workspace) take stable ids c1, c2, \u2026 used by interrupt and reported by agents(). eval { code } runs the code, then HOLDS THE CALL OPEN pumping settlements up to a soft bound (default 60 000 ms; per-call timeoutMs override; hard cap 120 000 ms). If everything the code waits on settles within the bound the result is the finished shape { output, result? } \u2014 output is ONE newline-joined string (console lines, checkpoint lines like "checkpoint c9: <question>", error renderings), result the completion value\'s repr. If the bound elapses first the result is the still-running shape { output, running: [call ids] } and the eval continues server-side \u2014 any later eval drains what settled, and eval with "" (the empty script) is the documented idempotent poll: it re-executes nothing, only reports. State survives MCP-session churn and daemon restarts: every eval and every settlement drain that changed state persists the workspace to the daemon\'s per-project repl store, and the first touch of a stored workspace restores it and reconciles every outstanding call. A stored snapshot that refuses (corrupt, a format upgrade, a wasm-binary mismatch) AUTO-RESETS \u2014 the file is renamed aside, never deleted, and the next eval\'s output leads with a notice naming the file and reason. Reconcile reports and drain errors live in workspace().diagnostics. On last-client disconnect the workspace drains in-flight subagent turns to completion and closes idle children; the next eligible queued turn re-attaches its founding session lazily. Subagent output passes through UNFILTERED \u2014 backend harness noise (e.g. codex\'s "Warning: Skill descriptions were shortened\u2026") is forwarded verbatim, never curated away. Every result carries the machine-readable shape (see the output schema) as structuredContent alongside the human text.',
|
|
33451
33859
|
// STRICT at the wire too: the MCP SDK strips unknown keys from a
|
|
33452
33860
|
// non-strict object schema before the handler runs, so a deleted
|
|
33453
33861
|
// surface like `refs` would be silently discarded instead of
|
|
@@ -33950,6 +34358,7 @@ function projectHarnessOptions(harnesses) {
|
|
|
33950
34358
|
model: harness.model,
|
|
33951
34359
|
probed: harness.probed,
|
|
33952
34360
|
error: harness.error,
|
|
34361
|
+
modes: harness.modes,
|
|
33953
34362
|
options: options.slice(0, MAX_OPTIONS_PER_HARNESS),
|
|
33954
34363
|
omittedOptions: Math.max(0, options.length - MAX_OPTIONS_PER_HARNESS)
|
|
33955
34364
|
});
|
|
@@ -34259,6 +34668,7 @@ var WorkflowScriptResources = class {
|
|
|
34259
34668
|
});
|
|
34260
34669
|
this.mcp.server.setRequestHandler(SubscribeRequestSchema, (request) => {
|
|
34261
34670
|
const uri = request.params.uri;
|
|
34671
|
+
if (this.externalReaders.has(uri)) return {};
|
|
34262
34672
|
const runId = workflowRunIdFromScriptUri(uri);
|
|
34263
34673
|
if (runId) {
|
|
34264
34674
|
if (!this.loadState(runId)) resourceNotFound(uri);
|
|
@@ -34273,6 +34683,7 @@ var WorkflowScriptResources = class {
|
|
|
34273
34683
|
});
|
|
34274
34684
|
this.mcp.server.setRequestHandler(UnsubscribeRequestSchema, (request) => {
|
|
34275
34685
|
const uri = request.params.uri;
|
|
34686
|
+
if (this.externalReaders.has(uri)) return {};
|
|
34276
34687
|
const runId = workflowRunIdFromScriptUri(uri);
|
|
34277
34688
|
if (runId) {
|
|
34278
34689
|
if (!this.loadState(runId) && !this.loadTombstone(runId) && !this.deletedRunIds.has(runId) && !this.subscriptions.has(uri)) resourceNotFound(uri);
|
|
@@ -34462,11 +34873,12 @@ var WorkflowScriptResources = class {
|
|
|
34462
34873
|
// ../mcp-server/src/server.ts
|
|
34463
34874
|
var SERVER_NAME = "agentprism-workflow";
|
|
34464
34875
|
var require2 = createRequire(import.meta.url);
|
|
34465
|
-
var SERVER_VERSION = true ? "0.
|
|
34876
|
+
var SERVER_VERSION = true ? "0.33.0" : require2("../package.json").version;
|
|
34466
34877
|
var SERVER_INSTRUCTIONS = [
|
|
34467
|
-
"This server exposes
|
|
34468
|
-
'\u2022
|
|
34469
|
-
'\u2022
|
|
34878
|
+
"This server exposes three model-facing tools for authoring and orchestrating multi-agent work. workflow and repl spawn subagents over the same ACP backends \u2014 the registry built-ins Claude, Codex, OpenCode, and pi, plus any registered custom agents \u2014 and key their durable state by an absolute projectDir (required on the shared daemon; defaults to the server's own project in single-project mode). Backend credentials come from each agent's own login (claude, codex, opencode, pi), so there is nothing auth-shaped to configure here.",
|
|
34879
|
+
'\u2022 docs \u2014 SELECTIVE VERSION-MATCHED REFERENCE. Omit topic or use topic:"index" for the bounded catalog, then read exactly one workflow/* or repl/* topic. It embeds the selected text/markdown resource, runs no code, opens no backend, and needs no projectDir. Use it when the compact tool descriptions do not contain enough syntax or lifecycle detail.',
|
|
34880
|
+
'\u2022 workflow \u2014 DETERMINISTIC BATCH orchestration. Supply a JavaScript workflow script (inline or by absolute scriptPath) that fans out agent() subagents and optional checkpoint() gates; it runs to completion in the foreground, or background:true returns a durable runId for bounded action:"await"/"inspect"/"stop" calls, with journaling, replay, and resumeFromRunId. Reach for it when the orchestration is known up front and you want it repeatable and resumable. action:"config" discovers the live backend/model option catalog without starting a run, and every run is statically checked, mock-executed, and config-probed before admission. Read docs topic workflow/quickstart first when authoring is unfamiliar.',
|
|
34881
|
+
'\u2022 repl \u2014 INTERACTIVE STATEFUL orchestration. A persistent per-project JavaScript VM you drive incrementally with action:"eval"; named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) persist between calls and survive daemon restarts. Console logging produces output text only and creates no persistent value. Reach for it when you want to inspect intermediate results and decide the next step adaptively, or keep a human in the loop via checkpoint(). Read docs topic repl/quickstart first when the persistent handle API is unfamiliar.',
|
|
34470
34882
|
"Rule of thumb: use workflow when you can script the whole plan ahead of time; use repl when you want a live, stateful session that evolves call by call."
|
|
34471
34883
|
].join("\n\n");
|
|
34472
34884
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["paused", "completed", "failed", "aborted"]);
|
|
@@ -35279,6 +35691,9 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35279
35691
|
const projects = options.projects ?? new WorkflowProjectRegistry(runner);
|
|
35280
35692
|
const defaultContext = requireProjectDir ? void 0 : projects.adopt(options.manager ?? new WorkflowManager3({ agent: runner }), options.backgroundRuns);
|
|
35281
35693
|
const scriptResources = new WorkflowScriptResources(mcp, { router: projects });
|
|
35694
|
+
registerAuthoringDocs(mcp, {
|
|
35695
|
+
registerResourceReader: (uri, read) => scriptResources.registerExternalResourceReader(uri, read)
|
|
35696
|
+
});
|
|
35282
35697
|
const backendApprovals = /* @__PURE__ */ new Set();
|
|
35283
35698
|
const replPresence = options.replPresence ?? new ReplPresenceLedger(options.replDrainBoundMs ?? REPL_DRAIN_BOUND_MS);
|
|
35284
35699
|
const resolveContext2 = (input) => {
|
|
@@ -35309,7 +35724,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35309
35724
|
});
|
|
35310
35725
|
const workflowToolConfig = {
|
|
35311
35726
|
title: "Discover, validate, run, inspect, await, stop, or narrow-cancel an agent workflow",
|
|
35312
|
-
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results.
|
|
35727
|
+
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results. The only agent option keys are label, phase, model, tier, mode, configOptions, schema, cwd, timeoutMs, retries, isolation:"worktree", resume, agentType, mcpServers, images, meta, promptMeta, and keepSession; unknown keys reject before admission. Every parallel entry must be a thunk: parallel([() => agent(...), () => agent(...)]). For deeper syntax, read docs topic workflow/quickstart and then one related workflow/* topic. Minimal script: `export const meta = { name: "review", description: "Review a target", phases: [{ title: "Review" }] }; phase("Review"); const report = await agent("Review " + args.target, { label: "review" }); return { report };`. Omit model for the server default, or use a backend name alone to preserve that backend\'s configured default. Before choosing a pinned model, mode, or configOptions, call action:"config" with projectDir and optional harnesses/modelFilter; after choosing a model, pass modelSpecs to read its model-specific options. Set mode only when that selected harness entry\'s modes.availableModes explicitly lists the exact id; modes:null means unsupported, so omit mode\u2014never infer a default from an absent value. Config opens no-prompt sessions, spends zero tokens, and starts no workflow. action:"run" automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded diagnostics with status:"rejected" and create no run ID, reserve no background slot, and spend no tokens. Run, resume, inspect, await, or stop an admitted workflow through the same tool. The script orchestrates agent() subagents (and optional checkpoint() gates) over registry built-ins\u2014currently Claude, Codex, OpenCode, and pi\u2014ACP backends, plus registered custom agents. Supply exactly one of inline script or absolute scriptPath; path content is read once and snapshotted at admission. ' + (requireProjectDir ? "config and run REQUIRE projectDir (absolute): it is the discovery cwd and selects the project-scoped run store/default execution cwd. " : "run optionally takes projectDir (absolute) to select the project-scoped run store; default is this server's own project. ") + 'inspect/await/stop take only a runId \u2014 it locates its project store automatically. Foreground is the default and streams progress; background:true returns a durable runId for bounded action:"await" calls. run and await honor _meta.progressToken with notifications/progress while they block. Pass resumeFromRunId to execute a new run from a prior journal prefix. In hosts that render MCP Apps, every call of this tool shows a live self-updating run-monitor panel and the panel reports phase starts, pauses, and terminal outcomes on its own \u2014 do NOT poll action:"inspect" to check on a run there; prefer a single bounded action:"await". Use action:"inspect" with a runId when you need machine-readable status data: a safe bounded status, log tail, and attributed call previews. Use action:"stop" to durably abort a live run; add callIndex to cancel only that in-flight agent and keep the run live. labelGlob remains an output filter in both forms. A whole-run stop returns the final run fate; resume is safe immediately, and only agent-session wind-down can remain asynchronous. Every admitted script is readable at workflow://runs/{runId}/script and results include resource links. Background runs are tracked per project, capped at four active/starting runs, and use headless checkpoint semantics; checkpointReplies continue a checkpoint pause in a new run.',
|
|
35313
35728
|
inputSchema: workflowToolInputShape,
|
|
35314
35729
|
outputSchema: workflowToolOutputShape,
|
|
35315
35730
|
annotations: void 0
|
|
@@ -40205,11 +40620,16 @@ if (isProcessEntryPoint2()) {
|
|
|
40205
40620
|
});
|
|
40206
40621
|
}
|
|
40207
40622
|
export {
|
|
40623
|
+
AUTHORING_DOCS_SCHEMA_VERSION,
|
|
40624
|
+
AUTHORING_DOC_MIME_TYPE,
|
|
40625
|
+
AUTHORING_DOC_TOPICS,
|
|
40626
|
+
AUTHORING_DOC_TOPIC_IDS,
|
|
40208
40627
|
AUTHORING_PROMPT_NAME,
|
|
40209
40628
|
BackgroundRunRegistry,
|
|
40210
40629
|
BoundedEventStore,
|
|
40211
40630
|
DAEMON_NAME,
|
|
40212
40631
|
DEFAULT_DAEMON_PORT,
|
|
40632
|
+
DOCS_TOOL_NAME,
|
|
40213
40633
|
DaemonPortInUseError,
|
|
40214
40634
|
EVENTS_RESOURCE_MIME_TYPE,
|
|
40215
40635
|
MAX_BACKGROUND_RUNS,
|
|
@@ -40222,6 +40642,8 @@ export {
|
|
|
40222
40642
|
WORKFLOW_EVENTS_TOOL_NAME,
|
|
40223
40643
|
WORKFLOW_RUN_EVENTS_SCHEMA_VERSION,
|
|
40224
40644
|
WorkflowProjectRegistry,
|
|
40645
|
+
authoringDocResource,
|
|
40646
|
+
authoringDocTopic,
|
|
40225
40647
|
buildAuthoringPromptText,
|
|
40226
40648
|
clampWorkflowInput,
|
|
40227
40649
|
createDaemon,
|
|
@@ -40231,6 +40653,8 @@ export {
|
|
|
40231
40653
|
dispatch,
|
|
40232
40654
|
disposeReplProjectState,
|
|
40233
40655
|
disposeRunnerWithDeadline,
|
|
40656
|
+
docsToolInputShape,
|
|
40657
|
+
docsToolOutputShape,
|
|
40234
40658
|
ensureDaemonRunning,
|
|
40235
40659
|
ensureReplWorkspace,
|
|
40236
40660
|
envFingerprint,
|
|
@@ -40240,6 +40664,7 @@ export {
|
|
|
40240
40664
|
parseWorkflowToolInput,
|
|
40241
40665
|
probeHealthz,
|
|
40242
40666
|
readDaemonInfo,
|
|
40667
|
+
registerAuthoringDocs,
|
|
40243
40668
|
registerAuthoringPrompt,
|
|
40244
40669
|
registerWorkflowAppUi,
|
|
40245
40670
|
renameAsideNeverOverwriting,
|