@chrok/pi-braid 0.3.1 → 0.3.3
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 +97 -1
- package/dist/command.js +346 -39
- package/dist/display.js +165 -18
- package/dist/index.js +23 -4
- package/dist/jobs.js +58 -3
- package/dist/review.js +64 -0
- package/dist/runner.js +90 -21
- package/dist/session-view.js +112 -0
- package/dist/transcript.js +100 -0
- package/package.json +2 -2
package/dist/runner.js
CHANGED
|
@@ -41,6 +41,9 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
41
41
|
const model = registry.find(name.slice(0, slash), name.slice(slash + 1));
|
|
42
42
|
if (!model)
|
|
43
43
|
throw new Error(`Pi model '${name}' is not registered; use an exact provider/modelId from /model`);
|
|
44
|
+
const activity = options.activity?.recorder(request);
|
|
45
|
+
if (!registry.stream)
|
|
46
|
+
activity?.limitation("This Pi model registry does not stream; model activity is observed only when a request starts and finishes.");
|
|
44
47
|
const workspace = request.workspace ?? {
|
|
45
48
|
nodeId: request.node.id, mode: "read-only", workingDirectory: sourceDirectory, state: "ready",
|
|
46
49
|
};
|
|
@@ -139,6 +142,14 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
139
142
|
tools: allToolDefinitions,
|
|
140
143
|
};
|
|
141
144
|
const toolByName = new Map(fileTools.map((tool) => [tool.name, tool]));
|
|
145
|
+
const executionId = request.execution.executionId;
|
|
146
|
+
const transcripts = executionId === undefined ? undefined : options.transcripts;
|
|
147
|
+
transcripts?.start(executionId, request.node.id, workingDirectory, {
|
|
148
|
+
prompt: request.node.prompt ?? `Default ${request.node.type} instructions`,
|
|
149
|
+
goal: request.goal,
|
|
150
|
+
predecessors: request.predecessors.length,
|
|
151
|
+
workspace: `${workspace.mode} · ${workingDirectory}`,
|
|
152
|
+
}, toolByName);
|
|
142
153
|
const systemPrompt = context.systemPrompt;
|
|
143
154
|
const sessionId = crypto.randomUUID();
|
|
144
155
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
@@ -171,14 +182,54 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
171
182
|
toolRounds,
|
|
172
183
|
phase: "model",
|
|
173
184
|
});
|
|
174
|
-
//
|
|
175
|
-
|
|
185
|
+
// The registry resolves stored API keys, OAuth, custom headers and endpoints.
|
|
186
|
+
// complete() is stream().result(); streaming only adds observation.
|
|
187
|
+
const requestOptions = {
|
|
176
188
|
signal: request.signal,
|
|
177
189
|
sessionId,
|
|
178
190
|
cacheRetention: "none",
|
|
179
191
|
transport: "sse",
|
|
180
192
|
maxRetries: 0,
|
|
193
|
+
};
|
|
194
|
+
activity?.modelRequest();
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
if (registry.stream) {
|
|
198
|
+
const stream = registry.stream(model, context, requestOptions);
|
|
199
|
+
for await (const event of stream) {
|
|
200
|
+
if (event.type !== "done" && event.type !== "error")
|
|
201
|
+
transcripts?.streaming(executionId, event.partial);
|
|
202
|
+
if (event.type === "text_delta")
|
|
203
|
+
activity?.modelStream("text", event.delta);
|
|
204
|
+
else if (event.type === "thinking_delta")
|
|
205
|
+
activity?.modelStream("reasoning", event.delta);
|
|
206
|
+
else if (event.type === "toolcall_start" || event.type === "toolcall_delta") {
|
|
207
|
+
const block = event.partial.content[event.contentIndex];
|
|
208
|
+
activity?.modelStream("tool_call", event.type === "toolcall_delta" ? event.delta : "", block?.type === "toolCall" ? block.name : undefined);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
response = await stream.result();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
response = await registry.complete(model, context, requestOptions);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
activity?.modelError(error instanceof Error ? error.message : String(error));
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
transcripts?.assistant(executionId, response);
|
|
222
|
+
activity?.modelResponse({
|
|
223
|
+
text: response.content.flatMap(block => block.type === "text" ? [block.text] : []).join("\n\n"),
|
|
224
|
+
reasoning: response.content.flatMap(block => block.type === "thinking" ? [block.thinking] : []).join("\n\n"),
|
|
225
|
+
toolCalls: response.content.flatMap(block => block.type === "toolCall" ? [{ id: block.id, name: block.name }] : []),
|
|
226
|
+
stopReason: response.stopReason,
|
|
227
|
+
inputTokens: response.usage.input + response.usage.cacheRead + response.usage.cacheWrite,
|
|
228
|
+
outputTokens: response.usage.output,
|
|
181
229
|
});
|
|
230
|
+
if (response.stopReason !== "stop" && response.stopReason !== "toolUse") {
|
|
231
|
+
activity?.modelError(response.errorMessage || `Pi model ended with '${response.stopReason}'`);
|
|
232
|
+
}
|
|
182
233
|
actualModel = `${response.provider}/${response.responseModel ?? response.model}`;
|
|
183
234
|
const providerContextTokens = response.usage.input +
|
|
184
235
|
response.usage.cacheRead +
|
|
@@ -234,7 +285,13 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
234
285
|
return toolResult(call, "Merge dispositions recorded. Return your final answer.", false);
|
|
235
286
|
}
|
|
236
287
|
const args = validateToolCall(allToolDefinitions, call);
|
|
237
|
-
const execute = () => tool.execute(call.id, args, request.signal,
|
|
288
|
+
const execute = () => tool.execute(call.id, args, request.signal,
|
|
289
|
+
// Bash sends cumulative, throttled output snapshots.
|
|
290
|
+
(partial) => {
|
|
291
|
+
activity?.toolOutput(call.id, partial.content
|
|
292
|
+
.flatMap(block => block.type === "text" ? [block.text] : []).join(""), "replace");
|
|
293
|
+
transcripts?.toolUpdate(executionId, call.id, { content: partial.content, details: partial.details });
|
|
294
|
+
});
|
|
238
295
|
const result = ["write", "edit", "bash", "powershell"].includes(call.name) && request.withWorkspaceWrite
|
|
239
296
|
? await request.withWorkspaceWrite(execute)
|
|
240
297
|
: await execute();
|
|
@@ -254,6 +311,24 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
254
311
|
return toolResult(call, error instanceof Error ? error.message : String(error), true);
|
|
255
312
|
}
|
|
256
313
|
};
|
|
314
|
+
const executeCall = async (call) => {
|
|
315
|
+
if (call.name !== "decide")
|
|
316
|
+
return executeFileTool(call);
|
|
317
|
+
if (request.node.type !== "decision") {
|
|
318
|
+
return toolResult(call, "decide is available only on decision nodes", true);
|
|
319
|
+
}
|
|
320
|
+
const args = call.arguments;
|
|
321
|
+
if (!args ||
|
|
322
|
+
Array.isArray(args) ||
|
|
323
|
+
Object.keys(args).length !== 1 ||
|
|
324
|
+
typeof args.choice !== "string") {
|
|
325
|
+
throw new Error("decide requires exactly one string argument: choice");
|
|
326
|
+
}
|
|
327
|
+
// No coercion or prose parsing: the core enforces the enum and exactly one call.
|
|
328
|
+
request.decide(args.choice);
|
|
329
|
+
decided = true;
|
|
330
|
+
return toolResult(call, JSON.stringify({ choice: args.choice }), false);
|
|
331
|
+
};
|
|
257
332
|
while (true) {
|
|
258
333
|
const response = await complete();
|
|
259
334
|
textParts.push(...response.content
|
|
@@ -287,26 +362,20 @@ export function createPiRunner(registry, onUsageOrOptions, cwd = process.cwd())
|
|
|
287
362
|
const results = [];
|
|
288
363
|
for (const call of calls) {
|
|
289
364
|
request.signal.throwIfAborted();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const args = call.arguments;
|
|
296
|
-
if (!args ||
|
|
297
|
-
Array.isArray(args) ||
|
|
298
|
-
Object.keys(args).length !== 1 ||
|
|
299
|
-
typeof args.choice !== "string") {
|
|
300
|
-
throw new Error("decide requires exactly one string argument: choice");
|
|
301
|
-
}
|
|
302
|
-
// No coercion or prose parsing: the core enforces the enum and exactly one call.
|
|
303
|
-
request.decide(args.choice);
|
|
304
|
-
decided = true;
|
|
305
|
-
results.push(toolResult(call, JSON.stringify({ choice: args.choice }), false));
|
|
365
|
+
activity?.toolStart(call.id, call.name, call.arguments);
|
|
366
|
+
transcripts?.toolStart(executionId, call.id);
|
|
367
|
+
let result;
|
|
368
|
+
try {
|
|
369
|
+
result = await executeCall(call);
|
|
306
370
|
}
|
|
307
|
-
|
|
308
|
-
|
|
371
|
+
catch (error) {
|
|
372
|
+
activity?.toolEnd(call.id, error instanceof Error ? error.message : String(error), true);
|
|
373
|
+
throw error;
|
|
309
374
|
}
|
|
375
|
+
activity?.toolEnd(call.id, result.content
|
|
376
|
+
.flatMap(block => block.type === "text" ? [block.text] : []).join("\n"), result.isError);
|
|
377
|
+
transcripts?.toolResult(executionId, result);
|
|
378
|
+
results.push(result);
|
|
310
379
|
}
|
|
311
380
|
context.messages.push(...results);
|
|
312
381
|
if (decided) {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AssistantMessageComponent, getMarkdownTheme, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, stripTerminalSequences, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
const plain = (value) => stripTerminalSequences(value).replace(/\p{Cc}/gu, " ");
|
|
4
|
+
/**
|
|
5
|
+
* Renders a worker conversation with Pi's own chat components, applying only
|
|
6
|
+
* what changed since the last sync, the way Pi's interactive mode streams.
|
|
7
|
+
*/
|
|
8
|
+
export class NodeSessionView {
|
|
9
|
+
tui;
|
|
10
|
+
theme;
|
|
11
|
+
chat = new Container();
|
|
12
|
+
transcript;
|
|
13
|
+
version = -1;
|
|
14
|
+
rendered = 0;
|
|
15
|
+
streaming;
|
|
16
|
+
tools = new Map();
|
|
17
|
+
partials = new Map();
|
|
18
|
+
expanded = false;
|
|
19
|
+
constructor(tui, theme) {
|
|
20
|
+
this.tui = tui;
|
|
21
|
+
this.theme = theme;
|
|
22
|
+
}
|
|
23
|
+
/** Tool output expansion, like Pi's ctrl+o. */
|
|
24
|
+
toggleExpanded() {
|
|
25
|
+
this.expanded = !this.expanded;
|
|
26
|
+
for (const tool of this.tools.values())
|
|
27
|
+
tool.setExpanded(this.expanded);
|
|
28
|
+
this.version = -1;
|
|
29
|
+
}
|
|
30
|
+
render(transcript, width) {
|
|
31
|
+
this.sync(transcript);
|
|
32
|
+
// Pi marks its own prompts for terminal shell integration; these are not prompts.
|
|
33
|
+
return this.chat.render(width).map(line => line.replace(/\x1b\]133;[^\x07\x1b]*(?:\x07|\x1b\\)/g, ""));
|
|
34
|
+
}
|
|
35
|
+
sync(transcript) {
|
|
36
|
+
if (transcript !== this.transcript)
|
|
37
|
+
this.reset(transcript);
|
|
38
|
+
if (transcript.version === this.version)
|
|
39
|
+
return;
|
|
40
|
+
this.version = transcript.version;
|
|
41
|
+
for (; this.rendered < transcript.messages.length; this.rendered++) {
|
|
42
|
+
const message = transcript.messages[this.rendered];
|
|
43
|
+
if (message.role === "toolResult") {
|
|
44
|
+
this.tools.get(message.toolCallId)?.updateResult(message);
|
|
45
|
+
this.partials.delete(message.toolCallId);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// The final message replaces the streamed partial in the same component.
|
|
49
|
+
const component = this.streaming ?? this.add(new AssistantMessageComponent(undefined, false, getMarkdownTheme()));
|
|
50
|
+
this.streaming = undefined;
|
|
51
|
+
component.updateContent(message, false);
|
|
52
|
+
this.toolCalls(message, transcript, true);
|
|
53
|
+
}
|
|
54
|
+
if (transcript.streaming) {
|
|
55
|
+
this.streaming ??= this.add(new AssistantMessageComponent(undefined, false, getMarkdownTheme()));
|
|
56
|
+
this.streaming.updateContent(transcript.streaming, true);
|
|
57
|
+
this.toolCalls(transcript.streaming, transcript, false);
|
|
58
|
+
}
|
|
59
|
+
for (const [callId, partial] of transcript.running) {
|
|
60
|
+
const tool = this.tools.get(callId);
|
|
61
|
+
if (!tool || this.partials.get(callId) === partial)
|
|
62
|
+
continue;
|
|
63
|
+
this.partials.set(callId, partial);
|
|
64
|
+
tool.markExecutionStarted();
|
|
65
|
+
if (partial)
|
|
66
|
+
tool.updateResult({ ...partial, isError: false }, true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
toolCalls(message, transcript, complete) {
|
|
70
|
+
for (const block of message.content) {
|
|
71
|
+
if (block.type !== "toolCall")
|
|
72
|
+
continue;
|
|
73
|
+
let tool = this.tools.get(block.id);
|
|
74
|
+
if (!tool) {
|
|
75
|
+
tool = new ToolExecutionComponent(block.name, block.id, block.arguments, undefined, transcript.renderers.get(block.name), this.tui, transcript.cwd);
|
|
76
|
+
tool.setExpanded(this.expanded);
|
|
77
|
+
this.tools.set(block.id, tool);
|
|
78
|
+
this.add(tool);
|
|
79
|
+
}
|
|
80
|
+
else if (!complete)
|
|
81
|
+
tool.updateArgs(block.arguments);
|
|
82
|
+
if (complete) {
|
|
83
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
84
|
+
tool.updateResult({ content: [{ type: "text", text: message.errorMessage || "Error" }], isError: true });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
tool.updateArgs(block.arguments);
|
|
88
|
+
tool.setArgsComplete();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
reset(transcript) {
|
|
94
|
+
this.transcript = transcript;
|
|
95
|
+
this.chat.clear();
|
|
96
|
+
this.tools.clear();
|
|
97
|
+
this.partials.clear();
|
|
98
|
+
this.streaming = undefined;
|
|
99
|
+
this.rendered = 0;
|
|
100
|
+
this.version = -1;
|
|
101
|
+
const intro = transcript.intro;
|
|
102
|
+
this.add(new UserMessageComponent(intro.prompt, getMarkdownTheme()));
|
|
103
|
+
this.add(new Text(this.theme.fg("dim", [
|
|
104
|
+
`goal: ${plain(intro.goal)}`,
|
|
105
|
+
`context: ${intro.predecessors} predecessor output${intro.predecessors === 1 ? "" : "s"} · workspace ${plain(intro.workspace)}`,
|
|
106
|
+
].join("\n")), 1, 0));
|
|
107
|
+
}
|
|
108
|
+
add(component) {
|
|
109
|
+
this.chat.addChild(component);
|
|
110
|
+
return component;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live worker conversations keyed by execution ID. Running executions and the
|
|
3
|
+
* newest finished ones are retained; older transcripts are dropped to bound memory.
|
|
4
|
+
*/
|
|
5
|
+
export class PiTranscripts {
|
|
6
|
+
onChange;
|
|
7
|
+
maxFinished;
|
|
8
|
+
transcripts = new Map();
|
|
9
|
+
finishedIds = [];
|
|
10
|
+
pending;
|
|
11
|
+
constructor(onChange = () => { }, maxFinished = 20) {
|
|
12
|
+
this.onChange = onChange;
|
|
13
|
+
this.maxFinished = maxFinished;
|
|
14
|
+
}
|
|
15
|
+
get(executionId) {
|
|
16
|
+
return this.transcripts.get(executionId);
|
|
17
|
+
}
|
|
18
|
+
start(executionId, nodeId, cwd, intro, renderers) {
|
|
19
|
+
this.transcripts.set(executionId, {
|
|
20
|
+
executionId, nodeId, cwd, intro, messages: [], running: new Map(), renderers, version: 0, finished: false,
|
|
21
|
+
});
|
|
22
|
+
this.changed();
|
|
23
|
+
}
|
|
24
|
+
streaming(executionId, partial) {
|
|
25
|
+
this.update(executionId, transcript => { transcript.streaming = partial; }, true);
|
|
26
|
+
}
|
|
27
|
+
assistant(executionId, message) {
|
|
28
|
+
this.update(executionId, transcript => {
|
|
29
|
+
delete transcript.streaming;
|
|
30
|
+
transcript.messages.push(message);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
toolStart(executionId, callId) {
|
|
34
|
+
this.update(executionId, transcript => { transcript.running.set(callId, undefined); });
|
|
35
|
+
}
|
|
36
|
+
toolUpdate(executionId, callId, partial) {
|
|
37
|
+
this.update(executionId, transcript => {
|
|
38
|
+
if (transcript.running.has(callId))
|
|
39
|
+
transcript.running.set(callId, partial);
|
|
40
|
+
}, true);
|
|
41
|
+
}
|
|
42
|
+
toolResult(executionId, result) {
|
|
43
|
+
this.update(executionId, transcript => {
|
|
44
|
+
transcript.running.delete(result.toolCallId);
|
|
45
|
+
transcript.messages.push(result);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Close interrupted streams and tools so the view does not show them as still running. */
|
|
49
|
+
finish(executionId, reason = "Execution ended before this step finished") {
|
|
50
|
+
if (executionId === undefined)
|
|
51
|
+
return;
|
|
52
|
+
this.update(executionId, transcript => {
|
|
53
|
+
if (transcript.streaming) {
|
|
54
|
+
transcript.messages.push({ ...transcript.streaming, stopReason: "aborted", errorMessage: reason });
|
|
55
|
+
delete transcript.streaming;
|
|
56
|
+
}
|
|
57
|
+
for (const callId of transcript.running.keys()) {
|
|
58
|
+
const call = transcript.messages.flatMap(message => message.role === "assistant" ? message.content : [])
|
|
59
|
+
.find(block => block.type === "toolCall" && block.id === callId);
|
|
60
|
+
transcript.messages.push({
|
|
61
|
+
role: "toolResult", toolCallId: callId, toolName: call?.type === "toolCall" ? call.name : "tool",
|
|
62
|
+
content: [{ type: "text", text: reason }], isError: true, timestamp: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
transcript.running.clear();
|
|
66
|
+
transcript.finished = true;
|
|
67
|
+
if (!this.finishedIds.includes(executionId))
|
|
68
|
+
this.finishedIds.push(executionId);
|
|
69
|
+
while (this.finishedIds.length > this.maxFinished)
|
|
70
|
+
this.transcripts.delete(this.finishedIds.shift());
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Token and output bursts coalesce into at most one notification per 100 ms. */
|
|
74
|
+
update(executionId, change, burst = false) {
|
|
75
|
+
const transcript = this.transcripts.get(executionId);
|
|
76
|
+
if (!transcript || transcript.finished)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
change(transcript);
|
|
80
|
+
transcript.version++;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Display state must never affect node execution.
|
|
84
|
+
}
|
|
85
|
+
if (!burst)
|
|
86
|
+
this.changed();
|
|
87
|
+
else if (!this.pending) {
|
|
88
|
+
this.pending = setTimeout(() => this.changed(), 100);
|
|
89
|
+
this.pending.unref?.();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
changed() {
|
|
93
|
+
clearTimeout(this.pending);
|
|
94
|
+
this.pending = undefined;
|
|
95
|
+
try {
|
|
96
|
+
this.onChange();
|
|
97
|
+
}
|
|
98
|
+
catch { /* Observers cannot fail a worker. */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrok/pi-braid",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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.3"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "*",
|