@osolmaz/pi-workflows 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/extension/executor.d.ts +58 -0
- package/dist/extension/executor.js +201 -0
- package/dist/extension/executor.js.map +1 -0
- package/dist/extension/index.d.ts +17 -0
- package/dist/extension/index.js +504 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/widget.d.ts +21 -0
- package/dist/extension/widget.js +142 -0
- package/dist/extension/widget.js.map +1 -0
- package/dist/render/ansi.d.ts +16 -0
- package/dist/render/ansi.js +42 -0
- package/dist/render/ansi.js.map +1 -0
- package/dist/render/canvas.d.ts +40 -0
- package/dist/render/canvas.js +177 -0
- package/dist/render/canvas.js.map +1 -0
- package/dist/render/format.d.ts +3 -0
- package/dist/render/format.js +17 -0
- package/dist/render/format.js.map +1 -0
- package/dist/render/graph-render.d.ts +22 -0
- package/dist/render/graph-render.js +520 -0
- package/dist/render/graph-render.js.map +1 -0
- package/dist/render/graph.d.ts +46 -0
- package/dist/render/graph.js +272 -0
- package/dist/render/graph.js.map +1 -0
- package/dist/viewer/cli.d.ts +10 -0
- package/dist/viewer/cli.js +132 -0
- package/dist/viewer/cli.js.map +1 -0
- package/dist/viewer/render.d.ts +19 -0
- package/dist/viewer/render.js +162 -0
- package/dist/viewer/render.js.map +1 -0
- package/dist/viewer/tui.d.ts +11 -0
- package/dist/viewer/tui.js +140 -0
- package/dist/viewer/tui.js.map +1 -0
- package/dist/viewer/watch.d.ts +9 -0
- package/dist/viewer/watch.js +46 -0
- package/dist/viewer/watch.js.map +1 -0
- package/dist/workflows/decision.d.ts +25 -0
- package/dist/workflows/decision.js +96 -0
- package/dist/workflows/decision.js.map +1 -0
- package/dist/workflows/definition.d.ts +9 -0
- package/dist/workflows/definition.js +61 -0
- package/dist/workflows/definition.js.map +1 -0
- package/dist/workflows/engine.d.ts +65 -0
- package/dist/workflows/engine.js +574 -0
- package/dist/workflows/engine.js.map +1 -0
- package/dist/workflows/errors.d.ts +9 -0
- package/dist/workflows/errors.js +24 -0
- package/dist/workflows/errors.js.map +1 -0
- package/dist/workflows/graph.d.ts +17 -0
- package/dist/workflows/graph.js +127 -0
- package/dist/workflows/graph.js.map +1 -0
- package/dist/workflows/index.d.ts +11 -0
- package/dist/workflows/index.js +11 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/json.d.ts +14 -0
- package/dist/workflows/json.js +134 -0
- package/dist/workflows/json.js.map +1 -0
- package/dist/workflows/loader.d.ts +28 -0
- package/dist/workflows/loader.js +94 -0
- package/dist/workflows/loader.js.map +1 -0
- package/dist/workflows/schema.d.ts +7 -0
- package/dist/workflows/schema.js +176 -0
- package/dist/workflows/schema.js.map +1 -0
- package/dist/workflows/shell.d.ts +9 -0
- package/dist/workflows/shell.js +177 -0
- package/dist/workflows/shell.js.map +1 -0
- package/dist/workflows/store.d.ts +35 -0
- package/dist/workflows/store.js +181 -0
- package/dist/workflows/store.js.map +1 -0
- package/dist/workflows/text.d.ts +10 -0
- package/dist/workflows/text.js +32 -0
- package/dist/workflows/text.js.map +1 -0
- package/dist/workflows/types.d.ts +280 -0
- package/dist/workflows/types.js +2 -0
- package/dist/workflows/types.js.map +1 -0
- package/docs/development.md +130 -0
- package/docs/run-bundles.md +114 -0
- package/docs/workflows.md +311 -0
- package/examples/workflows/autoimplement.workflow.ts +92 -0
- package/examples/workflows/autoresearch.workflow.ts +139 -0
- package/examples/workflows/branch.workflow.ts +63 -0
- package/examples/workflows/echo.workflow.ts +23 -0
- package/examples/workflows/elegant-solution.workflow.ts +95 -0
- package/examples/workflows/shell.workflow.ts +31 -0
- package/examples/workflows/two-turn.workflow.ts +64 -0
- package/package.json +80 -0
- package/src/extension/executor.ts +251 -0
- package/src/extension/index.ts +627 -0
- package/src/extension/widget.ts +183 -0
- package/src/render/ansi.ts +47 -0
- package/src/render/canvas.ts +196 -0
- package/src/render/format.ts +19 -0
- package/src/render/graph-render.ts +738 -0
- package/src/render/graph.ts +341 -0
- package/src/viewer/cli.ts +150 -0
- package/src/viewer/render.ts +236 -0
- package/src/viewer/tui.ts +159 -0
- package/src/viewer/watch.ts +55 -0
- package/src/workflows/decision.ts +127 -0
- package/src/workflows/definition.ts +104 -0
- package/src/workflows/engine.ts +793 -0
- package/src/workflows/errors.ts +27 -0
- package/src/workflows/graph.ts +161 -0
- package/src/workflows/index.ts +76 -0
- package/src/workflows/json.ts +155 -0
- package/src/workflows/loader.ts +123 -0
- package/src/workflows/schema.ts +218 -0
- package/src/workflows/shell.ts +199 -0
- package/src/workflows/store.ts +234 -0
- package/src/workflows/text.ts +34 -0
- package/src/workflows/types.ts +318 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
/**
|
|
3
|
+
* Context passed to node callbacks (prompt builders, compute/action runners,
|
|
4
|
+
* validators). `outputs` maps node ids to their accepted outputs; `results`
|
|
5
|
+
* maps node ids to the full result record of their latest attempt.
|
|
6
|
+
*/
|
|
7
|
+
export type WorkflowNodeContext<TInput = unknown> = {
|
|
8
|
+
input: TInput;
|
|
9
|
+
outputs: Record<string, unknown>;
|
|
10
|
+
results: Record<string, WorkflowNodeResult>;
|
|
11
|
+
state: WorkflowRunState;
|
|
12
|
+
/**
|
|
13
|
+
* Aborted when the node times out or the run is cancelled. Long-running
|
|
14
|
+
* callbacks should observe it (pass it to fetch/spawn or check
|
|
15
|
+
* `signal.aborted`) so side effects stop when the engine gives up on the
|
|
16
|
+
* node.
|
|
17
|
+
*/
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
};
|
|
20
|
+
export type WorkflowNodeCommon = {
|
|
21
|
+
/** Per-node timeout. Falls back to the engine default (15 minutes). */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** Short human-readable label shown in the viewer while the node runs. */
|
|
24
|
+
statusDetail?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Edges route between nodes. A node has at most one outgoing edge: either a
|
|
28
|
+
* plain `to` edge or a `switch` edge that routes on a JSON path into the
|
|
29
|
+
* node's output (`$.field`, `$output.field`) or result (`$result.outcome`).
|
|
30
|
+
*/
|
|
31
|
+
export type WorkflowEdge = {
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
} | {
|
|
35
|
+
from: string;
|
|
36
|
+
switch: {
|
|
37
|
+
on: string;
|
|
38
|
+
cases: Record<string, string>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A model-shaped step. The engine sends the prompt into the pi conversation
|
|
43
|
+
* and the model completes the step by calling the `workflow` tool with a JSON
|
|
44
|
+
* output. `expectedOutput` is appended to the step contract so the model
|
|
45
|
+
* knows what shape to submit. `validate` may reject (throw) or normalize the
|
|
46
|
+
* submitted output; rejections are surfaced to the model so it can retry
|
|
47
|
+
* within the same step.
|
|
48
|
+
*/
|
|
49
|
+
export type AgentNodeDefinition = WorkflowNodeCommon & {
|
|
50
|
+
nodeType: "agent";
|
|
51
|
+
prompt: (context: WorkflowNodeContext) => MaybePromise<string>;
|
|
52
|
+
expectedOutput?: string;
|
|
53
|
+
validate?: (output: unknown, context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
54
|
+
};
|
|
55
|
+
/** A pure local function: shape inputs, route, format, derive values. */
|
|
56
|
+
export type ComputeNodeDefinition = WorkflowNodeCommon & {
|
|
57
|
+
nodeType: "compute";
|
|
58
|
+
run: (context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
59
|
+
};
|
|
60
|
+
/** A deterministic runtime-owned step implemented as a local function. */
|
|
61
|
+
export type FunctionActionNodeDefinition = WorkflowNodeCommon & {
|
|
62
|
+
nodeType: "action";
|
|
63
|
+
run: (context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
export type ShellActionExecution = {
|
|
66
|
+
command: string;
|
|
67
|
+
args?: string[];
|
|
68
|
+
cwd?: string;
|
|
69
|
+
env?: Record<string, string>;
|
|
70
|
+
stdin?: string;
|
|
71
|
+
shell?: boolean | string;
|
|
72
|
+
allowNonZeroExit?: boolean;
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
/** Cap on captured stdout/stderr each, default 1,000,000 characters. */
|
|
75
|
+
maxOutputChars?: number;
|
|
76
|
+
};
|
|
77
|
+
export type ShellActionResult = {
|
|
78
|
+
command: string;
|
|
79
|
+
args: string[];
|
|
80
|
+
cwd: string;
|
|
81
|
+
stdout: string;
|
|
82
|
+
stderr: string;
|
|
83
|
+
exitCode: number | null;
|
|
84
|
+
signal: NodeJS.Signals | null;
|
|
85
|
+
durationMs: number;
|
|
86
|
+
};
|
|
87
|
+
/** A deterministic runtime-owned step implemented as a shell command. */
|
|
88
|
+
export type ShellActionNodeDefinition = WorkflowNodeCommon & {
|
|
89
|
+
nodeType: "action";
|
|
90
|
+
exec: (context: WorkflowNodeContext) => MaybePromise<ShellActionExecution>;
|
|
91
|
+
parse?: (result: ShellActionResult, context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
92
|
+
};
|
|
93
|
+
export type ActionNodeDefinition = FunctionActionNodeDefinition | ShellActionNodeDefinition;
|
|
94
|
+
/**
|
|
95
|
+
* A pause point. The run terminates with status `waiting` so a human (or an
|
|
96
|
+
* external trigger) can decide how to continue. The optional `run` callback
|
|
97
|
+
* produces the checkpoint's output before the run pauses.
|
|
98
|
+
*/
|
|
99
|
+
export type CheckpointNodeDefinition = WorkflowNodeCommon & {
|
|
100
|
+
nodeType: "checkpoint";
|
|
101
|
+
summary?: string;
|
|
102
|
+
run?: (context: WorkflowNodeContext) => MaybePromise<unknown>;
|
|
103
|
+
};
|
|
104
|
+
export type WorkflowNodeDefinition = AgentNodeDefinition | ComputeNodeDefinition | ActionNodeDefinition | CheckpointNodeDefinition;
|
|
105
|
+
export type WorkflowPresentationContext = {
|
|
106
|
+
/** Final persisted state of the workflow run. */
|
|
107
|
+
state: WorkflowRunState;
|
|
108
|
+
/** Convenience alias for `state.finalOutput`. */
|
|
109
|
+
finalOutput: unknown;
|
|
110
|
+
/** Aborted if a new run starts, the session closes, or prompt generation times out. */
|
|
111
|
+
signal: AbortSignal;
|
|
112
|
+
};
|
|
113
|
+
export type WorkflowDefinition = {
|
|
114
|
+
name: string;
|
|
115
|
+
/** Optional human-readable run title (static or derived from input). */
|
|
116
|
+
title?: string | ((context: {
|
|
117
|
+
input: unknown;
|
|
118
|
+
workflowName: string;
|
|
119
|
+
}) => MaybePromise<string | undefined>);
|
|
120
|
+
/**
|
|
121
|
+
* Optional instructions for a normal assistant response after the run ends.
|
|
122
|
+
* The Pi extension resolves this only after the final state is persisted;
|
|
123
|
+
* the engine and run bundle remain presentation-agnostic.
|
|
124
|
+
*/
|
|
125
|
+
presentationPrompt?: string | ((context: WorkflowPresentationContext) => MaybePromise<string | undefined>);
|
|
126
|
+
startAt: string;
|
|
127
|
+
nodes: Record<string, WorkflowNodeDefinition>;
|
|
128
|
+
edges: WorkflowEdge[];
|
|
129
|
+
/** Guard against unbounded loops. Defaults to the engine's maxSteps. */
|
|
130
|
+
maxSteps?: number;
|
|
131
|
+
};
|
|
132
|
+
export type WorkflowNodeOutcome = "ok" | "timed_out" | "failed" | "cancelled";
|
|
133
|
+
export type WorkflowNodeResult = {
|
|
134
|
+
attemptId: string;
|
|
135
|
+
nodeId: string;
|
|
136
|
+
nodeType: WorkflowNodeDefinition["nodeType"];
|
|
137
|
+
outcome: WorkflowNodeOutcome;
|
|
138
|
+
startedAt: string;
|
|
139
|
+
finishedAt: string;
|
|
140
|
+
durationMs: number;
|
|
141
|
+
output?: unknown;
|
|
142
|
+
error?: string;
|
|
143
|
+
};
|
|
144
|
+
export type WorkflowActionReceipt = {
|
|
145
|
+
actionType: "shell" | "function";
|
|
146
|
+
command?: string;
|
|
147
|
+
args?: string[];
|
|
148
|
+
cwd?: string;
|
|
149
|
+
exitCode?: number | null;
|
|
150
|
+
signal?: NodeJS.Signals | null;
|
|
151
|
+
durationMs?: number;
|
|
152
|
+
};
|
|
153
|
+
export type WorkflowStepRecord = {
|
|
154
|
+
attemptId: string;
|
|
155
|
+
nodeId: string;
|
|
156
|
+
nodeType: WorkflowNodeDefinition["nodeType"];
|
|
157
|
+
outcome: WorkflowNodeOutcome;
|
|
158
|
+
startedAt: string;
|
|
159
|
+
finishedAt: string;
|
|
160
|
+
promptText: string | null;
|
|
161
|
+
output: unknown;
|
|
162
|
+
error?: string;
|
|
163
|
+
action?: WorkflowActionReceipt;
|
|
164
|
+
};
|
|
165
|
+
export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed" | "timed_out" | "cancelled";
|
|
166
|
+
export type WorkflowRunState = {
|
|
167
|
+
runId: string;
|
|
168
|
+
workflowName: string;
|
|
169
|
+
runTitle?: string;
|
|
170
|
+
workflowPath?: string;
|
|
171
|
+
startedAt: string;
|
|
172
|
+
finishedAt?: string;
|
|
173
|
+
updatedAt: string;
|
|
174
|
+
status: WorkflowRunStatus;
|
|
175
|
+
input: unknown;
|
|
176
|
+
outputs: Record<string, unknown>;
|
|
177
|
+
results: Record<string, WorkflowNodeResult>;
|
|
178
|
+
steps: WorkflowStepRecord[];
|
|
179
|
+
currentNode?: string;
|
|
180
|
+
currentAttemptId?: string;
|
|
181
|
+
currentNodeType?: WorkflowNodeDefinition["nodeType"];
|
|
182
|
+
currentNodeStartedAt?: string;
|
|
183
|
+
statusDetail?: string;
|
|
184
|
+
/** True while the run is held at a step boundary by a pause request. */
|
|
185
|
+
paused?: boolean;
|
|
186
|
+
waitingOn?: string;
|
|
187
|
+
finalOutput?: unknown;
|
|
188
|
+
error?: string;
|
|
189
|
+
};
|
|
190
|
+
export type WorkflowNodeSnapshot = {
|
|
191
|
+
nodeType: WorkflowNodeDefinition["nodeType"];
|
|
192
|
+
timeoutMs?: number;
|
|
193
|
+
statusDetail?: string;
|
|
194
|
+
summary?: string;
|
|
195
|
+
expectedOutput?: string;
|
|
196
|
+
actionExecution?: "function" | "shell";
|
|
197
|
+
};
|
|
198
|
+
export type WorkflowDefinitionSnapshot = {
|
|
199
|
+
schema: "pi-workflows.definition-snapshot.v1";
|
|
200
|
+
name: string;
|
|
201
|
+
startAt: string;
|
|
202
|
+
nodes: Record<string, WorkflowNodeSnapshot>;
|
|
203
|
+
edges: WorkflowEdge[];
|
|
204
|
+
};
|
|
205
|
+
export type WorkflowTraceEvent = {
|
|
206
|
+
seq: number;
|
|
207
|
+
at: string;
|
|
208
|
+
scope: "run" | "node" | "agent" | "action";
|
|
209
|
+
type: string;
|
|
210
|
+
runId: string;
|
|
211
|
+
nodeId?: string;
|
|
212
|
+
attemptId?: string;
|
|
213
|
+
payload: Record<string, unknown>;
|
|
214
|
+
};
|
|
215
|
+
export type WorkflowTraceEventDraft = Omit<WorkflowTraceEvent, "seq" | "at" | "runId">;
|
|
216
|
+
export type WorkflowRunManifest = {
|
|
217
|
+
schema: "pi-workflows.run-bundle.v1";
|
|
218
|
+
runId: string;
|
|
219
|
+
workflowName: string;
|
|
220
|
+
runTitle?: string;
|
|
221
|
+
workflowPath?: string;
|
|
222
|
+
startedAt: string;
|
|
223
|
+
finishedAt?: string;
|
|
224
|
+
status: WorkflowRunStatus;
|
|
225
|
+
traceSchema: "pi-workflows.trace-event.v1";
|
|
226
|
+
paths: {
|
|
227
|
+
workflow: string;
|
|
228
|
+
state: string;
|
|
229
|
+
trace: string;
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
export type WorkflowRunResult = {
|
|
233
|
+
runDir: string;
|
|
234
|
+
state: WorkflowRunState;
|
|
235
|
+
};
|
|
236
|
+
/** The step contract handed to the executor alongside the prompt. */
|
|
237
|
+
export type AgentStepContract = {
|
|
238
|
+
runId: string;
|
|
239
|
+
workflowName: string;
|
|
240
|
+
nodeId: string;
|
|
241
|
+
attemptId: string;
|
|
242
|
+
expectedOutput?: string;
|
|
243
|
+
};
|
|
244
|
+
export type AgentStepRequest = {
|
|
245
|
+
contract: AgentStepContract;
|
|
246
|
+
prompt: string;
|
|
247
|
+
/**
|
|
248
|
+
* Validate a submission from the model. Returns the normalized output or an
|
|
249
|
+
* error message the executor should surface to the model for retry.
|
|
250
|
+
*/
|
|
251
|
+
accept: (output: unknown) => Promise<{
|
|
252
|
+
ok: true;
|
|
253
|
+
value: unknown;
|
|
254
|
+
} | {
|
|
255
|
+
ok: false;
|
|
256
|
+
error: string;
|
|
257
|
+
}>;
|
|
258
|
+
};
|
|
259
|
+
export type AgentStepSubmission = {
|
|
260
|
+
output: unknown;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Runs one agent step to completion. Implementations deliver the prompt to
|
|
264
|
+
* the model and resolve once a submission has been accepted via `accept`.
|
|
265
|
+
* Must reject with an `AbortError`-like error when `signal` aborts.
|
|
266
|
+
*/
|
|
267
|
+
export interface AgentStepExecutor {
|
|
268
|
+
runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission>;
|
|
269
|
+
}
|
|
270
|
+
export type WorkflowEngineOptions = {
|
|
271
|
+
executor: AgentStepExecutor;
|
|
272
|
+
/** Root directory for run bundles. Defaults to `~/.pi/agent/workflows/runs`. */
|
|
273
|
+
outputRoot?: string;
|
|
274
|
+
/** Default per-node timeout. Defaults to 15 minutes. */
|
|
275
|
+
defaultNodeTimeoutMs?: number;
|
|
276
|
+
/** Guard against unbounded graph loops. Defaults to 100 executed steps. */
|
|
277
|
+
maxSteps?: number;
|
|
278
|
+
/** Observer invoked after every persisted trace event. */
|
|
279
|
+
onEvent?: (event: WorkflowTraceEvent, state: WorkflowRunState) => void;
|
|
280
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/workflows/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Development guide
|
|
2
|
+
|
|
3
|
+
This document covers the standards for working on pi-workflows itself. For
|
|
4
|
+
authoring workflows, see [workflows.md](workflows.md).
|
|
5
|
+
|
|
6
|
+
## Layout and boundaries
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/workflows/ engine core: definitions, graph, engine, store, loader
|
|
10
|
+
src/extension/ pi integration: /workflow command, workflow tool, widget
|
|
11
|
+
src/viewer/ standalone TUI viewer over run bundles
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The dependency direction is enforced by `slophammer.yml`. `src/workflows`
|
|
15
|
+
imports nothing outside itself and never imports pi. `src/extension` and
|
|
16
|
+
`src/viewer` may import `src/workflows` and never each other. The viewer
|
|
17
|
+
observes runs purely through the bundle files, so it works from any process.
|
|
18
|
+
|
|
19
|
+
Within `src/render`, `graph.ts` computes a pure layered layout (ported from
|
|
20
|
+
the acpx replay viewer: labelled switch expansion, DFS back-edge detection,
|
|
21
|
+
longest-path layering, barycenter ordering, virtual pass-through cells for
|
|
22
|
+
long edges), `canvas.ts` is a character grid that merges box-drawing
|
|
23
|
+
characters by connectivity, and `graph-render.ts` turns a run bundle plus a
|
|
24
|
+
replay position into the drawn graph in one of two node styles: `box`
|
|
25
|
+
(bordered nodes, used by the viewer and the in-pi widget) or `line`
|
|
26
|
+
(single-line nodes). The widget windows the boxed graph around the active
|
|
27
|
+
node to stay inside pi's 10-line widget cap; `shift+↑`/`shift+↓` shortcuts
|
|
28
|
+
(registered through pi's `registerShortcut`) scroll that window manually, and
|
|
29
|
+
the scroll resets to follow mode when the run records a new step. `render.ts`
|
|
30
|
+
in `src/viewer`
|
|
31
|
+
composes the full detail view (header, graph, step timeline, step inspector)
|
|
32
|
+
and stays pure so tests can assert on rendered lines.
|
|
33
|
+
|
|
34
|
+
The renderer is built so that overlaps cannot corrupt the drawing: every
|
|
35
|
+
back edge owns exclusive lane rows and an exclusive gutter column, multiple
|
|
36
|
+
edges leaving one node fan out over separate exit columns, and labels are
|
|
37
|
+
drawn last through `textOverRun`/`textIfEmpty`, which refuse to overwrite
|
|
38
|
+
anything but a plain horizontal run or empty cells. `test/helpers/graph-verify.ts`
|
|
39
|
+
enforces this structurally: it re-parses the rendered characters, checks
|
|
40
|
+
every node box is unbroken, and traces every declared edge through the
|
|
41
|
+
actual box-drawing characters from source box to target arrow.
|
|
42
|
+
`test/graph-verify.test.ts` runs that verifier over 60 seeded random
|
|
43
|
+
workflow shapes at every replay position; if a rendering change breaks a
|
|
44
|
+
line, misplaces an arrow, or lets a label damage an edge, those tests fail
|
|
45
|
+
with the offending drawing in the assertion message.
|
|
46
|
+
|
|
47
|
+
Inside the engine, the pi-facing seam is the `AgentStepExecutor` interface.
|
|
48
|
+
The extension implements it on top of the live conversation
|
|
49
|
+
(`src/extension/executor.ts`), and tests implement it with a scripted fake
|
|
50
|
+
(`test/helpers.ts`). Anything that would couple the engine to pi belongs on
|
|
51
|
+
the extension side of that seam.
|
|
52
|
+
|
|
53
|
+
## Toolchain
|
|
54
|
+
|
|
55
|
+
Node 22+, ESM, TypeScript strict (including `exactOptionalPropertyTypes`).
|
|
56
|
+
Formatting is oxfmt, linting is oxlint with warnings denied, tests are vitest
|
|
57
|
+
with istanbul coverage. The single gate is:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm run check # format:check + lint + typecheck + build + test:coverage
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Coverage thresholds are 85% lines/functions/branches/statements, configured
|
|
64
|
+
in `vitest.config.ts`. The istanbul provider is deliberate. Workflow files are
|
|
65
|
+
loaded through jiti at runtime, and the v8 provider mismapped those modules;
|
|
66
|
+
istanbul instruments through the vitest transform pipeline only.
|
|
67
|
+
|
|
68
|
+
Slophammer runs in CI (coverage, complexity max 8, DRY max 0 findings,
|
|
69
|
+
dependency boundaries). Run it locally with:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx slophammer-ts@latest dry .
|
|
73
|
+
npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## End-to-end tests
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm run test:e2e
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The E2E suite (`test/e2e/`) is non-destructive and fully local. It starts a
|
|
83
|
+
mock OpenAI-compatible server (`test/e2e/mock-openai.ts`) whose scripted
|
|
84
|
+
"model" answers each step contract with a `workflow` tool call, then spawns
|
|
85
|
+
the real pi CLI from `devDependencies` in RPC mode with:
|
|
86
|
+
|
|
87
|
+
- `PI_CODING_AGENT_DIR` pointed at a temp agent dir containing a `models.json`
|
|
88
|
+
for the mock provider,
|
|
89
|
+
- `PI_WORKFLOWS_RUNS_DIR` pointed at a temp runs dir,
|
|
90
|
+
- the extension loaded from source with `-e src/extension/index.ts`.
|
|
91
|
+
|
|
92
|
+
It drives `/workflow` over the RPC protocol and asserts on the resulting run
|
|
93
|
+
bundle, then renders the finished run through the viewer CLI. Nothing outside
|
|
94
|
+
the temp directories is touched, and no real model is called.
|
|
95
|
+
|
|
96
|
+
## Publishing
|
|
97
|
+
|
|
98
|
+
The npm package is `@osolmaz/pi-workflows`. The first version must be published
|
|
99
|
+
locally because npm cannot configure a trusted publisher until the package
|
|
100
|
+
exists:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm publish --access public
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
After that first publish, configure npm trusted publishing for the
|
|
107
|
+
`osolmaz/pi-workflows` repository, `.github/workflows/publish.yml`, and the
|
|
108
|
+
`npm` GitHub environment. The workflow does not use a stored npm token.
|
|
109
|
+
|
|
110
|
+
For later versions:
|
|
111
|
+
|
|
112
|
+
1. Update `version` in `package.json` and `package-lock.json`, then merge that
|
|
113
|
+
change into the default branch.
|
|
114
|
+
2. Publish a GitHub Release whose tag is `v<version>`, such as `v0.2.0`.
|
|
115
|
+
3. Wait for the **Publish npm package** workflow to finish and verify the new
|
|
116
|
+
version on npm.
|
|
117
|
+
|
|
118
|
+
The workflow rejects mismatched tags, commits outside the default branch, and
|
|
119
|
+
versions already present on npm. It runs the full checks and end-to-end tests
|
|
120
|
+
before `npm publish --provenance`.
|
|
121
|
+
|
|
122
|
+
## Conventions
|
|
123
|
+
|
|
124
|
+
- Conventional Commits for commit messages and PR titles.
|
|
125
|
+
- Persisted JSON uses camelCase keys and versioned `schema` identifiers; see
|
|
126
|
+
[run-bundles.md](run-bundles.md). Breaking a persisted shape means bumping
|
|
127
|
+
the schema version string.
|
|
128
|
+
- Every exported API of the engine (`src/workflows/index.ts`) is covered by
|
|
129
|
+
unit tests; new node types or edge semantics need tests in `test/` and a
|
|
130
|
+
section in [workflows.md](workflows.md).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Run bundle format
|
|
2
|
+
|
|
3
|
+
Every workflow run persists to its own directory, called a run bundle. The
|
|
4
|
+
bundle is the contract between the engine and anything that observes runs,
|
|
5
|
+
including the bundled terminal viewer. This document specifies the format so
|
|
6
|
+
other tools can consume it.
|
|
7
|
+
|
|
8
|
+
## Location and layout
|
|
9
|
+
|
|
10
|
+
Bundles live under `~/.pi/agent/workflows/runs/` by default. The
|
|
11
|
+
`PI_WORKFLOWS_RUNS_DIR` environment variable overrides the location for both
|
|
12
|
+
the engine and the viewer, which is how the test suite keeps runs inside
|
|
13
|
+
temporary directories.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
~/.pi/agent/workflows/runs/
|
|
17
|
+
20260719T023912Z-autoimplement-3f2a9c1b/
|
|
18
|
+
manifest.json # pi-workflows.run-bundle.v1
|
|
19
|
+
workflow.json # pi-workflows.definition-snapshot.v1
|
|
20
|
+
state.json # full run projection
|
|
21
|
+
trace.ndjson # pi-workflows.trace-event.v1, append-only
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run ids are `<UTC timestamp>-<workflow slug>-<8 hex chars>`, so lexical order
|
|
25
|
+
is chronological order.
|
|
26
|
+
|
|
27
|
+
## Write discipline
|
|
28
|
+
|
|
29
|
+
Every JSON file in the bundle is written atomically (write to a temp file in
|
|
30
|
+
the same directory, then rename), so a reader never sees a partial document. `trace.ndjson` is append-only, one JSON object
|
|
31
|
+
per line, with writes serialized per file. After a run reaches a terminal
|
|
32
|
+
status (`completed`, `failed`, `timed_out`, `cancelled`, or `waiting`), the
|
|
33
|
+
bundle no longer changes.
|
|
34
|
+
|
|
35
|
+
A live viewer needs only two behaviors. Treat `state.json` as the current
|
|
36
|
+
projection and re-read it on any file change, and treat `trace.ndjson` as the
|
|
37
|
+
event timeline when history matters.
|
|
38
|
+
|
|
39
|
+
## manifest.json
|
|
40
|
+
|
|
41
|
+
Identity and pointers, kept in sync with the state on every snapshot:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"schema": "pi-workflows.run-bundle.v1",
|
|
46
|
+
"runId": "20260719T023912Z-autoimplement-3f2a9c1b",
|
|
47
|
+
"workflowName": "autoimplement",
|
|
48
|
+
"runTitle": "autoimplement: fix the flaky test",
|
|
49
|
+
"workflowPath": "/repo/.pi/workflows/autoimplement.workflow.ts",
|
|
50
|
+
"startedAt": "2026-07-19T02:39:12.412Z",
|
|
51
|
+
"finishedAt": "2026-07-19T02:41:03.977Z",
|
|
52
|
+
"status": "completed",
|
|
53
|
+
"traceSchema": "pi-workflows.trace-event.v1",
|
|
54
|
+
"paths": { "workflow": "workflow.json", "state": "state.json", "trace": "trace.ndjson" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## workflow.json
|
|
59
|
+
|
|
60
|
+
A serializable snapshot of the graph taken at run start. Functions such as
|
|
61
|
+
prompts and validators are not serialized. Each node keeps only its metadata
|
|
62
|
+
(`nodeType`, `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`,
|
|
63
|
+
`actionExecution`), and edges are copied verbatim. The snapshot is what lets
|
|
64
|
+
the viewer draw all nodes, including ones that have not run yet.
|
|
65
|
+
|
|
66
|
+
## state.json
|
|
67
|
+
|
|
68
|
+
The full run projection (`WorkflowRunState` in
|
|
69
|
+
[`src/workflows/types.ts`](../src/workflows/types.ts)). The `status` field is
|
|
70
|
+
one of `running`, `waiting`, `completed`, `failed`, `timed_out`, or
|
|
71
|
+
`cancelled`. While a node is executing, `currentNode`, `currentNodeType`,
|
|
72
|
+
`currentNodeStartedAt`, and `statusDetail` describe it, and they disappear
|
|
73
|
+
when the node finishes. While a pause request holds the run at a step
|
|
74
|
+
boundary, `paused` is `true` (with matching `run_paused`/`run_resumed` trace
|
|
75
|
+
events); it disappears when the run resumes or ends.
|
|
76
|
+
|
|
77
|
+
Per-node data lives in `outputs` (the accepted output of each finished node,
|
|
78
|
+
where the latest attempt wins on loops) and in `results` (the full result
|
|
79
|
+
record including the outcome and timing). The ordered history is `steps`,
|
|
80
|
+
with one record per node execution that includes the prompt text for agent
|
|
81
|
+
steps and an action receipt with the command, exit code, and duration for
|
|
82
|
+
action steps. When a run pauses at a checkpoint, `waitingOn` names the
|
|
83
|
+
checkpoint node. Terminal runs carry `finalOutput` on success and `error` on
|
|
84
|
+
failure.
|
|
85
|
+
|
|
86
|
+
## trace.ndjson
|
|
87
|
+
|
|
88
|
+
One event per line, monotonically sequenced per run:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"seq": 3,
|
|
93
|
+
"at": "2026-07-19T02:39:14.101Z",
|
|
94
|
+
"scope": "agent",
|
|
95
|
+
"type": "agent_prompt_sent",
|
|
96
|
+
"runId": "...",
|
|
97
|
+
"nodeId": "implement",
|
|
98
|
+
"attemptId": "...",
|
|
99
|
+
"payload": { "prompt": "..." }
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Event types: `run_started`, `node_started`, `agent_prompt_sent`,
|
|
104
|
+
`node_finished`, `node_failed`, and a terminal `run_<status>`. The `scope`
|
|
105
|
+
field (`run`, `node`, `agent`, `action`) groups them. Consumers should ignore
|
|
106
|
+
unknown event types so new ones can be added within the same schema version.
|
|
107
|
+
|
|
108
|
+
## Versioning
|
|
109
|
+
|
|
110
|
+
Each file carries a versioned schema identifier such as
|
|
111
|
+
`pi-workflows.run-bundle.v1`, and the identifier changes only on breaking
|
|
112
|
+
shape changes. Readers should check `manifest.json`'s `schema` field and skip
|
|
113
|
+
bundles they do not understand, which is exactly what the bundled viewer does
|
|
114
|
+
with unreadable directories.
|