pi-workflow-engine 1.0.0 → 1.2.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.
Files changed (59) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +236 -94
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +9 -6
  3. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +10 -3
  4. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
  5. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +71 -23
  6. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +12 -3
  7. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +51 -8
  8. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +14 -0
  9. package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
  10. package/.pi/extensions/pi-workflow-engine/src/codemode-slots.ts +73 -0
  11. package/.pi/extensions/pi-workflow-engine/src/config-file.ts +52 -0
  12. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +127 -58
  13. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -40
  14. package/.pi/extensions/pi-workflow-engine/src/engine.ts +78 -18
  15. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
  16. package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
  17. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +7 -1
  18. package/.pi/extensions/pi-workflow-engine/src/journal.ts +22 -4
  19. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +9 -47
  20. package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
  21. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
  22. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +1 -1
  23. package/.pi/extensions/pi-workflow-engine/src/progress.ts +32 -4
  24. package/.pi/extensions/pi-workflow-engine/src/prompt-sections.ts +15 -0
  25. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +8 -6
  26. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
  27. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +15 -3
  28. package/.pi/extensions/pi-workflow-engine/src/types.ts +68 -9
  29. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +1 -0
  30. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +9 -3
  31. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +69 -18
  32. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +17 -6
  33. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +8 -3
  34. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +16 -8
  35. package/.pi/extensions/pi-workflow-engine/src/usage.ts +20 -2
  36. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +123 -9
  37. package/.pi/extensions/pi-workflow-engine/src/workflow-catalog.ts +79 -0
  38. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +6 -2
  39. package/.pi/extensions/pi-workflow-engine/src/workflow-input.ts +13 -0
  40. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +21 -4
  41. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +26 -6
  42. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +2 -0
  43. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +69 -13
  44. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +23 -4
  45. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +27 -1
  46. package/.pi/extensions/pi-workflow-engine/src/workflow-run-seal.ts +83 -0
  47. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +30 -4
  48. package/.pi/extensions/pi-workflow-engine/src/workflow-script-file.ts +50 -0
  49. package/.pi/extensions/pi-workflow-engine/src/workflow-settings.ts +147 -0
  50. package/.pi/extensions/pi-workflow-engine/src/workflow-tool-request.ts +61 -0
  51. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +1 -0
  52. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +1 -0
  53. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +1 -0
  54. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +1 -0
  55. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +12 -4
  56. package/README.md +12 -4
  57. package/USAGE.md +167 -23
  58. package/package.json +13 -4
  59. package/skills/workflow-authoring/SKILL.md +160 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
6
6
  "keywords": [
@@ -29,8 +29,12 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "pi": {
32
- "extensions": [".pi/extensions/pi-workflow-engine/index.ts"],
33
- "skills": ["skills"],
32
+ "extensions": [
33
+ ".pi/extensions/pi-workflow-engine/index.ts"
34
+ ],
35
+ "skills": [
36
+ "skills"
37
+ ],
34
38
  "image": "https://raw.githubusercontent.com/timbrinded/pi-workflow-engine/master/assets/preview.png"
35
39
  },
36
40
  "scripts": {
@@ -55,7 +59,12 @@
55
59
  "typebox": "*"
56
60
  },
57
61
  "devDependencies": {
58
- "@types/node": "^22.20.0",
62
+ "@types/node": "^22.20.5",
59
63
  "typescript": "^7.0.2"
64
+ },
65
+ "allowScripts": {
66
+ "esbuild": true,
67
+ "protobufjs": true,
68
+ "@google/genai": true
60
69
  }
61
70
  }
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: workflow-authoring
3
+ description: "Reference for writing pi-workflow-engine workflow scripts: the inline script contract, the injected api (agent, parallel, pipeline, classify, budget, workflow), quality patterns, resume, and saving a reusable workflow. Read before writing or editing an inline script for the workflow tool. It does not itself authorize running a workflow."
4
+ ---
5
+
6
+ # Writing workflow scripts
7
+
8
+ Read this before you write or edit a script for the `workflow` tool. It does not grant permission to run one: call the tool only when the user opted in (the `dynamax` token, sticky `/workflow:dynamax`, an explicit request to run or author a workflow, or a command or skill instruction).
9
+
10
+ A workflow is plain code that fans work out to subagents, passes validated data between stages, and returns one result. Prefer a registered workflow (`name`) when one fits; write a `script` when none does.
11
+
12
+ ## Script contract
13
+
14
+ ```js
15
+ export const meta = {
16
+ name: "auth-audit",
17
+ description: "Audit the auth middleware for bypasses.",
18
+ phases: [{ title: "Find" }, { title: "Verify" }],
19
+ };
20
+
21
+ export default async function run(api) {
22
+ const Finding = Type.Object({ file: Type.String(), summary: Type.String() });
23
+ const findings = await api.agent(`Find auth bypasses in ${api.args || "src/"}`, {
24
+ label: "find", phase: "Find", profile: "small", schema: Type.Object({ items: Type.Array(Finding, { maxItems: 20 }) }),
25
+ });
26
+ return { summary: `${findings.items.length} candidate bypasses`, findings: findings.items };
27
+ }
28
+ ```
29
+
30
+ - The script is plain JavaScript compiled in-process: no TypeScript annotations, `as` casts, or interfaces.
31
+ - It must start with `export const meta = { ... };`, a pure literal: strings, numbers, booleans, `null`, arrays, and objects only. No variables, calls, template literals, spreads, computed keys, or raw newlines inside strings. `name` is required; `description`, `whenToUse` (the situation that should make a model pick it), and `phases` are optional. Each phase is `{ title, profile? }`.
32
+ - Exactly one `export default` follows the meta: an `async function` or `async (api) => { ... }` with a block body. Nothing may follow it but `;` and comments. Put helpers and constants inside the function body.
33
+ - No `import` statements, no dynamic `import()`, no other exports.
34
+ - `Type` (TypeBox) is an injected global: write `Type.Object(...)` directly. It is not a property of `api`. Using the injected `Type` keeps schemas on pi's bundled TypeBox, so validation works.
35
+ - The return value becomes the tool result. Return a string or an object with a short `summary` first; the result the host model sees is truncated when it is very large, so return conclusions, not raw transcripts.
36
+
37
+ ## The api
38
+
39
+ | Member | What it does |
40
+ | --- | --- |
41
+ | `agent(prompt, opts?)` | Runs one subagent session. `prompt` must be a plain string (build it with a template string first). Without `schema` it resolves to the final assistant text, a string. With `schema` it resolves to an object already validated against the schema. |
42
+ | `parallel(thunks)` | Runs `() => Promise` thunks concurrently and waits for all. A recoverable failure becomes a `null` slot; the other slots keep their results. |
43
+ | `parallel(thunks, { settled: true })` | Same, but every slot is `{ ok: true, value }` or `{ ok: false, error: { name?, message, code?, details? } }`, in input order. |
44
+ | `pipeline(items, ...stages)` | Runs every item through all stages independently. Each stage is `(prev, item, index) => Promise`; the first stage receives the item as `prev`. A failed stage turns that item's slot into `null` and skips its later stages. |
45
+ | `classify(context, opts?)` | Answers typed `choice`/`score`/`bool` questions about JSON `state` with a classifier model and no chat session. |
46
+ | `workflow(nameOrRef, args?)` | Runs another workflow as a sub-step and returns its result: a registered workflow by name, or an inline-format script file via `{ scriptPath }`, relative to the workflow cwd. `args` is a string or any JSON value, which the child reads as `input` and `args`. One level of nesting only. Rejects on an unknown name, an unreadable `scriptPath`, or a script that does not compile. |
47
+ | `phase(title)` / `log(message)` / `progress(event)` | Drive the live progress tree, logs, counters, and lanes. |
48
+ | `budget` | `{ total, spent(), remaining() }` in output tokens; `total` is `null` when the run has no budget. |
49
+ | `input` | The arguments verbatim: the `/workflow` command's string, or the JSON value the tool call or `workflow()` passed; `""` when none was given. |
50
+ | `args`, `cwd`, `signal` | The arguments as text (a string as is, any other JSON value as JSON text, `""` when none), the host working directory, and the run's abort signal. |
51
+
52
+ Agent options:
53
+
54
+ | Option | Meaning |
55
+ | --- | --- |
56
+ | `profile` | `"small"`, `"medium"`, or `"big"`: the user's central model routes. Set one on every agent. |
57
+ | `label`, `phase` | Row label and phase group in the progress tree. |
58
+ | `schema` | TypeBox schema; the agent must answer through a validated `final_answer` tool. Two repair attempts, then the call rejects with code `WORKFLOW_STRUCTURED_OUTPUT_NONCOMPLIANCE`. |
59
+ | `tools` | Strict allowlist of tool names, for example `["read", "bash", "grep", "find", "ls"]`. `[]` gives a tool-free agent. |
60
+ | `toolHints` | `["search"]` adds installed grep/find/code-search tools; `["external-search"]` adds web search, browsing, and URL-extraction tools. `requireToolHints: true` fails before prompting when a hint matched nothing. |
61
+ | `skills` | Pi skills to expose to this subagent; none by default. |
62
+ | `isolation: "worktree"` | Run in a disposable git worktree and return the patch (see below). |
63
+ | `model`, `thinkingLevel` | Intentional per-call overrides of the profile. `model` is `"provider/id"`. |
64
+ | `resume`, `resumeInputs`, `cacheKey` | Replay controls (see Resume). |
65
+
66
+ Add `"codemode"` to an agent's `tools` when it should batch or filter many tool calls in one script; those scripts can call only the agent's other allowed tools. Host MCP tools reach subagents only in synchronous `workflow` tool runs: name them in `tools` (for example `"mcp__linear__create_issue"`) or use `toolHints: ["external-search"]`. Background runs and `/workflow` commands cannot bridge them, and an agent that names one fails before prompting.
67
+
68
+ ## Profiles, labels, and phases
69
+
70
+ - Set `profile` on every `agent()` call: `small` for scouting, finding, and verifying; `medium` for judgement and synthesis; `big` for the hardest reasoning. Use `model`/`thinkingLevel` only for a deliberate override.
71
+ - A phase declared as `{ title: "Verify", profile: "small" }` routes agents that pass `phase: "Verify"` and set neither `profile` nor `model`. Routing order: the agent's `model`, its `profile`, its phase's `profile`, then the host model.
72
+ - Inside `parallel` and `pipeline`, pass `phase` and `label` in each agent's options. The global `phase()` is one shared value that every concurrent branch reads and writes, so calling it inside fan-out branches races and misfiles rows. Call `phase()` only between sequential stages.
73
+
74
+ ## Concurrency rules
75
+
76
+ **Use `api.parallel`, never `Promise.all` or `Promise.allSettled` over agents.** `api.parallel` turns a recoverable failure (schema exhaustion, an agent timeout, the budget or agent limit, a provider error) into a `null` slot and the survivors continue; `Promise.all` rejects the whole stage on the first failure and discards every sibling result. `api.parallel` also applies the run's submission limit and cancels siblings on a genuine abort. Filter the nulls before using the results:
77
+
78
+ ```js
79
+ const found = (await api.parallel(lenses.map((lens) => () =>
80
+ api.agent(`Review ${api.args} for ${lens}`, { label: `find:${lens}`, phase: "Find", profile: "small", schema: Findings }),
81
+ ))).filter((result) => result !== null);
82
+ ```
83
+
84
+ Use settled mode when the error detail matters, for example to report which branches failed and why instead of silently shrinking coverage.
85
+
86
+ **Default to `api.pipeline(items, ...stages)`.** With no barrier between stages, item A can be verified while item B is still being found, and the run's concurrency cap still bounds the number of live agents, so it is safe to start every item at once:
87
+
88
+ ```js
89
+ const verified = await api.pipeline(
90
+ files,
91
+ (file) => api.agent(`Find defects in ${file}`, { label: `find:${file}`, phase: "Find", profile: "small", schema: Findings }),
92
+ (found, file) => api.parallel(found.items.map((finding, i) => () =>
93
+ api.agent(`Try to refute this finding in ${file}: ${JSON.stringify(finding)}`, {
94
+ label: `verify:${file}:${i}`, phase: "Verify", profile: "small", schema: Verdict,
95
+ }),
96
+ )),
97
+ );
98
+ ```
99
+
100
+ Barrier smell test: before writing `await api.parallel(...)` followed by another fan-out over its results, ask whether the next stage needs every result of the previous one. A barrier is justified only for cross-item dedupe or merge, an early exit on a zero count, or a stage that compares against all prior results. Flatten, map, or filter alone is not a reason: do it inside a pipeline stage.
101
+
102
+ Host-written scripts in practice reached for `Promise.all` and never for `pipeline`; both habits lose results or wall-clock time. Check every fan-out against these two rules.
103
+
104
+ ## Quality patterns
105
+
106
+ - **Adversarial verify.** Give each finding to a separate verifier told to refute it, and keep only what survives. Thin evidence means not substantiated, not confirmed.
107
+ - **Perspective-diverse verify.** For findings that matter, use two or three verifiers with different methods (read the callers, reproduce it, check the tests) and require agreement.
108
+ - **Judge panel.** Generate several independent answers or designs, then have a judge (or several) pick or merge them against stated criteria.
109
+ - **Loop until dry.** Repeat discovery rounds, deduping each round against everything seen so far, not only the last round. Stop when a round adds nothing new, or at a round cap or budget guard.
110
+ - **Multi-modal sweep.** Cover the space from different angles (per file, per lens, per symptom, search-driven and reading-driven) and take the union.
111
+ - **Completeness critic.** After synthesis, have an agent check the result against the original request or work list and name the gaps; follow up only on material ones.
112
+ - **No silent caps.** Whenever you slice a list, cap rounds, or skip items, `api.log()` what was dropped and report it in the result, for example in a `gaps` or `skipped` field.
113
+ - **Adaptive multi-pass, only when needed.** A single-pass fan-out is the default when it can answer the question. When the first pass may expose gaps, conflicts, weak claims, or missing evidence: run the bounded first-pass agents; give their surviving results to a structured gap-analysis agent so the LLM decides what needs follow-up; put a hard `maxItems` bound on LLM-authored task arrays and defensively slice them before fan-out; use ordinary conditionals or bounded loops to launch follow-up agents only when gaps exist; synthesize the first-pass and follow-up results together. Do not generate a second pass when the first pass is sufficient, and do not invent iteration, quorum, graph, reduction, or retry primitives for this.
114
+
115
+ ## Plan the run
116
+
117
+ - **Scout inline first, then orchestrate.** Use your own tools to find the work list (the files, failing tests, or findings) before writing the script, then fan out over that concrete list. Embed the list in the script or pass it as a JSON `args` value and read it from `api.input`.
118
+ - **One single-phase workflow per turn.** For multi-phase work (understand, design, implement, review), run one workflow per phase and read each result before choosing the next, rather than one script that guesses every phase in advance.
119
+ - **Scale to the request and the size guideline.** A focused question needs a few agents, a broad sweep more. The `<workflows>` section's size guideline line (set by the user with `/workflow:size`) bounds each workflow unless the user asks for more. The run's `maxAgents` limit (default 64) is a separate hard backstop, not a target.
120
+ - **Use `classify()` as a cheap gate** between stages: dedupe, triage, or route items without a full agent. It rejects when no classifier model is configured, so call it inside `api.parallel` or a `try`/`catch` and fall back to an agent or to keeping the item. Results are not journaled, so a resumed run classifies again.
121
+
122
+ ```js
123
+ const answers = await api.classify(
124
+ { state: { finding }, questions: { real: { type: "bool", instructions: "Is this a real defect?", criteria: { true: "Concrete and reachable", false: "Speculative" } } } },
125
+ { label: `triage:${finding.id}` },
126
+ );
127
+ const keep = answers.real?.type === "bool" ? answers.real.probability > 0.5 : true;
128
+ ```
129
+
130
+ ## Budget
131
+
132
+ `api.budget.total` is the output-token ceiling (`null` when unset); `spent()` and `remaining()` are live. When the user's prompt carries a `+500k`-style directive, every run that turn is capped at the remaining pool (its own `budget` can only lower that), and synchronous runs draw the pool down; once it is spent the tool refuses further runs that turn. Once the ceiling is reached `agent()` rejects with `WorkflowBudgetExceededError` (a `null` slot inside `parallel`); agents already running can overshoot. Scale fleets from `total`, and guard every loop with it so an unbudgeted run still terminates:
133
+
134
+ ```js
135
+ const width = api.budget.total ? Math.max(2, Math.min(8, Math.floor(api.budget.total / 40_000))) : 4;
136
+ for (let round = 0; round < 5 && (api.budget.total === null || api.budget.remaining() > 30_000); round++) {
137
+ // one discovery round
138
+ }
139
+ ```
140
+
141
+ ## Edits in worktrees
142
+
143
+ Give `isolation: "worktree"` only to agents that edit files. The agent runs in a disposable git worktree, and `agent()` resolves to `{ result, patch, changed, baselineOid }`: `result` is the usual text or schema object, `patch` is its `git diff`, `changed` says whether it touched anything. The user's checkout is never modified; return the patches so the host can review and apply them. It requires the workflow `cwd` to be inside a git work tree.
144
+
145
+ ## Resume and diagnosis
146
+
147
+ - Every result names its run ID. Pass `resumeFromRunId: "<run id>"` to the tool to replay completed agent calls whose prompt, options, and execution identity still match; the rest run live.
148
+ - Shared-workspace agents replay only with `resume: "read-only"` (use it for advisory agents); isolated agents replay by default. `resume: "off"` forces a live call. Give repeated identical calls a distinct `cacheKey`, such as `` `verify:${finding.id}` ``.
149
+ - To iterate on an inline run, edit the persisted script its result names (`.pi/.workflow-runs/<runId>.script.js`) in place, then call the tool with `scriptPath` set to that file and `resumeFromRunId`. `scriptPath` implies `resumeEditedWorkflow`, so unchanged replayable calls are reused and edited or new ones run live. Edit that file, not a copy: it is excluded from the repository fingerprint, while editing any other copy in the repository, or a saved `.pi/workflows` script, changes repository state, so every shared `read-only` call reruns live.
150
+ - After editing the script, `resumeEditedWorkflow: true` (with `resumeFromRunId`) reuses calls that did not change. Use it only when the user accepts that.
151
+ - The journal at `.pi/.workflow-runs/<runId>.jsonl` holds one line per completed replayable agent call (`resume: "read-only"` or isolated) with its result; `<runId>.run.json` beside it holds the run state, compact progress, and the final result or error. Read them before diagnosing an empty or unexpected result: a replayable call missing from the journal failed or never ran, while calls without a replay policy are never journaled.
152
+
153
+ ## Run modes
154
+
155
+ - `background: true` only when the user wants the run to continue after this tool call; the tool returns a run ID and the result arrives later as a message naming the run's record and journal (and script, for an inline run). Under Dynamax that message carries the full result or error and starts a new turn, so you wake to read it and choose the next step; otherwise it arrives silently for the next turn with only the summary, and the record holds the rest. `autoResumeOnUsageLimit: true` only for such a background run when the user wants bounded automatic continuation after a provider usage window.
156
+ - Every call provides exactly one of `name`, `script`, or `scriptPath`.
157
+
158
+ ## Save a reusable workflow
159
+
160
+ When a script is worth keeping, write it to `.pi/workflows/<name>.js` with the same contract (pure-literal `meta`, one default export, no imports, injected `Type`). Saved workflows are discovered by name, so later runs can use `name: "<name>"` or `/workflow <name>`; `scriptPath: ".pi/workflows/<name>.js"` runs the file directly. The file is read again on every run by name, so a script saved or edited this session runs with its current content right away, but it appears in the `<workflows>` catalog only from the next session.