@chrok/pi-braid 0.3.6 → 0.5.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/README.md +167 -46
- package/dist/command.js +126 -7
- package/dist/display.js +62 -14
- package/dist/drain.js +171 -0
- package/dist/index.js +143 -76
- package/dist/jobs.js +152 -15
- package/dist/runner.js +166 -80
- package/dist/status-text.js +89 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,32 +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.0/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.0/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.0/docs/compatibility.md#migrating-from-02-to-03),
|
|
16
|
+
and users coming from 0.1 the
|
|
17
|
+
[0.1 → 0.2 guide](https://github.com/Epsirom/braid/blob/v0.5.0/docs/compatibility.md#migrating-from-01-to-02).
|
|
15
18
|
For the previous release, use the
|
|
16
|
-
[0.
|
|
19
|
+
[0.4.0 guide](https://github.com/Epsirom/braid/blob/v0.4.0/integrations/pi/README.md).
|
|
17
20
|
|
|
18
21
|
It registers:
|
|
19
22
|
|
|
20
23
|
- `braid` — submit a graph with optional bounded loops; receive a `jobId`
|
|
21
|
-
immediately
|
|
24
|
+
immediately (print/JSON runs wait for it before exiting).
|
|
22
25
|
- `braid_status` — retrieve progress and results with `{ "jobId": "..." }`, or
|
|
23
26
|
add `"executionId": "..."` for an exact invocation's full output/error.
|
|
24
27
|
`"nodeId": "..."` selects that definition's latest invocation. Omit all IDs
|
|
25
28
|
to list jobs in the current session.
|
|
26
29
|
- `braid_update` / `braid_resume` — edit live definitions or release paused executions.
|
|
27
|
-
- `
|
|
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.
|
|
34
|
+
- `braid_finish` — print/JSON runs only: record the run outcome with
|
|
35
|
+
`{ "status": "success" | "failure", "summary": "..." }`.
|
|
28
36
|
- `/braid [jobId]` — open a live flow panel in interactive Pi.
|
|
29
|
-
- `/braid:blocking [on|off]` — experimental: enable or disable blocking for new `braid` tool calls;
|
|
30
|
-
omit the argument to show the current mode.
|
|
31
37
|
- `/braid:review [target or focus]` — run a bounded parallel review/fix loop.
|
|
32
38
|
- `/braid:clear` — clear the progress widget above the editor; run it again while
|
|
33
39
|
the widget is empty to restore what was cleared.
|
|
@@ -53,37 +59,62 @@ work, or end its turn if it needs the result, and not to sleep, use a timer, or
|
|
|
53
59
|
poll in a loop to wait for the job.
|
|
54
60
|
Stopping the foreground response does not stop background jobs.
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
> command, and behavior may change or be replaced by an async-preserving CI mode
|
|
58
|
-
> that keeps reminders, `pauseAfter`, and live updates
|
|
59
|
-
> ([#80](https://github.com/Epsirom/braid/issues/80)).
|
|
62
|
+
### Single-shot and CI runs
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
In print mode (`pi -p`) and `--mode json`, background execution works unchanged:
|
|
65
|
+
the run stays open until every Braid job reaches a terminal status and its
|
|
66
|
+
reminder has been delivered, then exits after the parent's final response.
|
|
62
67
|
|
|
63
68
|
```sh
|
|
64
|
-
pi
|
|
69
|
+
pi -p "Use Braid to review the changes and summarize the results."
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
The parent submits, ends its turn, and is resumed by the reminder as in
|
|
73
|
+
interactive Pi, so `pauseAfter`, `notifyOnCompletion`, `braid_update`, and
|
|
74
|
+
`braid_resume` keep working. Paused executions are never resumed automatically.
|
|
75
|
+
If the parent ends its turn while only paused executions remain, it receives one
|
|
76
|
+
warning naming them; ending the next turn without resuming them cancels their
|
|
77
|
+
jobs, gives the parent a final turn on the cancellation reminder, and exits 1.
|
|
78
|
+
|
|
79
|
+
`--braid-ci-timeout <duration>` bounds the whole run, starting at its first
|
|
80
|
+
prompt. It accepts milliseconds or an `s`, `m`, or `h` suffix (for example
|
|
81
|
+
`--braid-ci-timeout 30m`). On expiry, outstanding jobs are cancelled, the parent
|
|
82
|
+
gets a final turn on the cancellation reminders, and the run exits 1. Either
|
|
83
|
+
forced cancellation prints a `Braid:` line on stderr.
|
|
84
|
+
|
|
85
|
+
A failed graph does not by itself fail the run. Instead, the parent is told to
|
|
86
|
+
call `braid_finish` with `status` `success` or `failure` and a short `summary`
|
|
87
|
+
once the task is done. The tool only records the outcome; the run still ends
|
|
88
|
+
when the parent ends its turn with no Braid job outstanding, and a later call
|
|
89
|
+
replaces the earlier outcome. A final `failure` exits 1 and prints
|
|
90
|
+
`Braid: run reported failure: <summary>` on stderr. `success` never overrides a
|
|
91
|
+
non-zero exit from a forced cancellation or from Pi itself. If the parent never
|
|
92
|
+
calls it, the exit status is Pi's own apart from forced cancellations.
|
|
93
|
+
|
|
94
|
+
In `--mode json`, the final outcome is also written as a session entry, after the
|
|
95
|
+
`tool_execution_end` event of the call itself:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{"type":"entry_appended","entry":{"type":"custom","customType":"braid-finish","data":{"status":"failure","summary":"Review found 2 blocking issues.","forcedFailures":[]},...}}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`forcedFailures` lists any forced cancellations, which fail the run whatever the
|
|
102
|
+
reported status. The entry is not part of model context, and text mode still
|
|
103
|
+
prints only the parent's final response. `braid_finish` is not offered in
|
|
104
|
+
interactive or RPC sessions.
|
|
105
|
+
|
|
106
|
+
Pi SDK sessions default to print mode too, so `session.prompt()` returns only
|
|
107
|
+
after outstanding Braid jobs drain. Hosts that stay running and want reminders
|
|
108
|
+
to start new runs after settling should bind with another mode, for example
|
|
109
|
+
`session.bindExtensions({ mode: "rpc" })`.
|
|
110
|
+
|
|
111
|
+
### Migrating from blocking mode
|
|
112
|
+
|
|
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.
|
|
87
118
|
|
|
88
119
|
Jobs live in memory for the current Pi session. Quitting, reloading extensions,
|
|
89
120
|
or switching/forking sessions aborts outstanding work and suppresses its
|
|
@@ -203,7 +234,18 @@ the current graph revision and paused execution IDs, then:
|
|
|
203
234
|
```
|
|
204
235
|
|
|
205
236
|
Pass this to `braid_update`. It validates and commits the full change and resume
|
|
206
|
-
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,
|
|
207
249
|
expectedRevision, executionIds})`. Definitions can be changed while executions
|
|
208
250
|
are running or waiting, including inside a loop. Existing instances keep their
|
|
209
251
|
captured prompt, inputs, and policy; completion routes through the latest graph.
|
|
@@ -240,6 +282,50 @@ See [execution control](../../docs/execution-control.md) for loop schemas, exact
|
|
|
240
282
|
update semantics, historical dependencies, and workspace lineage. There is no
|
|
241
283
|
scheduler persistence across Pi reloads. Reminders alone do not pause execution.
|
|
242
284
|
|
|
285
|
+
### Pausing running executions
|
|
286
|
+
|
|
287
|
+
`pauseAfter` holds a result after the execution finishes. To stop work that is
|
|
288
|
+
still running, call `braid_pause({jobId, executionIds})`. Each execution finishes
|
|
289
|
+
its running tool, then stops before its next model request. It keeps its
|
|
290
|
+
conversation, workspace, and tool budgets, and releases its concurrency slot to
|
|
291
|
+
other nodes. Messages sent to it from the `/braid` assistant while it is paused
|
|
292
|
+
are delivered when it resumes. Resume with `braid_resume({jobId, expectedRevision,
|
|
293
|
+
executionIds})` or `braid_update` with `resume`; the worker continues the same
|
|
294
|
+
conversation. A transient-error retry also waits at the pause point before resending.
|
|
295
|
+
|
|
296
|
+
Omit `executionIds` to pause the whole job: no new executions start and running
|
|
297
|
+
ones pause. `braid_resume({jobId})` resumes the job and the executions that the
|
|
298
|
+
job pause paused; executions paused separately stay paused.
|
|
299
|
+
|
|
300
|
+
The node timeout stops while an execution is suspended; the graph timeout keeps
|
|
301
|
+
running. `braid_status` shows suspended executions, a job pause, and who paused
|
|
302
|
+
them.
|
|
303
|
+
|
|
304
|
+
### Node timeouts
|
|
305
|
+
|
|
306
|
+
When an execution exceeds `options.nodeTimeoutMs`, it is paused instead of
|
|
307
|
+
failed. Its current model request or shell command is stopped; the interrupted
|
|
308
|
+
call, with any output it produced, and any calls after it in the same batch are
|
|
309
|
+
recorded in its conversation. Its conversation, workspace, and tool budgets are
|
|
310
|
+
kept, and its successors wait. The parent is woken with a reminder that says how
|
|
311
|
+
long the execution ran, what it was doing, and what it can do next:
|
|
312
|
+
|
|
313
|
+
- `braid_resume({jobId, expectedRevision, executionIds})` gives it the job's
|
|
314
|
+
`nodeTimeoutMs` again; add `nodeTimeoutMs` for a different budget.
|
|
315
|
+
- `braid_update` with `resume` and `resumeOptions: { nodeTimeoutMs }` resumes it
|
|
316
|
+
together with graph changes.
|
|
317
|
+
- `braid_cancel({jobId, executionIds})` fails only that execution with
|
|
318
|
+
`NODE_TIMEOUT`; its successors follow the usual failure rules.
|
|
319
|
+
|
|
320
|
+
Resuming the whole job does not resume a timeout pause. Set
|
|
321
|
+
`options.onNodeTimeout: "fail"` to fail on timeout instead.
|
|
322
|
+
|
|
323
|
+
Users can pause and hold from the panel too (`p` and `h`, below). The parent is not woken for
|
|
324
|
+
that: one notice with the latest state per execution or job is added to its next
|
|
325
|
+
turn, and it is told not to resume work the user paused unless the user asks.
|
|
326
|
+
In print and JSON runs, a job held only by pauses counts as stalled: the parent
|
|
327
|
+
gets one warning, and the job is cancelled if it ends the next turn without resuming.
|
|
328
|
+
|
|
243
329
|
## Live flow panel
|
|
244
330
|
|
|
245
331
|
In interactive Pi, background progress appears automatically **above the input
|
|
@@ -265,13 +351,24 @@ The bordered panel keeps the job header and keyboard controls visible while
|
|
|
265
351
|
you scroll the flow and event log. It refreshes as nodes start, finish, fail,
|
|
266
352
|
and pass outputs downstream.
|
|
267
353
|
Use Left/Right to select jobs, Up/Down or Page Up/Page Down to scroll, `c` to
|
|
268
|
-
cancel the selected job, and Escape or `q` to close the panel.
|
|
354
|
+
cancel the selected job, `p` to pause or resume it, and Escape or `q` to close the panel.
|
|
355
|
+
In node selection or an open session, `p` pauses or resumes that node's latest
|
|
356
|
+
execution instead; a suspended node shows `‖ PAUSED`, and one paused by its node
|
|
357
|
+
timeout also shows `timed out`. `h` holds that execution's result once it
|
|
358
|
+
finishes, removes the hold if one is set (its status shows `· hold`), or
|
|
359
|
+
releases a result that is already held. Press `x` twice there to cancel only
|
|
360
|
+
that execution; the parent gets a notice with its next turn. Closing the panel
|
|
269
361
|
leaves jobs running. In RPC or noninteractive modes, use `braid_status`.
|
|
270
362
|
|
|
271
363
|
A flowchart wider than the terminal is cropped to the terminal width around the
|
|
272
364
|
first running node, or from its left edge; a `[chart 1–76 of 245 cols]` line
|
|
273
365
|
shows the visible columns.
|
|
274
366
|
|
|
367
|
+
For loops, a forward edge opposite a feedback edge is drawn dotted, while the
|
|
368
|
+
retry feedback edge stays solid. For example, `/braid:review` shows the direct
|
|
369
|
+
`scope → check` handoff dotted and `check → scope` (`retry`) solid. All edges and
|
|
370
|
+
choice labels remain visible; runtime dependencies and handoffs are unchanged.
|
|
371
|
+
|
|
275
372
|
Press `v` to select a node: the arrow keys move a `◆` marker through the
|
|
276
373
|
flowchart (starting at a running node), and the view pans horizontally and
|
|
277
374
|
scrolls vertically to keep the selected node in sight. If the flowchart cannot be
|
|
@@ -559,7 +656,9 @@ pass any of these fields in the `braid` tool's `options`:
|
|
|
559
656
|
| --- | --- |
|
|
560
657
|
| `maxToolRounds` | Maximum assistant responses containing tool calls, per node |
|
|
561
658
|
| `maxToolCalls` | Maximum total requested tool calls, per node |
|
|
562
|
-
| `
|
|
659
|
+
| `maxModelRetries` | Retries per model request for transient provider errors; default 3, `0` disables, at most 10 |
|
|
660
|
+
| `nodeTimeoutMs` | Active time allowed for each node after it starts, excluding pauses, in milliseconds |
|
|
661
|
+
| `onNodeTimeout` | `pause` (default) suspends a timed-out node and wakes the parent; `fail` fails it with `NODE_TIMEOUT` |
|
|
563
662
|
| `graphTimeoutMs` | Time allowed for the entire graph, including queueing and pauses, in milliseconds |
|
|
564
663
|
| `maxExecutions` | Total materialized execution records, including skips; default 1000, positive safe integer |
|
|
565
664
|
|
|
@@ -637,9 +736,8 @@ fails that node cleanly and does not invoke another provider.
|
|
|
637
736
|
|
|
638
737
|
The Pi extension has no node or graph time limit by default. Use `braid_cancel`
|
|
639
738
|
or press `c` in the flow panel to abort running nodes and mark queued nodes
|
|
640
|
-
`cancelled`.
|
|
641
|
-
|
|
642
|
-
foreground response also cancels its Braid job. To set a node or graph timeout,
|
|
739
|
+
`cancelled`. Escape stops the foreground response or closes the panel without
|
|
740
|
+
cancelling jobs. To set a node or graph timeout,
|
|
643
741
|
supply milliseconds in the submission tool input under `options`; each omitted
|
|
644
742
|
timeout remains unlimited:
|
|
645
743
|
|
|
@@ -651,8 +749,8 @@ timeout remains unlimited:
|
|
|
651
749
|
}
|
|
652
750
|
```
|
|
653
751
|
|
|
654
|
-
The adapter sets `
|
|
655
|
-
|
|
752
|
+
The adapter sets `cacheRetention: "none"` and gives each node a fresh provider
|
|
753
|
+
context. Tool budgets are unlimited unless `maxToolRounds` or
|
|
656
754
|
`maxToolCalls` is set. Missing paths, invalid arguments, disallowed writes, and
|
|
657
755
|
unavailable tools are returned as tool errors so the node can recover. Provider
|
|
658
756
|
authentication and calls may still incur normal provider costs. Braid cannot
|
|
@@ -661,6 +759,29 @@ cancellation. Cancellation prevents further tool calls but does not roll back
|
|
|
661
759
|
writes already made. Core waits for tracked writes, preserves checkpoints, and
|
|
662
760
|
removes worker worktrees before completing cancellation.
|
|
663
761
|
|
|
762
|
+
### Transient model errors
|
|
763
|
+
|
|
764
|
+
A transient provider error, such as a rate limit (HTTP 429), overload, a 5xx
|
|
765
|
+
response, or a dropped connection, is retried inside the running node execution.
|
|
766
|
+
The node keeps its conversation, tool results, workspace, decision, merge state,
|
|
767
|
+
and tool-budget counts; the failed request is sent again with the same context.
|
|
768
|
+
Tools run only after a complete model response, so a retry never repeats a
|
|
769
|
+
completed tool call. `options.maxModelRetries` sets the retries for each model
|
|
770
|
+
request (default 3; `0` disables them):
|
|
771
|
+
|
|
772
|
+
- If the provider rejects a request before its response starts, Pi's provider
|
|
773
|
+
client retries it with exponential backoff and honors `Retry-After`. A requested
|
|
774
|
+
wait longer than 60 seconds fails immediately.
|
|
775
|
+
- If a response stream fails after it started, Braid discards the partial
|
|
776
|
+
response and restarts the request after 2, 4, then 8 seconds.
|
|
777
|
+
|
|
778
|
+
Quota, billing, and other non-transient errors are not retried. Retries stop on
|
|
779
|
+
cancellation, and Braid does not start a wait that would outlast the node or
|
|
780
|
+
graph deadline. When retries run out, the node fails with the provider error and
|
|
781
|
+
follows the usual failure rules, including `requireSuccess`. Failed attempts stay
|
|
782
|
+
visible in the session view, and their reported usage is counted. Braid does not
|
|
783
|
+
retry a whole node execution.
|
|
784
|
+
|
|
664
785
|
The tool output is capped at 50KB/2000 lines to protect Pi context. When exceeded,
|
|
665
786
|
the adapter writes the complete text to a temporary file (mode 600 on POSIX;
|
|
666
787
|
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));
|
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,12 @@ 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 "graph_paused": return `job paused · by ${compact(event.source, 20)}`;
|
|
303
|
+
case "graph_resumed": return "job resumed";
|
|
288
304
|
case "loop_started": return `loop ${compact(event.loopId, 40)} · iteration ${event.iteration}`;
|
|
289
305
|
case "loop_completed": return `loop ${compact(event.loopId, 40)} completed · iteration ${event.iteration}`;
|
|
290
306
|
case "graph_created":
|
|
@@ -347,7 +363,7 @@ export function createLiveState() {
|
|
|
347
363
|
progress: Object.create(null),
|
|
348
364
|
events: [],
|
|
349
365
|
latencyMs: 0,
|
|
350
|
-
observedAt: Date.now(), revision: 0, pausedExecutionIds: [], iterations: Object.create(null),
|
|
366
|
+
observedAt: Date.now(), revision: 0, pausedExecutionIds: [], suspendedExecutionIds: [], iterations: Object.create(null),
|
|
351
367
|
};
|
|
352
368
|
}
|
|
353
369
|
export function applyEvent(state, event) {
|
|
@@ -374,12 +390,16 @@ export function applyEvent(state, event) {
|
|
|
374
390
|
from: event.from,
|
|
375
391
|
to: event.to,
|
|
376
392
|
...(event.choice ? { choice: event.choice } : {}),
|
|
393
|
+
...(event.feedback ? { feedback: event.feedback } : {}),
|
|
394
|
+
...(event.executionId ? { executionId: event.executionId } : {}),
|
|
377
395
|
});
|
|
378
396
|
if (state.events.length > MAX_VISIBLE_EVENTS)
|
|
379
397
|
state.events.shift();
|
|
380
398
|
if (event.type === "graph_completed" || event.type === "graph_failed") {
|
|
381
399
|
state.status = event.type === "graph_completed" ? "completed" : "failed";
|
|
382
400
|
state.pausedExecutionIds = [];
|
|
401
|
+
state.suspendedExecutionIds = [];
|
|
402
|
+
delete state.pause;
|
|
383
403
|
if (event.type === "graph_failed")
|
|
384
404
|
state.error = { ...event.error };
|
|
385
405
|
return;
|
|
@@ -416,8 +436,16 @@ export function applyEvent(state, event) {
|
|
|
416
436
|
}
|
|
417
437
|
if (event.type === "execution_paused")
|
|
418
438
|
state.pausedExecutionIds = [...(state.pausedExecutionIds ?? []), event.executionId];
|
|
439
|
+
if (event.type === "graph_paused")
|
|
440
|
+
state.pause = { source: event.source, pausedAt: event.timestamp };
|
|
441
|
+
if (event.type === "graph_resumed")
|
|
442
|
+
delete state.pause;
|
|
443
|
+
if (event.type === "execution_suspended")
|
|
444
|
+
state.suspendedExecutionIds = [...(state.suspendedExecutionIds ?? []), event.executionId];
|
|
419
445
|
if (event.type === "execution_resumed")
|
|
420
446
|
state.pausedExecutionIds = state.pausedExecutionIds?.filter(id => id !== event.executionId) ?? [];
|
|
447
|
+
if (event.type === "execution_resumed" || event.type === "node_completed" || event.type === "node_failed")
|
|
448
|
+
state.suspendedExecutionIds = state.suspendedExecutionIds?.filter(id => id !== event.executionId) ?? [];
|
|
421
449
|
const node = "nodeId" in event && Object.hasOwn(state.nodes, event.nodeId)
|
|
422
450
|
? state.nodes[event.nodeId]
|
|
423
451
|
: undefined;
|
|
@@ -445,8 +473,24 @@ export function applyEvent(state, event) {
|
|
|
445
473
|
node.status = "running";
|
|
446
474
|
node.startedAt = event.timestamp;
|
|
447
475
|
break;
|
|
476
|
+
case "execution_suspending":
|
|
477
|
+
node.suspension = { state: "pausing", source: event.source, requestedAt: event.timestamp };
|
|
478
|
+
break;
|
|
479
|
+
case "execution_suspended":
|
|
480
|
+
node.suspension = { state: "suspended", source: event.source, requestedAt: node.suspension?.requestedAt ?? event.timestamp, suspendedAt: event.timestamp };
|
|
481
|
+
break;
|
|
482
|
+
case "execution_resumed":
|
|
483
|
+
delete node.suspension;
|
|
484
|
+
break;
|
|
485
|
+
case "execution_hold_added":
|
|
486
|
+
node.holdAfter = { source: event.source, requestedAt: event.timestamp };
|
|
487
|
+
break;
|
|
488
|
+
case "execution_hold_removed":
|
|
489
|
+
delete node.holdAfter;
|
|
490
|
+
break;
|
|
448
491
|
case "node_completed":
|
|
449
492
|
case "node_failed":
|
|
493
|
+
delete node.suspension;
|
|
450
494
|
node.status = event.type === "node_completed" ? "completed" : "failed";
|
|
451
495
|
node.finishedAt = event.timestamp;
|
|
452
496
|
node.latencyMs = event.latencyMs;
|
|
@@ -625,8 +669,12 @@ export function renderGraphResult(result, expanded, isPartial, theme, fallback =
|
|
|
625
669
|
}
|
|
626
670
|
if (result.error)
|
|
627
671
|
lines.push(theme.fg("error", `${result.error.code}: ${compact(result.error.message, 120)}`));
|
|
672
|
+
if ("pause" in result && result.pause)
|
|
673
|
+
lines.push(theme.fg("warning", `job paused by ${compact(result.pause.source, 20)} · no new executions start`));
|
|
628
674
|
if ("pausedExecutionIds" in result && result.pausedExecutionIds?.length)
|
|
629
675
|
lines.push(theme.fg("warning", `${result.pausedExecutionIds.length} paused executions · revision ${result.revision ?? 0}`));
|
|
676
|
+
if ("suspendedExecutionIds" in result && result.suspendedExecutionIds?.length)
|
|
677
|
+
lines.push(theme.fg("warning", `${result.suspendedExecutionIds.length} suspended executions · revision ${result.revision ?? 0}`));
|
|
630
678
|
if ("executions" in result)
|
|
631
679
|
lines.push(theme.fg("dim", `${Object.keys(result.executions).length} executions · revision ${result.revision}`));
|
|
632
680
|
const chartStart = lines.length;
|