@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,311 @@
|
|
|
1
|
+
# Workflow authoring reference
|
|
2
|
+
|
|
3
|
+
This document is the authoring reference for pi-workflows definitions. It
|
|
4
|
+
covers the file format, every node type, edge routing, the step contract the
|
|
5
|
+
model sees, and how runs behave at runtime. For the on-disk run format, see
|
|
6
|
+
[run-bundles.md](run-bundles.md).
|
|
7
|
+
|
|
8
|
+
## Workflow files
|
|
9
|
+
|
|
10
|
+
A workflow is a TypeScript module whose default export is `defineWorkflow(...)`.
|
|
11
|
+
Files are discovered by suffix (`.workflow.ts`, `.workflow.js`, `.workflow.mts`,
|
|
12
|
+
`.workflow.mjs`) from two directories, in precedence order:
|
|
13
|
+
|
|
14
|
+
1. `.pi/workflows/` in the project (highest precedence on name collisions)
|
|
15
|
+
2. `~/.pi/agent/workflows/` globally
|
|
16
|
+
|
|
17
|
+
The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
|
|
18
|
+
runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
|
|
19
|
+
Files are loaded with [jiti](https://github.com/unjs/jiti), so plain TypeScript
|
|
20
|
+
works without a build step, and `import ... from "@osolmaz/pi-workflows"` resolves to
|
|
21
|
+
the engine that loaded the file.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { agent, compute, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
25
|
+
|
|
26
|
+
export default defineWorkflow({
|
|
27
|
+
name: "example",
|
|
28
|
+
title: ({ input }) => `example: ${(input as { task?: string }).task}`,
|
|
29
|
+
presentationPrompt: "Present the final answer clearly and concisely.",
|
|
30
|
+
startAt: "ask",
|
|
31
|
+
maxSteps: 50,
|
|
32
|
+
nodes: {
|
|
33
|
+
ask: agent({
|
|
34
|
+
prompt: ({ input }) => `Answer: ${(input as { task?: string }).task}`,
|
|
35
|
+
expectedOutput: `{ "answer": "text" }`,
|
|
36
|
+
}),
|
|
37
|
+
finish: compute({ run: ({ outputs }) => outputs.ask }),
|
|
38
|
+
},
|
|
39
|
+
edges: [{ from: "ask", to: "finish" }],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Top-level fields:
|
|
44
|
+
|
|
45
|
+
| Field | Type | Notes |
|
|
46
|
+
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
47
|
+
| `name` | `string` | Required. Used in run ids and the step contract. `cancel`, `list`, `pause`, and `resume` are reserved for `/workflow` subcommands. |
|
|
48
|
+
| `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
|
|
49
|
+
| `presentationPrompt` | `string` or function | Optional instructions for a normal assistant response after the run. A function receives `{ state, finalOutput, signal }` and may return a prompt or `undefined`. See [Result presentation](#result-presentation). |
|
|
50
|
+
| `startAt` | `string` | Required. Id of the first node. |
|
|
51
|
+
| `nodes` | `Record<string, node>` | Required, non-empty. Node ids must match `[A-Za-z_][A-Za-z0-9_-]*`. |
|
|
52
|
+
| `edges` | `WorkflowEdge[]` | Required. See routing below. |
|
|
53
|
+
| `maxSteps` | `number` | Optional loop bound, default 100. The run fails when exceeded. |
|
|
54
|
+
|
|
55
|
+
`defineWorkflow` validates the shape eagerly (node ids, edge shapes, function
|
|
56
|
+
fields) and validates the graph (unknown targets, duplicate outgoing edges,
|
|
57
|
+
unreachable nodes) when a run starts.
|
|
58
|
+
|
|
59
|
+
## Node context
|
|
60
|
+
|
|
61
|
+
Every node callback receives the same context object:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type WorkflowNodeContext = {
|
|
65
|
+
input: unknown; // the run input
|
|
66
|
+
outputs: Record<string, unknown>; // accepted output per finished node id
|
|
67
|
+
results: Record<string, WorkflowNodeResult>; // full result records, including failures
|
|
68
|
+
state: WorkflowRunState; // the live run state (read-only by convention)
|
|
69
|
+
signal: AbortSignal; // aborted on node timeout or run cancellation
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`outputs` only contains nodes that finished with outcome `ok`. When a node runs
|
|
74
|
+
more than once (a loop), the latest result wins. A failed retry removes the
|
|
75
|
+
node's earlier output from `outputs`.
|
|
76
|
+
|
|
77
|
+
Long-running compute, action, and checkpoint callbacks should observe
|
|
78
|
+
`context.signal` (pass it to `fetch`/`spawn`, or check `signal.aborted` between
|
|
79
|
+
steps). When the node times out or the run is cancelled, the engine stops
|
|
80
|
+
waiting immediately, but only cooperative callbacks stop doing work.
|
|
81
|
+
|
|
82
|
+
## Node types
|
|
83
|
+
|
|
84
|
+
### agent
|
|
85
|
+
|
|
86
|
+
Sends a prompt into the current pi conversation and waits for the model to
|
|
87
|
+
submit output through the `workflow` tool.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
agent({
|
|
91
|
+
prompt: ({ outputs }) => `Review this: ${JSON.stringify(outputs.implement)}`,
|
|
92
|
+
expectedOutput: `{ "verdict": "clean" | "issues_found" }`,
|
|
93
|
+
validate: (output) => output, // optional; throw to reject the submission
|
|
94
|
+
timeoutMs: 30 * 60_000, // optional; default 15 minutes
|
|
95
|
+
statusDetail: "reviewing", // optional; shown in widget and viewer
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The engine appends a step contract to the prompt (see below). When the model
|
|
100
|
+
calls the tool, the output passes through normalization (a JSON string is
|
|
101
|
+
parsed tolerantly) and then `validate`. If `validate` throws, the tool call
|
|
102
|
+
returns an error and the model can retry within the same step. If the agent
|
|
103
|
+
ends its turn without submitting, the extension nudges it, twice by default,
|
|
104
|
+
then fails the step.
|
|
105
|
+
|
|
106
|
+
### compute
|
|
107
|
+
|
|
108
|
+
Runs a TypeScript function inline. Use it for pure data shaping.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
compute({ run: ({ outputs }) => ({ merged: { ...outputs } }) });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### action
|
|
115
|
+
|
|
116
|
+
Performs a side effect. Two forms exist. The function form runs arbitrary
|
|
117
|
+
TypeScript:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
action({ run: async ({ input }) => await deployPreview(input) });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The shell form (`shell` is a synonym that requires `exec`) runs a command owned
|
|
124
|
+
by the runtime, so the workflow author decides exactly what executes, with a
|
|
125
|
+
timeout and captured output:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
shell({
|
|
129
|
+
exec: ({ input }) => ({
|
|
130
|
+
command: "git",
|
|
131
|
+
args: ["status", "--porcelain"],
|
|
132
|
+
cwd: "/path/to/repo",
|
|
133
|
+
timeoutMs: 10_000,
|
|
134
|
+
allowNonZeroExit: false,
|
|
135
|
+
}),
|
|
136
|
+
parse: (result) => ({ dirty: result.stdout.trim().length > 0 }),
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Without `parse`, the node output is the full `ShellActionResult` (`stdout`,
|
|
141
|
+
`stderr`, `exitCode`, `signal`, `durationMs`). A non-zero exit fails the node
|
|
142
|
+
unless `allowNonZeroExit` is set. Captured stdout and stderr are each capped
|
|
143
|
+
(default 1,000,000 characters, configurable with `maxOutputChars`) so verbose
|
|
144
|
+
commands cannot exhaust memory. Both action forms record a receipt (command,
|
|
145
|
+
exit code, duration) in the step record for auditability, including when the
|
|
146
|
+
command fails.
|
|
147
|
+
|
|
148
|
+
### checkpoint
|
|
149
|
+
|
|
150
|
+
Ends the run in a `waiting` state for human review. Runs after a checkpoint do
|
|
151
|
+
not resume automatically; the checkpoint output is the run's final output.
|
|
152
|
+
Because nothing resumes past a checkpoint, graph validation rejects outgoing
|
|
153
|
+
edges from checkpoint nodes.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
checkpoint({
|
|
157
|
+
summary: "human decides how to proceed",
|
|
158
|
+
run: ({ outputs }) => outputs.reconcile, // optional; default output is { summary }
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### decision
|
|
163
|
+
|
|
164
|
+
`decision` is sugar over `agent` for constrained choices. It builds the prompt
|
|
165
|
+
suffix listing the choices, sets `expectedOutput`, and validates that the
|
|
166
|
+
submitted object carries one of the allowed values in the decision field
|
|
167
|
+
(default `route`).
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const choices = ["y", "n"] as const;
|
|
171
|
+
|
|
172
|
+
decision({
|
|
173
|
+
choices,
|
|
174
|
+
question: ({ outputs }) => `Same as proposed? ${JSON.stringify(outputs.propose)}`,
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Pair it with `decisionEdge`, which builds the matching `switch` edge and makes
|
|
179
|
+
a missing case a compile-time error:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
decisionEdge({ from: "compare", choices, cases: { y: "implement", n: "reconcile" } });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Edges and routing
|
|
186
|
+
|
|
187
|
+
Each node has at most one outgoing edge. A plain edge is unconditional:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
{ from: "a", to: "b" }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A `switch` edge routes on a JSON path evaluated against the node's result:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
{ from: "review", switch: { on: "$.route", cases: { clean: "done", issues_found: "fix" } } }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Path roots:
|
|
200
|
+
|
|
201
|
+
- `$.field` and `$output.field` read from the node's accepted output.
|
|
202
|
+
- `$result.field` reads from the result record. `$result.outcome` is the main
|
|
203
|
+
use, with values `ok`, `failed`, `timed_out`, or `cancelled`, which lets a
|
|
204
|
+
workflow route failures to a recovery node instead of failing the run.
|
|
205
|
+
|
|
206
|
+
A missing case for the resolved value fails the run with a routing error. A
|
|
207
|
+
node with no outgoing edge (or no matching failure route) ends the run:
|
|
208
|
+
`completed` on success, `failed`/`timed_out`/`cancelled` otherwise.
|
|
209
|
+
|
|
210
|
+
## The step contract
|
|
211
|
+
|
|
212
|
+
Every `agent` prompt ends with a step contract block naming the workflow, the
|
|
213
|
+
step id, the attempt id, and the expected output shape:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
---
|
|
217
|
+
Workflow step contract (workflow: autoimplement, step: review, attempt: 6f9d…)
|
|
218
|
+
|
|
219
|
+
Complete this step by calling the `workflow` tool exactly once with:
|
|
220
|
+
{"step": "review", "attempt": "6f9d…", "output": <your result>}
|
|
221
|
+
Expected output: { "route": "clean" | "issues_found", "reason": "short justification" }
|
|
222
|
+
The step is complete only after the workflow tool accepts the output.
|
|
223
|
+
If the tool reports a validation error, correct the output and call it again.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The `workflow` tool takes `{ step, attempt, output }`. Submissions are
|
|
227
|
+
rejected (with a reason the model sees) when no step is pending, the step id
|
|
228
|
+
is wrong, the attempt id belongs to an earlier attempt of the same node (loops
|
|
229
|
+
revisit node ids, so each attempt gets a fresh id), or `validate` throws.
|
|
230
|
+
Acceptance resolves the step and the engine advances; the next agent prompt
|
|
231
|
+
arrives as a new user message in the same conversation.
|
|
232
|
+
|
|
233
|
+
## Result presentation
|
|
234
|
+
|
|
235
|
+
Workflow nodes produce structured JSON for routing and persistence. When a
|
|
236
|
+
person should see a normal prose response after the run, add
|
|
237
|
+
`presentationPrompt` at the top level:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
export default defineWorkflow({
|
|
241
|
+
name: "report",
|
|
242
|
+
presentationPrompt: ({ state, finalOutput }) =>
|
|
243
|
+
state.status === "waiting"
|
|
244
|
+
? `Explain this recommendation and ask the user to decide: ${JSON.stringify(finalOutput)}`
|
|
245
|
+
: "Summarize the completed result and any remaining limitations.",
|
|
246
|
+
// ...startAt, nodes, and edges
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
After the final run state has been persisted, the Pi extension sends the
|
|
251
|
+
presentation instructions and bounded final result to the model as a hidden
|
|
252
|
+
follow-up message. The next visible message is a normal assistant response.
|
|
253
|
+
Returning `undefined`, returning an empty string, or omitting
|
|
254
|
+
`presentationPrompt` produces no follow-up. Cancelled runs are never
|
|
255
|
+
presented. Async prompt builders have 30 seconds to finish and receive an
|
|
256
|
+
`AbortSignal` that fires on timeout, session shutdown, or when a new workflow
|
|
257
|
+
or normal user turn starts; stale presentations are discarded. Once a presentation message has
|
|
258
|
+
been queued, another workflow cannot start until that assistant response
|
|
259
|
+
settles, so results cannot interleave.
|
|
260
|
+
|
|
261
|
+
Presentation is outside the workflow graph: it cannot route to another node,
|
|
262
|
+
change the run status, or alter the run bundle. If prompt generation or message
|
|
263
|
+
delivery fails, the extension reports a warning and leaves the finished run
|
|
264
|
+
unchanged. Opting in adds one hidden custom message and one assistant response
|
|
265
|
+
to the normal Pi session; it adds no other persistent data and uses no Pi
|
|
266
|
+
internals.
|
|
267
|
+
|
|
268
|
+
## Runtime behavior
|
|
269
|
+
|
|
270
|
+
Runs execute one node at a time. Every transition is persisted to the run
|
|
271
|
+
bundle before the engine moves on, which is what makes the live viewer
|
|
272
|
+
possible. Defaults worth knowing:
|
|
273
|
+
|
|
274
|
+
- Node timeout is 15 minutes unless the node sets `timeoutMs`. A timed-out
|
|
275
|
+
node has outcome `timed_out` and can be routed with `$result.outcome`.
|
|
276
|
+
- `maxSteps` (workflow-level, default 100) bounds loops built from cycles in
|
|
277
|
+
the graph.
|
|
278
|
+
- `/workflow pause` requests a pause: the current step finishes normally,
|
|
279
|
+
then the run holds at the step boundary (`paused: true` in the run state,
|
|
280
|
+
`run_paused` in the trace) until `/workflow resume` or `/workflow cancel`.
|
|
281
|
+
Pausing never interrupts a node mid-flight.
|
|
282
|
+
- Interrupting a turn (escape) auto-pauses the run: the pending agent step is
|
|
283
|
+
held without nudges and the engine pauses at the next boundary. Node
|
|
284
|
+
timeouts keep ticking while held, so a long-abandoned step still times out.
|
|
285
|
+
`/workflow resume` re-delivers the pending step prompt.
|
|
286
|
+
- `/workflow cancel` aborts the current node and marks the run `cancelled`.
|
|
287
|
+
When no run is live but the widget still shows a parked or finished run,
|
|
288
|
+
the same command clears the widget.
|
|
289
|
+
- One workflow runs per session at a time.
|
|
290
|
+
- Agent nudges: if the model ends its turn without submitting the pending
|
|
291
|
+
step, it gets a reminder, twice by default, then the step fails.
|
|
292
|
+
|
|
293
|
+
## Using the engine outside pi
|
|
294
|
+
|
|
295
|
+
The engine is pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`, so
|
|
296
|
+
tests (and other hosts) can script agent steps:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { WorkflowEngine, type AgentStepExecutor } from "@osolmaz/pi-workflows";
|
|
300
|
+
|
|
301
|
+
const executor: AgentStepExecutor = {
|
|
302
|
+
async runAgentStep(request) {
|
|
303
|
+
const accepted = await request.accept({ answer: "42" });
|
|
304
|
+
if (!accepted.ok) throw new Error(accepted.error);
|
|
305
|
+
return { output: accepted.value };
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const engine = new WorkflowEngine({ executor, outputRoot: "/tmp/runs" });
|
|
310
|
+
const { state } = await engine.run(workflow, { task: "..." });
|
|
311
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { agent, compute, decision, decisionEdge, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type AutoimplementInput = {
|
|
4
|
+
task?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const reviewChoices = ["clean", "issues_found"] as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Implement, verify, then loop a self-review until it comes back clean. The
|
|
11
|
+
* decision edge routes `issues_found` back to the fix step, and the engine's
|
|
12
|
+
* maxSteps guard bounds the loop.
|
|
13
|
+
*/
|
|
14
|
+
export default defineWorkflow({
|
|
15
|
+
name: "autoimplement",
|
|
16
|
+
title: ({ input }) => {
|
|
17
|
+
const task = (input as AutoimplementInput).task;
|
|
18
|
+
return task ? `autoimplement: ${task.slice(0, 60)}` : undefined;
|
|
19
|
+
},
|
|
20
|
+
presentationPrompt:
|
|
21
|
+
"Summarize what was implemented, what verification passed, and any remaining limitation. Be concise and direct.",
|
|
22
|
+
maxSteps: 20,
|
|
23
|
+
startAt: "implement",
|
|
24
|
+
nodes: {
|
|
25
|
+
implement: agent({
|
|
26
|
+
timeoutMs: 60 * 60_000,
|
|
27
|
+
statusDetail: "implementing",
|
|
28
|
+
prompt: ({ input }) => {
|
|
29
|
+
const task =
|
|
30
|
+
(input as AutoimplementInput).task ?? "the plan discussed so far in this conversation";
|
|
31
|
+
return [
|
|
32
|
+
`Implement ${task} end-to-end.`,
|
|
33
|
+
"Aim for the most elegant, long-term production-ready solution without gold-plating.",
|
|
34
|
+
].join("\n");
|
|
35
|
+
},
|
|
36
|
+
expectedOutput: `{ "summary": "what was implemented", "files": ["changed file", "changed file"] }`,
|
|
37
|
+
}),
|
|
38
|
+
verify: agent({
|
|
39
|
+
timeoutMs: 30 * 60_000,
|
|
40
|
+
statusDetail: "verifying",
|
|
41
|
+
prompt: () =>
|
|
42
|
+
[
|
|
43
|
+
"Verify the implementation.",
|
|
44
|
+
"Run the test suite plus any relevant builds, linters, or local smoke tests.",
|
|
45
|
+
"Do not run destructive commands.",
|
|
46
|
+
].join("\n"),
|
|
47
|
+
expectedOutput: `{ "passed": true | false, "details": "what was run and what happened" }`,
|
|
48
|
+
}),
|
|
49
|
+
review: decision({
|
|
50
|
+
choices: reviewChoices,
|
|
51
|
+
question: ({ outputs }) =>
|
|
52
|
+
[
|
|
53
|
+
"Critically review your implementation as a strict reviewer.",
|
|
54
|
+
"Look for correctness bugs, missed requirements, and failing checks.",
|
|
55
|
+
"Pick `issues_found` if anything must be fixed, otherwise `clean`.",
|
|
56
|
+
"",
|
|
57
|
+
`Verification: ${JSON.stringify(outputs.verify)}`,
|
|
58
|
+
].join("\n"),
|
|
59
|
+
}),
|
|
60
|
+
fix: agent({
|
|
61
|
+
timeoutMs: 30 * 60_000,
|
|
62
|
+
statusDetail: "fixing",
|
|
63
|
+
prompt: ({ outputs }) =>
|
|
64
|
+
[
|
|
65
|
+
"Fix the issues you found in review, then stop.",
|
|
66
|
+
"",
|
|
67
|
+
`Review: ${JSON.stringify(outputs.review)}`,
|
|
68
|
+
].join("\n"),
|
|
69
|
+
expectedOutput: `{ "fixed": "what was changed" }`,
|
|
70
|
+
}),
|
|
71
|
+
finalize: compute({
|
|
72
|
+
run: ({ outputs }) => ({
|
|
73
|
+
implementation: outputs.implement,
|
|
74
|
+
verification: outputs.verify,
|
|
75
|
+
review: outputs.review,
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
edges: [
|
|
80
|
+
{ from: "implement", to: "verify" },
|
|
81
|
+
{ from: "verify", to: "review" },
|
|
82
|
+
decisionEdge({
|
|
83
|
+
from: "review",
|
|
84
|
+
choices: reviewChoices,
|
|
85
|
+
cases: {
|
|
86
|
+
clean: "finalize",
|
|
87
|
+
issues_found: "fix",
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
{ from: "fix", to: "verify" },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { agent, compute, decision, decisionEdge, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type AutoresearchInput = {
|
|
4
|
+
/** What to search for, e.g. "a feature separating group A from group B". */
|
|
5
|
+
goal?: string;
|
|
6
|
+
/** Loop directory for the three artifacts; created if missing. */
|
|
7
|
+
dir?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const assessChoices = ["continue", "plateau", "dead_end"] as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An iterative feature-search loop in the style of karpathy/autoresearch.
|
|
14
|
+
* The discipline lives in three artifacts kept in one directory: a harness
|
|
15
|
+
* that never changes during the search, a single feature file that changes
|
|
16
|
+
* every experiment, and a journal that records every run whether it worked
|
|
17
|
+
* or not. Each loop iteration is one generation of candidates; the assess
|
|
18
|
+
* decision keeps looping until a kept result plateaus or a diverse
|
|
19
|
+
* generation all fails.
|
|
20
|
+
*/
|
|
21
|
+
export default defineWorkflow({
|
|
22
|
+
name: "autoresearch",
|
|
23
|
+
title: ({ input }) => {
|
|
24
|
+
const goal = (input as AutoresearchInput).goal;
|
|
25
|
+
return goal ? `autoresearch: ${goal.slice(0, 60)}` : undefined;
|
|
26
|
+
},
|
|
27
|
+
presentationPrompt:
|
|
28
|
+
"Report the research conclusion in plain language: the winner and its numbers, or the strongest negative result, plus the journal path.",
|
|
29
|
+
maxSteps: 40,
|
|
30
|
+
startAt: "setup",
|
|
31
|
+
nodes: {
|
|
32
|
+
setup: agent({
|
|
33
|
+
timeoutMs: 20 * 60_000,
|
|
34
|
+
statusDetail: "setting up harness",
|
|
35
|
+
prompt: ({ input }) => {
|
|
36
|
+
const { goal, dir } = input as AutoresearchInput;
|
|
37
|
+
return [
|
|
38
|
+
`Set up an autoresearch loop for: ${goal ?? "the research goal discussed so far in this conversation"}.`,
|
|
39
|
+
`Create the loop directory (${dir ?? "pick a sensible project-local directory"}) with three artifacts:`,
|
|
40
|
+
"",
|
|
41
|
+
"1. A harness that is FROZEN after the first run. It loads the dataset",
|
|
42
|
+
" once, calls the candidate feature on every item, and prints the",
|
|
43
|
+
" metrics that decide keep-or-discard (for a two-group separation:",
|
|
44
|
+
" AUC, edge margin, and leave-one-out accuracy with a refit",
|
|
45
|
+
" threshold). Held-out items are printed but never used for selection.",
|
|
46
|
+
"2. A feature file with one function of a fixed signature. This is the",
|
|
47
|
+
" ONLY file that changes between experiments; state the input",
|
|
48
|
+
" contract in its docstring.",
|
|
49
|
+
"3. A program.md recording the goal, the input contract, what is in",
|
|
50
|
+
" and out of bounds, the keep criterion, and the rule that every run",
|
|
51
|
+
" is journaled. Start an empty journal file next to it.",
|
|
52
|
+
"",
|
|
53
|
+
"Run the harness once on a trivial baseline feature to prove the",
|
|
54
|
+
"plumbing works, then journal that baseline as experiment 0.",
|
|
55
|
+
].join("\n");
|
|
56
|
+
},
|
|
57
|
+
expectedOutput: `{ "dir": "loop directory", "keepCriterion": "what counts as a keeper", "baseline": "baseline numbers" }`,
|
|
58
|
+
}),
|
|
59
|
+
experiment: agent({
|
|
60
|
+
timeoutMs: 30 * 60_000,
|
|
61
|
+
statusDetail: "running experiments",
|
|
62
|
+
prompt: ({ outputs }) =>
|
|
63
|
+
[
|
|
64
|
+
"Run the next generation of experiments in the loop directory.",
|
|
65
|
+
"",
|
|
66
|
+
"One experiment is one edit to the feature file followed by one",
|
|
67
|
+
"harness run. Batch cheap candidates: when testing many ideas in one",
|
|
68
|
+
"generation, evaluate them all in one sweep process. The harness",
|
|
69
|
+
"stays frozen; if a bug forces a change, rerun every kept result and",
|
|
70
|
+
"journal the change.",
|
|
71
|
+
"",
|
|
72
|
+
"Decide the next edit from what the numbers said, not the original",
|
|
73
|
+
"plan. Chase the failure case closest to the boundary. Vary the",
|
|
74
|
+
"current winner before trusting it: a result that survives only one",
|
|
75
|
+
"parameterization is a tuned cliff, a plateau across neighboring",
|
|
76
|
+
"parameters is a finding. Never select on held-out items.",
|
|
77
|
+
"",
|
|
78
|
+
"Journal every candidate — negatives with the same care as",
|
|
79
|
+
"positives — as: idea in one line, the numbers, keep or discard,",
|
|
80
|
+
"and what to try next.",
|
|
81
|
+
"",
|
|
82
|
+
`Setup: ${JSON.stringify(outputs.setup)}`,
|
|
83
|
+
].join("\n"),
|
|
84
|
+
expectedOutput: `{ "generation": "what was tried", "best": "best numbers so far", "kept": true | false, "next": "most informative next experiment, if any" }`,
|
|
85
|
+
}),
|
|
86
|
+
assess: decision({
|
|
87
|
+
choices: assessChoices,
|
|
88
|
+
statusDetail: "assessing progress",
|
|
89
|
+
question: ({ outputs }) =>
|
|
90
|
+
[
|
|
91
|
+
"Assess the search after this generation.",
|
|
92
|
+
"",
|
|
93
|
+
"- `continue`: there is an informative next experiment to run.",
|
|
94
|
+
"- `plateau`: a kept result is stable across neighboring parameter",
|
|
95
|
+
" choices and further edits only trade margin sideways.",
|
|
96
|
+
"- `dead_end`: a generation of diverse candidates all failed, so the",
|
|
97
|
+
" signal is not where the program said it would be.",
|
|
98
|
+
"",
|
|
99
|
+
`Latest generation: ${JSON.stringify(outputs.experiment)}`,
|
|
100
|
+
].join("\n"),
|
|
101
|
+
}),
|
|
102
|
+
conclude: agent({
|
|
103
|
+
timeoutMs: 15 * 60_000,
|
|
104
|
+
statusDetail: "writing conclusions",
|
|
105
|
+
prompt: ({ outputs }) =>
|
|
106
|
+
[
|
|
107
|
+
"Finish the journal with a conclusions section: the winning feature",
|
|
108
|
+
"and its numbers (or the strongest negative result), the plateau",
|
|
109
|
+
"evidence, and the negative results that stop future re-litigation.",
|
|
110
|
+
"Only after the journal is complete, promote the winner out of the",
|
|
111
|
+
"loop directory into the real analysis code, if there is one.",
|
|
112
|
+
"",
|
|
113
|
+
`Assessment: ${JSON.stringify(outputs.assess)}`,
|
|
114
|
+
`Latest generation: ${JSON.stringify(outputs.experiment)}`,
|
|
115
|
+
].join("\n"),
|
|
116
|
+
expectedOutput: `{ "outcome": "plateau" | "dead_end", "winner": "winning feature and numbers, or null", "journal": "path to the finished journal" }`,
|
|
117
|
+
}),
|
|
118
|
+
finalize: compute({
|
|
119
|
+
run: ({ outputs }) => ({
|
|
120
|
+
setup: outputs.setup,
|
|
121
|
+
conclusions: outputs.conclude,
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
edges: [
|
|
126
|
+
{ from: "setup", to: "experiment" },
|
|
127
|
+
{ from: "experiment", to: "assess" },
|
|
128
|
+
decisionEdge({
|
|
129
|
+
from: "assess",
|
|
130
|
+
choices: assessChoices,
|
|
131
|
+
cases: {
|
|
132
|
+
continue: "experiment",
|
|
133
|
+
plateau: "conclude",
|
|
134
|
+
dead_end: "conclude",
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
{ from: "conclude", to: "finalize" },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { agent, checkpoint, decision, decisionEdge, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type BranchInput = {
|
|
4
|
+
task?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const classifyChoices = ["continue", "checkpoint"] as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* decision() + decisionEdge() constrained-choice classification, then a
|
|
11
|
+
* deterministic branch. Analogous to the acpx branch example.
|
|
12
|
+
*/
|
|
13
|
+
export default defineWorkflow({
|
|
14
|
+
name: "branch",
|
|
15
|
+
presentationPrompt: ({ state }) =>
|
|
16
|
+
state.status === "waiting"
|
|
17
|
+
? "Explain briefly why the task needs clarification, then ask the user one concrete clarification question."
|
|
18
|
+
: "Tell the user in one concise sentence how the workflow recommends proceeding.",
|
|
19
|
+
startAt: "classify",
|
|
20
|
+
nodes: {
|
|
21
|
+
classify: decision({
|
|
22
|
+
choices: classifyChoices,
|
|
23
|
+
question: ({ input }) => {
|
|
24
|
+
const task =
|
|
25
|
+
(input as BranchInput).task ??
|
|
26
|
+
"Investigate a flaky test and decide whether the request is clear enough to continue.";
|
|
27
|
+
return [
|
|
28
|
+
"Read the task below.",
|
|
29
|
+
"Pick `continue` if it is concrete and scoped.",
|
|
30
|
+
"Pick `checkpoint` if it is ambiguous or needs clarification.",
|
|
31
|
+
"",
|
|
32
|
+
`Task: ${task}`,
|
|
33
|
+
].join("\n");
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
continue_lane: agent({
|
|
37
|
+
prompt: ({ outputs }) =>
|
|
38
|
+
[
|
|
39
|
+
"We are on the continue path.",
|
|
40
|
+
`Decision: ${JSON.stringify(outputs.classify)}`,
|
|
41
|
+
"Summarize in one sentence how you would proceed.",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
expectedOutput: `{ "summary": "short explanation" }`,
|
|
44
|
+
}),
|
|
45
|
+
checkpoint_lane: checkpoint({
|
|
46
|
+
summary: "needs clarification",
|
|
47
|
+
run: ({ outputs }) => ({
|
|
48
|
+
route: "checkpoint",
|
|
49
|
+
summary: (outputs.classify as { reason?: string }).reason ?? "Needs clarification.",
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
edges: [
|
|
54
|
+
decisionEdge({
|
|
55
|
+
from: "classify",
|
|
56
|
+
choices: classifyChoices,
|
|
57
|
+
cases: {
|
|
58
|
+
continue: "continue_lane",
|
|
59
|
+
checkpoint: "checkpoint_lane",
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { agent, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
2
|
+
|
|
3
|
+
type EchoInput = {
|
|
4
|
+
task?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** Smallest possible workflow: one agent step that submits a JSON reply. */
|
|
8
|
+
export default defineWorkflow({
|
|
9
|
+
name: "echo",
|
|
10
|
+
presentationPrompt:
|
|
11
|
+
"Give the user the concise reply from the workflow result, with no extra commentary.",
|
|
12
|
+
startAt: "reply",
|
|
13
|
+
nodes: {
|
|
14
|
+
reply: agent({
|
|
15
|
+
prompt: ({ input }) => {
|
|
16
|
+
const request = (input as EchoInput).task ?? "Summarize this repository in one sentence.";
|
|
17
|
+
return `Answer the following request concisely.\n\nRequest: ${request}`;
|
|
18
|
+
},
|
|
19
|
+
expectedOutput: `{ "reply": "your concise answer" }`,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
edges: [],
|
|
23
|
+
});
|