@fyeeme/pi-dynamic-workflows 0.1.1
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/LICENSE +21 -0
- package/README.md +373 -0
- package/README.zh-CN.md +359 -0
- package/index.ts +650 -0
- package/package.json +58 -0
- package/sessions/spawn.ts +15 -0
- package/src/agent/dispatch.ts +76 -0
- package/src/budget/caps.ts +42 -0
- package/src/budget/index.ts +8 -0
- package/src/budget/pool.ts +118 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/journal.ts +184 -0
- package/src/cache/key.ts +97 -0
- package/src/determinism/ast-guard.ts +196 -0
- package/src/errors.ts +55 -0
- package/src/format.ts +27 -0
- package/src/index.ts +28 -0
- package/src/inspect.ts +237 -0
- package/src/lifecycle.ts +75 -0
- package/src/loader.ts +50 -0
- package/src/outcomes.ts +113 -0
- package/src/planner.ts +66 -0
- package/src/runner/index.ts +188 -0
- package/src/runner/stage-executor.ts +1078 -0
- package/src/state/index.ts +1 -0
- package/src/state/names.ts +33 -0
- package/src/types.ts +332 -0
- package/src/ui-groups.ts +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fyeeme
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# @fyeeme/pi-dynamic-workflows
|
|
2
|
+
|
|
3
|
+
**Deterministic TypeScript workflow orchestration for [pi](https://github.com/earendil-works/pi-mono).**
|
|
4
|
+
|
|
5
|
+
Define a workflow as a declarative list of typed steps, run it, and get resumable, budget-bounded, abortable execution. Fuses the pi-dynamic-workflows design (10 step primitives + heuristic planner + outcome collectors) with Claude Code's workflow-engine coordination mechanisms (deterministic sandbox, cache-key resume, per-agent abort, dynamic budget, runaway caps).
|
|
6
|
+
|
|
7
|
+
Languages: **English** | [中文](README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
A workflow run is a list of steps (`agent` / `code` / `log` / `fan_out` / `loop_until` / `loop_until_dry` / `adversarial` / `tournament` / `classify_route` / `sub_workflow`). The engine guarantees:
|
|
14
|
+
|
|
15
|
+
- **Determinism** — workflow `.ts` files are AST-guarded against `Date.now()` / `Math.random()` / `new Date()`; run ids are a pure function of `(timestamp, sequence)`.
|
|
16
|
+
- **Resume without re-dispatch** — every agent call is keyed by `sha256(workflow + prompt + signature)` and journaled; re-running the same workflow replays cached agents (zero subprocess spawns).
|
|
17
|
+
- **Per-agent abort** — each in-flight agent owns an `AbortController`; `skipAgent`/`retryAgent` targets one call without disturbing siblings.
|
|
18
|
+
- **Budget + runaway caps** — `maxAgents` / `maxTokens` enforced via a live pool; `MAX_BATCH=4096`, `MAX_LIFETIME_AGENTS=1000` throw `BudgetExceededError` on exceed (no silent truncation).
|
|
19
|
+
- **Testable without `pi`** — the agent dispatch is injectable; tests pass a fake and run with no binary, no provider API, no tokens.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
This is a pi extension package (workspace / local), not yet published to npm. From a pi workspace:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install --ignore-scripts # hydrate (the package is a workspace dep)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This resolves [`@fyeeme/pi-subagent-core`](https://www.npmjs.com/package/@fyeeme/pi-subagent-core)
|
|
32
|
+
(`^0.3.0`, from the npm registry — no sibling-repo layout requirement).
|
|
33
|
+
|
|
34
|
+
Then import the public API from the package root module (a TypeScript barrel; the package ships `.ts` source):
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { defineWorkflow, runWorkflow } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> The package's `pi.extensions` entry (`./index.ts`) registers the `run_workflow`
|
|
41
|
+
> tool and the `/wf-inspect` command. The engine is also fully usable via the
|
|
42
|
+
> imports shown here.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { defineWorkflow, runWorkflow } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
50
|
+
|
|
51
|
+
const wf = defineWorkflow({
|
|
52
|
+
name: "draft-and-refine",
|
|
53
|
+
steps: [
|
|
54
|
+
{ id: "draft", type: "agent", prompt: "Draft a one-paragraph release note." },
|
|
55
|
+
{ id: "refine", type: "agent", prompt: (ctx) => `Refine this into crisp prose:\n\n${ctx.step("draft").results}` },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await runWorkflow({ workflow: wf, cwd: process.cwd(), now: Date.now() });
|
|
60
|
+
console.log(result.status, result.steps[1].results);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`runWorkflow` spawns one `pi --mode json -p --no-session` subprocess per agent call (the default dispatch), so you need `pi` on `PATH` with a configured provider. For tests or offline runs, inject a fake dispatch (see the tutorial).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Tutorial
|
|
68
|
+
|
|
69
|
+
### 1. Define a workflow
|
|
70
|
+
|
|
71
|
+
`defineWorkflow` is a typed identity helper — it gives you full checking on the `steps` discriminated union.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const wf = defineWorkflow({
|
|
75
|
+
name: "research",
|
|
76
|
+
budget: { maxAgents: 10, maxTokens: 50_000 },
|
|
77
|
+
steps: [
|
|
78
|
+
{ id: "gather", type: "agent", prompt: "List 3 sources on topic X." },
|
|
79
|
+
{ id: "summarize", type: "agent", prompt: (ctx) => `Summarize:\n${ctx.step("gather").results}` },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`ctx.input` is the run's initial input; `ctx.step(id)` returns a prior step's `{ results, stats }` (throws if that id hasn't executed yet).
|
|
85
|
+
|
|
86
|
+
### 2. Run it
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const result = await runWorkflow({
|
|
90
|
+
workflow: wf,
|
|
91
|
+
cwd: process.cwd(),
|
|
92
|
+
now: 1700000000000, // deterministic inception time (ms); also the journal/run-id seed
|
|
93
|
+
input: "topic X",
|
|
94
|
+
});
|
|
95
|
+
// result.status: "completed" | "failed" | "aborted"
|
|
96
|
+
// result.steps: StepResult[] (one per executed step, in order)
|
|
97
|
+
// result.stats: aggregated { tokens, cost, durationMs, agents, failures }
|
|
98
|
+
// result.journalFile: path to the per-workflow JSONL journal
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`now` is **required and deterministic** — pass the run's inception time; the engine never reads the clock for identity. The same `(workflow, prompts)` always produces the same cache keys.
|
|
102
|
+
|
|
103
|
+
### 3. fan_out — parallel agents + merge
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const wf = defineWorkflow({
|
|
107
|
+
name: "parallel-research",
|
|
108
|
+
steps: [
|
|
109
|
+
{
|
|
110
|
+
id: "fan",
|
|
111
|
+
type: "fan_out",
|
|
112
|
+
over: () => ["alpha", "beta", "gamma"],
|
|
113
|
+
agent: (topic) => ({ prompt: `Research ${topic}.` }),
|
|
114
|
+
parallelism: 3,
|
|
115
|
+
merge: (results) => results.join("\n---\n"),
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`fan_out` pre-checks the whole batch fits the budget (`MAX_BATCH=4096`); each item is cache-keyed and abortable independently.
|
|
122
|
+
|
|
123
|
+
### 4. loop_until — iterate until a condition / budget
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const wf = defineWorkflow({
|
|
127
|
+
name: "refine-loop",
|
|
128
|
+
steps: [
|
|
129
|
+
{
|
|
130
|
+
id: "loop",
|
|
131
|
+
type: "loop_until",
|
|
132
|
+
prompt: (ctx, i) => `Draft ${i + 1}. Current:\n${ctx.step("loop")?.results ?? ctx.input}`,
|
|
133
|
+
until: (ctx, i) => i >= 3,
|
|
134
|
+
maxIterations: 5,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each iteration is its own cache-keyed agent call; `maxIterations` + the budget bound the loop.
|
|
141
|
+
|
|
142
|
+
### 5. Composites — adversarial / tournament / classify_route
|
|
143
|
+
|
|
144
|
+
Built on the same `dispatchAgentCall`, so they get cache-resume, budget, and abort for free.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Produce a candidate, then N judges grade it against a rubric and tally.
|
|
148
|
+
defineWorkflow({
|
|
149
|
+
name: "review",
|
|
150
|
+
steps: [
|
|
151
|
+
{
|
|
152
|
+
id: "adv",
|
|
153
|
+
type: "adversarial",
|
|
154
|
+
produce: { prompt: "Write the function." },
|
|
155
|
+
rubric: ["correctness", "handles empty input", "no off-by-one"],
|
|
156
|
+
judges: 3, // default; minPass defaults to majority
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
// results: { candidate, passed, passCount, minPass, judges: [{pass, reason}] }
|
|
161
|
+
|
|
162
|
+
// N distinct candidates, M judges rank them, pick the majority winner.
|
|
163
|
+
defineWorkflow({
|
|
164
|
+
name: "pick",
|
|
165
|
+
steps: [{ id: "tmt", type: "tournament", candidates: 3, judges: 2, produce: { prompt: "Solve X." } }],
|
|
166
|
+
});
|
|
167
|
+
// results: { candidates, winner, judges: [{winner, reason}] }
|
|
168
|
+
|
|
169
|
+
// Classify input, then run the matching route's sub-steps.
|
|
170
|
+
defineWorkflow({
|
|
171
|
+
name: "route",
|
|
172
|
+
steps: [
|
|
173
|
+
{
|
|
174
|
+
id: "cr",
|
|
175
|
+
type: "classify_route",
|
|
176
|
+
classifier: { prompt: (ctx) => `Classify intent: ${ctx.input}` },
|
|
177
|
+
routes: {
|
|
178
|
+
bug: [{ id: "file", type: "agent", prompt: "File a bug report." }],
|
|
179
|
+
faq: [{ id: "answer", type: "agent", prompt: "Answer the FAQ." }],
|
|
180
|
+
},
|
|
181
|
+
fallback: [{ id: "escalate", type: "agent", prompt: "Escalate to a human." }],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
// results: { category, matched, route: StepResult[], routeStatus }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Judge/classifier JSON is parsed leniently (LLMs return `"true"`/`"0"` as strings); route nesting is depth-capped to catch cycles.
|
|
189
|
+
|
|
190
|
+
### 6. Resume — re-run dispatches nothing
|
|
191
|
+
|
|
192
|
+
The journal lives at `<cwd>/.pi/workflows/<workflow.name>/journal.jsonl` (per-workflow, so the **same workflow re-run hits the cache across runs**; run id is excluded from the key):
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const first = await runWorkflow({ workflow: wf, cwd, now: T0 }); // dispatches every agent
|
|
196
|
+
const second = await runWorkflow({ workflow: wf, cwd, now: T1 }); // dispatches ZERO agents — all cached
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A changed prompt changes its cache key → that agent re-dispatches; unchanged ones replay.
|
|
200
|
+
|
|
201
|
+
### 7. Per-agent abort & skip
|
|
202
|
+
|
|
203
|
+
The runner owns an `AgentSpawnRegistry`. Grab it and target one in-flight call:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { createSpawnRegistry, skipAgent } from "@fyeeme/pi-dynamic-workflows/sessions/spawn.ts";
|
|
207
|
+
|
|
208
|
+
const registry = createSpawnRegistry();
|
|
209
|
+
const runP = runWorkflow({ workflow: fanOutWf, cwd, now, registry });
|
|
210
|
+
// ...once `fan#2` is in flight:
|
|
211
|
+
skipAgent(registry, "fan#2"); // aborts only that call; siblings keep running
|
|
212
|
+
const result = await runP; // status "completed" — the batch finished without item 2
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`abortAgent(registry, callId)` aborts one call and fires `onAgentSkip`; `retryAgent(registry, callId)` aborts one call and fires `onAgentRetry`. **No automatic re-dispatch is wired yet** — the aborted call settles as skipped/failed and the run continues/fails per the step's normal settle semantics; `retryAgent` is a notify-and-abort primitive for external controllers today. Call ids are `${step.id}#${n}` (1-based).
|
|
216
|
+
|
|
217
|
+
### 8. Budget enforcement
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const wf = defineWorkflow({
|
|
221
|
+
name: "capped",
|
|
222
|
+
budget: { maxAgents: 2 },
|
|
223
|
+
steps: [{ id: "fan", type: "fan_out", over: () => [1, 2, 3], agent: (i) => ({ prompt: `${i}` }) }],
|
|
224
|
+
});
|
|
225
|
+
const result = await runWorkflow({ workflow: wf, cwd, now });
|
|
226
|
+
// result.status === "failed", result.error matches /budget|exhausted/i
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`maxAgents` is checked before a batch/agent spawns; `maxTokens` is enforced as agents settle (`BudgetPool.isExhausted`). Both throw `BudgetExceededError` — never silently truncate.
|
|
230
|
+
|
|
231
|
+
### 9. Outcome collectors
|
|
232
|
+
|
|
233
|
+
Extract a structured value from an agent's text output:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { collect } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
237
|
+
|
|
238
|
+
const urls = collect<string[]>({ kind: "url" }, result.steps[0].results as string);
|
|
239
|
+
const json = collect({ kind: "json" }, agentText); // first balanced JSON value
|
|
240
|
+
const paths = collect<string[]>({ kind: "file_path" }, agentText);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`url` / `file_path` / `json` are pure functions of text — apply them on any `StepResult.results`.
|
|
244
|
+
|
|
245
|
+
### 10. Heuristic planner
|
|
246
|
+
|
|
247
|
+
A keyword sketch that turns a goal into a single-step workflow scaffold (compare → tournament, review → adversarial, classify → classify_route, else agent). A starting point to edit, not a real NL planner:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { heuristicallyPlan } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
251
|
+
|
|
252
|
+
const wf = heuristicallyPlan("compare three sorting approaches", { judges: 3 });
|
|
253
|
+
// wf.steps[0].type === "tournament"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 11. Load a `.ts` workflow file
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { loadWorkflowModule } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
260
|
+
|
|
261
|
+
const mod = await loadWorkflowModule<{ workflow: ReturnType<typeof defineWorkflow> }>({
|
|
262
|
+
filePath: "./my-workflow.ts",
|
|
263
|
+
});
|
|
264
|
+
const wf = mod.workflow;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The loader runs the deterministic AST guard **before** jiti-imports the file — a workflow body that calls `Date.now()` / `Math.random()` / `new Date()` is rejected at load time (those would destabilize cache keys). Note: the guard scans the entry file only; keep workflows single-file or guard imported helpers separately.
|
|
268
|
+
|
|
269
|
+
### 12. Test without `pi`
|
|
270
|
+
|
|
271
|
+
Inject a fake dispatch — no binary, no provider, no tokens. This is exactly how the package's own 108 tests work:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { runWorkflow } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
275
|
+
import type { AgentDispatch } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
276
|
+
|
|
277
|
+
const fake: AgentDispatch = async (_registry, opts) => ({
|
|
278
|
+
callId: opts.callId,
|
|
279
|
+
exitCode: 0,
|
|
280
|
+
messages: [{ role: "assistant", content: [{ type: "text", text: `out:${opts.task}` }], /* ...rest */ } as never],
|
|
281
|
+
stderr: "",
|
|
282
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 15, turns: 1 },
|
|
283
|
+
model: "fake",
|
|
284
|
+
stopReason: "stop",
|
|
285
|
+
aborted: false,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const result = await runWorkflow({ workflow: wf, cwd: tempDir, now: 1000, dispatch: fake });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Step types reference
|
|
294
|
+
|
|
295
|
+
| type | payload highlights | result |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `agent` | `prompt: string \| (ctx)=>string`, `model?`, `tools?`, `systemPrompt?` | final assistant text |
|
|
298
|
+
| `code` | `transform: (ctx) => unknown` (pure, no dispatch, not cached) | the transform's return value |
|
|
299
|
+
| `log` | `message: string \| (ctx)=>string` (narrative, zero dispatch/tokens) | the message (fires `onLog`) |
|
|
300
|
+
| `fan_out` | `over()`, `agent(item,i)`, `parallelism?`, `merge?` | merged array (or `merge` output) |
|
|
301
|
+
| `loop_until` | `prompt(ctx,i)`, `until(ctx,i)`, `maxIterations?` | array of per-iteration outputs |
|
|
302
|
+
| `adversarial` | `produce`, `rubric[]`, `judges?`, `minPass?` | `{ candidate, passed, passCount, judges }` |
|
|
303
|
+
| `tournament` | `candidates`, `judges`, `produce` | `{ candidates, winner, judges }` |
|
|
304
|
+
| `classify_route` | `classifier`, `routes: Record<cat, Step[]>`, `fallback?` | `{ category, matched, route, routeStatus }` |
|
|
305
|
+
| `sub_workflow` | `workflow: WorkflowDefinition`, `input?`, `inheritBudget?` | `{ steps, status, workflowName, error }` |
|
|
306
|
+
| `loop_until_dry` | `agent(item, i)`, `keyOf?`, `merge?`, `maxRounds?`, `dryThreshold?` | array of discovered items |
|
|
307
|
+
|
|
308
|
+
Every step accepts `id`, `retry?: { maxRetries }`, and
|
|
309
|
+
`onBudgetExhaust?: "throw" | "null"` — under `"null"`, a step whose budget
|
|
310
|
+
runs out returns `null` (degraded) instead of aborting the run; the run result
|
|
311
|
+
reports degraded steps in `degradedSteps`. Default `"throw"` preserves the
|
|
312
|
+
fail-fast guarantee.
|
|
313
|
+
|
|
314
|
+
**Template strictness** — string prompts are filled in a single left-to-right
|
|
315
|
+
pass over `{{input}}` / `{{item}}` / `{{step.<id>}}`. Any other `{{...}}` (or an
|
|
316
|
+
out-of-context token, e.g. `{{item}}` in a non-fan_out prompt) raises a
|
|
317
|
+
`compile`-category error that aborts the run: a literal `{{...}}` in a prompt
|
|
318
|
+
(e.g. mustache/jinja examples) must be avoided or the prompt text adjusted, it
|
|
319
|
+
is no longer passed through verbatim (0.1.0 behavior).
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## API reference
|
|
324
|
+
|
|
325
|
+
### `runWorkflow(opts)` → `Promise<RunResult>`
|
|
326
|
+
|
|
327
|
+
| option | | |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `workflow` | `WorkflowDefinition` | required |
|
|
330
|
+
| `cwd` | `string` | required (journal base) |
|
|
331
|
+
| `now` | `number` | required — deterministic inception ms |
|
|
332
|
+
| `input?` | `unknown` | `ctx.input` |
|
|
333
|
+
| `budget?` | `Budget` | overrides `workflow.budget` |
|
|
334
|
+
| `signal?` | `AbortSignal` | run-wide abort |
|
|
335
|
+
| `listeners?` | `AgentLifecycleListeners` | `onAgentStart/End/Skip/Retry/CacheHit` + `onLog` (log steps) + `onUpdate` (streamed deltas) |
|
|
336
|
+
| `dispatch?` | `AgentDispatch` | default = real `spawnAgent` |
|
|
337
|
+
| `maxPromptBytes?` | `number` | A6 size guard — resolved task prompt **and** effective systemPrompt over this byte size throw a `size-limit` error before spawn (default 256 KB) |
|
|
338
|
+
| `policyGate?` | `(wf) => { allow, reason? }` | A6 gate — return `{ allow: false }` to abort the run before any dispatch |
|
|
339
|
+
| `registry?` | `AgentSpawnRegistry` | for external `skipAgent`/`abortAgent` |
|
|
340
|
+
| `journalDir?` | `string` | default `<cwd>/.pi/workflows/<name>` |
|
|
341
|
+
| `sequence?` | `number` | run-id disambiguator |
|
|
342
|
+
|
|
343
|
+
`RunResult = { runId, status, steps: StepResult[], stats: StepStats, journalFile?, error?, errorCategory?, degradedSteps? }`.
|
|
344
|
+
|
|
345
|
+
### Also exported
|
|
346
|
+
`defineWorkflow`, `loadWorkflowModule`, `collect` (+ `urlCollector`/`filePathCollector`/`jsonCollector`/`parseFirstJson`), `heuristicallyPlan`, `createSpawnRegistry`/`abortAgent`/`skipAgent`/`retryAgent` (from `sessions/spawn.ts`), and all step/result/context types.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Design — Claude Code fusion
|
|
351
|
+
|
|
352
|
+
| mechanism | module | what it does |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| deterministic sandbox | `src/determinism/ast-guard.ts` | AST-bans non-deterministic APIs in workflow source |
|
|
355
|
+
| deterministic run id | `src/state/names.ts` | `generateRunId({timestamp, sequence})` is pure |
|
|
356
|
+
| cache-key resume | `src/cache/{key,journal}.ts` | `sha256(workflow+prompt+signature)` + per-run JSONL journal |
|
|
357
|
+
| per-agent abort | `sessions/spawn.ts` | `Map<callId, ChildProcess>` + per-call `AbortController`; abort → SIGTERM on one process |
|
|
358
|
+
| budget + caps | `src/budget/{pool,caps}.ts` | live `BudgetPool` + `MAX_BATCH`/`MAX_LIFETIME_AGENTS` |
|
|
359
|
+
|
|
360
|
+
The three paradigm conflicts (imperative CC ↔ declarative graph) are resolved: the budget loop becomes a pre-check value fan_out reads; in-process AbortControllers become a subprocess map; the vm sandbox becomes a load-time AST guard over jiti-loaded source.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Testing
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
node_modules/.bin/tsc -p packages/extensions/pi-dynamic-workflows/tsconfig.json --noEmit # typecheck
|
|
368
|
+
node_modules/.bin/vitest --run packages/extensions/pi-dynamic-workflows # 108 tests
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
A real-`pi` subprocess smoke (default dispatch) lives at `examples/smoke-real-pi.ts` — run it manually when `pi` + a provider are configured.
|
|
372
|
+
|
|
373
|
+
License: MIT.
|