@chrok/pi-braid 0.3.4 → 0.3.5
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 +28 -21
- package/dist/command.js +0 -3
- package/dist/display.js +2 -2
- package/dist/index.js +41 -82
- package/dist/jobs.js +0 -14
- package/dist/runner.js +14 -2
- package/dist/status-text.js +179 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -44,8 +44,10 @@ On completion, failure, or cancellation, the extension sends a custom
|
|
|
44
44
|
Pi queues it as steering during streaming: it enters context after the current
|
|
45
45
|
assistant response and its entire tool batch, before the next model step. It
|
|
46
46
|
does not wait for the whole foreground task to finish or skip remaining tools.
|
|
47
|
-
When idle, it starts a new agent turn automatically.
|
|
48
|
-
|
|
47
|
+
When idle, it starts a new agent turn automatically. Tool descriptions, the
|
|
48
|
+
system prompt, and the submission receipt tell the agent to continue independent
|
|
49
|
+
work, or end its turn if it needs the result, and not to sleep, use a timer, or
|
|
50
|
+
poll in a loop to wait for the job.
|
|
49
51
|
Stopping the foreground response does not stop background jobs.
|
|
50
52
|
|
|
51
53
|
Jobs live in memory for the current Pi session. Quitting, reloading extensions,
|
|
@@ -107,9 +109,10 @@ All four types accept `model`, `notifyOnCompletion`, `pauseAfter`, and
|
|
|
107
109
|
default to writable worktrees in Git; outside Git, all workers are read-only.
|
|
108
110
|
The model-facing schema exposes these fields in one object with a `type` enum;
|
|
109
111
|
core validation enforces the type-specific requirements before a submission or
|
|
110
|
-
update takes effect.
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
update takes effect. The submission reply gives the job ID and how to wait for
|
|
113
|
+
it; it does not repeat the graph the agent just sent. Update replies list the
|
|
114
|
+
resulting graph without prompts. If a definition problem repeats,
|
|
115
|
+
inspect/report the mismatch instead of launching more probe jobs.
|
|
113
116
|
|
|
114
117
|
Edges use exact node IDs in `from` and `to`. `choice` is an exact label declared
|
|
115
118
|
by the source decision; omitting it makes the edge unconditional. Pass `edges: []`
|
|
@@ -152,7 +155,7 @@ drops the queued message. Session shutdown suppresses delivery.
|
|
|
152
155
|
|
|
153
156
|
Set `pauseAfter: true` to hold an execution's outgoing scheduling and receive a
|
|
154
157
|
pause reminder. Independent branches continue. Fetch `braid_status` to obtain
|
|
155
|
-
|
|
158
|
+
the current graph revision and paused execution IDs, then:
|
|
156
159
|
|
|
157
160
|
```json
|
|
158
161
|
{
|
|
@@ -178,9 +181,9 @@ Pause reminders describe the event when it occurred: check current status and
|
|
|
178
181
|
paused IDs before attempting an update/resume, since the job can time out or be
|
|
179
182
|
cancelled before the parent handles the reminder.
|
|
180
183
|
|
|
181
|
-
Focused `braid_status` reads also
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
Focused `braid_status` reads also start with the current control fields: graph
|
|
185
|
+
status, revision, and paused execution IDs. Use that current revision for
|
|
186
|
+
updates even when inspecting an older invocation.
|
|
184
187
|
After cancellation or finalization there are no resumable paused executions;
|
|
185
188
|
the event log still records where pauses occurred.
|
|
186
189
|
|
|
@@ -317,9 +320,15 @@ counts, and the execution log. Active nodes are marked `▶ ACTIVE`. The status
|
|
|
317
320
|
tool also renders a flowchart; expand its result to see more log events.
|
|
318
321
|
When selecting `nodeId` or `executionId`, it instead shows that invocation's
|
|
319
322
|
output/error and activity, with the full text and history available on expansion.
|
|
320
|
-
|
|
323
|
+
|
|
324
|
+
The Braid tools return plain-text documents to the model, not JSON. A
|
|
325
|
+
whole-job read lists the control state, every execution, their outputs,
|
|
326
|
+
workspaces, and the current graph without prompts. A focused read shows one
|
|
327
|
+
execution's output and its activity with the latest 40 history entries; pass
|
|
321
328
|
`activityBefore` with an entry sequence to page earlier ones. While the invocation
|
|
322
|
-
runs,
|
|
329
|
+
runs, the activity comes before the output so it survives preview truncation.
|
|
330
|
+
Worker `git` results read like Pi's bash output, and `decide` returns
|
|
331
|
+
`Recorded choice: <choice>`.
|
|
323
332
|
|
|
324
333
|
`/braid` now opens this panel; it no longer arms the next prompt. To request
|
|
325
334
|
Braid explicitly, ask the agent to analyze the task using Braid.
|
|
@@ -330,11 +339,9 @@ model lookup, credentials/OAuth, provider transport, filesystem tool execution,
|
|
|
330
339
|
and token/cost accounting. The first
|
|
331
340
|
whole-job `braid_status` retrieval of a finished job reports its accumulated Pi usage;
|
|
332
341
|
subsequent retrievals do not count the same usage again.
|
|
333
|
-
|
|
334
|
-
rounds from nodes that later fail or time out
|
|
335
|
-
|
|
336
|
-
returned by runners. Large running status reads save a complete snapshot too,
|
|
337
|
-
so truncation never requires waiting for job completion to inspect the graph.
|
|
342
|
+
The whole-job status text reports Pi's totals as `Usage`, including completed
|
|
343
|
+
provider rounds from nodes that later fail or time out; core `metadata.usage`
|
|
344
|
+
only includes usage returned by runners.
|
|
338
345
|
|
|
339
346
|
## When Pi will use Braid
|
|
340
347
|
|
|
@@ -378,7 +385,7 @@ only the current source IDs and diagnoses missing, duplicate or unexpected IDs.
|
|
|
378
385
|
|
|
379
386
|
## Install from npm
|
|
380
387
|
|
|
381
|
-
Requires Node.js 22.19+ and Pi 1.0
|
|
388
|
+
Requires Node.js 22.19+ and Pi 1.2.0 (the tested version):
|
|
382
389
|
|
|
383
390
|
```sh
|
|
384
391
|
pi install npm:@chrok/pi-braid
|
|
@@ -389,7 +396,7 @@ The package depends on the exact matching `@chrok/braid` release; npm installs
|
|
|
389
396
|
core automatically. It does not bundle core or depend on a source checkout.
|
|
390
397
|
Pi supplies its core peer packages at runtime.
|
|
391
398
|
Their wildcard ranges follow Pi's packaging convention, not universal version
|
|
392
|
-
compatibility. Development and CI pin Pi 1.0.
|
|
399
|
+
compatibility. Development and CI pin Pi 1.2.0.
|
|
393
400
|
|
|
394
401
|
## Install this local checkout in Pi
|
|
395
402
|
|
|
@@ -618,9 +625,9 @@ writes already made. Core waits for tracked writes, preserves checkpoints, and
|
|
|
618
625
|
removes worker worktrees before completing cancellation.
|
|
619
626
|
|
|
620
627
|
The tool output is capped at 50KB/2000 lines to protect Pi context. When exceeded,
|
|
621
|
-
the adapter writes
|
|
622
|
-
inherited ACLs on Windows) and
|
|
623
|
-
|
|
628
|
+
the adapter writes the complete text to a temporary file (mode 600 on POSIX;
|
|
629
|
+
inherited ACLs on Windows) and ends the preview with its path, for running jobs
|
|
630
|
+
as well as finished ones.
|
|
624
631
|
|
|
625
632
|
For a test of proactive selection, start a fresh Pi turn with a task such as:
|
|
626
633
|
|
package/dist/command.js
CHANGED
|
@@ -201,9 +201,6 @@ export class BraidPanel {
|
|
|
201
201
|
...(current.result ?? current.live),
|
|
202
202
|
progress: current.live.progress,
|
|
203
203
|
...(current.workspaces ? { workspaces: current.workspaces } : {}),
|
|
204
|
-
...(current.fullOutputPath
|
|
205
|
-
? { fullOutputPath: current.fullOutputPath }
|
|
206
|
-
: {}),
|
|
207
204
|
}, true, false, this.theme, "", false, this.choosing).render(width),
|
|
208
205
|
]
|
|
209
206
|
: ["No Braid jobs in this session."];
|
package/dist/display.js
CHANGED
|
@@ -551,7 +551,7 @@ export function renderNodeResult(node, expanded, theme, fullOutputPath, progress
|
|
|
551
551
|
...(activity && activity.executionId === node.executionId ? renderActivity(activity, expanded, theme, now) : []),
|
|
552
552
|
...(node.output ? [expanded ? clean(node.output) : compact(node.output, 400)] : []),
|
|
553
553
|
...(!expanded && node.output && (node.output.length > 400 || node.output.includes("\n")) ? [theme.fg("dim", "Expand for full node output")] : []),
|
|
554
|
-
...(fullOutputPath ? [theme.fg("dim", `full
|
|
554
|
+
...(fullOutputPath ? [theme.fg("dim", `full text: ${compact(fullOutputPath, 240)}`)] : []),
|
|
555
555
|
];
|
|
556
556
|
return new Text(lines.join("\n"), 0, 0);
|
|
557
557
|
}
|
|
@@ -655,7 +655,7 @@ export function renderGraphResult(result, expanded, isPartial, theme, fallback =
|
|
|
655
655
|
if (expanded)
|
|
656
656
|
lines.push(...renderEventLog(result.events ?? [], true, theme));
|
|
657
657
|
if ("fullOutputPath" in result && result.fullOutputPath)
|
|
658
|
-
lines.push(theme.fg("dim", `full
|
|
658
|
+
lines.push(theme.fg("dim", `full text: ${compact(result.fullOutputPath, 240)}`));
|
|
659
659
|
return new FixedLines(lines, chartStart, chart.length, nodeList, [
|
|
660
660
|
...(selectedNodeId === undefined ? [] : [{ mark: SELECTED_NODE_MARK, label: "selected node" }]),
|
|
661
661
|
{ mark: "▶ ACTIVE", label: "running node" },
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { BraidJobs } from "./jobs.js";
|
|
|
8
8
|
import { registerBraidCommand, registerBraidWidget } from "./command.js";
|
|
9
9
|
import { registerReviewCommand } from "./review.js";
|
|
10
10
|
import { renderGraphCall, renderGraphResult, renderNodeResult } from "./display.js";
|
|
11
|
+
import { formatJobList, formatJobStatus, formatNodeStatus, formatReceipt, formatResume, formatUpdate } from "./status-text.js";
|
|
11
12
|
const text = (description) => Type.String({ minLength: 1, pattern: "\\S", ...(description ? { description } : {}) });
|
|
12
13
|
const prompt = () => Type.Union([
|
|
13
14
|
text("Instructions for this node; it does not receive the parent conversation."),
|
|
@@ -89,14 +90,24 @@ const BRAID_FILESYSTEM_GUIDANCE = "Every execute/decision activation gets a fres
|
|
|
89
90
|
"merge combines selected predecessor checkpoints into a new isolated worktree; integrate writes selected changes to the invoking checkout while preserving user edits. Workers apply changes with Git/file tools, then call finish_merge using executionId for each source; finish_merge only records dispositions and does not apply changes. There is no automatic final integration. " +
|
|
90
91
|
"Do not set workspace on merge/integrate nodes. Checkpoints remain recoverable after cleanup. Optional failed predecessors pass errors and partial work along unconditional edges; requireSuccess=true makes failure abort the job. " +
|
|
91
92
|
"Nodes have local read/ls and Git inspection; writable nodes have write/edit and Pi shell tools for dependencies, builds, and tests, while merge/integrate also have local Git integration tools. Search tools require local rg/fd. Outside Git all filesystem access is read-only; read-only nodes have no shell. Worktrees are not an OS sandbox: prompts constrain shell writes and shared resources. Checkpoints omit ignored new files. Parent extension/MCP tools and recursive Braid calls are not provided. Nodes should verify their changes; the parent reviews results and performs any remaining validation after integration.";
|
|
93
|
+
/** Lifecycle guidance shared by the system prompt and tool descriptions. */
|
|
94
|
+
export const BRAID_ASYNC_GUIDANCE = "Braid is asynchronous: braid returns a jobId immediately and the job keeps running independently of the foreground response; ending your turn does not cancel it. " +
|
|
95
|
+
"Completion, failure, or cancellation sends a reminder: an idle agent is resumed automatically, and a running agent receives it at the next model step. " +
|
|
96
|
+
"After submitting, continue useful independent work. If nothing else can proceed until the result arrives, end your turn and let the reminder resume the task. " +
|
|
97
|
+
"Do not call sleep, run a shell sleep, use a timer, or enter a sleep/poll loop solely to wait for a Braid job; delays required by the user's actual task remain allowed. " +
|
|
98
|
+
"On the reminder, retrieve results with braid_status and continue the original task. Between reminders, use braid_status only for deliberate progress checks or diagnosis.";
|
|
99
|
+
/** Short reminder repeated in every submission receipt. */
|
|
100
|
+
export const BRAID_WAIT_REMINDER = "Braid runs asynchronously and notifies you when the job finishes. Continue independent work, or end your turn if you need the result before proceeding; the completion reminder will resume you. " +
|
|
101
|
+
"Do not sleep or poll in a loop to wait for this job. Retrieve its results with braid_status after the reminder.";
|
|
92
102
|
const BRAID_USAGE_GUIDANCE = [
|
|
93
103
|
"Braid is a proactive execution primitive, not only a user-requested command.",
|
|
94
104
|
"Selection rule: for a code review, bug investigation, design comparison, test-planning request, or change spanning multiple files, call braid FIRST when two or more concerns can be handled independently. Nodes can analyze the project and implement changes in isolated Git worktrees. Do this without waiting for the user to say Braid; do not read everything in the parent and then decide whether to delegate.",
|
|
95
105
|
BRAID_FILESYSTEM_GUIDANCE,
|
|
96
106
|
"For repeated instructions, define promptTemplates once and use prompt={template: name, variables: {name: value}} on nodes. Values are strings inserted literally into {{name}} placeholders; plain-string prompts remain supported.",
|
|
97
|
-
"When Braid fits, submit a graph: use parallel execute nodes for independent analysis or implementation, execute nodes to synthesize findings, merge nodes to combine code snapshots, and integrate nodes to apply changes to the working branch.
|
|
107
|
+
"When Braid fits, submit a graph: use parallel execute nodes for independent analysis or implementation, execute nodes to synthesize findings, merge nodes to combine code snapshots, and integrate nodes to apply changes to the working branch. Use braid_status with the jobId to retrieve terminal outputs before relying on them.",
|
|
108
|
+
BRAID_ASYNC_GUIDANCE,
|
|
98
109
|
"Set notifyOnCompletion=true on selected nodes to receive intermediate success/failure reminders. Use braid_status({jobId, executionId}) to retrieve the exact execution; nodeId selects the latest instance. Use pauseAfter=true to hold outgoing scheduling. Definitions can always be changed with braid_update using expectedRevision; existing executions retain their captured inputs. Use resume in the same update to apply changes and release held executions atomically, or braid_resume for no graph changes.",
|
|
99
|
-
"Make the delegation choice once per user request. Reminders and definition errors are not new tasks.
|
|
110
|
+
"Make the delegation choice once per user request. Reminders and definition errors are not new tasks. If the same definition problem recurs, stop resubmitting and report the concrete mismatch; do not launch repeated probe/replacement jobs. In braid_update, add new nodes together with their dependencies and loops: a node added without incoming edges can start immediately, even while another execution is paused.",
|
|
100
111
|
"Do not use braid for a simple one-step answer, a trivial direct edit, a single shell command, or when decomposition adds no value. Writable nodes can implement and test their work. The parent reviews results and performs any remaining validation after Braid completes.",
|
|
101
112
|
].join("\n");
|
|
102
113
|
function isJobSnapshot(value) {
|
|
@@ -110,21 +121,15 @@ function isJobSnapshot(value) {
|
|
|
110
121
|
}
|
|
111
122
|
/** Focused reads return the latest activity entries; activityBefore pages earlier ones. */
|
|
112
123
|
export const STATUS_ACTIVITY_ENTRIES = 40;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return graph && {
|
|
123
|
-
// Echo accepted types/policies without duplicating potentially large prompts.
|
|
124
|
-
nodes: graph.nodes.map(({ prompt: _prompt, ...node }) => node),
|
|
125
|
-
edges: graph.edges,
|
|
126
|
-
...(graph.loops ? { loops: graph.loops } : {}),
|
|
127
|
-
};
|
|
124
|
+
/** Bound a text result; the complete document stays readable in a private file. */
|
|
125
|
+
async function boundedText(text, name) {
|
|
126
|
+
const preview = truncateHead(text);
|
|
127
|
+
if (!preview.truncated)
|
|
128
|
+
return { text };
|
|
129
|
+
const directory = await mkdtemp(join(tmpdir(), "braid-status-"));
|
|
130
|
+
const path = join(directory, name);
|
|
131
|
+
await writeFile(path, text, { mode: 0o600 });
|
|
132
|
+
return { text: `${preview.content}\n[Truncated. Full text: ${path}]`, path };
|
|
128
133
|
}
|
|
129
134
|
export function createBraidTools(jobs) {
|
|
130
135
|
const braidTool = defineTool({
|
|
@@ -141,7 +146,8 @@ export function createBraidTools(jobs) {
|
|
|
141
146
|
"no parent history, inherited extension/MCP tools, or recursive Braid tools. " +
|
|
142
147
|
BRAID_FILESYSTEM_GUIDANCE + " " +
|
|
143
148
|
"Do not use it for a simple one-step answer or trivial direct edit. " +
|
|
144
|
-
"Use
|
|
149
|
+
"Use braid_cancel(jobId) to stop it. Humans can open /braid for the live flow panel. " +
|
|
150
|
+
BRAID_ASYNC_GUIDANCE + " " +
|
|
145
151
|
"Set notifyOnCompletion=true on selected nodes for intermediate success/failure reminders; retrieve their output/error with braid_status({jobId, nodeId}). Notifications do not pause scheduling; use pauseAfter for a hold, and braid_update to edit definitions. " +
|
|
146
152
|
"Read result.status: failed graphs can still return successful terminal outputs.",
|
|
147
153
|
promptSnippet: "Use FIRST for nontrivial code review/debug/design/implementation work; set workspace=read-only for analysis/synthesis, use worktrees for edits, merge to combine snapshots, and integrate to apply changes",
|
|
@@ -183,13 +189,7 @@ export function createBraidTools(jobs) {
|
|
|
183
189
|
content: [
|
|
184
190
|
{
|
|
185
191
|
type: "text",
|
|
186
|
-
text:
|
|
187
|
-
jobId: job.handle,
|
|
188
|
-
canonicalJobId: job.jobId,
|
|
189
|
-
status: job.status,
|
|
190
|
-
graph: graphReceipt(job),
|
|
191
|
-
message: "Running in background. Check the accepted graph above. Use braid_status to retrieve progress/results. Reminders arrive after the current step or resume you when idle; do not poll repeatedly.",
|
|
192
|
-
}),
|
|
192
|
+
text: formatReceipt(job, `${BRAID_WAIT_REMINDER} Humans can follow it live in /braid.`),
|
|
193
193
|
},
|
|
194
194
|
],
|
|
195
195
|
details: job,
|
|
@@ -209,7 +209,7 @@ export function createBraidTools(jobs) {
|
|
|
209
209
|
const statusTool = defineTool({
|
|
210
210
|
name: "braid_status",
|
|
211
211
|
label: "Braid status",
|
|
212
|
-
description: "Retrieve a background Braid job's status, node progress, and final results by exact session handle (e.g. job-1) or UUID in jobId. Add executionId for an exact execution, or nodeId for the latest instance, including intermediate results while the job runs. Focused reads include live activity: phase, time in phase and since the last observed signal, model request/stream state, running tools, and recent history. Lack of observed activity is a diagnostic signal, not proof of a stall.
|
|
212
|
+
description: "Retrieve a background Braid job's status, node progress, and final results by exact session handle (e.g. job-1) or UUID in jobId. Add executionId for an exact execution, or nodeId for the latest instance, including intermediate results while the job runs. Focused reads include live activity: phase, time in phase and since the last observed signal, model request/stream state, running tools, and recent history. Lack of observed activity is a diagnostic signal, not proof of a stall. Every job read starts with the current graph revision and paused execution IDs for live control. Results are text; long ones are truncated and the full text is saved to a file whose path is shown. Usage is Pi's provider accounting, including rounds from failed nodes. Prefer the short handle from submission/reminders. Omit all IDs to list jobs in this session. Completion reminders arrive automatically: read results after the reminder, or deliberately for a progress check or diagnosis; do not sleep, use a timer, or poll in a loop to wait for a job.",
|
|
213
213
|
parameters: statusParameters,
|
|
214
214
|
renderResult(result, options, theme, ctx) {
|
|
215
215
|
const job = result.details;
|
|
@@ -249,7 +249,7 @@ export function createBraidTools(jobs) {
|
|
|
249
249
|
throw new Error("activityBefore requires nodeId or executionId");
|
|
250
250
|
if (!params.jobId)
|
|
251
251
|
return {
|
|
252
|
-
content: [{ type: "text", text:
|
|
252
|
+
content: [{ type: "text", text: formatJobList(jobs.list()) }],
|
|
253
253
|
details: undefined,
|
|
254
254
|
};
|
|
255
255
|
const job = jobs.get(params.jobId);
|
|
@@ -257,63 +257,22 @@ export function createBraidTools(jobs) {
|
|
|
257
257
|
throw jobs.unknownJob(params.jobId);
|
|
258
258
|
if (params.nodeId !== undefined || params.executionId !== undefined) {
|
|
259
259
|
const node = jobs.getNode(params.jobId, params.nodeId, params.executionId);
|
|
260
|
-
const
|
|
261
|
-
const activity = executionId ? jobs.getActivity(params.jobId, executionId, {
|
|
260
|
+
const activity = node.executionId ? jobs.getActivity(params.jobId, node.executionId, {
|
|
262
261
|
limit: STATUS_ACTIVITY_ENTRIES, ...(params.activityBefore === undefined ? {} : { before: params.activityBefore }),
|
|
263
262
|
}) : undefined;
|
|
264
|
-
|
|
265
|
-
// so it comes first and history cannot push it out of the preview.
|
|
266
|
-
const running = activity && activity.finishedAt === undefined;
|
|
267
|
-
const live = activity ? { observedAt: job.live.observedAt, activity } : {};
|
|
268
|
-
const full = JSON.stringify({
|
|
269
|
-
jobId: job.jobId, handle: job.handle, status: job.status, execution: executionControl(job),
|
|
270
|
-
...(running ? live : {}),
|
|
271
|
-
node: { id, executionId, status, error, output, ...nodeDetails },
|
|
272
|
-
...(running ? {} : live),
|
|
273
|
-
}, null, 2);
|
|
274
|
-
const preview = truncateHead(full);
|
|
275
|
-
let suffix = "";
|
|
276
|
-
let nodeOutputPath;
|
|
277
|
-
if (preview.truncated) {
|
|
278
|
-
const directory = await mkdtemp(join(tmpdir(), "braid-node-result-"));
|
|
279
|
-
const path = join(directory, "result.json");
|
|
280
|
-
await writeFile(path, full, { mode: 0o600 });
|
|
281
|
-
nodeOutputPath = path;
|
|
282
|
-
suffix = `\n[Preview truncated. Full node result: ${path}]`;
|
|
283
|
-
}
|
|
263
|
+
const result = await boundedText(formatNodeStatus({ job, node, ...(activity ? { activity } : {}), now: Date.now() }), "node.txt");
|
|
284
264
|
// Focused reads do not claim the whole job's usage; final job retrieval does.
|
|
285
|
-
return { content: [{ type: "text", text:
|
|
286
|
-
...job, selectedNode: node, ...(activity ? { selectedActivity: activity } : {}), ...(
|
|
265
|
+
return { content: [{ type: "text", text: result.text }], details: {
|
|
266
|
+
...job, selectedNode: node, ...(activity ? { selectedActivity: activity } : {}), ...(result.path ? { nodeOutputPath: result.path } : {}),
|
|
287
267
|
} };
|
|
288
268
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const full = JSON.stringify({
|
|
292
|
-
jobId, handle, status,
|
|
293
|
-
error: error ?? job.result?.error ?? execution?.error,
|
|
294
|
-
usage: providerUsage, fullOutputPath,
|
|
295
|
-
execution: execution && { ...executionControl(job), ...execution },
|
|
296
|
-
...snapshot,
|
|
297
|
-
}, null, 2);
|
|
298
|
-
const preview = truncateHead(full);
|
|
299
|
-
let suffix = "";
|
|
300
|
-
let details = job;
|
|
301
|
-
if (preview.truncated) {
|
|
302
|
-
if (job.fullOutputPath)
|
|
303
|
-
suffix = `\n[Preview truncated. Full result/log: ${job.fullOutputPath}]`;
|
|
304
|
-
else {
|
|
305
|
-
const directory = await mkdtemp(join(tmpdir(), "braid-status-"));
|
|
306
|
-
const path = join(directory, "status.json");
|
|
307
|
-
await writeFile(path, full, { mode: 0o600 });
|
|
308
|
-
details = { ...job, fullOutputPath: path };
|
|
309
|
-
suffix = `\n[Preview truncated. Full status snapshot: ${path}]`;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
269
|
+
const result = await boundedText(formatJobStatus(job, Date.now()), "status.txt");
|
|
270
|
+
const details = result.path ? { ...job, fullOutputPath: result.path } : job;
|
|
312
271
|
// Saving a running snapshot can race job completion; claim usage only
|
|
313
272
|
// when this response actually contains a terminal snapshot.
|
|
314
273
|
const usage = job.status === "running" ? undefined : jobs.claimUsage(job.jobId);
|
|
315
274
|
return {
|
|
316
|
-
content: [{ type: "text", text:
|
|
275
|
+
content: [{ type: "text", text: result.text }],
|
|
317
276
|
details,
|
|
318
277
|
...(usage ? { usage } : {}),
|
|
319
278
|
};
|
|
@@ -342,14 +301,14 @@ export function createBraidTools(jobs) {
|
|
|
342
301
|
});
|
|
343
302
|
const updateParameters = Type.Object({
|
|
344
303
|
jobId: text("Exact session handle (e.g. job-1) or UUID."),
|
|
345
|
-
expectedRevision: Type.Integer({ minimum: 0, description: "Current
|
|
304
|
+
expectedRevision: Type.Integer({ minimum: 0, description: "Current graph revision from braid_status. A stale revision rejects the entire update." }),
|
|
346
305
|
upsertNodes: Type.Optional(Type.Array(nodeParameters, { description: "Complete node definitions to add or replace, not partial patches. Add dependencies and loops in this SAME update: new roots may start immediately even if another execution is paused. Already admitted executions keep their captured definitions." })),
|
|
347
306
|
removeNodeIds: Type.Optional(Type.Array(text(), { description: "Node IDs to remove, together with all incident edges." })),
|
|
348
307
|
addEdges: Type.Optional(Type.Array(updateEdgeParameters, { description: "Edges to add. Pin a historical source using executionId when needed." })),
|
|
349
|
-
removeEdges: Type.Optional(Type.Array(updateEdgeParameters, { description: "Exact edge identities to remove: copy from, to, and every present choice/feedback/executionId from
|
|
308
|
+
removeEdges: Type.Optional(Type.Array(updateEdgeParameters, { description: "Exact edge identities to remove: copy from, to, and every present choice/feedback/executionId from the current graph edges in braid_status. Omitted optional fields do not act as wildcards." })),
|
|
350
309
|
promptTemplates: Type.Optional(Type.Record(Type.String(), text(), { description: `${templateDescription} Replaces the entire template map; omit to preserve it. Include every template still referenced by nodes.` })),
|
|
351
310
|
loops: Type.Optional(Type.Array(loopParameters, { description: "Replaces all loop definitions; omit to preserve them, or use [] to remove all (also remove their feedback edges)." })),
|
|
352
|
-
resume: Type.Optional(Type.Array(text(), { uniqueItems: true, description: "Paused execution IDs from
|
|
311
|
+
resume: Type.Optional(Type.Array(text(), { uniqueItems: true, description: "Paused execution IDs from braid_status, not node IDs. Releases them atomically with the graph edit." })),
|
|
353
312
|
}, { additionalProperties: false });
|
|
354
313
|
const updateTool = defineTool({
|
|
355
314
|
name: "braid_update", label: "Update Braid",
|
|
@@ -364,13 +323,13 @@ export function createBraidTools(jobs) {
|
|
|
364
323
|
catch (error) {
|
|
365
324
|
throw new Error(`Braid update rejected; no changes or resumes were applied. ${error instanceof Error ? error.message : String(error)}. Retry the complete corrected patch, including its edges/loops/resume; adding disconnected nodes separately can start them immediately.`);
|
|
366
325
|
}
|
|
367
|
-
return { content: [{ type: "text", text:
|
|
326
|
+
return { content: [{ type: "text", text: formatUpdate(job) }], details: job };
|
|
368
327
|
},
|
|
369
328
|
});
|
|
370
329
|
const resumeParameters = Type.Object({
|
|
371
330
|
jobId: text("Exact session handle (e.g. job-1) or UUID."),
|
|
372
|
-
expectedRevision: Type.Integer({ minimum: 0, description: "Current
|
|
373
|
-
executionIds: Type.Array(text(), { minItems: 1, uniqueItems: true, description: "IDs from
|
|
331
|
+
expectedRevision: Type.Integer({ minimum: 0, description: "Current graph revision from braid_status." }),
|
|
332
|
+
executionIds: Type.Array(text(), { minItems: 1, uniqueItems: true, description: "Paused execution IDs from braid_status, not node IDs. Only these completed, paused executions are released." }),
|
|
374
333
|
}, { additionalProperties: false });
|
|
375
334
|
const resumeTool = defineTool({
|
|
376
335
|
name: "braid_resume", label: "Resume Braid",
|
|
@@ -378,7 +337,7 @@ export function createBraidTools(jobs) {
|
|
|
378
337
|
parameters: resumeParameters,
|
|
379
338
|
async execute(_id, params) {
|
|
380
339
|
const job = jobs.resume(params.jobId, params.executionIds, params.expectedRevision);
|
|
381
|
-
return { content: [{ type: "text", text:
|
|
340
|
+
return { content: [{ type: "text", text: formatResume(job) }], details: job };
|
|
382
341
|
},
|
|
383
342
|
});
|
|
384
343
|
return { braidTool, statusTool, cancelTool, updateTool, resumeTool };
|
package/dist/jobs.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { setImmediate as nextTurn } from "node:timers/promises";
|
|
5
|
-
import { truncateHead, } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { ExecutionActivityTracker, startBraid, validateGraph, } from "@chrok/braid";
|
|
7
3
|
import { applyEvent, applyProgress, createLiveState, } from "./display.js";
|
|
8
4
|
import { createPiRunner, sumPiUsage } from "./runner.js";
|
|
@@ -182,16 +178,6 @@ export class BraidJobs {
|
|
|
182
178
|
const status = job.result.error?.code === "CANCELLED"
|
|
183
179
|
? "cancelled"
|
|
184
180
|
: job.result.status;
|
|
185
|
-
// Core metadata counts usage returned by runners; Pi also records completed
|
|
186
|
-
// provider rounds from workers that later fail or time out.
|
|
187
|
-
const full = JSON.stringify({ ...job.result, workspaces: job.workspaces, piUsage: job.usage }, null, 2);
|
|
188
|
-
const preview = JSON.stringify({ ...this.get(job.jobId), status }, null, 2);
|
|
189
|
-
if (truncateHead(preview).truncated) {
|
|
190
|
-
const directory = await mkdtemp(join(tmpdir(), "braid-result-"));
|
|
191
|
-
const path = join(directory, "result.json");
|
|
192
|
-
await writeFile(path, full, { mode: 0o600 });
|
|
193
|
-
job.fullOutputPath = path;
|
|
194
|
-
}
|
|
195
181
|
job.status = status;
|
|
196
182
|
}
|
|
197
183
|
catch (error) {
|
package/dist/runner.js
CHANGED
|
@@ -5,6 +5,18 @@ import { createWorktreeWriteTools } from "./write-tools.js";
|
|
|
5
5
|
import { createAvailableReadTools } from "./read-tools.js";
|
|
6
6
|
import { createWorkspaceShellTools } from "./shell-tools.js";
|
|
7
7
|
import { formatHumanInput } from "./human-input.js";
|
|
8
|
+
/**
|
|
9
|
+
* Present a Git result the way Pi's bash tool presents command output. Output
|
|
10
|
+
* stays byte-exact, so a diff can be passed back to git apply.
|
|
11
|
+
*/
|
|
12
|
+
export function formatGitResult(result) {
|
|
13
|
+
let output = result.stdout;
|
|
14
|
+
if (result.stderr)
|
|
15
|
+
output += (output && !output.endsWith("\n") ? "\n" : "") + result.stderr;
|
|
16
|
+
if (result.exitCode === 0)
|
|
17
|
+
return output || "(no output)";
|
|
18
|
+
return `${output ? `${output.replace(/\n$/, "")}\n\n` : ""}Command exited with code ${result.exitCode}`;
|
|
19
|
+
}
|
|
8
20
|
/** Keep Pi's provider/auth plumbing and filesystem capabilities out of Braid's core. */
|
|
9
21
|
export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd()) {
|
|
10
22
|
const options = typeof onUsageOrOptions === "function"
|
|
@@ -289,7 +301,7 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
289
301
|
if (call.name === "git" && request.git) {
|
|
290
302
|
const args = parseGitToolArguments(call.arguments, (request.node.type === "merge" || request.node.type === "integrate"));
|
|
291
303
|
const result = await request.git(args.args, args.input);
|
|
292
|
-
return toolResult(call,
|
|
304
|
+
return toolResult(call, formatGitResult(result), result.exitCode !== 0);
|
|
293
305
|
}
|
|
294
306
|
if (call.name === "finish_merge" && request.merge) {
|
|
295
307
|
const dispositions = parseFinishMergeArguments(call.arguments, request.merge.sources.map(source => source.executionId));
|
|
@@ -339,7 +351,7 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
339
351
|
// No coercion or prose parsing: the core enforces the enum and exactly one call.
|
|
340
352
|
request.decide(args.choice);
|
|
341
353
|
decided = true;
|
|
342
|
-
return toolResult(call,
|
|
354
|
+
return toolResult(call, `Recorded choice: ${args.choice}`, false);
|
|
343
355
|
};
|
|
344
356
|
while (true) {
|
|
345
357
|
const response = await complete();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** Model-facing text documents for the parent Braid tools. */
|
|
2
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
3
|
+
const duration = (ms) => ms < 1000 ? `${Math.max(0, Math.round(ms))}ms` : ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
4
|
+
const failure = (error) => `${error.code}: ${error.message}`;
|
|
5
|
+
const ids = (values) => values?.length ? values.join(", ") : "none";
|
|
6
|
+
const GOAL_CHARS = 2000;
|
|
7
|
+
const clip = (text, limit) => text.length > limit ? `${text.slice(0, limit)}… [${text.length} chars]` : text;
|
|
8
|
+
const indent = (text) => text.split("\n").map(line => line ? ` ${line}` : line).join("\n");
|
|
9
|
+
export function formatUsage(usage) {
|
|
10
|
+
if (!usage)
|
|
11
|
+
return undefined;
|
|
12
|
+
const parts = [`input ${usage.input}`, `output ${usage.output}`];
|
|
13
|
+
if (usage.cacheRead)
|
|
14
|
+
parts.push(`cache read ${usage.cacheRead}`);
|
|
15
|
+
if (usage.cacheWrite)
|
|
16
|
+
parts.push(`cache write ${usage.cacheWrite}`);
|
|
17
|
+
const cost = usage.cost?.total ? `, $${usage.cost.total.toFixed(4)}` : "";
|
|
18
|
+
return `${parts.join(", ")} (${usage.totalTokens} tokens${cost})`;
|
|
19
|
+
}
|
|
20
|
+
function nodeLine(node) {
|
|
21
|
+
const settings = [node.type];
|
|
22
|
+
if ("workspace" in node && node.workspace)
|
|
23
|
+
settings.push(`workspace=${node.workspace}`);
|
|
24
|
+
if ("choices" in node)
|
|
25
|
+
settings.push(`choices=${node.choices.join("|")}`);
|
|
26
|
+
if (node.model)
|
|
27
|
+
settings.push(`model=${node.model}`);
|
|
28
|
+
if (typeof node.prompt === "object")
|
|
29
|
+
settings.push(`template=${node.prompt.template}`);
|
|
30
|
+
for (const flag of ["pauseAfter", "notifyOnCompletion", "requireSuccess"])
|
|
31
|
+
if (node[flag])
|
|
32
|
+
settings.push(flag);
|
|
33
|
+
return `- ${node.id}: ${settings.join(", ")}`;
|
|
34
|
+
}
|
|
35
|
+
/** Accepted definitions without prompts, which the caller already sent. */
|
|
36
|
+
export function formatGraph(graph) {
|
|
37
|
+
const lines = ["Nodes:", ...graph.nodes.map(nodeLine), "Edges:"];
|
|
38
|
+
lines.push(...graph.edges.length ? graph.edges.map(edge => {
|
|
39
|
+
const labels = [edge.choice && `choice=${edge.choice}`, edge.feedback && `feedback=${edge.feedback}`, edge.executionId && `execution=${edge.executionId}`].filter(Boolean);
|
|
40
|
+
return `- ${edge.from} -> ${edge.to}${labels.length ? ` [${labels.join(", ")}]` : ""}`;
|
|
41
|
+
}) : ["- none"]);
|
|
42
|
+
if (graph.loops?.length)
|
|
43
|
+
lines.push("Loops:", ...graph.loops.map(loop => `- ${loop.id}: entry ${loop.entry}, at most ${loop.maxIterations} iterations`));
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
function workspaceLine(workspace) {
|
|
47
|
+
const parts = [`${workspace.mode} ${workspace.workingDirectory}`, workspace.state];
|
|
48
|
+
for (const root of [workspace.worktreeRoot, workspace.sourceRoot])
|
|
49
|
+
if (root && root !== workspace.workingDirectory)
|
|
50
|
+
parts.push(`root ${root}`);
|
|
51
|
+
if (workspace.checkpointRef)
|
|
52
|
+
parts.push(`checkpoint ${workspace.checkpointRef}`);
|
|
53
|
+
if (workspace.backupRef)
|
|
54
|
+
parts.push(`backup ${workspace.backupRef}`);
|
|
55
|
+
if (workspace.reason)
|
|
56
|
+
parts.push(workspace.reason);
|
|
57
|
+
for (const item of workspace.dispositions ?? [])
|
|
58
|
+
parts.push(`${item.executionId} ${item.disposition}: ${item.reason}`);
|
|
59
|
+
return parts.join(" · ");
|
|
60
|
+
}
|
|
61
|
+
function executionLine(execution) {
|
|
62
|
+
const parts = [execution.executionId ?? "(no execution)", `node ${execution.id}`];
|
|
63
|
+
if (execution.iteration)
|
|
64
|
+
parts.push(`iteration ${execution.iteration}`);
|
|
65
|
+
parts.push(execution.skipReason ? `${execution.status} (${execution.skipReason})` : execution.status);
|
|
66
|
+
if (execution.latencyMs !== undefined)
|
|
67
|
+
parts.push(duration(execution.latencyMs));
|
|
68
|
+
if (execution.decision)
|
|
69
|
+
parts.push(`decision ${execution.decision}`);
|
|
70
|
+
if (execution.error)
|
|
71
|
+
parts.push(failure(execution.error));
|
|
72
|
+
return `- ${parts.join(" · ")}`;
|
|
73
|
+
}
|
|
74
|
+
export function formatJobList(jobs) {
|
|
75
|
+
if (!jobs.length)
|
|
76
|
+
return "No Braid jobs in this session.";
|
|
77
|
+
return [`${jobs.length} Braid job${jobs.length === 1 ? "" : "s"} in this session, newest first:`,
|
|
78
|
+
...jobs.map(job => `- ${job.handle} · ${job.status} · started ${iso(job.createdAt)} · id ${job.jobId}\n Goal: ${job.goal}`)].join("\n");
|
|
79
|
+
}
|
|
80
|
+
function control(execution) {
|
|
81
|
+
return execution ? `Graph ${execution.status} · revision ${execution.revision} · paused executions: ${ids(execution.pausedExecutionIds)}` : "Graph not started";
|
|
82
|
+
}
|
|
83
|
+
/** The submission receipt. The caller already has the graph it submitted. */
|
|
84
|
+
export function formatReceipt(job, message) {
|
|
85
|
+
return [`Braid job ${job.handle} started (${job.status}).`, `ID: ${job.jobId}`, message].join("\n");
|
|
86
|
+
}
|
|
87
|
+
export function formatUpdate(job) {
|
|
88
|
+
return [`Braid job ${job.handle} updated to revision ${job.execution.revision}.`,
|
|
89
|
+
`Paused executions: ${ids(job.execution.pausedExecutionIds)}`, "Current graph:", formatGraph(job.execution.graph)].join("\n");
|
|
90
|
+
}
|
|
91
|
+
export function formatResume(job) {
|
|
92
|
+
return `Braid job ${job.handle} resumed at revision ${job.execution.revision}. Still paused: ${ids(job.execution.pausedExecutionIds)}.`;
|
|
93
|
+
}
|
|
94
|
+
export function formatActivity(activity, now, jobId) {
|
|
95
|
+
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}.`];
|
|
96
|
+
const model = activity.model;
|
|
97
|
+
if (model && model.completedAt === undefined) {
|
|
98
|
+
const stream = model.firstStreamAt === undefined ? "no stream received yet"
|
|
99
|
+
: `receiving ${model.receiving ?? "output"}${model.toolName ? ` for ${model.toolName}` : ""}, ${model.receivedChars} chars, last chunk ${duration(now - (model.lastStreamAt ?? model.firstStreamAt))} ago`;
|
|
100
|
+
lines.push(`Model request ${model.round}: started ${duration(now - model.startedAt)} ago; ${stream}.`);
|
|
101
|
+
}
|
|
102
|
+
for (const tool of activity.tools) {
|
|
103
|
+
lines.push(`Running tool ${tool.name} for ${duration(now - tool.startedAt)}: ${tool.arguments}`);
|
|
104
|
+
if (tool.outputTail)
|
|
105
|
+
lines.push(indent(`Output tail:\n${tool.outputTail}`));
|
|
106
|
+
}
|
|
107
|
+
for (const limitation of activity.limitations)
|
|
108
|
+
lines.push(`Not observable: ${limitation}`);
|
|
109
|
+
if (activity.entries.length) {
|
|
110
|
+
lines.push("History:");
|
|
111
|
+
for (const entry of activity.entries) {
|
|
112
|
+
lines.push(`- #${entry.sequence} ${iso(entry.timestamp)} ${entry.kind}${entry.toolName ? ` ${entry.toolName}` : ""}${entry.isError ? " (error)" : ""}${entry.durationMs !== undefined ? ` ${duration(entry.durationMs)}` : ""}: ${entry.summary}`);
|
|
113
|
+
if (entry.detail)
|
|
114
|
+
lines.push(indent(entry.detail + (entry.detailLength ? `\n[preview of ${entry.detailLength} chars]` : "")));
|
|
115
|
+
}
|
|
116
|
+
const first = activity.entries[0].sequence;
|
|
117
|
+
if (first > 1)
|
|
118
|
+
lines.push(first - 1 > activity.droppedEntries
|
|
119
|
+
? `Earlier entries: braid_status({"jobId":"${jobId}","executionId":"${activity.executionId}","activityBefore":${first}})`
|
|
120
|
+
: `${activity.droppedEntries} earlier entries are no longer retained.`);
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
/** A focused read: the current control state, then one execution in full. */
|
|
125
|
+
export function formatNodeStatus({ job, node, activity, now }) {
|
|
126
|
+
const lines = [`Braid job ${job.handle} · ${job.status} · ${control(job.execution)}`,
|
|
127
|
+
`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
|
+
const timing = [node.startedAt !== undefined && `started ${iso(node.startedAt)}`, node.latencyMs !== undefined && `took ${duration(node.latencyMs)}`,
|
|
129
|
+
node.model && `model ${node.model}`, node.usage && `usage input ${node.usage.inputTokens}, output ${node.usage.outputTokens}`].filter(Boolean);
|
|
130
|
+
if (timing.length)
|
|
131
|
+
lines.push(timing.join(" · "));
|
|
132
|
+
if (node.decision)
|
|
133
|
+
lines.push(`Decision: ${node.decision}`);
|
|
134
|
+
if (node.error)
|
|
135
|
+
lines.push(`Error: ${failure(node.error)}`);
|
|
136
|
+
if (node.workspace)
|
|
137
|
+
lines.push(`Workspace: ${workspaceLine(node.workspace)}`);
|
|
138
|
+
if (node.humanInputs?.length)
|
|
139
|
+
lines.push("Human input delivered during this execution:", ...node.humanInputs.map(input => `- ${iso(input.timestamp)}: ${input.text}`));
|
|
140
|
+
const output = `Output:\n${node.output ?? "(none)"}`;
|
|
141
|
+
// While running, activity is the useful part; afterwards the output is.
|
|
142
|
+
const running = activity && activity.finishedAt === undefined;
|
|
143
|
+
const sections = [lines.join("\n"), ...running ? [formatActivity(activity, now, job.handle), output] : [output, ...activity ? [formatActivity(activity, now, job.handle)] : []]];
|
|
144
|
+
return sections.join("\n\n");
|
|
145
|
+
}
|
|
146
|
+
/** The whole job: control state, every execution, then their outputs and the current definition. */
|
|
147
|
+
export function formatJobStatus(job, now) {
|
|
148
|
+
const state = job.result ?? job.execution;
|
|
149
|
+
// Control fields lead: a long goal or output must not push them out of a truncated preview.
|
|
150
|
+
const header = [`Braid job ${job.handle} · ${job.status}`, `ID: ${job.jobId}`, control(job.execution)];
|
|
151
|
+
const error = job.error ?? (job.result?.error ? failure(job.result.error) : job.execution?.error ? failure(job.execution.error) : undefined);
|
|
152
|
+
if (error)
|
|
153
|
+
header.push(`Error: ${error}`);
|
|
154
|
+
const usage = formatUsage(job.usage);
|
|
155
|
+
if (usage)
|
|
156
|
+
header.push(`Usage: ${usage}`);
|
|
157
|
+
if (job.result)
|
|
158
|
+
header.push(`Finished ${iso(job.result.metadata.finishedAt)} after ${duration(job.result.metadata.latencyMs)}`);
|
|
159
|
+
else
|
|
160
|
+
header.push(`Started ${iso(job.createdAt)}, ${duration(now - job.createdAt)} ago`);
|
|
161
|
+
header.push(`Goal: ${clip(job.goal, GOAL_CHARS)}`);
|
|
162
|
+
const sections = [header.join("\n")];
|
|
163
|
+
const executions = Object.values(state?.executions ?? {});
|
|
164
|
+
if (executions.length)
|
|
165
|
+
sections.push([`Executions (${executions.length}):`, ...executions.map(executionLine)].join("\n"));
|
|
166
|
+
const pending = Object.values(state?.nodes ?? {}).filter(node => !node.executionId);
|
|
167
|
+
if (pending.length)
|
|
168
|
+
sections.push(["Nodes without executions:", ...pending.map(executionLine)].join("\n"));
|
|
169
|
+
const terminal = new Set(job.result?.terminalExecutionIds ?? []);
|
|
170
|
+
const outputs = executions.filter(execution => execution.output !== undefined);
|
|
171
|
+
if (outputs.length)
|
|
172
|
+
sections.push(["Outputs:", ...outputs.map(execution => `## ${execution.id} · ${execution.executionId}${terminal.has(execution.executionId) ? " · terminal" : ""}${execution.decision ? ` · decision ${execution.decision}` : ""}\n${execution.output}`)].join("\n\n"));
|
|
173
|
+
const workspaces = executions.filter(execution => execution.workspace);
|
|
174
|
+
if (workspaces.length)
|
|
175
|
+
sections.push(["Workspaces:", ...workspaces.map(execution => `- ${execution.executionId}: ${workspaceLine(execution.workspace)}`)].join("\n"));
|
|
176
|
+
if (job.execution)
|
|
177
|
+
sections.push(`Current graph (revision ${job.execution.revision}):\n${formatGraph(job.execution.graph)}`);
|
|
178
|
+
return sections.join("\n\n");
|
|
179
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrok/pi-braid",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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.3.
|
|
9
|
+
"@chrok/braid": "0.3.5"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "*",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"@earendil-works/pi-tui": "*"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@earendil-works/pi-ai": "1.0
|
|
18
|
-
"@earendil-works/pi-tui": "1.0
|
|
19
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
17
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
18
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
19
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
20
20
|
"typescript": "^5.0.0",
|
|
21
21
|
"tsx": "^4.0.0",
|
|
22
22
|
"@types/node": "^22.0.0"
|