@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Onur Solmaz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# pi-workflows
|
|
2
|
+
|
|
3
|
+
pi-workflows is a workflow extension for the [pi coding agent](https://pi.dev).
|
|
4
|
+
It lets you define multi-step agent workflows as TypeScript graphs, trigger
|
|
5
|
+
them at any point in a pi conversation with `/workflow`, and watch them run
|
|
6
|
+
live in a standalone terminal viewer.
|
|
7
|
+
|
|
8
|
+
The workflow model is a port of [openclaw/acpx](https://github.com/openclaw/acpx)
|
|
9
|
+
flows into pi itself. Agent steps run inside your current pi conversation, so
|
|
10
|
+
the model keeps everything it already knows from the discussion. The model
|
|
11
|
+
completes each step by calling a JSON `workflow` tool, which gives the engine
|
|
12
|
+
structured, validated output to route on.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@osolmaz/pi-workflows
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can also install directly from GitHub:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install git:github.com/osolmaz/pi-workflows
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or try the npm package without installing it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi -e npm:@osolmaz/pi-workflows
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `pi-workflows` viewer binary is part of the same package. To get it on
|
|
33
|
+
your PATH, clone the repo and run `npm install && npm run build && npm link`,
|
|
34
|
+
or run it in place with `npx tsx src/viewer/cli.ts`.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
Put a workflow file in `.pi/workflows/` (project) or `~/.pi/agent/workflows/`
|
|
39
|
+
(global):
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// .pi/workflows/echo.workflow.ts
|
|
43
|
+
import { agent, defineWorkflow } from "@osolmaz/pi-workflows";
|
|
44
|
+
|
|
45
|
+
export default defineWorkflow({
|
|
46
|
+
name: "echo",
|
|
47
|
+
presentationPrompt: "Give the user the concise reply from the workflow result.",
|
|
48
|
+
startAt: "reply",
|
|
49
|
+
nodes: {
|
|
50
|
+
reply: agent({
|
|
51
|
+
prompt: ({ input }) => `Answer concisely: ${(input as { task?: string }).task}`,
|
|
52
|
+
expectedOutput: `{ "reply": "your concise answer" }`,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
edges: [],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then, from any pi conversation:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/workflow echo summarize this repository
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`/workflow` with no arguments lists discovered workflows. `/workflow pause`
|
|
66
|
+
lets the current step finish and then holds the run before the next node —
|
|
67
|
+
useful when you want to interject in the conversation mid-workflow —
|
|
68
|
+
and `/workflow resume` continues it. Pressing escape to interrupt a turn
|
|
69
|
+
pauses the workflow automatically, so the run never nudges the model while
|
|
70
|
+
you have taken the conversation back; `/workflow resume` re-delivers the
|
|
71
|
+
pending step prompt. `/workflow cancel` stops the active run; if the last run
|
|
72
|
+
already ended (for example parked at a checkpoint), it clears the leftover
|
|
73
|
+
widget instead. Trailing text becomes `{ task: "..." }`; pass arbitrary input
|
|
74
|
+
with `--input-json {"key": "value"}`. The names `cancel`, `list`, `pause`,
|
|
75
|
+
and `resume` are reserved and rejected as workflow names.
|
|
76
|
+
|
|
77
|
+
While a run is on screen, the footer status bar shows a compact
|
|
78
|
+
`wf <name> [status] <node>` indicator alongside the widget.
|
|
79
|
+
|
|
80
|
+
`presentationPrompt` is optional. When present, pi-workflows uses it after the
|
|
81
|
+
structured run ends to request one normal, human-readable assistant response.
|
|
82
|
+
Workflows without it remain silent after their final structured output, which
|
|
83
|
+
keeps shell-only and machine-consumed workflows model-free.
|
|
84
|
+
|
|
85
|
+
Because the workflow runs in your current conversation, you can have a long
|
|
86
|
+
discussion first and then trigger a workflow that builds on it. The
|
|
87
|
+
`elegant-solution` example does exactly that. It asks the model for the most
|
|
88
|
+
elegant long-term production-ready solution to the problem you discussed, then
|
|
89
|
+
for the holy grail, then whether the two are the same (y/n). On `y` it routes
|
|
90
|
+
straight into implementation, and on `n` it asks the model to reconcile the
|
|
91
|
+
gap and pauses at a checkpoint for you to decide. In either case, its
|
|
92
|
+
`presentationPrompt` turns the final structured result into a plain assistant
|
|
93
|
+
response.
|
|
94
|
+
|
|
95
|
+
## Watching a run
|
|
96
|
+
|
|
97
|
+
Runs persist to `~/.pi/agent/workflows/runs/` as they execute. The viewer
|
|
98
|
+
tails that directory and re-renders on every state change:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pi-workflows view # interactive picker, live updates
|
|
102
|
+
pi-workflows view <runId> # jump straight to one run
|
|
103
|
+
pi-workflows runs # plain list of recent runs
|
|
104
|
+
pi-workflows view --once # print a snapshot and exit (good for scripts)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The run detail view draws the workflow as a boxed graph, like the acpx replay
|
|
108
|
+
viewer: every node sits in a box (heavy border for the active node), branches
|
|
109
|
+
carry their case labels, the taken path is highlighted, and loops route
|
|
110
|
+
through a gutter on the right back into their target from above. `←/→` scrubs
|
|
111
|
+
backwards and forwards through the recorded steps and re-derives every node's
|
|
112
|
+
status as of that step, with the selected step's full output shown below;
|
|
113
|
+
scrubbing to the end snaps back to following the run live.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
│ ┌──────────────────┐
|
|
117
|
+
▼ ▼ │
|
|
118
|
+
┌──────────────────────────┐ │
|
|
119
|
+
│ ✓ verify [action] 8.0s ×2 │ │
|
|
120
|
+
└──────────────────────────┘ │
|
|
121
|
+
│ │
|
|
122
|
+
▼ │
|
|
123
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
|
|
124
|
+
┃ ◐ review [agent] 12s ×2 ┃ │
|
|
125
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
|
|
126
|
+
┌─ clean ─┘ └─ issues ─┐ │
|
|
127
|
+
▼ ▼ │
|
|
128
|
+
┌───────────┐ ┌───────────────┐ │
|
|
129
|
+
│ · done │ │ ✓ fix [agent] │ │
|
|
130
|
+
└───────────┘ └───────────────┘ │
|
|
131
|
+
└──────────┘
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Inside pi, a widget above the editor shows the same boxed graph while a
|
|
135
|
+
workflow is running, windowed around the active node when it is taller than
|
|
136
|
+
pi's widget budget. Scroll the window with `shift+↑` / `shift+↓`; it snaps back
|
|
137
|
+
to following the active node whenever the workflow advances a step.
|
|
138
|
+
|
|
139
|
+
## Node types
|
|
140
|
+
|
|
141
|
+
A workflow is a graph of named nodes with exactly one entry point. Each node
|
|
142
|
+
finishes with a JSON output, and edges decide what runs next.
|
|
143
|
+
|
|
144
|
+
An `agent` node sends a prompt into the pi conversation and waits for the
|
|
145
|
+
model to submit its output through the `workflow` tool. A `compute` node runs
|
|
146
|
+
a pure TypeScript function. An `action` node performs a side effect, either a
|
|
147
|
+
TypeScript function (`action({ run })`) or a runtime-owned shell command
|
|
148
|
+
(`shell({ exec, parse })`). A `checkpoint` node ends the run in a `waiting`
|
|
149
|
+
state so a human can pick it up. On top of `agent`, the `decision` helper asks
|
|
150
|
+
the model to pick from a fixed set of choices and validates the answer, and
|
|
151
|
+
`decisionEdge` routes on the result with compile-time case checking.
|
|
152
|
+
|
|
153
|
+
See [docs/workflows.md](docs/workflows.md) for the full authoring reference
|
|
154
|
+
and [docs/run-bundles.md](docs/run-bundles.md) for the on-disk run format.
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
The [examples/workflows/](examples/workflows/) directory mirrors the acpx
|
|
159
|
+
example set. Copy any of them into `.pi/workflows/` to use them:
|
|
160
|
+
|
|
161
|
+
- `echo` is the smallest possible workflow, one agent step.
|
|
162
|
+
- `branch` classifies a task with a `decision` and routes to either a
|
|
163
|
+
continue lane or a clarification checkpoint.
|
|
164
|
+
- `shell` runs a runtime-owned shell command and parses its output, with no
|
|
165
|
+
agent step at all.
|
|
166
|
+
- `two-turn` chains three agent steps that build on each other's outputs in
|
|
167
|
+
the same conversation.
|
|
168
|
+
- `elegant-solution` is the mid-conversation trigger described above.
|
|
169
|
+
- `autoimplement` runs an implement, verify, review loop where the review
|
|
170
|
+
decision routes `issues_found` back to a fix step until it comes back
|
|
171
|
+
`clean`, bounded by `maxSteps`.
|
|
172
|
+
- `autoresearch` runs an iterative feature-search loop in the style of
|
|
173
|
+
[karpathy/autoresearch](https://github.com/karpathy/autoresearch): setup
|
|
174
|
+
creates a frozen evaluation harness, one editable feature file, and a
|
|
175
|
+
journal; each loop iteration runs one generation of experiments and
|
|
176
|
+
journals every result; an assess decision keeps looping until a kept
|
|
177
|
+
result plateaus or a diverse generation all fails, then conclusions are
|
|
178
|
+
written before the winner is promoted out of the loop directory.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AgentStepExecutor, AgentStepRequest, AgentStepSubmission } from "../workflows/types.js";
|
|
2
|
+
export type SubmissionResult = {
|
|
3
|
+
accepted: true;
|
|
4
|
+
message: string;
|
|
5
|
+
} | {
|
|
6
|
+
accepted: false;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
export type PromptDelivery = {
|
|
10
|
+
prompt: string;
|
|
11
|
+
/** True when the agent is known to be mid-run, so delivery must be queued. */
|
|
12
|
+
streaming: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type ConversationStepExecutorOptions = {
|
|
15
|
+
/** Deliver a prompt into the pi conversation. */
|
|
16
|
+
sendPrompt: (delivery: PromptDelivery) => void;
|
|
17
|
+
/** Reminders sent when the agent settles without submitting. Default 2. */
|
|
18
|
+
maxNudges?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* AgentStepExecutor that runs steps inside the current pi conversation. The
|
|
22
|
+
* engine hands it a prompt; it delivers the prompt as a user message and
|
|
23
|
+
* resolves once the model submits an accepted output through the `workflow`
|
|
24
|
+
* tool. If the agent settles without submitting, it nudges the model a
|
|
25
|
+
* bounded number of times before failing the step.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ConversationStepExecutor implements AgentStepExecutor {
|
|
28
|
+
private readonly sendPrompt;
|
|
29
|
+
private readonly maxNudges;
|
|
30
|
+
private pending;
|
|
31
|
+
private streaming;
|
|
32
|
+
private heldByUser;
|
|
33
|
+
constructor(options: ConversationStepExecutorOptions);
|
|
34
|
+
/** Track agent streaming state (wire to agent_start / agent_settled). */
|
|
35
|
+
setStreaming(streaming: boolean): void;
|
|
36
|
+
get pendingStepId(): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Hold the pending step for the user: no nudges are sent while held, so an
|
|
39
|
+
* escape-interrupted conversation stays quiet until the user resumes.
|
|
40
|
+
*/
|
|
41
|
+
hold(): void;
|
|
42
|
+
get held(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Release a user hold. When a step is still pending, its prompt is
|
|
45
|
+
* re-delivered so the model picks the step back up.
|
|
46
|
+
*/
|
|
47
|
+
release(): void;
|
|
48
|
+
runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission>;
|
|
49
|
+
/** Called by the `workflow` tool when the model submits a step output. */
|
|
50
|
+
submit(stepId: string, attemptId: string, output: unknown): Promise<SubmissionResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Called when the agent settles. Returns true when a nudge was sent, false
|
|
53
|
+
* when there was nothing to do. Fails the pending step once the nudge
|
|
54
|
+
* budget is exhausted.
|
|
55
|
+
*/
|
|
56
|
+
handleAgentSettled(): boolean;
|
|
57
|
+
private clearPending;
|
|
58
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const DEFAULT_MAX_NUDGES = 2;
|
|
2
|
+
/**
|
|
3
|
+
* AgentStepExecutor that runs steps inside the current pi conversation. The
|
|
4
|
+
* engine hands it a prompt; it delivers the prompt as a user message and
|
|
5
|
+
* resolves once the model submits an accepted output through the `workflow`
|
|
6
|
+
* tool. If the agent settles without submitting, it nudges the model a
|
|
7
|
+
* bounded number of times before failing the step.
|
|
8
|
+
*/
|
|
9
|
+
export class ConversationStepExecutor {
|
|
10
|
+
sendPrompt;
|
|
11
|
+
maxNudges;
|
|
12
|
+
pending = null;
|
|
13
|
+
streaming = false;
|
|
14
|
+
heldByUser = false;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.sendPrompt = options.sendPrompt;
|
|
17
|
+
this.maxNudges = options.maxNudges ?? DEFAULT_MAX_NUDGES;
|
|
18
|
+
}
|
|
19
|
+
/** Track agent streaming state (wire to agent_start / agent_settled). */
|
|
20
|
+
setStreaming(streaming) {
|
|
21
|
+
this.streaming = streaming;
|
|
22
|
+
}
|
|
23
|
+
get pendingStepId() {
|
|
24
|
+
return this.pending?.request.contract.nodeId ?? null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Hold the pending step for the user: no nudges are sent while held, so an
|
|
28
|
+
* escape-interrupted conversation stays quiet until the user resumes.
|
|
29
|
+
*/
|
|
30
|
+
hold() {
|
|
31
|
+
this.heldByUser = true;
|
|
32
|
+
}
|
|
33
|
+
get held() {
|
|
34
|
+
return this.heldByUser;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Release a user hold. When a step is still pending, its prompt is
|
|
38
|
+
* re-delivered so the model picks the step back up.
|
|
39
|
+
*/
|
|
40
|
+
release() {
|
|
41
|
+
if (!this.heldByUser) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.heldByUser = false;
|
|
45
|
+
const pending = this.pending;
|
|
46
|
+
if (!pending) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
this.sendPrompt({ prompt: pending.request.prompt, streaming: this.streaming });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.clearPending();
|
|
54
|
+
pending.reject(error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async runAgentStep(request, signal) {
|
|
58
|
+
if (this.pending) {
|
|
59
|
+
throw new Error("Another workflow step is already awaiting output");
|
|
60
|
+
}
|
|
61
|
+
return await new Promise((resolve, reject) => {
|
|
62
|
+
const onAbort = () => {
|
|
63
|
+
const reason = signal.reason ?? new Error("Workflow step aborted");
|
|
64
|
+
this.clearPending();
|
|
65
|
+
reject(reason);
|
|
66
|
+
};
|
|
67
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
let markCleared;
|
|
69
|
+
const cleared = new Promise((resolveCleared) => {
|
|
70
|
+
markCleared = resolveCleared;
|
|
71
|
+
});
|
|
72
|
+
this.pending = {
|
|
73
|
+
request,
|
|
74
|
+
resolve,
|
|
75
|
+
reject,
|
|
76
|
+
nudgesSent: 0,
|
|
77
|
+
cleanup: () => signal.removeEventListener("abort", onAbort),
|
|
78
|
+
cleared,
|
|
79
|
+
markCleared,
|
|
80
|
+
};
|
|
81
|
+
if (signal.aborted) {
|
|
82
|
+
onAbort();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
this.sendPrompt({ prompt: request.prompt, streaming: this.streaming });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// A failed delivery must not leave the step installed, or every
|
|
90
|
+
// subsequent agent node would fail with "already awaiting output".
|
|
91
|
+
this.clearPending();
|
|
92
|
+
reject(error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Called by the `workflow` tool when the model submits a step output. */
|
|
97
|
+
async submit(stepId, attemptId, output) {
|
|
98
|
+
const pending = this.pending;
|
|
99
|
+
if (!pending) {
|
|
100
|
+
return {
|
|
101
|
+
accepted: false,
|
|
102
|
+
message: "No workflow step is awaiting output. Do not call the workflow tool outside an active workflow step.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const expected = pending.request.contract.nodeId;
|
|
106
|
+
if (stepId !== expected) {
|
|
107
|
+
return {
|
|
108
|
+
accepted: false,
|
|
109
|
+
message: `Wrong step id ${JSON.stringify(stepId)}; the pending step is ${JSON.stringify(expected)}.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Loops revisit the same node id, so a delayed duplicate submission from
|
|
113
|
+
// an earlier attempt would otherwise be accepted as this attempt's output.
|
|
114
|
+
const expectedAttempt = pending.request.contract.attemptId;
|
|
115
|
+
if (attemptId !== expectedAttempt) {
|
|
116
|
+
return {
|
|
117
|
+
accepted: false,
|
|
118
|
+
message: `Stale attempt id ${JSON.stringify(attemptId)} for step ${JSON.stringify(stepId)}; the pending attempt is ${JSON.stringify(expectedAttempt)}. Use the attempt id from the latest step contract.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Race validation against the step being cleared: a hung `validate`
|
|
122
|
+
// callback must not leave this tool call (and therefore pi) blocked after
|
|
123
|
+
// a timeout or cancel already resolved the run.
|
|
124
|
+
const result = await Promise.race([
|
|
125
|
+
pending.request.accept(output),
|
|
126
|
+
pending.cleared.then(() => null),
|
|
127
|
+
]);
|
|
128
|
+
// The step may have timed out or been cancelled (and a newer step
|
|
129
|
+
// installed) while validation was awaited; a stale submission must not
|
|
130
|
+
// clear or resolve the newer pending step.
|
|
131
|
+
if (result === null || this.pending !== pending) {
|
|
132
|
+
return {
|
|
133
|
+
accepted: false,
|
|
134
|
+
message: `Step ${JSON.stringify(stepId)} is no longer awaiting output.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
return {
|
|
139
|
+
accepted: false,
|
|
140
|
+
message: `Output rejected for step ${JSON.stringify(stepId)}: ${result.error}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
this.clearPending();
|
|
144
|
+
pending.resolve({ output: result.value });
|
|
145
|
+
return {
|
|
146
|
+
accepted: true,
|
|
147
|
+
message: [
|
|
148
|
+
`Output accepted for step ${JSON.stringify(stepId)}.`,
|
|
149
|
+
"If the workflow continues, the next step arrives as a new user message. End your turn now.",
|
|
150
|
+
].join(" "),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Called when the agent settles. Returns true when a nudge was sent, false
|
|
155
|
+
* when there was nothing to do. Fails the pending step once the nudge
|
|
156
|
+
* budget is exhausted.
|
|
157
|
+
*/
|
|
158
|
+
handleAgentSettled() {
|
|
159
|
+
const pending = this.pending;
|
|
160
|
+
if (!pending) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (this.heldByUser) {
|
|
164
|
+
// The user interrupted deliberately; reminding the model now would
|
|
165
|
+
// steal the conversation back. The step waits for an explicit resume.
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (pending.nudgesSent >= this.maxNudges) {
|
|
169
|
+
this.clearPending();
|
|
170
|
+
pending.reject(new Error(`Agent settled ${pending.nudgesSent + 1} times without submitting step ${JSON.stringify(pending.request.contract.nodeId)} via the workflow tool`));
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
pending.nudgesSent += 1;
|
|
174
|
+
const { nodeId, attemptId } = pending.request.contract;
|
|
175
|
+
try {
|
|
176
|
+
this.sendPrompt({
|
|
177
|
+
prompt: [
|
|
178
|
+
`Reminder: workflow step ${JSON.stringify(nodeId)} is still awaiting your output.`,
|
|
179
|
+
"Complete it by calling the `workflow` tool with:",
|
|
180
|
+
`{"step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
181
|
+
`Expected output: ${pending.request.contract.expectedOutput ?? "a JSON object with your result"}`,
|
|
182
|
+
].join("\n"),
|
|
183
|
+
streaming: this.streaming,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// No reminder turn was started, so nothing would settle the step; fail
|
|
188
|
+
// it promptly instead of waiting out the node timeout.
|
|
189
|
+
this.clearPending();
|
|
190
|
+
pending.reject(error);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
clearPending() {
|
|
196
|
+
this.pending?.cleanup();
|
|
197
|
+
this.pending?.markCleared();
|
|
198
|
+
this.pending = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../src/extension/executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export type ParsedWorkflowArgs = {
|
|
3
|
+
kind: "list";
|
|
4
|
+
} | {
|
|
5
|
+
kind: "cancel";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "pause";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "resume";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "run";
|
|
12
|
+
ref: string;
|
|
13
|
+
input: unknown;
|
|
14
|
+
};
|
|
15
|
+
/** Parse `/workflow` arguments. Exported for tests. */
|
|
16
|
+
export declare function parseWorkflowArgs(args: string): ParsedWorkflowArgs;
|
|
17
|
+
export default function piWorkflows(pi: ExtensionAPI): void;
|