@fyeeme/pi-dynamic-workflows 1.1.0 → 2.0.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/README.md +20 -9
- package/README.zh-CN.md +8 -2
- package/index.ts +130 -36
- package/package.json +18 -10
- package/prompts/implement-and-review.md +29 -0
- package/prompts/wf-review.md +30 -0
- package/sessions/spawn.ts +1 -1
- package/skills/workflow-author/SKILL.md +69 -0
- package/src/agent/dispatch.ts +4 -4
- package/src/format.ts +1 -1
- package/src/library.ts +143 -0
- package/src/runner/index.ts +2 -1
- package/src/runner/stage-executor.ts +24 -2
- package/src/types.ts +9 -0
- package/workflows/review-extension.ts +51 -0
- package/workflows/review-local-diff.ts +64 -0
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @fyeeme/pi-dynamic-workflows
|
|
2
2
|
|
|
3
|
+
**2.0.0 — major release** (from 1.1.0 on npm), part of the 2.0 extensions family wave:
|
|
4
|
+
|
|
5
|
+
- **Composed sub-agent stack** — the extension factory composes [`@fyeeme/pi-subagents`](https://www.npmjs.com/package/@fyeeme/pi-subagents) 2.1.1 from its npm dependency: the live agent fleet surface and the `subagent` tool work out of the box; no manifest path wiring, no separate install.
|
|
6
|
+
- **Run persistence** — journaled runs under `.pi/workflows/` (journal + manifest): a re-run resumes cached agent calls keyed by `sha256(workflow + prompt + signature)` with zero re-dispatch, and the manifest tracks live state for inspection.
|
|
7
|
+
- **Per-call budget override** — `run_workflow` accepts a `budget` object that merges over the workflow definition's own `maxAgents`/`maxTokens`, making "halve the fan-out for large diffs" an actual parameter.
|
|
8
|
+
- **Authoring aids shipped** — `workflow-author` skill, `/wf-*` prompts, and seed workflows (`review-local-diff`, `review-extension`) using `import type` for load determinism.
|
|
9
|
+
|
|
3
10
|
**Deterministic TypeScript workflow orchestration for [pi](https://github.com/earendil-works/pi-mono).**
|
|
4
11
|
|
|
5
12
|
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).
|
|
@@ -20,16 +27,20 @@ A workflow run is a list of steps (`agent` / `code` / `log` / `fan_out` / `loop_
|
|
|
20
27
|
|
|
21
28
|
---
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
**Batteries included**: this package's extension factory composes
|
|
31
|
+
[`@fyeeme/pi-subagents`](https://www.npmjs.com/package/@fyeeme/pi-subagents)
|
|
32
|
+
(the single below-editor fleet surface with its conversation viewer, and the `subagent` tool)
|
|
33
|
+
from its version-pinned dependency copy, so workflow runs are observable out
|
|
34
|
+
of the box. A standalone pi-subagents install is optional and coexists
|
|
35
|
+
(idempotent composition).
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
## Install
|
|
26
38
|
|
|
27
39
|
```bash
|
|
28
|
-
|
|
40
|
+
pi install npm:@fyeeme/pi-dynamic-workflows
|
|
29
41
|
```
|
|
30
42
|
|
|
31
|
-
This resolves [`@fyeeme/pi-
|
|
32
|
-
(`^0.5.0`, from the npm registry — no sibling-repo layout requirement).
|
|
43
|
+
This resolves [`@fyeeme/pi-subagents`](https://www.npmjs.com/package/@fyeeme/pi-subagents) 2.1.1 from the npm registry (the composed sub-agent stack lights up with no separate install).
|
|
33
44
|
|
|
34
45
|
Then import the public API from the package root module (a TypeScript barrel; the package ships `.ts` source):
|
|
35
46
|
|
|
@@ -38,10 +49,10 @@ import { defineWorkflow, runWorkflow } from "@fyeeme/pi-dynamic-workflows/src/in
|
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
> The package's `pi.extensions` entry registers the `run_workflow` tool plus
|
|
41
|
-
> the shared sub-agent UI from pi-
|
|
42
|
-
>
|
|
43
|
-
>
|
|
44
|
-
> fully usable via the imports shown here.
|
|
52
|
+
> the shared sub-agent UI from pi-subagents (the below-editor fleet roster —
|
|
53
|
+
> every spawned workflow agent appears there under its stable id; ↓ at an
|
|
54
|
+
> empty editor opens the list, Enter shows an agent's live transcript). The
|
|
55
|
+
> engine is also fully usable via the imports shown here.
|
|
45
56
|
|
|
46
57
|
---
|
|
47
58
|
|
package/README.zh-CN.md
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
**开箱即用**:本包的扩展工厂会组合
|
|
24
|
+
[`@fyeeme/pi-subagents`](https://www.npmjs.com/package/@fyeeme/pi-subagents)
|
|
25
|
+
(实时代理 UI——编辑器上方 widget、FleetView、`/agents`——以及 `subagent`
|
|
26
|
+
工具),且版本钉定在自身依赖副本上,工作流运行的可观测性开箱即得。
|
|
27
|
+
单独安装 pi-subagents 是可选的,二者可共存(组合幂等)。
|
|
28
|
+
|
|
23
29
|
## 安装
|
|
24
30
|
|
|
25
31
|
这是一个 pi 扩展包(workspace / 本地),尚未发布到 npm。在 pi workspace 中:
|
|
@@ -28,7 +34,7 @@
|
|
|
28
34
|
npm install --ignore-scripts # 水合(本包是 workspace 依赖)
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
这会解析 npm registry 上的 [`@fyeeme/pi-
|
|
37
|
+
这会解析 npm registry 上的 [`@fyeeme/pi-subagents`](https://www.npmjs.com/package/@fyeeme/pi-subagents)(无需保持同级仓库目录结构)。
|
|
32
38
|
|
|
33
39
|
随后从包根模块导入公共 API(TypeScript barrel,包直接以 `.ts` 源码分发):
|
|
34
40
|
|
|
@@ -36,7 +42,7 @@ npm install --ignore-scripts # 水合(本包是 workspace 依赖)
|
|
|
36
42
|
import { defineWorkflow, runWorkflow } from "@fyeeme/pi-dynamic-workflows/src/index.ts";
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
> 包的 `pi.extensions` 入口注册 `run_workflow` 工具,并接入 pi-
|
|
45
|
+
> 包的 `pi.extensions` 入口注册 `run_workflow` 工具,并接入 pi-subagents 的共享子代理 UI(编辑器上方实时 agent widget、下方 FleetView、`/agents` 转录查看器——每个工作流 agent 以其 step id 出现在其中;需先安装 @fyeeme/pi-subagents,见上方提示)。引擎本身也可经上述导入直接使用。
|
|
40
46
|
|
|
41
47
|
---
|
|
42
48
|
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* only adapts the agent's JSON args into the code-form WorkflowDefinition and
|
|
7
7
|
* runs it with the default dispatch (real `pi --mode json` subprocesses).
|
|
8
8
|
*
|
|
9
|
-
* Live progress UI is delegated to the shared `@fyeeme/pi-
|
|
9
|
+
* Live progress UI is delegated to the shared `@fyeeme/pi-subagents`
|
|
10
10
|
* extension (registered via the `pi.extensions` manifest alongside this
|
|
11
11
|
* entry): every spawned workflow agent notifies the process-global monitor
|
|
12
12
|
* through spawnAgent, rendering in the shared above-editor agent widget, the
|
|
@@ -15,17 +15,21 @@
|
|
|
15
15
|
* `/wf-inspect` command were removed in favor of that shared surface.
|
|
16
16
|
*/
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
18
20
|
import { Type } from "typebox";
|
|
21
|
+
import piSubagents from "@fyeeme/pi-subagents";
|
|
19
22
|
import { defineWorkflow, runWorkflow } from "./src/index.ts";
|
|
20
23
|
import type { Budget, StepContext, StepDefinition, WorkflowDefinition } from "./src/types.ts";
|
|
21
24
|
import { WorkflowError } from "./src/errors.ts";
|
|
25
|
+
import { discoverWorkflowLibrary, loadLibraryWorkflow } from "./src/library.ts";
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Parameter schema (the JSON-serializable workflow subset)
|
|
25
29
|
// ---------------------------------------------------------------------------
|
|
26
30
|
|
|
27
31
|
const BudgetExhaustPolicy = Type.Optional(
|
|
28
|
-
|
|
32
|
+
StringEnum(["throw", "null"] as const, {
|
|
29
33
|
description: "Budget-exhaustion policy for this step: \"throw\" (default) aborts the run; \"null\" degrades this step to a null result so siblings/downstream continue (the run result records degraded steps).",
|
|
30
34
|
}),
|
|
31
35
|
);
|
|
@@ -103,7 +107,30 @@ const WorkflowSchema = Type.Object({
|
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
const RunWorkflowParams = Type.Object({
|
|
106
|
-
|
|
110
|
+
source: Type.Optional(
|
|
111
|
+
StringEnum(["inline", "library"] as const, {
|
|
112
|
+
description:
|
|
113
|
+
'Where the workflow comes from. "inline" (default): the `workflow` parameter (JSON subset). "library": a named workflow from the discovered library (bundled workflows/ + project .pi/workflows/lib/, project overrides bundled) — full step set incl. loop_until, loaded through the determinism guard.',
|
|
114
|
+
default: "inline",
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
name: Type.Optional(
|
|
118
|
+
Type.String({ description: 'Library workflow name (required for source: "library").' }),
|
|
119
|
+
),
|
|
120
|
+
workflow: Type.Optional(WorkflowSchema),
|
|
121
|
+
budget: Type.Optional(
|
|
122
|
+
Type.Object(
|
|
123
|
+
{
|
|
124
|
+
maxAgents: Type.Optional(Type.Number()),
|
|
125
|
+
maxTokens: Type.Optional(Type.Number()),
|
|
126
|
+
maxDurationMs: Type.Optional(Type.Number()),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
description:
|
|
130
|
+
"Per-call budget override (library mode only — inline workflows declare budget inside `workflow.budget`). Fields merge over the workflow definition's own budget; use it to tighten a library workflow for a large input without editing the definition. Omitted fields keep the definition's values.",
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
),
|
|
107
134
|
input: Type.Optional(Type.String({ description: "Initial ctx.input (also {{input}} in prompts)" })),
|
|
108
135
|
cwd: Type.Optional(Type.String({ description: "Working dir + journal base. Default: session cwd" })),
|
|
109
136
|
now: Type.Optional(Type.Number({ description: "Deterministic inception ms (resume seed). Default: Date.now()" })),
|
|
@@ -287,6 +314,13 @@ function dropInvalidModel(id: string, model: string | undefined, validIds: Set<s
|
|
|
287
314
|
}
|
|
288
315
|
|
|
289
316
|
export default function (pi: ExtensionAPI): void {
|
|
317
|
+
// Compose the subagent stack (live agent UI — widget / FleetView /agents —
|
|
318
|
+
// and the `subagent` tool) from the pinned dependency copy. The tool
|
|
319
|
+
// registers exactly once per process (guard in pi-subagents' index.ts):
|
|
320
|
+
// coexists with a standalone pi-subagents install and with other consumers
|
|
321
|
+
// (pi-review) composing it.
|
|
322
|
+
piSubagents(pi);
|
|
323
|
+
|
|
290
324
|
pi.registerTool({
|
|
291
325
|
name: "run_workflow",
|
|
292
326
|
label: "Run workflow",
|
|
@@ -301,38 +335,78 @@ export default function (pi: ExtensionAPI): void {
|
|
|
301
335
|
parameters: RunWorkflowParams,
|
|
302
336
|
|
|
303
337
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return { ...s, model, routes, fallback };
|
|
338
|
+
// Library mode: resolve the named workflow from the discovered
|
|
339
|
+
// library (ast-guard + jiti via the existing loader), then run it
|
|
340
|
+
// through the same runner as inline mode. Unknown name → list the
|
|
341
|
+
// available workflows instead of spawning anything.
|
|
342
|
+
let workflowDef: WorkflowDefinition | undefined;
|
|
343
|
+
if (params.source === "library") {
|
|
344
|
+
if (!params.name)
|
|
345
|
+
throw new Error('run_workflow: source "library" requires a workflow `name`.');
|
|
346
|
+
try {
|
|
347
|
+
const entry = await loadLibraryWorkflow(params.name, params.cwd ?? ctx.cwd);
|
|
348
|
+
if (!entry) {
|
|
349
|
+
const lib = await discoverWorkflowLibrary(params.cwd ?? ctx.cwd);
|
|
350
|
+
const available = [...lib.values()]
|
|
351
|
+
.map((e) => `- ${e.name}${e.description ? ` — ${e.description}` : ""} (${e.filePath})`)
|
|
352
|
+
.join("\n");
|
|
353
|
+
throw new Error(`run_workflow: no library workflow named "${params.name}". Available:\n${available || "(none)"}`);
|
|
354
|
+
}
|
|
355
|
+
workflowDef = entry.workflow;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
if (e instanceof Error && e.message.startsWith("run_workflow:")) throw e;
|
|
358
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
359
|
+
throw new Error(`run_workflow failed: ${msg}`);
|
|
327
360
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
ctx.ui.notify(`Invalid model(s) dropped, using default: ${dropped.join(", ")}`, "warning");
|
|
361
|
+
} else {
|
|
362
|
+
if (!params.workflow)
|
|
363
|
+
throw new Error('run_workflow: provide either a `workflow` (inline) or `name` with source "library".');
|
|
332
364
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
let workflow: WorkflowDefinition;
|
|
368
|
+
if (params.source === "library") {
|
|
369
|
+
// Library workflows are authored TS (models already validated at
|
|
370
|
+
// authoring time; the determinism guard ran at load). A per-call
|
|
371
|
+
// `budget` merges over the definition's own budget.
|
|
372
|
+
workflow =
|
|
373
|
+
workflowDef && params.budget
|
|
374
|
+
? { ...workflowDef, budget: { ...workflowDef.budget, ...params.budget } }
|
|
375
|
+
: workflowDef!;
|
|
376
|
+
} else {
|
|
377
|
+
// Scoped-models: when the session restricts models (--models /
|
|
378
|
+
// enabledModels), validate step models against that scope; otherwise
|
|
379
|
+
// fall back to the full registry. Invalid ids (e.g. "sonnet") are
|
|
380
|
+
// dropped so the subprocess uses the default session model.
|
|
381
|
+
const scoped = ctx.scopedModels;
|
|
382
|
+
const validIds =
|
|
383
|
+
scoped && scoped.length > 0
|
|
384
|
+
? new Set(scoped.map((s) => s.model.id))
|
|
385
|
+
: new Set(ctx.modelRegistry.getAll().map((m) => m.id));
|
|
386
|
+
const dropped: string[] = [];
|
|
387
|
+
const sanitizedSteps = params.workflow!.steps.map((s) => {
|
|
388
|
+
const model = "model" in s ? dropInvalidModel(s.id, s.model, validIds, dropped) : undefined;
|
|
389
|
+
if (s.type === "classify_route") {
|
|
390
|
+
// Route/fallback sub-step models must be sanitized too — the schema
|
|
391
|
+
// promises "Invalid ids are dropped", which routeStepToCode otherwise
|
|
392
|
+
// passes straight through to the subprocess.
|
|
393
|
+
const routes = Object.fromEntries(
|
|
394
|
+
Object.entries(s.routes).map(([cat, rs]) => [
|
|
395
|
+
cat,
|
|
396
|
+
rs.map((r) => ({ ...r, model: dropInvalidModel(`${s.id}.${r.id}`, r.model, validIds, dropped) })),
|
|
397
|
+
]),
|
|
398
|
+
) as typeof s.routes;
|
|
399
|
+
const fallback = s.fallback?.map((r) => ({ ...r, model: dropInvalidModel(`${s.id}.${r.id}`, r.model, validIds, dropped) }));
|
|
400
|
+
return { ...s, model, routes, fallback };
|
|
401
|
+
}
|
|
402
|
+
return { ...s, model };
|
|
403
|
+
});
|
|
404
|
+
if (dropped.length > 0) {
|
|
405
|
+
ctx.ui.notify(`Invalid model(s) dropped, using default: ${dropped.join(", ")}`, "warning");
|
|
406
|
+
}
|
|
407
|
+
const sanitizedWorkflow = { ...params.workflow!, steps: sanitizedSteps };
|
|
408
|
+
workflow = buildWorkflow(sanitizedWorkflow);
|
|
409
|
+
}
|
|
336
410
|
const result = await runWorkflow({
|
|
337
411
|
workflow,
|
|
338
412
|
input: params.input,
|
|
@@ -347,14 +421,34 @@ export default function (pi: ExtensionAPI): void {
|
|
|
347
421
|
`stats: ${result.stats.agents} agent(s), ${result.stats.tokens} tokens, $${result.stats.cost.toFixed(4)}`,
|
|
348
422
|
];
|
|
349
423
|
if (result.error) lines.push(`error: ${result.error}`);
|
|
424
|
+
if (result.journalFile) lines.push(`full run details: ${result.journalFile}`);
|
|
425
|
+
|
|
426
|
+
// Failed/aborted runs are tool errors: throw so pi sets isError and
|
|
427
|
+
// reports the summary to the model (returning a value never sets the
|
|
428
|
+
// error flag). Completed runs with degraded steps stay a normal result.
|
|
429
|
+
const text = truncateHead(lines.join("\n"), { maxLines: 2000, maxBytes: 50_000 }).content;
|
|
430
|
+
if (result.status !== "completed") {
|
|
431
|
+
throw new Error(text);
|
|
432
|
+
}
|
|
433
|
+
const u = result.stats.usage;
|
|
350
434
|
return {
|
|
351
|
-
content: [{ type: "text" as const, text
|
|
435
|
+
content: [{ type: "text" as const, text }],
|
|
352
436
|
details: result,
|
|
353
|
-
|
|
437
|
+
// Surface nested-agent usage so pi's footer //session totals include it.
|
|
438
|
+
usage: u
|
|
439
|
+
? {
|
|
440
|
+
input: u.input,
|
|
441
|
+
output: u.output,
|
|
442
|
+
cacheRead: u.cacheRead,
|
|
443
|
+
cacheWrite: u.cacheWrite,
|
|
444
|
+
totalTokens: result.stats.tokens,
|
|
445
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: result.stats.cost },
|
|
446
|
+
}
|
|
447
|
+
: undefined,
|
|
354
448
|
};
|
|
355
449
|
} catch (e) {
|
|
356
450
|
const msg = e instanceof Error ? e.message : String(e);
|
|
357
|
-
|
|
451
|
+
throw new Error(`run_workflow failed: ${msg}`);
|
|
358
452
|
}
|
|
359
453
|
},
|
|
360
454
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-dynamic-workflows",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Deterministic TypeScript workflow orchestration for pi. Fuses the pi-dynamic-workflows design (declarative graph, 10 step primitives, heuristic planner, outcome collectors) with Claude Code's workflow engine coordination mechanisms (deterministic sandbox, cache-key resume, per-agent abort map, dynamic budget, runaway caps).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,14 +22,22 @@
|
|
|
22
22
|
"*.ts",
|
|
23
23
|
"src/**/*.ts",
|
|
24
24
|
"sessions/**/*.ts",
|
|
25
|
+
"workflows/**/*.ts",
|
|
26
|
+
"skills/**/*.md",
|
|
27
|
+
"prompts/**/*.md",
|
|
25
28
|
"README.md",
|
|
26
29
|
"README.zh-CN.md",
|
|
27
30
|
"LICENSE"
|
|
28
31
|
],
|
|
29
32
|
"pi": {
|
|
30
33
|
"extensions": [
|
|
31
|
-
"./index.ts"
|
|
32
|
-
|
|
34
|
+
"./index.ts"
|
|
35
|
+
],
|
|
36
|
+
"skills": [
|
|
37
|
+
"skills/workflow-author"
|
|
38
|
+
],
|
|
39
|
+
"prompts": [
|
|
40
|
+
"prompts"
|
|
33
41
|
]
|
|
34
42
|
},
|
|
35
43
|
"scripts": {
|
|
@@ -37,20 +45,20 @@
|
|
|
37
45
|
"typecheck": "tsc"
|
|
38
46
|
},
|
|
39
47
|
"dependencies": {
|
|
40
|
-
"@fyeeme/pi-
|
|
48
|
+
"@fyeeme/pi-subagents": "2.1.1"
|
|
41
49
|
},
|
|
42
50
|
"peerDependencies": {
|
|
43
|
-
"@earendil-works/pi-ai": ">=0.84.
|
|
44
|
-
"@earendil-works/pi-coding-agent": ">=0.84.
|
|
45
|
-
"@earendil-works/pi-tui": ">=0.84.
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
52
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
53
|
+
"@earendil-works/pi-tui": ">=0.84.4",
|
|
46
54
|
"jiti": ">=2.0.0",
|
|
47
55
|
"typebox": ">=1.0.0",
|
|
48
56
|
"typescript": ">=5.0.0"
|
|
49
57
|
},
|
|
50
58
|
"devDependencies": {
|
|
51
|
-
"@earendil-works/pi-ai": "0.84.
|
|
52
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
53
|
-
"@earendil-works/pi-tui": "0.84.
|
|
59
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
61
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
54
62
|
"@types/node": "22.19.19",
|
|
55
63
|
"jiti": "2.7.0",
|
|
56
64
|
"typebox": "1.1.38",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Implement → review → fix loop via successive subagent calls
|
|
3
|
+
argument-hint: <what to implement>
|
|
4
|
+
---
|
|
5
|
+
Execute this three-stage loop with the `subagent` tool — one call per stage.
|
|
6
|
+
You are the coordinator: pass each stage's complete output text into the next
|
|
7
|
+
stage's `task` yourself (chain mode no longer exists).
|
|
8
|
+
|
|
9
|
+
1. **Implement** — call `subagent` with agent `worker`, task: $@ plus any repo
|
|
10
|
+
context it needs. Ask for the final implementation summary in the result.
|
|
11
|
+
|
|
12
|
+
2. **Review** — call `subagent` with agent `reviewer`. Its task must begin:
|
|
13
|
+
"Review the following implementation." followed by the worker's COMPLETE
|
|
14
|
+
result verbatim, plus the files/areas touched.
|
|
15
|
+
|
|
16
|
+
3. **Fix** — if and only if the reviewer reported issues, one more `worker`
|
|
17
|
+
call: "Apply this feedback to your earlier implementation." followed by the
|
|
18
|
+
reviewer's findings verbatim.
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
|
|
22
|
+
- Forward results between stages verbatim — never paraphrase or summarize;
|
|
23
|
+
each subagent has zero memory of its predecessor.
|
|
24
|
+
- Run stages strictly sequentially; a later stage always needs the earlier
|
|
25
|
+
stage's full output as input.
|
|
26
|
+
- Skip stages 2–3 only for trivial changes (< ~20 lines, no behavior change);
|
|
27
|
+
state explicitly that you did so and why.
|
|
28
|
+
- Finish by reporting: what was implemented, the review verdict, and anything
|
|
29
|
+
still open.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Heavy review pipeline via deterministic workflow (budget + resume + adversarial verify)
|
|
3
|
+
---
|
|
4
|
+
Run the heavy review pipeline now. Target: $@
|
|
5
|
+
|
|
6
|
+
First resolve the diff yourself (run `git diff` against the appropriate
|
|
7
|
+
range — upstream merge-base if configured, else HEAD; pass the diff text as
|
|
8
|
+
the workflow `input`).
|
|
9
|
+
|
|
10
|
+
Then call `run_workflow` with `source: "library"`:
|
|
11
|
+
|
|
12
|
+
- For a plain local diff → name `review-local-diff` (5 finders at
|
|
13
|
+
parallelism 4, adversarial verify judges 3 / minPass 2, budget 12 agents /
|
|
14
|
+
2M tokens).
|
|
15
|
+
- For changed packages under an extensions submodule → name
|
|
16
|
+
`review-extension` (one reviewer per changed package at parallelism 3).
|
|
17
|
+
|
|
18
|
+
Budget guidance: pass a per-call `budget` override on the run_workflow call —
|
|
19
|
+
halve `maxAgents` when the diff exceeds ~150 files; without an override the
|
|
20
|
+
workflow's own declared budget applies.
|
|
21
|
+
|
|
22
|
+
Resume behavior: on journal keys already present from a prior partial run
|
|
23
|
+
the engine skips completed steps — do not re-dispatch them yourself.
|
|
24
|
+
|
|
25
|
+
When the run completes, report the merged, verified findings via
|
|
26
|
+
`review_report` (if available) or as a markdown list ranked most-severe
|
|
27
|
+
first.
|
|
28
|
+
|
|
29
|
+
For tuning the pipeline itself (steps, rubric, determinism constraints),
|
|
30
|
+
load the workflow-author skill.
|
package/sessions/spawn.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Thin barrel over src/agent/dispatch.ts: the spawn registry + per-agent
|
|
5
5
|
* abort primitives. The core spawn implementation lives in
|
|
6
|
-
* `@fyeeme/pi-
|
|
6
|
+
* `@fyeeme/pi-subagents`; skip/retry (workflows-specific) stay in
|
|
7
7
|
* dispatch.ts.
|
|
8
8
|
*/
|
|
9
9
|
export {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-author
|
|
3
|
+
description: "Author and tune deterministic workflow definitions for run_workflow: step-type selection, rubric writing, the single-file determinism constraint, budget/parallelism as declared data, and resume semantics."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring workflow-library definitions
|
|
7
|
+
|
|
8
|
+
A library workflow is a single `.ts` file under `.pi/workflows/lib/`
|
|
9
|
+
(project) or the package's bundled `workflows/` (reference examples:
|
|
10
|
+
`review-local-diff`, `review-extension`). It exports a workflow and becomes
|
|
11
|
+
runnable by name (`run_workflow` with `source: "library"`) on the next call —
|
|
12
|
+
dropping the file in IS the registration; no code change, no restart.
|
|
13
|
+
|
|
14
|
+
## The hard constraint: single-file determinism
|
|
15
|
+
|
|
16
|
+
The loader runs the ast determinism guard on the entry source and REJECTS
|
|
17
|
+
`Date.now()`, `Math.random()`, and `new Date()` **before** executing. This is
|
|
18
|
+
not a style rule — cache-key resume is only sound when the same workflow
|
|
19
|
+
source produces the same cache keys. The guard scans **only the entry file**:
|
|
20
|
+
a helper imported from another file could smuggle in non-determinism and
|
|
21
|
+
silently break resume. Therefore: **library workflows must stay
|
|
22
|
+
single-file.** Inline everything; if you need `now`, the engine passes it
|
|
23
|
+
(the `now` run parameter / `ctx` plumbing) — never read the clock yourself.
|
|
24
|
+
|
|
25
|
+
## Choosing step types
|
|
26
|
+
|
|
27
|
+
| Need | Step | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| One LLM call | `agent` | prompt may be a function of `ctx.input` / `ctx.step(id)` |
|
|
30
|
+
| N parallel calls over a list | `fan_out` | `over` may be dynamic (`ctx => …`); declare `parallelism`; `merge` folds results |
|
|
31
|
+
| Produce + judge | `adversarial` | rubric criteria become judge prompts; `judges`/`minPass` (majority default) |
|
|
32
|
+
| Best-of-N | `tournament` | candidates × judges |
|
|
33
|
+
| Route by classification | `classify_route` | classifier replies `{category}`; routes + fallback |
|
|
34
|
+
| Iterate to convergence | `loop_until` | `until` condition + `maxIterations` (TS-only — library files only, not inline JSON) |
|
|
35
|
+
| Zero-token narrative | `log` | journal annotation only |
|
|
36
|
+
|
|
37
|
+
## Writing rubrics (adversarial / tournament)
|
|
38
|
+
|
|
39
|
+
A rubric entry is a judge prompt, not a checkbox. Write each criterion as a
|
|
40
|
+
falsifiable, self-contained sentence: `"finding is concretely actionable
|
|
41
|
+
(file:line present)"` — not `"quality"`. 2–4 criteria; a judge that cannot
|
|
42
|
+
verify a criterion from the material in front of it will guess, and guessing
|
|
43
|
+
flattens the verdict distribution.
|
|
44
|
+
|
|
45
|
+
## Budget and parallelism are data
|
|
46
|
+
|
|
47
|
+
`budget: { maxAgents, maxTokens, maxDurationMs }` at the workflow level and
|
|
48
|
+
`parallelism` on fan-out steps are declared fields the engine enforces
|
|
49
|
+
(fan-out pre-checks the batch fits; a step exceeding budget follows its
|
|
50
|
+
`onBudgetExhaust` policy — throw, or degrade to null). Tune them by editing
|
|
51
|
+
the file; orchestration prompts may also pass tighter values per call.
|
|
52
|
+
|
|
53
|
+
## Resume semantics
|
|
54
|
+
|
|
55
|
+
Every dispatched call is cache-keyed (workflow source + prompts + inputs).
|
|
56
|
+
Re-running with unchanged definition and inputs skips completed steps from
|
|
57
|
+
the journal — do not "help" by re-dispatching; change an input only when you
|
|
58
|
+
want a step actually re-run. Editing the workflow source changes the keys:
|
|
59
|
+
that is the intended way to invalidate stale cache.
|
|
60
|
+
|
|
61
|
+
## Checklist before shipping a library workflow
|
|
62
|
+
|
|
63
|
+
1. Single file, and imports type-only: `import type { WorkflowDefinition } from ...` (erased at load — nothing outside the file executes, so the entry-only guard scan covers everything). NEVER value-import the engine by relative path: from `.pi/workflows/lib/` it does not resolve (bricking the whole library), and any value import puts un-scanned code on the load path. Plain `export const workflow = { name, steps }` with no import at all works too (the loader validates shape, not `defineWorkflow`).
|
|
64
|
+
2. No `Date.now` / `Math.random` / `new Date` anywhere in the source.
|
|
65
|
+
3. `name` is unique and descriptive (it is the invocation key); `description`
|
|
66
|
+
one line — it appears in the unknown-name listing.
|
|
67
|
+
4. Budget and parallelism declared, sized to the worst expected input.
|
|
68
|
+
5. Verified locally: `run_workflow` with `source: "library"`, then re-run to
|
|
69
|
+
see journal hits skip completed steps.
|
package/src/agent/dispatch.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The core spawn primitive (`spawnAgent`, `mapWithConcurrencyLimit`,
|
|
5
5
|
* `createSpawnRegistry`, `abortAgent`, `getPiInvocation` + the registry/
|
|
6
|
-
* options/result types) lives in the shared `@fyeeme/pi-
|
|
6
|
+
* options/result types) lives in the shared `@fyeeme/pi-subagents`
|
|
7
7
|
* package — extracted from the duplicate copies that used to live here and
|
|
8
8
|
* in pi-review. This module keeps the workflows-specific layer on top:
|
|
9
9
|
* `skipAgent`/`retryAgent` (with `AbortReason` semantics) and the lifecycle
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { AgentLifecycleListeners } from "../lifecycle.ts";
|
|
21
21
|
import { notifyRetry, notifySkip } from "../lifecycle.ts";
|
|
22
|
-
import type { AgentSpawnRegistry } from "@fyeeme/pi-
|
|
22
|
+
import type { AgentSpawnRegistry } from "@fyeeme/pi-subagents";
|
|
23
23
|
|
|
24
24
|
// Re-export the core dispatch surface so existing importers of this module
|
|
25
25
|
// (`../agent/dispatch.ts`) keep working unchanged.
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
getPiInvocation,
|
|
30
30
|
mapWithConcurrencyLimit,
|
|
31
31
|
spawnAgent,
|
|
32
|
-
} from "@fyeeme/pi-
|
|
32
|
+
} from "@fyeeme/pi-subagents";
|
|
33
33
|
export type {
|
|
34
34
|
AgentAbortMap,
|
|
35
35
|
AgentCallId,
|
|
@@ -37,7 +37,7 @@ export type {
|
|
|
37
37
|
AgentSpawnRegistry,
|
|
38
38
|
AgentSpawnResult,
|
|
39
39
|
AgentUsage,
|
|
40
|
-
} from "@fyeeme/pi-
|
|
40
|
+
} from "@fyeeme/pi-subagents";
|
|
41
41
|
|
|
42
42
|
export type AbortReason = "user-skip" | "user-retry";
|
|
43
43
|
|
package/src/format.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* monitor `displayName` for the shared sub-agent UI). The former ANSI color
|
|
7
7
|
* helpers + fmtTokens were progress-widget/`/wf-inspect` rendering aids and
|
|
8
8
|
* were removed together with those surfaces (live progress is now rendered by
|
|
9
|
-
* the shared @fyeeme/pi-
|
|
9
|
+
* the shared @fyeeme/pi-subagents extension).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** Extract the step id from a callId of the form `${stepId}#${n}` (e.g.
|
package/src/library.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/library.ts — named workflow library: the persistence layer for
|
|
3
|
+
* workflows (PR3 of the sandwich refactor).
|
|
4
|
+
*
|
|
5
|
+
* Workflows are single-file `.ts` definitions (full step set including the
|
|
6
|
+
* TS-only steps like loop_until) discovered from:
|
|
7
|
+
*
|
|
8
|
+
* 1. project — nearest `.pi/workflows/lib/` walking up from cwd
|
|
9
|
+
* 2. bundled — this package's own `workflows/` directory (seed workflows)
|
|
10
|
+
*
|
|
11
|
+
* The project entry overrides a bundled entry of the same name. Discovery and
|
|
12
|
+
* loading go through the existing loader (jiti + the ast determinism guard),
|
|
13
|
+
* so a library workflow is rejected before execution when its source
|
|
14
|
+
* smuggles in non-deterministic APIs — the precondition cache-key resume
|
|
15
|
+
* relies on.
|
|
16
|
+
*
|
|
17
|
+
* The library directory is deliberately distinct from the journal output
|
|
18
|
+
* tree (`.pi/workflows/runs/<runId>/`): definitions and run state never
|
|
19
|
+
* collide. Adding a workflow = dropping a file; no code change.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { loadWorkflowModule } from "./loader.ts";
|
|
25
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import type { WorkflowDefinition } from "./types.ts";
|
|
27
|
+
|
|
28
|
+
/** This file lives at <pkg>/src/ → the bundled library is <pkg>/workflows. */
|
|
29
|
+
const PKG_ROOT = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
|
+
const BUNDLED_LIB_DIR = path.join(PKG_ROOT, "workflows");
|
|
31
|
+
|
|
32
|
+
/** Project-level library directory, relative to the project root. */
|
|
33
|
+
const PROJECT_LIB_REL = path.join(CONFIG_DIR_NAME, "workflows", "lib");
|
|
34
|
+
|
|
35
|
+
/** One library entry, after loading the module. */
|
|
36
|
+
export interface LibraryEntry {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly description?: string;
|
|
39
|
+
readonly filePath: string;
|
|
40
|
+
readonly workflow: WorkflowDefinition;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isDirectory(p: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
return fs.statSync(p).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Nearest project library dir walking up from cwd (like agent discovery). */
|
|
52
|
+
export function findProjectLibDir(cwd: string): string | null {
|
|
53
|
+
let dir = path.resolve(cwd);
|
|
54
|
+
for (;;) {
|
|
55
|
+
const candidate = path.join(dir, PROJECT_LIB_REL);
|
|
56
|
+
if (isDirectory(candidate)) return candidate;
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) return null;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Library directories in priority order (later wins on name collision). */
|
|
64
|
+
function libDirs(cwd: string): string[] {
|
|
65
|
+
const dirs = [BUNDLED_LIB_DIR];
|
|
66
|
+
const project = findProjectLibDir(cwd);
|
|
67
|
+
if (project) dirs.push(project);
|
|
68
|
+
return dirs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isValidWorkflow(value: unknown): value is WorkflowDefinition {
|
|
72
|
+
return (
|
|
73
|
+
!!value &&
|
|
74
|
+
typeof value === "object" &&
|
|
75
|
+
typeof (value as WorkflowDefinition).name === "string" &&
|
|
76
|
+
Array.isArray((value as WorkflowDefinition).steps)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Load one workflow file: ast-guard + jiti import + shape validation. */
|
|
81
|
+
async function loadEntry(filePath: string): Promise<LibraryEntry | null> {
|
|
82
|
+
try {
|
|
83
|
+
const mod = (await loadWorkflowModule({ filePath })) as {
|
|
84
|
+
workflow?: unknown;
|
|
85
|
+
default?: unknown;
|
|
86
|
+
};
|
|
87
|
+
const wf = mod.workflow ?? mod.default;
|
|
88
|
+
if (!isValidWorkflow(wf)) return null;
|
|
89
|
+
return {
|
|
90
|
+
name: wf.name,
|
|
91
|
+
description: wf.description,
|
|
92
|
+
filePath,
|
|
93
|
+
workflow: wf,
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Fail-fast, deliberately: a library file that fails the determinism
|
|
97
|
+
// guard or errors at import must NOT silently disappear from the
|
|
98
|
+
// available list (a poisoned entry re-appearing would break cache-key
|
|
99
|
+
// resume). The error names the offending file; remove or fix the file
|
|
100
|
+
// to restore discovery. Shape-invalid files (below) are the exception
|
|
101
|
+
// — they are skipped silently as non-workflows.
|
|
102
|
+
throw new Error(`library workflow ${path.basename(filePath)} failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Discover the library: load every `.ts` workflow in the library dirs and
|
|
108
|
+
* index by workflow name (later dirs override earlier same-name entries).
|
|
109
|
+
* A file whose module import or the determinism guard check FAILS aborts the
|
|
110
|
+
* whole discovery with an error naming the file (fail-fast — see loadEntry);
|
|
111
|
+
* a file that imports cleanly but is not a valid workflow shape is skipped
|
|
112
|
+
* silently.
|
|
113
|
+
*/
|
|
114
|
+
export async function discoverWorkflowLibrary(cwd: string): Promise<Map<string, LibraryEntry>> {
|
|
115
|
+
const byName = new Map<string, LibraryEntry>();
|
|
116
|
+
for (const dir of libDirs(cwd)) {
|
|
117
|
+
let entries: fs.Dirent[];
|
|
118
|
+
try {
|
|
119
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
120
|
+
} catch {
|
|
121
|
+
continue; // unreadable/missing dir — skip
|
|
122
|
+
}
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.name.endsWith(".ts") || entry.name.startsWith(".")) continue;
|
|
125
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
126
|
+
const loaded = await loadEntry(path.join(dir, entry.name));
|
|
127
|
+
if (loaded) byName.set(loaded.name, loaded);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return byName;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Resolve one workflow by name, or null when not in the library. */
|
|
134
|
+
export async function loadLibraryWorkflow(
|
|
135
|
+
name: string,
|
|
136
|
+
cwd: string,
|
|
137
|
+
): Promise<LibraryEntry | null> {
|
|
138
|
+
// Fast path: only load the files that could define the name. Files export
|
|
139
|
+
// the workflow's name, so discovery is required either way — but skipping
|
|
140
|
+
// the project/bundled distinction keeps override semantics identical.
|
|
141
|
+
const lib = await discoverWorkflowLibrary(cwd);
|
|
142
|
+
return lib.get(name) ?? null;
|
|
143
|
+
}
|
package/src/runner/index.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import { BudgetPool } from "../budget/index.ts";
|
|
18
19
|
import { Journal, type RunManifest } from "../cache/index.ts";
|
|
19
20
|
import type { AgentLifecycleListeners } from "../lifecycle.ts";
|
|
@@ -81,7 +82,7 @@ export async function runWorkflow(opts: RunWorkflowOptions): Promise<RunResult>
|
|
|
81
82
|
const budget = opts.budget ?? workflow.budget ?? {};
|
|
82
83
|
const journalDir =
|
|
83
84
|
opts.journalDir ??
|
|
84
|
-
path.join(cwd,
|
|
85
|
+
path.join(cwd, CONFIG_DIR_NAME, "workflows", sanitizeWorkflowName(workflow.name));
|
|
85
86
|
|
|
86
87
|
await fs.promises.mkdir(journalDir, { recursive: true });
|
|
87
88
|
const journal = new Journal({ dir: journalDir });
|
|
@@ -986,7 +986,7 @@ function dispatchOpts(
|
|
|
986
986
|
signal,
|
|
987
987
|
allowChildRecursion,
|
|
988
988
|
// UI display name for the shared sub-agent widget/FleetView (purely
|
|
989
|
-
// observational metadata consumed by the pi-
|
|
989
|
+
// observational metadata consumed by the pi-subagents monitor): the
|
|
990
990
|
// step id ("fan", "adv"), so workflow rows are distinguishable from other
|
|
991
991
|
// agents. Cache hits never spawn, so they never appear — zero dispatch.
|
|
992
992
|
displayName: stepIdOf(callId),
|
|
@@ -1018,6 +1018,12 @@ function usageStats(res: AgentSpawnResult, durationMs: number, ok: boolean): Ste
|
|
|
1018
1018
|
durationMs,
|
|
1019
1019
|
agents: 1,
|
|
1020
1020
|
failures: ok ? 0 : 1,
|
|
1021
|
+
usage: {
|
|
1022
|
+
input: res.usage.input,
|
|
1023
|
+
output: res.usage.output,
|
|
1024
|
+
cacheRead: res.usage.cacheRead,
|
|
1025
|
+
cacheWrite: res.usage.cacheWrite,
|
|
1026
|
+
},
|
|
1021
1027
|
};
|
|
1022
1028
|
}
|
|
1023
1029
|
|
|
@@ -1028,6 +1034,20 @@ function addStats(a: StepStats, b: StepStats): StepStats {
|
|
|
1028
1034
|
durationMs: a.durationMs + b.durationMs,
|
|
1029
1035
|
agents: a.agents + b.agents,
|
|
1030
1036
|
failures: a.failures + b.failures,
|
|
1037
|
+
usage: mergeUsage(a.usage, b.usage),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function mergeUsage(
|
|
1042
|
+
a: StepStats["usage"],
|
|
1043
|
+
b: StepStats["usage"],
|
|
1044
|
+
): StepStats["usage"] {
|
|
1045
|
+
if (!a && !b) return undefined;
|
|
1046
|
+
return {
|
|
1047
|
+
input: (a?.input ?? 0) + (b?.input ?? 0),
|
|
1048
|
+
output: (a?.output ?? 0) + (b?.output ?? 0),
|
|
1049
|
+
cacheRead: (a?.cacheRead ?? 0) + (b?.cacheRead ?? 0),
|
|
1050
|
+
cacheWrite: (a?.cacheWrite ?? 0) + (b?.cacheWrite ?? 0),
|
|
1031
1051
|
};
|
|
1032
1052
|
}
|
|
1033
1053
|
|
|
@@ -1038,14 +1058,16 @@ export function aggregateStats(stats: readonly StepStats[], durationMs: number):
|
|
|
1038
1058
|
let agents = 0;
|
|
1039
1059
|
let failures = 0;
|
|
1040
1060
|
let dur = 0;
|
|
1061
|
+
let usage: StepStats["usage"];
|
|
1041
1062
|
for (const s of stats) {
|
|
1042
1063
|
tokens += s.tokens;
|
|
1043
1064
|
cost += s.cost;
|
|
1044
1065
|
agents += s.agents;
|
|
1045
1066
|
failures += s.failures;
|
|
1046
1067
|
dur += s.durationMs;
|
|
1068
|
+
usage = mergeUsage(usage, s.usage);
|
|
1047
1069
|
}
|
|
1048
|
-
return { tokens, cost, durationMs: durationMs > 0 ? durationMs : dur, agents, failures };
|
|
1070
|
+
return { tokens, cost, durationMs: durationMs > 0 ? durationMs : dur, agents, failures, usage };
|
|
1049
1071
|
}
|
|
1050
1072
|
|
|
1051
1073
|
function withDuration(stats: StepStats, start: number): StepStats {
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,15 @@ export interface StepStats {
|
|
|
55
55
|
readonly durationMs: number;
|
|
56
56
|
readonly agents: number;
|
|
57
57
|
readonly failures: number;
|
|
58
|
+
/** Nested-LLM usage split, populated for agent-dispatch steps and surfaced
|
|
59
|
+
* as `usage` on the run_workflow tool result (pi usage accounting). Absent
|
|
60
|
+
* for zero-dispatch steps (code/log) and journals written before this field. */
|
|
61
|
+
readonly usage?: {
|
|
62
|
+
readonly input: number;
|
|
63
|
+
readonly output: number;
|
|
64
|
+
readonly cacheRead: number;
|
|
65
|
+
readonly cacheWrite: number;
|
|
66
|
+
};
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
export interface StepResult<T = unknown> {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed library workflow: review-extension — the extensions-submodule review
|
|
3
|
+
* fan-out distilled from this repository's review-local-extensions-fanout
|
|
4
|
+
* pipeline: scope the changed packages first, then one reviewer per changed
|
|
5
|
+
* extension directory (parallelism 3), then merge.
|
|
6
|
+
*
|
|
7
|
+
* Input: newline-separated list of changed paths under packages/extensions
|
|
8
|
+
* (or a single path). Prompts use prompt functions (ctx.input /
|
|
9
|
+
* ctx.step(id)) — the TS API does not substitute {{...}} tokens.
|
|
10
|
+
* Runtime-single-file: `import type` is erased at load (see
|
|
11
|
+
* review-local-diff.ts).
|
|
12
|
+
*/
|
|
13
|
+
import type { WorkflowDefinition } from "../src/index.ts";
|
|
14
|
+
|
|
15
|
+
export const workflow: WorkflowDefinition = {
|
|
16
|
+
name: "review-extension",
|
|
17
|
+
description: "Review each changed extension package: one reviewer per package (parallelism 3), merged findings",
|
|
18
|
+
budget: { maxAgents: 9, maxTokens: 2_000_000 },
|
|
19
|
+
steps: [
|
|
20
|
+
{
|
|
21
|
+
id: "scope",
|
|
22
|
+
type: "agent",
|
|
23
|
+
prompt: (ctx) =>
|
|
24
|
+
"The input lists changed paths under packages/extensions. Reduce it to the set of " +
|
|
25
|
+
"distinct top-level extension directories (e.g. \"pi-review\", \"pi-subagents\"). " +
|
|
26
|
+
"Return ONLY the directory names, one per line, no commentary.\n\n" +
|
|
27
|
+
String(ctx.input),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "reviewers",
|
|
31
|
+
type: "fan_out",
|
|
32
|
+
over: (ctx) =>
|
|
33
|
+
String(ctx.step("scope").results)
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((l) => l.trim())
|
|
36
|
+
.filter(Boolean),
|
|
37
|
+
parallelism: 3,
|
|
38
|
+
agent: (pkg, _index, ctx) => ({
|
|
39
|
+
prompt:
|
|
40
|
+
`Review the uncommitted changes in packages/extensions/${pkg} (run ` +
|
|
41
|
+
`\`git -C packages/extensions/${pkg} diff\` yourself; read the touched files for ` +
|
|
42
|
+
`context). Report findings as \`file:line\` — one-line summary — the concrete cost. ` +
|
|
43
|
+
`Cover correctness, reuse, simplification, efficiency, altitude. Report only.\n\n` +
|
|
44
|
+
`Changed paths:\n${ctx.input}`,
|
|
45
|
+
}),
|
|
46
|
+
merge: (results) => results.join("\n---\n"),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default workflow;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed library workflow: review-local-diff — the local-diff review pipeline
|
|
3
|
+
* actually run in this repository (distilled from the .pi/workflows journals
|
|
4
|
+
* of review-local-changes / review-local-extensions-fanout runs).
|
|
5
|
+
*
|
|
6
|
+
* Input: the unified diff to review (run input). Fan out one finder per
|
|
7
|
+
* angle (parallelism 4), adversarially verify a merged candidate list, and
|
|
8
|
+
* return the verified findings. Budget and parallelism are declared here as
|
|
9
|
+
* data — the engine enforces them.
|
|
10
|
+
*
|
|
11
|
+
* Prompts reference run context via prompt FUNCTIONS (ctx.input /
|
|
12
|
+
* ctx.step(id)) — the {{...}} template tokens are the inline-JSON tool
|
|
13
|
+
* surface only; the TS API never substitutes them in plain strings.
|
|
14
|
+
*
|
|
15
|
+
* Runtime-single-file: `import type` is erased at load, so nothing outside
|
|
16
|
+
* this file executes and the ast determinism guard's entry-only scan covers
|
|
17
|
+
* everything that runs (a VALUE import from another file would not be
|
|
18
|
+
* scanned — keep imports type-only).
|
|
19
|
+
*/
|
|
20
|
+
import type { WorkflowDefinition } from "../src/index.ts";
|
|
21
|
+
|
|
22
|
+
const ANGLES = ["correctness", "reuse", "simplification", "efficiency", "altitude"] as const;
|
|
23
|
+
|
|
24
|
+
export const workflow: WorkflowDefinition = {
|
|
25
|
+
name: "review-local-diff",
|
|
26
|
+
description: "Fan out diff-scoped finders, adversarially verify the merged candidates, return verified findings",
|
|
27
|
+
budget: { maxAgents: 12, maxTokens: 2_000_000 },
|
|
28
|
+
steps: [
|
|
29
|
+
{
|
|
30
|
+
id: "finders",
|
|
31
|
+
type: "fan_out",
|
|
32
|
+
over: () => [...ANGLES],
|
|
33
|
+
parallelism: 4,
|
|
34
|
+
agent: (angle, _index, ctx) => ({
|
|
35
|
+
prompt:
|
|
36
|
+
`You are a code-review finder for the "${angle}" angle. Review the diff below. ` +
|
|
37
|
+
`Report each finding as \`file:line\` — one-line summary — the concrete cost ` +
|
|
38
|
+
`(for correctness: the input/state that triggers it → wrong output). Report only; ` +
|
|
39
|
+
`an empty list is a valid answer.\n\nDiff:\n${ctx.input}`,
|
|
40
|
+
}),
|
|
41
|
+
merge: (results) => results.join("\n---\n"),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "verify",
|
|
45
|
+
type: "adversarial",
|
|
46
|
+
produce: {
|
|
47
|
+
prompt: (ctx) =>
|
|
48
|
+
"Dedup and merge these code-review findings into a numbered candidate list " +
|
|
49
|
+
"(same defect + same location + same reason → keep one; different reasons for " +
|
|
50
|
+
"the same line are NOT duplicates — keep both). Return only the numbered list.\n\n" +
|
|
51
|
+
String(ctx.step("finders").results),
|
|
52
|
+
},
|
|
53
|
+
rubric: [
|
|
54
|
+
"finding is concretely actionable (file:line present)",
|
|
55
|
+
"the failure scenario or cost is stated and realistic",
|
|
56
|
+
"not a duplicate of another kept finding",
|
|
57
|
+
],
|
|
58
|
+
judges: 3,
|
|
59
|
+
minPass: 2,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default workflow;
|