@chrok/pi-braid 0.3.3 → 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.
@@ -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
+ }
@@ -45,6 +45,20 @@ export class PiTranscripts {
45
45
  transcript.messages.push(result);
46
46
  });
47
47
  }
48
+ /** Record human messages: delivered before a model request, or left over when the execution ended. */
49
+ human(executionId, texts, delivered) {
50
+ if (!texts.length)
51
+ return;
52
+ this.update(executionId, transcript => {
53
+ for (const text of texts)
54
+ transcript.messages.push({ role: "human", text, timestamp: Date.now(), delivered });
55
+ });
56
+ }
57
+ /** Accepting messages: the execution's session has started and has not finished. */
58
+ isLive(executionId) {
59
+ const transcript = this.transcripts.get(executionId);
60
+ return transcript !== undefined && !transcript.finished;
61
+ }
48
62
  /** Close interrupted streams and tools so the view does not show them as still running. */
49
63
  finish(executionId, reason = "Execution ended before this step finished") {
50
64
  if (executionId === undefined)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@chrok/pi-braid",
3
- "version": "0.3.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.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.1",
18
- "@earendil-works/pi-tui": "1.0.1",
19
- "@earendil-works/pi-coding-agent": "1.0.1",
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"