@chrok/pi-braid 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -56
- package/dist/command.js +126 -7
- package/dist/core-version.js +17 -0
- package/dist/display.js +63 -14
- package/dist/drain.js +11 -5
- package/dist/index.js +155 -95
- package/dist/jobs.js +150 -16
- package/dist/runner.js +183 -80
- package/dist/status-text.js +111 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,37 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@chrok/pi-braid)
|
|
4
4
|
|
|
5
|
-
This optional Pi extension runs Braid agent graphs as background jobs
|
|
5
|
+
This optional Pi extension runs Braid agent graphs as background jobs, with
|
|
6
6
|
bounded loops, live updates, pause/resume, and a live flow panel.
|
|
7
7
|
|
|
8
|
-
**0.
|
|
8
|
+
**0.5 API:** The npm badge shows the published version; see
|
|
9
9
|
[GitHub releases](https://github.com/Epsirom/braid/releases) for release notes.
|
|
10
|
-
Review the [0.
|
|
11
|
-
before upgrading:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
Review the [0.4 → 0.5 migration guide](https://github.com/Epsirom/braid/blob/v0.5.1/docs/compatibility.md#migrating-from-04-to-05)
|
|
11
|
+
before upgrading: running executions can pause and resume, configured node
|
|
12
|
+
timeouts pause by default, and blocking mode is removed. Users coming from
|
|
13
|
+
0.3 also need the [0.3 → 0.4 guide](https://github.com/Epsirom/braid/blob/v0.5.1/docs/compatibility.md#migrating-from-03-to-04)
|
|
14
|
+
for print/JSON job draining and Pi SDK behavior. Users coming from 0.2 also need the
|
|
15
|
+
[0.2 → 0.3 guide](https://github.com/Epsirom/braid/blob/v0.5.1/docs/compatibility.md#migrating-from-02-to-03),
|
|
15
16
|
and users coming from 0.1 the
|
|
16
|
-
[0.1 → 0.2 guide](https://github.com/Epsirom/braid/blob/v0.
|
|
17
|
+
[0.1 → 0.2 guide](https://github.com/Epsirom/braid/blob/v0.5.1/docs/compatibility.md#migrating-from-01-to-02).
|
|
17
18
|
For the previous release, use the
|
|
18
|
-
[0.
|
|
19
|
+
[0.5.0 guide](https://github.com/Epsirom/braid/blob/v0.5.0/integrations/pi/README.md).
|
|
19
20
|
|
|
20
21
|
It registers:
|
|
21
22
|
|
|
22
23
|
- `braid` — submit a graph with optional bounded loops; receive a `jobId`
|
|
23
|
-
immediately
|
|
24
|
-
final results in deprecated blocking mode.
|
|
24
|
+
immediately (print/JSON runs wait for it before exiting).
|
|
25
25
|
- `braid_status` — retrieve progress and results with `{ "jobId": "..." }`, or
|
|
26
26
|
add `"executionId": "..."` for an exact invocation's full output/error.
|
|
27
27
|
`"nodeId": "..."` selects that definition's latest invocation. Omit all IDs
|
|
28
28
|
to list jobs in the current session.
|
|
29
29
|
- `braid_update` / `braid_resume` — edit live definitions or release paused executions.
|
|
30
|
-
- `
|
|
30
|
+
- `braid_pause` — pause running executions, or the whole job, without losing their
|
|
31
|
+
work; with `after: true`, hold a running execution's result once it finishes.
|
|
32
|
+
- `braid_cancel` — cancel a job with `{ "jobId": "..." }`, or add
|
|
33
|
+
`"executionIds"` to fail only those executions.
|
|
31
34
|
- `braid_finish` — print/JSON runs only: record the run outcome with
|
|
32
35
|
`{ "status": "success" | "failure", "summary": "..." }`.
|
|
33
36
|
- `/braid [jobId]` — open a live flow panel in interactive Pi.
|
|
34
|
-
- `/braid:blocking [on|off]` — deprecated, removed in 0.5.0: enable or disable
|
|
35
|
-
blocking for new `braid` tool calls; omit the argument to show the current mode.
|
|
36
37
|
- `/braid:review [target or focus]` — run a bounded parallel review/fix loop.
|
|
37
38
|
- `/braid:clear` — clear the progress widget above the editor; run it again while
|
|
38
39
|
the widget is empty to restore what was cleared.
|
|
@@ -107,41 +108,13 @@ after outstanding Braid jobs drain. Hosts that stay running and want reminders
|
|
|
107
108
|
to start new runs after settling should bind with another mode, for example
|
|
108
109
|
`session.bindExtensions({ mode: "rpc" })`.
|
|
109
110
|
|
|
110
|
-
###
|
|
111
|
+
### Migrating from blocking mode
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
> prints a one-time warning: on stderr in print/JSON mode, or as a UI notice.
|
|
118
|
-
|
|
119
|
-
Until then, to make each `braid` call wait in the foreground, enable blocking when starting Pi:
|
|
120
|
-
|
|
121
|
-
```sh
|
|
122
|
-
pi --braid-blocking -p "Use Braid to review the changes and summarize the results."
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
With `@chrok/pi-braid` installed, `--braid-blocking` makes every `braid` tool call
|
|
126
|
-
wait for the entire graph and workspace cleanup, then return final status, outputs,
|
|
127
|
-
and usage directly. The parent can summarize those results before `pi -p` exits.
|
|
128
|
-
It also works with `--mode json` for structured tool results. The CLI flag sets the
|
|
129
|
-
initial mode; within a session, use `/braid:blocking on` to enable it,
|
|
130
|
-
`/braid:blocking off` to disable it, or `/braid:blocking` to check the current mode.
|
|
131
|
-
Switching affects only new `braid` tool calls; running jobs keep their original
|
|
132
|
-
mode. The session override is not persisted. These are user controls, not model
|
|
133
|
-
tool parameters; the default remains background execution.
|
|
134
|
-
For CI pass/fail decisions, inspect the returned job status: a failed graph does
|
|
135
|
-
not by itself set Pi's process exit code.
|
|
136
|
-
Blocking calls emit an initial progress update with the job handle while waiting.
|
|
137
|
-
They suppress node/job completion reminders except for cancellation: stopping the
|
|
138
|
-
foreground response cancels their work, waits for cleanup, and sends a reminder
|
|
139
|
-
so the next turn can retrieve partial outputs and recovery checkpoints.
|
|
140
|
-
Wait/result retrieval errors include the existing job handle and any terminal
|
|
141
|
-
usage; retrieve that job with `braid_status` instead of submitting the graph again.
|
|
142
|
-
`pauseAfter: true` is rejected before starting a blocking job because it requires
|
|
143
|
-
a later resume call. The interactive
|
|
144
|
-
`/braid:review` command continues to use its background review workflow.
|
|
113
|
+
Blocking mode, `--braid-blocking`, and `/braid:blocking` are removed in 0.5.0.
|
|
114
|
+
Drop the flag or command. Print and JSON runs wait for background jobs before
|
|
115
|
+
exiting, and `braid_finish` sets the run outcome. The parent submits the graph,
|
|
116
|
+
ends its turn, and retrieves results with `braid_status` after the completion
|
|
117
|
+
reminder. `pauseAfter`, reminders, and live updates remain available.
|
|
145
118
|
|
|
146
119
|
Jobs live in memory for the current Pi session. Quitting, reloading extensions,
|
|
147
120
|
or switching/forking sessions aborts outstanding work and suppresses its
|
|
@@ -261,7 +234,18 @@ the current graph revision and paused execution IDs, then:
|
|
|
261
234
|
```
|
|
262
235
|
|
|
263
236
|
Pass this to `braid_update`. It validates and commits the full change and resume
|
|
264
|
-
atomically.
|
|
237
|
+
atomically.
|
|
238
|
+
|
|
239
|
+
`pauseAfter` is captured when an execution starts, so changing it with
|
|
240
|
+
`braid_update` affects only later executions. To review the result of an
|
|
241
|
+
execution that is already running, call `braid_pause({jobId, executionIds,
|
|
242
|
+
after: true})`. The execution is not suspended: it keeps running, and once it
|
|
243
|
+
finishes, completed or failed, it is held exactly like `pauseAfter`, with a pause
|
|
244
|
+
reminder. `braid_resume({jobId, executionIds, after: true})` removes the hold
|
|
245
|
+
while it is still running, including one that came from `pauseAfter`. Either
|
|
246
|
+
way only those executions change, not their node definitions. A result that is
|
|
247
|
+
already held is released with the usual `braid_resume` or `braid_update`
|
|
248
|
+
`resume`. To continue without edits, call `braid_resume({jobId,
|
|
265
249
|
expectedRevision, executionIds})`. Definitions can be changed while executions
|
|
266
250
|
are running or waiting, including inside a loop. Existing instances keep their
|
|
267
251
|
captured prompt, inputs, and policy; completion routes through the latest graph.
|
|
@@ -274,6 +258,19 @@ Pause reminders describe the event when it occurred: check current status and
|
|
|
274
258
|
paused IDs before attempting an update/resume, since the job can time out or be
|
|
275
259
|
cancelled before the parent handles the reminder.
|
|
276
260
|
|
|
261
|
+
Execute and decision nodes have a `request_replan` tool. A node calls it to
|
|
262
|
+
report a finding that contradicts its prompt's assumptions, makes its task
|
|
263
|
+
unnecessary, or affects more than its task, with a reason and optionally a
|
|
264
|
+
suggested graph edit (`upsertNodes`, `removeNodeIds`, `addEdges`, `removeEdges`)
|
|
265
|
+
that core checks against the current graph. Nodes cannot change the graph
|
|
266
|
+
themselves, and they see only the goal, their prompt, and direct predecessors:
|
|
267
|
+
the parent decides which planned work a finding affects. Saying in a node's
|
|
268
|
+
prompt what related work other nodes handle makes its requests more precise. The parent gets a waking reminder at once;
|
|
269
|
+
the node keeps working, and its result is held when it finishes, like
|
|
270
|
+
`pauseAfter`. `braid_status` shows the reason and suggestion. The parent can stop
|
|
271
|
+
other branches with `braid_pause`, then change the graph and release the node
|
|
272
|
+
with `braid_update` and `resume`, keep the plan with `braid_resume`, or cancel.
|
|
273
|
+
|
|
277
274
|
Focused `braid_status` reads also start with the current control fields: graph
|
|
278
275
|
status, revision, and paused execution IDs. Use that current revision for
|
|
279
276
|
updates even when inspecting an older invocation.
|
|
@@ -298,6 +295,50 @@ See [execution control](../../docs/execution-control.md) for loop schemas, exact
|
|
|
298
295
|
update semantics, historical dependencies, and workspace lineage. There is no
|
|
299
296
|
scheduler persistence across Pi reloads. Reminders alone do not pause execution.
|
|
300
297
|
|
|
298
|
+
### Pausing running executions
|
|
299
|
+
|
|
300
|
+
`pauseAfter` holds a result after the execution finishes. To stop work that is
|
|
301
|
+
still running, call `braid_pause({jobId, executionIds})`. Each execution finishes
|
|
302
|
+
its running tool, then stops before its next model request. It keeps its
|
|
303
|
+
conversation, workspace, and tool budgets, and releases its concurrency slot to
|
|
304
|
+
other nodes. Messages sent to it from the `/braid` assistant while it is paused
|
|
305
|
+
are delivered when it resumes. Resume with `braid_resume({jobId, expectedRevision,
|
|
306
|
+
executionIds})` or `braid_update` with `resume`; the worker continues the same
|
|
307
|
+
conversation. A transient-error retry also waits at the pause point before resending.
|
|
308
|
+
|
|
309
|
+
Omit `executionIds` to pause the whole job: no new executions start and running
|
|
310
|
+
ones pause. `braid_resume({jobId})` resumes the job and the executions that the
|
|
311
|
+
job pause paused; executions paused separately stay paused.
|
|
312
|
+
|
|
313
|
+
The node timeout stops while an execution is suspended; the graph timeout keeps
|
|
314
|
+
running. `braid_status` shows suspended executions, a job pause, and who paused
|
|
315
|
+
them.
|
|
316
|
+
|
|
317
|
+
### Node timeouts
|
|
318
|
+
|
|
319
|
+
When an execution exceeds `options.nodeTimeoutMs`, it is paused instead of
|
|
320
|
+
failed. Its current model request or shell command is stopped; the interrupted
|
|
321
|
+
call, with any output it produced, and any calls after it in the same batch are
|
|
322
|
+
recorded in its conversation. Its conversation, workspace, and tool budgets are
|
|
323
|
+
kept, and its successors wait. The parent is woken with a reminder that says how
|
|
324
|
+
long the execution ran, what it was doing, and what it can do next:
|
|
325
|
+
|
|
326
|
+
- `braid_resume({jobId, expectedRevision, executionIds})` gives it the job's
|
|
327
|
+
`nodeTimeoutMs` again; add `nodeTimeoutMs` for a different budget.
|
|
328
|
+
- `braid_update` with `resume` and `resumeOptions: { nodeTimeoutMs }` resumes it
|
|
329
|
+
together with graph changes.
|
|
330
|
+
- `braid_cancel({jobId, executionIds})` fails only that execution with
|
|
331
|
+
`NODE_TIMEOUT`; its successors follow the usual failure rules.
|
|
332
|
+
|
|
333
|
+
Resuming the whole job does not resume a timeout pause. Set
|
|
334
|
+
`options.onNodeTimeout: "fail"` to fail on timeout instead.
|
|
335
|
+
|
|
336
|
+
Users can pause and hold from the panel too (`p` and `h`, below). The parent is not woken for
|
|
337
|
+
that: one notice with the latest state per execution or job is added to its next
|
|
338
|
+
turn, and it is told not to resume work the user paused unless the user asks.
|
|
339
|
+
In print and JSON runs, a job held only by pauses counts as stalled: the parent
|
|
340
|
+
gets one warning, and the job is cancelled if it ends the next turn without resuming.
|
|
341
|
+
|
|
301
342
|
## Live flow panel
|
|
302
343
|
|
|
303
344
|
In interactive Pi, background progress appears automatically **above the input
|
|
@@ -323,13 +364,24 @@ The bordered panel keeps the job header and keyboard controls visible while
|
|
|
323
364
|
you scroll the flow and event log. It refreshes as nodes start, finish, fail,
|
|
324
365
|
and pass outputs downstream.
|
|
325
366
|
Use Left/Right to select jobs, Up/Down or Page Up/Page Down to scroll, `c` to
|
|
326
|
-
cancel the selected job, and Escape or `q` to close the panel.
|
|
367
|
+
cancel the selected job, `p` to pause or resume it, and Escape or `q` to close the panel.
|
|
368
|
+
In node selection or an open session, `p` pauses or resumes that node's latest
|
|
369
|
+
execution instead; a suspended node shows `‖ PAUSED`, and one paused by its node
|
|
370
|
+
timeout also shows `timed out`. `h` holds that execution's result once it
|
|
371
|
+
finishes, removes the hold if one is set (its status shows `· hold`), or
|
|
372
|
+
releases a result that is already held. Press `x` twice there to cancel only
|
|
373
|
+
that execution; the parent gets a notice with its next turn. Closing the panel
|
|
327
374
|
leaves jobs running. In RPC or noninteractive modes, use `braid_status`.
|
|
328
375
|
|
|
329
376
|
A flowchart wider than the terminal is cropped to the terminal width around the
|
|
330
377
|
first running node, or from its left edge; a `[chart 1–76 of 245 cols]` line
|
|
331
378
|
shows the visible columns.
|
|
332
379
|
|
|
380
|
+
For loops, a forward edge opposite a feedback edge is drawn dotted, while the
|
|
381
|
+
retry feedback edge stays solid. For example, `/braid:review` shows the direct
|
|
382
|
+
`scope → check` handoff dotted and `check → scope` (`retry`) solid. All edges and
|
|
383
|
+
choice labels remain visible; runtime dependencies and handoffs are unchanged.
|
|
384
|
+
|
|
333
385
|
Press `v` to select a node: the arrow keys move a `◆` marker through the
|
|
334
386
|
flowchart (starting at a running node), and the view pans horizontally and
|
|
335
387
|
scrolls vertically to keep the selected node in sight. If the flowchart cannot be
|
|
@@ -617,7 +669,9 @@ pass any of these fields in the `braid` tool's `options`:
|
|
|
617
669
|
| --- | --- |
|
|
618
670
|
| `maxToolRounds` | Maximum assistant responses containing tool calls, per node |
|
|
619
671
|
| `maxToolCalls` | Maximum total requested tool calls, per node |
|
|
620
|
-
| `
|
|
672
|
+
| `maxModelRetries` | Retries per model request for transient provider errors; default 3, `0` disables, at most 10 |
|
|
673
|
+
| `nodeTimeoutMs` | Active time allowed for each node after it starts, excluding pauses, in milliseconds |
|
|
674
|
+
| `onNodeTimeout` | `pause` (default) suspends a timed-out node and wakes the parent; `fail` fails it with `NODE_TIMEOUT` |
|
|
621
675
|
| `graphTimeoutMs` | Time allowed for the entire graph, including queueing and pauses, in milliseconds |
|
|
622
676
|
| `maxExecutions` | Total materialized execution records, including skips; default 1000, positive safe integer |
|
|
623
677
|
|
|
@@ -695,9 +749,8 @@ fails that node cleanly and does not invoke another provider.
|
|
|
695
749
|
|
|
696
750
|
The Pi extension has no node or graph time limit by default. Use `braid_cancel`
|
|
697
751
|
or press `c` in the flow panel to abort running nodes and mark queued nodes
|
|
698
|
-
`cancelled`.
|
|
699
|
-
|
|
700
|
-
foreground response also cancels its Braid job. To set a node or graph timeout,
|
|
752
|
+
`cancelled`. Escape stops the foreground response or closes the panel without
|
|
753
|
+
cancelling jobs. To set a node or graph timeout,
|
|
701
754
|
supply milliseconds in the submission tool input under `options`; each omitted
|
|
702
755
|
timeout remains unlimited:
|
|
703
756
|
|
|
@@ -709,8 +762,8 @@ timeout remains unlimited:
|
|
|
709
762
|
}
|
|
710
763
|
```
|
|
711
764
|
|
|
712
|
-
The adapter sets `
|
|
713
|
-
|
|
765
|
+
The adapter sets `cacheRetention: "none"` and gives each node a fresh provider
|
|
766
|
+
context. Tool budgets are unlimited unless `maxToolRounds` or
|
|
714
767
|
`maxToolCalls` is set. Missing paths, invalid arguments, disallowed writes, and
|
|
715
768
|
unavailable tools are returned as tool errors so the node can recover. Provider
|
|
716
769
|
authentication and calls may still incur normal provider costs. Braid cannot
|
|
@@ -719,6 +772,29 @@ cancellation. Cancellation prevents further tool calls but does not roll back
|
|
|
719
772
|
writes already made. Core waits for tracked writes, preserves checkpoints, and
|
|
720
773
|
removes worker worktrees before completing cancellation.
|
|
721
774
|
|
|
775
|
+
### Transient model errors
|
|
776
|
+
|
|
777
|
+
A transient provider error, such as a rate limit (HTTP 429), overload, a 5xx
|
|
778
|
+
response, or a dropped connection, is retried inside the running node execution.
|
|
779
|
+
The node keeps its conversation, tool results, workspace, decision, merge state,
|
|
780
|
+
and tool-budget counts; the failed request is sent again with the same context.
|
|
781
|
+
Tools run only after a complete model response, so a retry never repeats a
|
|
782
|
+
completed tool call. `options.maxModelRetries` sets the retries for each model
|
|
783
|
+
request (default 3; `0` disables them):
|
|
784
|
+
|
|
785
|
+
- If the provider rejects a request before its response starts, Pi's provider
|
|
786
|
+
client retries it with exponential backoff and honors `Retry-After`. A requested
|
|
787
|
+
wait longer than 60 seconds fails immediately.
|
|
788
|
+
- If a response stream fails after it started, Braid discards the partial
|
|
789
|
+
response and restarts the request after 2, 4, then 8 seconds.
|
|
790
|
+
|
|
791
|
+
Quota, billing, and other non-transient errors are not retried. Retries stop on
|
|
792
|
+
cancellation, and Braid does not start a wait that would outlast the node or
|
|
793
|
+
graph deadline. When retries run out, the node fails with the provider error and
|
|
794
|
+
follows the usual failure rules, including `requireSuccess`. Failed attempts stay
|
|
795
|
+
visible in the session view, and their reported usage is counted. Braid does not
|
|
796
|
+
retry a whole node execution.
|
|
797
|
+
|
|
722
798
|
The tool output is capped at 50KB/2000 lines to protect Pi context. When exceeded,
|
|
723
799
|
the adapter writes the complete text to a temporary file (mode 600 on POSIX;
|
|
724
800
|
inherited ACLs on Windows) and ends the preview with its path, for running jobs
|
package/dist/command.js
CHANGED
|
@@ -38,6 +38,10 @@ export class BraidPanel {
|
|
|
38
38
|
chatOffset = 0;
|
|
39
39
|
chatMaxOffset = 0;
|
|
40
40
|
chatScope;
|
|
41
|
+
/** The outcome of the last pause, resume, or cancel key, until the next key. */
|
|
42
|
+
notice;
|
|
43
|
+
/** An execution x was pressed for once; a second x cancels it. */
|
|
44
|
+
cancelling;
|
|
41
45
|
input;
|
|
42
46
|
session;
|
|
43
47
|
chat;
|
|
@@ -163,20 +167,20 @@ export class BraidPanel {
|
|
|
163
167
|
? ["Enter send Esc stop or hide Tab hide PgUp/PgDn or wheel scroll ctrl+c close",
|
|
164
168
|
"Enter send · Esc hide · Tab hide", "Enter send · Tab hide"]
|
|
165
169
|
: this.focus
|
|
166
|
-
? [`${tab}Esc back ←/→ executions ↑/↓ scroll End follow ctrl+o tools c cancel q close`,
|
|
167
|
-
"Esc back · ←/→ executions ·
|
|
170
|
+
? [`${tab}Esc back ←/→ executions ↑/↓ scroll End follow ctrl+o tools p pause h hold x cancel execution c cancel job q close`,
|
|
171
|
+
"Esc back · ←/→ executions · p pause · q close", "Esc back · q close"]
|
|
168
172
|
: this.choosing !== undefined
|
|
169
|
-
? [`${tab}↑/↓/←/→ choose node Enter open session Esc back PgUp/PgDn page q close`,
|
|
170
|
-
"↑/↓ choose · Enter open · Esc back", "Enter open · Esc back"]
|
|
171
|
-
: [`${tab}v select node ←/→ jobs ↑/↓ scroll
|
|
172
|
-
"v select node · ←/→ jobs ·
|
|
173
|
+
? [`${tab}↑/↓/←/→ choose node Enter open session p pause h hold x cancel execution Esc back PgUp/PgDn page q close`,
|
|
174
|
+
"↑/↓ choose · Enter open · p pause · Esc back", "Enter open · Esc back"]
|
|
175
|
+
: [`${tab}v select node ←/→ jobs ↑/↓ scroll p pause job c cancel Esc close`,
|
|
176
|
+
"v select node · ←/→ jobs · p pause job · Esc close", "v select · Esc close"];
|
|
173
177
|
const help = contentWidth >= 80 ? wide : contentWidth >= 42 ? medium : narrow;
|
|
174
178
|
const status = graph ? graph.range : "Assistant";
|
|
175
179
|
return [
|
|
176
180
|
...header,
|
|
177
181
|
...body.map(line => line === SEPARATOR ? rule("├", "┤") : row(line)),
|
|
178
182
|
rule("├", "┤"),
|
|
179
|
-
row(this.theme.fg("dim", `${status}${contentWidth >= 60 ? " · Jobs keep running when this panel closes" : ""}`)),
|
|
183
|
+
row(this.theme.fg("dim", this.notice ? plain(this.notice) : `${status}${contentWidth >= 60 ? " · Jobs keep running when this panel closes" : ""}`)),
|
|
180
184
|
row(this.theme.fg("muted", help)),
|
|
181
185
|
rule("╰", "╯"),
|
|
182
186
|
];
|
|
@@ -396,6 +400,24 @@ export class BraidPanel {
|
|
|
396
400
|
const jobs = this.jobs.list();
|
|
397
401
|
const index = Math.max(0, jobs.findIndex((job) => job.jobId === this.selected));
|
|
398
402
|
const job = this.selected ? this.jobs.get(this.selected) : undefined;
|
|
403
|
+
this.notice = undefined;
|
|
404
|
+
const confirming = this.cancelling;
|
|
405
|
+
this.cancelling = undefined;
|
|
406
|
+
if (data === "p" && job) {
|
|
407
|
+
this.togglePause(job);
|
|
408
|
+
this.refresh();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (data === "h" && job && (this.focus || this.choosing !== undefined)) {
|
|
412
|
+
this.toggleHold(job);
|
|
413
|
+
this.refresh();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (data === "x" && job && (this.focus || this.choosing !== undefined)) {
|
|
417
|
+
this.cancelExecution(job, confirming);
|
|
418
|
+
this.refresh();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
399
421
|
if (this.focus)
|
|
400
422
|
this.sessionInput(data, job);
|
|
401
423
|
else if (this.choosing !== undefined && job)
|
|
@@ -419,6 +441,103 @@ export class BraidPanel {
|
|
|
419
441
|
this.jobs.cancel(this.selected);
|
|
420
442
|
this.refresh();
|
|
421
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* p pauses or resumes what is in view: the open execution, the selected
|
|
446
|
+
* node's latest execution, or the whole job. The parent agent gets a notice.
|
|
447
|
+
*/
|
|
448
|
+
togglePause(job) {
|
|
449
|
+
const execution = job.execution;
|
|
450
|
+
try {
|
|
451
|
+
if (!execution || job.status !== "running")
|
|
452
|
+
throw new Error("Only a running job can be paused");
|
|
453
|
+
const executionId = this.executionInView(job);
|
|
454
|
+
if (!this.focus && this.choosing === undefined) {
|
|
455
|
+
if (execution.pause) {
|
|
456
|
+
this.jobs.resumeJob(job.jobId, "user");
|
|
457
|
+
this.notice = "Job resumed";
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this.jobs.pause(job.jobId, undefined, "user");
|
|
461
|
+
this.notice = "Job paused · no new executions start; running ones stop before their next model request";
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!executionId)
|
|
466
|
+
throw new Error("This node has not run yet");
|
|
467
|
+
const record = execution.executions[executionId];
|
|
468
|
+
if (record?.suspension && record.suspension.state !== "resuming") {
|
|
469
|
+
this.jobs.resume(job.jobId, [executionId], execution.revision, "user");
|
|
470
|
+
this.notice = "Execution resumed";
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
this.jobs.pause(job.jobId, [executionId], "user");
|
|
474
|
+
this.notice = "Pausing · the execution stops before its next model request";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
this.notice = `Cannot pause or resume: ${error instanceof Error ? error.message : String(error)}`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* h toggles the hold on the execution in view: a running one keeps running and
|
|
483
|
+
* is held when it finishes; a held result is released. The parent agent gets a notice.
|
|
484
|
+
*/
|
|
485
|
+
toggleHold(job) {
|
|
486
|
+
const execution = job.execution;
|
|
487
|
+
try {
|
|
488
|
+
if (!execution || job.status !== "running")
|
|
489
|
+
throw new Error("Only a running job can hold results");
|
|
490
|
+
const executionId = this.focus
|
|
491
|
+
? "executionId" in this.focus ? this.focus.executionId : this.executionsOf(job, this.focus.nodeId).at(-1)
|
|
492
|
+
: this.executionsOf(job, this.choosing).at(-1);
|
|
493
|
+
if (!executionId)
|
|
494
|
+
throw new Error("This node has not run yet");
|
|
495
|
+
if (execution.pausedExecutionIds.includes(executionId)) {
|
|
496
|
+
this.jobs.resume(job.jobId, [executionId], execution.revision, "user");
|
|
497
|
+
this.notice = "Released · the result goes downstream";
|
|
498
|
+
}
|
|
499
|
+
else if (execution.executions[executionId]?.holdAfter) {
|
|
500
|
+
this.jobs.holdAfter(job.jobId, [executionId], false, "user");
|
|
501
|
+
this.notice = "Hold removed · the result goes downstream when the execution finishes";
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
this.jobs.holdAfter(job.jobId, [executionId], true, "user");
|
|
505
|
+
this.notice = "Holding · the execution keeps running; its result waits for release when it finishes";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
this.notice = `Cannot hold or release: ${error instanceof Error ? error.message : String(error)}`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** The open execution, or the selected node's latest one. */
|
|
513
|
+
executionInView(job) {
|
|
514
|
+
return this.focus
|
|
515
|
+
? "executionId" in this.focus ? this.focus.executionId : this.executionsOf(job, this.focus.nodeId).at(-1)
|
|
516
|
+
: this.choosing !== undefined ? this.executionsOf(job, this.choosing).at(-1) : undefined;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* x cancels the execution in view after a second x: only it fails, and its
|
|
520
|
+
* successors follow the usual failure rules. The parent agent gets a notice.
|
|
521
|
+
*/
|
|
522
|
+
cancelExecution(job, confirming) {
|
|
523
|
+
try {
|
|
524
|
+
if (!job.execution || job.status !== "running")
|
|
525
|
+
throw new Error("The job is not running");
|
|
526
|
+
const executionId = this.executionInView(job);
|
|
527
|
+
if (!executionId)
|
|
528
|
+
throw new Error("This node has not run yet");
|
|
529
|
+
if (confirming !== executionId) {
|
|
530
|
+
this.cancelling = executionId;
|
|
531
|
+
this.notice = "Press x again to cancel this execution · it fails and its successors follow the failure rules";
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.jobs.cancelExecutions(job.jobId, [executionId], "user");
|
|
535
|
+
this.notice = "Execution cancelled";
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
this.notice = `Cannot cancel: ${error instanceof Error ? error.message : String(error)}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
422
541
|
chooseInput(data, job) {
|
|
423
542
|
const ids = this.nodeIds(job);
|
|
424
543
|
const at = Math.max(0, ids.indexOf(this.choosing));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as core from "@chrok/braid";
|
|
2
|
+
/** The exact core release this extension depends on; they release together. */
|
|
3
|
+
export const REQUIRED_CORE_VERSION = "0.5.1";
|
|
4
|
+
/**
|
|
5
|
+
* A Pi process that loaded core before an upgrade can keep it while loading the new extension,
|
|
6
|
+
* which then fails on its first missing core helper as an opaque model error. Releases before
|
|
7
|
+
* 0.5.1 do not export a version at all.
|
|
8
|
+
*/
|
|
9
|
+
export function assertCoreVersion(loaded = core) {
|
|
10
|
+
const version = loaded.BRAID_VERSION;
|
|
11
|
+
if (version === REQUIRED_CORE_VERSION)
|
|
12
|
+
return;
|
|
13
|
+
const found = typeof version === "string" ? `@chrok/braid ${version}` : "an older @chrok/braid";
|
|
14
|
+
throw new Error(`@chrok/pi-braid requires @chrok/braid ${REQUIRED_CORE_VERSION}, but ${found} is loaded. ` +
|
|
15
|
+
"Install matching versions (check with `npm ls @chrok/pi-braid @chrok/braid`), then restart Pi: " +
|
|
16
|
+
"a running Pi process keeps the core it loaded before an upgrade.");
|
|
17
|
+
}
|
package/dist/display.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render as renderMermaid } from "grok-mermaid";
|
|
2
2
|
import { Text, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
3
|
-
import { formatActivityStatus, } from "@chrok/braid";
|
|
3
|
+
import { formatActivityStatus, formatPauseSource, } from "@chrok/braid";
|
|
4
4
|
export const MAX_VISIBLE_EVENTS = 80;
|
|
5
5
|
export const MAX_VISIBLE_NODES = 80;
|
|
6
6
|
class FixedLines {
|
|
@@ -123,6 +123,8 @@ function graphParts(result) {
|
|
|
123
123
|
from: event.from,
|
|
124
124
|
to: event.to,
|
|
125
125
|
...(event.choice ? { choice: event.choice } : {}),
|
|
126
|
+
...(event.feedback ? { feedback: event.feedback } : {}),
|
|
127
|
+
...(event.executionId ? { executionId: event.executionId } : {}),
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -164,22 +166,27 @@ function mermaidLabelLines(id, result, selected = false) {
|
|
|
164
166
|
const node = result.nodes[id];
|
|
165
167
|
if (!node)
|
|
166
168
|
return [compact(id, 24), "pending"];
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
const suspended = node.suspension && node.suspension.state !== "pausing";
|
|
170
|
+
const icon = suspended
|
|
171
|
+
? "‖ PAUSED"
|
|
172
|
+
: node.status === "running"
|
|
173
|
+
? "▶ ACTIVE"
|
|
174
|
+
: {
|
|
175
|
+
completed: "✓",
|
|
176
|
+
failed: "✗",
|
|
177
|
+
skipped: "·",
|
|
178
|
+
runnable: "◇",
|
|
179
|
+
pending: "○",
|
|
180
|
+
}[node.status];
|
|
181
|
+
const status = node.suspension
|
|
182
|
+
? `${node.suspension.state}${node.suspension.source === "node_timeout" ? " · timed out" : ""}`
|
|
183
|
+
: node.status === "completed" ? "done" : node.status;
|
|
177
184
|
const decision = node.decision === undefined ? "" : ` → ${compact(node.decision, 16)}`;
|
|
178
185
|
const now = "observedAt" in result ? result.observedAt : Date.now();
|
|
179
186
|
const iteration = "executions" in result ? result.executions[node.executionId ?? ""]?.iteration : result.iterations?.[id];
|
|
180
187
|
const lines = [
|
|
181
188
|
`${selected ? SELECTED_NODE_MARK + icon.slice(1) : icon} ${compact(id, 24)}${iteration ? ` #${iteration}` : ""}`,
|
|
182
|
-
`${status}${decision} · ${nodeElapsed(node, now)}`,
|
|
189
|
+
`${status}${node.holdAfter && (node.status === "running" || node.status === "runnable") ? " · hold" : ""}${decision} · ${nodeElapsed(node, now)}`,
|
|
183
190
|
];
|
|
184
191
|
if (progress) {
|
|
185
192
|
const window = progress.contextWindow === undefined
|
|
@@ -192,6 +199,7 @@ function mermaidLabelLines(id, result, selected = false) {
|
|
|
192
199
|
}
|
|
193
200
|
function mermaidSource(result, selected) {
|
|
194
201
|
const parts = graphParts(result);
|
|
202
|
+
const feedbackSources = new Map(parts.edges.filter(edge => edge.feedback !== undefined).map(edge => [edge.to, edge.from]));
|
|
195
203
|
const ids = Object.keys(parts.nodes);
|
|
196
204
|
const names = new Map(ids.map((id, index) => [id, `n${index}`]));
|
|
197
205
|
const escapeMermaidText = (value) => stripTerminalSequences(value)
|
|
@@ -214,7 +222,9 @@ function mermaidSource(result, selected) {
|
|
|
214
222
|
lines.push(` ${names.get(id)}${shape}"${mermaidLabelLines(id, result, id === selected).map(escapeMermaidText).join("<br/>")}"${close}`);
|
|
215
223
|
}
|
|
216
224
|
for (const edge of parts.edges) {
|
|
217
|
-
|
|
225
|
+
// Distinguish the direct data handoff from its reverse retry edge.
|
|
226
|
+
const arrow = edge.feedback === undefined && feedbackSources.get(edge.from) === edge.to ? "-.->" : "-->";
|
|
227
|
+
lines.push(` ${names.get(edge.from)} ${arrow}${edge.choice ? `|${escapeMermaidText(edge.choice)}|` : ""} ${names.get(edge.to)}`);
|
|
218
228
|
}
|
|
219
229
|
return lines.join("\n");
|
|
220
230
|
}
|
|
@@ -285,6 +295,13 @@ function eventText(event) {
|
|
|
285
295
|
case "graph_updated": return `graph updated · revision ${event.revision}`;
|
|
286
296
|
case "execution_paused": return `paused · ${compact(event.nodeId, 40)} · ${event.executionId}`;
|
|
287
297
|
case "execution_resumed": return `resumed · ${compact(event.nodeId, 40)} · ${event.executionId}`;
|
|
298
|
+
case "execution_suspending": return `pausing · ${compact(event.nodeId, 40)} · by ${compact(formatPauseSource(event.source), 20)}`;
|
|
299
|
+
case "execution_suspended": return `suspended · ${compact(event.nodeId, 40)} · by ${compact(formatPauseSource(event.source), 20)}`;
|
|
300
|
+
case "execution_hold_added": return `hold added · ${compact(event.nodeId, 40)} · by ${compact(event.source, 20)}`;
|
|
301
|
+
case "execution_hold_removed": return `hold removed · ${compact(event.nodeId, 40)} · by ${compact(event.source, 20)}`;
|
|
302
|
+
case "replan_requested": return `replan requested · ${compact(event.nodeId, 40)}: ${compact(event.reason, 64)}`;
|
|
303
|
+
case "graph_paused": return `job paused · by ${compact(event.source, 20)}`;
|
|
304
|
+
case "graph_resumed": return "job resumed";
|
|
288
305
|
case "loop_started": return `loop ${compact(event.loopId, 40)} · iteration ${event.iteration}`;
|
|
289
306
|
case "loop_completed": return `loop ${compact(event.loopId, 40)} completed · iteration ${event.iteration}`;
|
|
290
307
|
case "graph_created":
|
|
@@ -347,7 +364,7 @@ export function createLiveState() {
|
|
|
347
364
|
progress: Object.create(null),
|
|
348
365
|
events: [],
|
|
349
366
|
latencyMs: 0,
|
|
350
|
-
observedAt: Date.now(), revision: 0, pausedExecutionIds: [], iterations: Object.create(null),
|
|
367
|
+
observedAt: Date.now(), revision: 0, pausedExecutionIds: [], suspendedExecutionIds: [], iterations: Object.create(null),
|
|
351
368
|
};
|
|
352
369
|
}
|
|
353
370
|
export function applyEvent(state, event) {
|
|
@@ -374,12 +391,16 @@ export function applyEvent(state, event) {
|
|
|
374
391
|
from: event.from,
|
|
375
392
|
to: event.to,
|
|
376
393
|
...(event.choice ? { choice: event.choice } : {}),
|
|
394
|
+
...(event.feedback ? { feedback: event.feedback } : {}),
|
|
395
|
+
...(event.executionId ? { executionId: event.executionId } : {}),
|
|
377
396
|
});
|
|
378
397
|
if (state.events.length > MAX_VISIBLE_EVENTS)
|
|
379
398
|
state.events.shift();
|
|
380
399
|
if (event.type === "graph_completed" || event.type === "graph_failed") {
|
|
381
400
|
state.status = event.type === "graph_completed" ? "completed" : "failed";
|
|
382
401
|
state.pausedExecutionIds = [];
|
|
402
|
+
state.suspendedExecutionIds = [];
|
|
403
|
+
delete state.pause;
|
|
383
404
|
if (event.type === "graph_failed")
|
|
384
405
|
state.error = { ...event.error };
|
|
385
406
|
return;
|
|
@@ -416,8 +437,16 @@ export function applyEvent(state, event) {
|
|
|
416
437
|
}
|
|
417
438
|
if (event.type === "execution_paused")
|
|
418
439
|
state.pausedExecutionIds = [...(state.pausedExecutionIds ?? []), event.executionId];
|
|
440
|
+
if (event.type === "graph_paused")
|
|
441
|
+
state.pause = { source: event.source, pausedAt: event.timestamp };
|
|
442
|
+
if (event.type === "graph_resumed")
|
|
443
|
+
delete state.pause;
|
|
444
|
+
if (event.type === "execution_suspended")
|
|
445
|
+
state.suspendedExecutionIds = [...(state.suspendedExecutionIds ?? []), event.executionId];
|
|
419
446
|
if (event.type === "execution_resumed")
|
|
420
447
|
state.pausedExecutionIds = state.pausedExecutionIds?.filter(id => id !== event.executionId) ?? [];
|
|
448
|
+
if (event.type === "execution_resumed" || event.type === "node_completed" || event.type === "node_failed")
|
|
449
|
+
state.suspendedExecutionIds = state.suspendedExecutionIds?.filter(id => id !== event.executionId) ?? [];
|
|
421
450
|
const node = "nodeId" in event && Object.hasOwn(state.nodes, event.nodeId)
|
|
422
451
|
? state.nodes[event.nodeId]
|
|
423
452
|
: undefined;
|
|
@@ -445,8 +474,24 @@ export function applyEvent(state, event) {
|
|
|
445
474
|
node.status = "running";
|
|
446
475
|
node.startedAt = event.timestamp;
|
|
447
476
|
break;
|
|
477
|
+
case "execution_suspending":
|
|
478
|
+
node.suspension = { state: "pausing", source: event.source, requestedAt: event.timestamp };
|
|
479
|
+
break;
|
|
480
|
+
case "execution_suspended":
|
|
481
|
+
node.suspension = { state: "suspended", source: event.source, requestedAt: node.suspension?.requestedAt ?? event.timestamp, suspendedAt: event.timestamp };
|
|
482
|
+
break;
|
|
483
|
+
case "execution_resumed":
|
|
484
|
+
delete node.suspension;
|
|
485
|
+
break;
|
|
486
|
+
case "execution_hold_added":
|
|
487
|
+
node.holdAfter = { source: event.source, requestedAt: event.timestamp };
|
|
488
|
+
break;
|
|
489
|
+
case "execution_hold_removed":
|
|
490
|
+
delete node.holdAfter;
|
|
491
|
+
break;
|
|
448
492
|
case "node_completed":
|
|
449
493
|
case "node_failed":
|
|
494
|
+
delete node.suspension;
|
|
450
495
|
node.status = event.type === "node_completed" ? "completed" : "failed";
|
|
451
496
|
node.finishedAt = event.timestamp;
|
|
452
497
|
node.latencyMs = event.latencyMs;
|
|
@@ -625,8 +670,12 @@ export function renderGraphResult(result, expanded, isPartial, theme, fallback =
|
|
|
625
670
|
}
|
|
626
671
|
if (result.error)
|
|
627
672
|
lines.push(theme.fg("error", `${result.error.code}: ${compact(result.error.message, 120)}`));
|
|
673
|
+
if ("pause" in result && result.pause)
|
|
674
|
+
lines.push(theme.fg("warning", `job paused by ${compact(result.pause.source, 20)} · no new executions start`));
|
|
628
675
|
if ("pausedExecutionIds" in result && result.pausedExecutionIds?.length)
|
|
629
676
|
lines.push(theme.fg("warning", `${result.pausedExecutionIds.length} paused executions · revision ${result.revision ?? 0}`));
|
|
677
|
+
if ("suspendedExecutionIds" in result && result.suspendedExecutionIds?.length)
|
|
678
|
+
lines.push(theme.fg("warning", `${result.suspendedExecutionIds.length} suspended executions · revision ${result.revision ?? 0}`));
|
|
630
679
|
if ("executions" in result)
|
|
631
680
|
lines.push(theme.fg("dim", `${Object.keys(result.executions).length} executions · revision ${result.revision}`));
|
|
632
681
|
const chartStart = lines.length;
|