@juicesharp/rpiv-workflow 1.16.0 → 1.16.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/iterate.ts +142 -0
- package/package.json +3 -2
package/iterate.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iterate iteration — the sequential, accumulating dual of `fanout.ts`. When a
|
|
3
|
+
* stage opts in via `StageDef.iterate`, the runner pulls units one at a time
|
|
4
|
+
* (calling the user's `IterateFn` per unit, feeding it the prior units'
|
|
5
|
+
* validated `Output`s) and runs one Pi session per unit. Unlike fanout, each
|
|
6
|
+
* unit runs the stage's `outcome` collector — it reuses `runStageSession`
|
|
7
|
+
* verbatim, so every unit gets the same collector → validate → record →
|
|
8
|
+
* accumulate path as a one-shot `produces` stage.
|
|
9
|
+
*
|
|
10
|
+
* `runner.ts` (via `stage-lifecycle.ts`) injects the runner primitives through
|
|
11
|
+
* `IterateDeps` so this module never imports back (cycle-free), mirroring how
|
|
12
|
+
* `runFanout` receives `FanoutDeps`.
|
|
13
|
+
*
|
|
14
|
+
* Two run-wide bounds are the only safety nets: the generator's own `null`
|
|
15
|
+
* terminator, and `run.maxIterations` (the backstop for a generator that never
|
|
16
|
+
* returns null). No markdown regex, no per-convention counter — rpiv-workflow
|
|
17
|
+
* stays convention-agnostic; the `IterateFn` body owns the convention.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { StageDef } from "./api.js";
|
|
21
|
+
import { iterateRowStage } from "./audit.js";
|
|
22
|
+
import type { Artifact } from "./handle.js";
|
|
23
|
+
import { MSG_ITERATE_ZERO_UNITS, MSG_STAGE_COMPLETE, STATUS_ITERATE_UNIT, STATUS_KEY } from "./messages.js";
|
|
24
|
+
import type { Output } from "./output.js";
|
|
25
|
+
import type { RunContext, RunnerCtx, StageSession } from "./types.js";
|
|
26
|
+
|
|
27
|
+
export interface IterateDeps {
|
|
28
|
+
/**
|
|
29
|
+
* Dispatch one unit through the standard stage-session path (collector →
|
|
30
|
+
* validate → record → accumulate). The same `runStageSession` a normal
|
|
31
|
+
* `produces` stage uses — iterate adds no bespoke session machinery.
|
|
32
|
+
*/
|
|
33
|
+
runStageSession: (ctx: RunnerCtx, s: StageSession) => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Resume the chain after the iterate node's units finish (generator
|
|
36
|
+
* returned null). Receives the iterate node's REAL name so routing looks up
|
|
37
|
+
* the outgoing edge from it — the per-unit audit decoration never leaks
|
|
38
|
+
* into routing.
|
|
39
|
+
*/
|
|
40
|
+
advanceAfter: (curCtx: RunnerCtx, completedName: string, completedIdx: number, run: RunContext) => Promise<void>;
|
|
41
|
+
/** Re-capture the outcome's pre-stage snapshot per unit (each unit is its own produces pass). */
|
|
42
|
+
captureSnapshot: (def: StageDef, idx: number, run: RunContext) => Promise<unknown>;
|
|
43
|
+
/** Record the terminal failure when the `maxIterations` backstop trips. */
|
|
44
|
+
haltIterations: (curCtx: RunnerCtx, run: RunContext, stageName: string, count: number) => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `skill` is the bundled skill body (threaded by the runner), not the node
|
|
49
|
+
* name — aliased nodes tag unit rows + prompts with the skill body so audit
|
|
50
|
+
* consumers don't see two labels for the same work.
|
|
51
|
+
*
|
|
52
|
+
* `currentName` is the iterate node's REAL name in the workflow — passed to
|
|
53
|
+
* `advanceAfter` once the generator terminates, and used (undecorated) for
|
|
54
|
+
* `state.named` keying via `resolvePublishName`.
|
|
55
|
+
*
|
|
56
|
+
* `entryArtifact` is the stage-entry primary, FROZEN across every unit (the
|
|
57
|
+
* rolling primary advances to each unit's output, but the generator keeps
|
|
58
|
+
* seeing its true source — see `IterateContext.artifact`).
|
|
59
|
+
*
|
|
60
|
+
* `accumulated` carries this stage's prior validated `Output`s in order. The
|
|
61
|
+
* continuation-style self-call appends the unit just produced (read from
|
|
62
|
+
* `run.state.output`, which `tryRecordStage` sets immediately before
|
|
63
|
+
* `onSuccess`).
|
|
64
|
+
*/
|
|
65
|
+
export async function runIterate(
|
|
66
|
+
curCtx: RunnerCtx,
|
|
67
|
+
stageIdx: number,
|
|
68
|
+
currentName: string,
|
|
69
|
+
skill: string,
|
|
70
|
+
def: StageDef,
|
|
71
|
+
entryArtifact: Artifact | undefined,
|
|
72
|
+
accumulated: readonly Output[],
|
|
73
|
+
run: RunContext,
|
|
74
|
+
deps: IterateDeps,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const unit = await def.iterate!({
|
|
77
|
+
cwd: run.cwd,
|
|
78
|
+
artifact: entryArtifact,
|
|
79
|
+
state: run.state,
|
|
80
|
+
accumulated,
|
|
81
|
+
index: accumulated.length,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Generator terminated — complete the stage and resume the chain from the
|
|
85
|
+
// real node name. A first-call null is the zero-unit no-op: nothing is
|
|
86
|
+
// published, the primary stays at the entry artifact — warn (not error) so
|
|
87
|
+
// the author notices the empty input. A null after ≥1 unit is a normal
|
|
88
|
+
// completion.
|
|
89
|
+
if (!unit) {
|
|
90
|
+
if (accumulated.length === 0) curCtx.ui.notify(MSG_ITERATE_ZERO_UNITS(skill), "warning");
|
|
91
|
+
else curCtx.ui.notify(MSG_STAGE_COMPLETE(skill), "info");
|
|
92
|
+
await deps.advanceAfter(curCtx, currentName, stageIdx, run);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Backstop: the generator wants another unit but we've hit the run-wide
|
|
97
|
+
// cap. Halt with a terminal failure (mirrors the backward-jump guard) so a
|
|
98
|
+
// runaway generator can't loop forever.
|
|
99
|
+
if (accumulated.length >= run.maxIterations) {
|
|
100
|
+
await deps.haltIterations(curCtx, run, currentName, accumulated.length);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
curCtx.ui.setStatus(STATUS_KEY, STATUS_ITERATE_UNIT(stageIdx + 1, run.totalStages, skill, unit.label));
|
|
105
|
+
const snapshot = await deps.captureSnapshot(def, stageIdx, run);
|
|
106
|
+
|
|
107
|
+
await deps.runStageSession(curCtx, {
|
|
108
|
+
cwd: run.cwd,
|
|
109
|
+
runId: run.runId,
|
|
110
|
+
state: run.state,
|
|
111
|
+
prompt: `/skill:${skill} ${unit.prompt}`,
|
|
112
|
+
// Decorated for the JSONL row + status; named keying still resolves to
|
|
113
|
+
// outcome.name (mandatory for iterate), so the decoration never splits
|
|
114
|
+
// the accumulation slot.
|
|
115
|
+
stageName: iterateRowStage(currentName, unit.id ?? unit.label),
|
|
116
|
+
skill,
|
|
117
|
+
lifecycle: run.lifecycle,
|
|
118
|
+
runIdentity: { workflow: run.workflow.name, totalStages: run.totalStages, trigger: run.trigger },
|
|
119
|
+
stage: def,
|
|
120
|
+
stageIndex: stageIdx,
|
|
121
|
+
snapshot,
|
|
122
|
+
branchOffset: undefined,
|
|
123
|
+
onFailure: undefined,
|
|
124
|
+
onSuccess: (freshCtx) => {
|
|
125
|
+
// `tryRecordStage` set `state.output` to this unit's validated Output
|
|
126
|
+
// (and `maybeAdvancePrimary` already pushed it onto state.named) before
|
|
127
|
+
// onSuccess fired. Thread it into `accumulated` for the next pull.
|
|
128
|
+
const produced = run.state.output!;
|
|
129
|
+
return runIterate(
|
|
130
|
+
freshCtx,
|
|
131
|
+
stageIdx,
|
|
132
|
+
currentName,
|
|
133
|
+
skill,
|
|
134
|
+
def,
|
|
135
|
+
entryArtifact,
|
|
136
|
+
[...accumulated, produced],
|
|
137
|
+
run,
|
|
138
|
+
deps,
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-workflow",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "Pi extension. Chain skills into typed multi-stage workflows with audited JSONL state, predicate routing, and per-stage output validation. Skill-agnostic — bring your own skills.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"handle.ts",
|
|
43
43
|
"host.ts",
|
|
44
44
|
"internal-utils.ts",
|
|
45
|
+
"iterate.ts",
|
|
45
46
|
"layers.ts",
|
|
46
47
|
"lifecycle.ts",
|
|
47
48
|
"load",
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
]
|
|
77
78
|
},
|
|
78
79
|
"dependencies": {
|
|
79
|
-
"@juicesharp/rpiv-config": "^1.16.
|
|
80
|
+
"@juicesharp/rpiv-config": "^1.16.1",
|
|
80
81
|
"jiti": "^2.7.0"
|
|
81
82
|
},
|
|
82
83
|
"peerDependencies": {
|