@chrok/pi-braid 0.4.0 → 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 +119 -56
- package/dist/command.js +126 -7
- package/dist/display.js +62 -14
- package/dist/drain.js +11 -5
- package/dist/index.js +136 -90
- package/dist/jobs.js +136 -16
- package/dist/runner.js +166 -80
- package/dist/status-text.js +89 -3
- package/package.json +2 -2
package/dist/status-text.js
CHANGED
|
@@ -58,11 +58,23 @@ function workspaceLine(workspace) {
|
|
|
58
58
|
parts.push(`${item.executionId} ${item.disposition}: ${item.reason}`);
|
|
59
59
|
return parts.join(" · ");
|
|
60
60
|
}
|
|
61
|
+
/** Who paused an execution; a node timeout pause also says how long it ran. */
|
|
62
|
+
export function pausedBy(suspension) {
|
|
63
|
+
if (suspension.source !== "node_timeout")
|
|
64
|
+
return `paused by ${suspension.source}`;
|
|
65
|
+
return suspension.timeout ? `paused by node timeout after ${duration(suspension.timeout.activeMs)} (budget ${duration(suspension.timeout.nodeTimeoutMs)})` : "paused by node timeout";
|
|
66
|
+
}
|
|
67
|
+
/** A node timeout pause waits for the parent's decision. */
|
|
68
|
+
const TIMEOUT_PAUSE_NOTE = "Its node timeout ran out. Resume it with braid_resume (optionally with nodeTimeoutMs; the default is the job's nodeTimeoutMs again), or fail only this execution with braid_cancel and executionIds. Its successors wait until then.";
|
|
61
69
|
function executionLine(execution) {
|
|
62
70
|
const parts = [execution.executionId ?? "(no execution)", `node ${execution.id}`];
|
|
63
71
|
if (execution.iteration)
|
|
64
72
|
parts.push(`iteration ${execution.iteration}`);
|
|
65
73
|
parts.push(execution.skipReason ? `${execution.status} (${execution.skipReason})` : execution.status);
|
|
74
|
+
if (execution.suspension)
|
|
75
|
+
parts.push(`${execution.suspension.state}, ${pausedBy(execution.suspension)}`);
|
|
76
|
+
if (pendingHold(execution))
|
|
77
|
+
parts.push(`holds when finished, set by ${holdSource(execution.holdAfter)}`);
|
|
66
78
|
if (execution.latencyMs !== undefined)
|
|
67
79
|
parts.push(duration(execution.latencyMs));
|
|
68
80
|
if (execution.decision)
|
|
@@ -77,8 +89,27 @@ export function formatJobList(jobs) {
|
|
|
77
89
|
return [`${jobs.length} Braid job${jobs.length === 1 ? "" : "s"} in this session, newest first:`,
|
|
78
90
|
...jobs.map(job => `- ${job.handle} · ${job.status} · started ${iso(job.createdAt)} · id ${job.jobId}\n Goal: ${job.goal}`)].join("\n");
|
|
79
91
|
}
|
|
92
|
+
/** Held (pauseAfter) and suspended executions are resumed the same way; a job pause has its own resume. */
|
|
80
93
|
function control(execution) {
|
|
81
|
-
|
|
94
|
+
if (!execution)
|
|
95
|
+
return "Graph not started";
|
|
96
|
+
const parts = [`Graph ${execution.status}`, `revision ${execution.revision}`, `paused executions: ${ids(execution.pausedExecutionIds)}`];
|
|
97
|
+
if (execution.suspendedExecutionIds?.length)
|
|
98
|
+
parts.push(`suspended executions: ${ids(execution.suspendedExecutionIds)}`);
|
|
99
|
+
if (execution.pause)
|
|
100
|
+
parts.push(`job paused by ${execution.pause.source} at ${iso(execution.pause.pausedAt)}`);
|
|
101
|
+
return parts.join(" · ");
|
|
102
|
+
}
|
|
103
|
+
/** Paused-by-user work is the user's call: say so wherever the parent sees it. */
|
|
104
|
+
const USER_PAUSE_NOTE = "The user paused this; do not resume it unless the user asks.";
|
|
105
|
+
const USER_HOLD_NOTE = "The user set this hold; do not remove it or release the result unless the user asks.";
|
|
106
|
+
/** A hold that applies once a still-unsettled execution finishes. Settled holds are paused executions. */
|
|
107
|
+
function pendingHold(execution) {
|
|
108
|
+
return Boolean(execution.holdAfter) && (execution.status === "running" || execution.status === "runnable");
|
|
109
|
+
}
|
|
110
|
+
const holdSource = (hold) => hold.source === "definition" ? "pauseAfter" : hold.source;
|
|
111
|
+
function userPaused(execution, executions) {
|
|
112
|
+
return execution?.pause?.source === "user" || executions.some(item => item.suspension?.source === "user" || item.holdAfter?.source === "user");
|
|
82
113
|
}
|
|
83
114
|
/** The submission receipt. The caller already has the graph it submitted. */
|
|
84
115
|
export function formatReceipt(job, message) {
|
|
@@ -86,10 +117,40 @@ export function formatReceipt(job, message) {
|
|
|
86
117
|
}
|
|
87
118
|
export function formatUpdate(job) {
|
|
88
119
|
return [`Braid job ${job.handle} updated to revision ${job.execution.revision}.`,
|
|
89
|
-
`Paused executions: ${ids(job.execution.pausedExecutionIds)}`,
|
|
120
|
+
`Paused executions: ${ids(job.execution.pausedExecutionIds)}`,
|
|
121
|
+
...job.execution.suspendedExecutionIds?.length ? [`Suspended executions: ${ids(job.execution.suspendedExecutionIds)}`] : [],
|
|
122
|
+
"Current graph:", formatGraph(job.execution.graph)].join("\n");
|
|
90
123
|
}
|
|
91
124
|
export function formatResume(job) {
|
|
92
|
-
|
|
125
|
+
const execution = job.execution;
|
|
126
|
+
return `Braid job ${job.handle} resumed at revision ${execution.revision}. Still paused: ${ids(execution.pausedExecutionIds)}. ` +
|
|
127
|
+
`Still suspended: ${ids(execution.suspendedExecutionIds)}.${execution.pause ? ` The job itself is still paused by ${execution.pause.source}.` : ""}`;
|
|
128
|
+
}
|
|
129
|
+
/** The result of braid_pause or braid_resume with after=true. */
|
|
130
|
+
export function formatHold(job, executionIds, hold) {
|
|
131
|
+
const execution = job.execution;
|
|
132
|
+
if (!hold)
|
|
133
|
+
return [`Braid job ${job.handle}: hold removed for ${ids(executionIds)}; their results go downstream when they finish.`, control(execution)].join("\n");
|
|
134
|
+
return [
|
|
135
|
+
`Braid job ${job.handle}: ${ids(executionIds)} keep running and will be held when they finish; downstream work waits until you release them.`,
|
|
136
|
+
control(execution),
|
|
137
|
+
"Each one appears in paused executions once it finishes, and you get a pause reminder. Review it with braid_status, then release it with " +
|
|
138
|
+
`braid_resume({"jobId":"${job.handle}","expectedRevision":<current>,"executionIds":[...]}), or atomically with braid_update resume. ` +
|
|
139
|
+
`To remove the hold before it finishes, use braid_resume({"jobId":"${job.handle}","executionIds":[...],"after":true}).`,
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
/** The result of braid_pause: what is pausing now and how to continue. */
|
|
143
|
+
export function formatPause(job, executionIds) {
|
|
144
|
+
const execution = job.execution;
|
|
145
|
+
const pausing = Object.values(execution.executions).filter(item => item.suspension?.state === "pausing").map(item => item.executionId);
|
|
146
|
+
return [
|
|
147
|
+
executionIds ? `Braid job ${job.handle}: pause requested for ${ids(executionIds)}.` : `Braid job ${job.handle} paused: no new executions start, and running ones pause.`,
|
|
148
|
+
control(execution),
|
|
149
|
+
`Pausing (each stops before its next model request; a running tool finishes first): ${ids(pausing)}.`,
|
|
150
|
+
"Suspended executions keep their conversation and workspace and release their concurrency slot. The node timeout stops while suspended; the graph timeout keeps running. " +
|
|
151
|
+
(executionIds ? `Continue with braid_resume({"jobId":"${job.handle}","expectedRevision":${execution.revision},"executionIds":[...]}).`
|
|
152
|
+
: `Continue with braid_resume({"jobId":"${job.handle}"}).`),
|
|
153
|
+
].join("\n");
|
|
93
154
|
}
|
|
94
155
|
export function formatActivity(activity, now, jobId) {
|
|
95
156
|
const lines = [`Activity: ${activity.phase} for ${duration(now - activity.phaseStartedAt)}; last observed signal ${duration(now - activity.lastActivityAt)} ago (${activity.lastActivity}). Model requests ${activity.modelRequests}, tool calls ${activity.toolCalls}.`];
|
|
@@ -124,6 +185,8 @@ export function formatActivity(activity, now, jobId) {
|
|
|
124
185
|
/** A focused read: the current control state, then one execution in full. */
|
|
125
186
|
export function formatNodeStatus({ job, node, activity, now }) {
|
|
126
187
|
const lines = [`Braid job ${job.handle} · ${job.status} · ${control(job.execution)}`,
|
|
188
|
+
...node.suspension ? [`Execution is ${node.suspension.state}, ${pausedBy(node.suspension)}${node.suspension.suspendedAt !== undefined ? ` since ${iso(node.suspension.suspendedAt)}` : ""}.${node.suspension.source === "user" ? ` ${USER_PAUSE_NOTE}` : node.suspension.source === "node_timeout" ? ` ${TIMEOUT_PAUSE_NOTE}` : ""}`] : [],
|
|
189
|
+
...pendingHold(node) ? [`Its result will be held when it finishes (set by ${holdSource(node.holdAfter)}).${node.holdAfter.source === "user" ? ` ${USER_HOLD_NOTE}` : ""}`] : [],
|
|
127
190
|
`Execution ${node.executionId ?? "(none yet)"} · node ${node.id}${node.node ? ` (${node.node.type})` : ""}${node.iteration ? ` · iteration ${node.iteration}` : ""} · ${node.skipReason ? `${node.status} (${node.skipReason})` : node.status}`];
|
|
128
191
|
const timing = [node.startedAt !== undefined && `started ${iso(node.startedAt)}`, node.latencyMs !== undefined && `took ${duration(node.latencyMs)}`,
|
|
129
192
|
node.model && `model ${node.model}`, node.usage && `usage input ${node.usage.inputTokens}, output ${node.usage.outputTokens}`].filter(Boolean);
|
|
@@ -148,6 +211,8 @@ export function formatJobStatus(job, now) {
|
|
|
148
211
|
const state = job.result ?? job.execution;
|
|
149
212
|
// Control fields lead: a long goal or output must not push them out of a truncated preview.
|
|
150
213
|
const header = [`Braid job ${job.handle} · ${job.status}`, `ID: ${job.jobId}`, control(job.execution)];
|
|
214
|
+
if (job.status === "running" && userPaused(job.execution, Object.values(job.execution?.executions ?? {})))
|
|
215
|
+
header.push(USER_PAUSE_NOTE);
|
|
151
216
|
const error = job.error ?? (job.result?.error ? failure(job.result.error) : job.execution?.error ? failure(job.execution.error) : undefined);
|
|
152
217
|
if (error)
|
|
153
218
|
header.push(`Error: ${error}`);
|
|
@@ -177,3 +242,24 @@ export function formatJobStatus(job, now) {
|
|
|
177
242
|
sections.push(`Current graph (revision ${job.execution.revision}):\n${formatGraph(job.execution.graph)}`);
|
|
178
243
|
return sections.join("\n\n");
|
|
179
244
|
}
|
|
245
|
+
/** The waking reminder for an execution its node timeout paused. */
|
|
246
|
+
export function formatTimeoutPause(pause) {
|
|
247
|
+
const execution = `${pause.executionId}${pause.iteration ? ` (iteration ${pause.iteration})` : ""}`;
|
|
248
|
+
return [
|
|
249
|
+
`Braid job ${pause.handle} node ${JSON.stringify(pause.nodeId)} execution ${execution} was paused because it exceeded its node timeout:`,
|
|
250
|
+
`it ran ${duration(pause.activeMs)} against a budget of ${duration(pause.nodeTimeoutMs)}.`,
|
|
251
|
+
pause.activity ? `When the timeout fired it was: ${pause.activity}.` : "",
|
|
252
|
+
"Its conversation, workspace, and tool budgets are kept; the step that was interrupted is reported to it after resume.",
|
|
253
|
+
"Its successors wait until you decide. Check it with",
|
|
254
|
+
`braid_status(${JSON.stringify({ jobId: pause.handle, executionId: pause.executionId })}), then either`,
|
|
255
|
+
`resume it with braid_resume({"jobId":"${pause.handle}","executionIds":["${pause.executionId}"],"expectedRevision":<current>}) — it gets the job's nodeTimeoutMs again, or pass nodeTimeoutMs —`,
|
|
256
|
+
"or resume it in braid_update with resume and resumeOptions together with graph changes,",
|
|
257
|
+
`or fail only this execution with braid_cancel({"jobId":"${pause.handle}","executionIds":["${pause.executionId}"]}).`,
|
|
258
|
+
"The graph timeout keeps running while it is paused.",
|
|
259
|
+
].filter(Boolean).join(" ");
|
|
260
|
+
}
|
|
261
|
+
/** The result of braid_cancel with executionIds. */
|
|
262
|
+
export function formatCancelExecutions(job, executionIds) {
|
|
263
|
+
return [`Braid job ${job.handle}: cancellation requested for ${ids(executionIds)}. They fail; their successors follow the usual failure rules.`,
|
|
264
|
+
control(job.execution)].join("\n");
|
|
265
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrok/pi-braid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Pi extension for Braid: background agent graphs, bounded loops, live updates, and a flow panel",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"grok-mermaid": "^0.2.2",
|
|
9
|
-
"@chrok/braid": "0.
|
|
9
|
+
"@chrok/braid": "0.5.0"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "*",
|