@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-fork.1

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. package/trajectory/src/server.ts +112 -12
@@ -1,6 +1,7 @@
1
- import type { AgentToolResult, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
  import { formatCost, formatStalledDuration, sanitizeDisplayText, WORKFLOW_AGENT_STALL_THRESHOLD_MS } from "../../src/index.js";
4
+ import { drawFrame, formatElapsed, formatTokens, mark, QUIET_MS, type Paint } from "../../src/background-widget.js";
4
5
  import type { SubagentRunRequest, SubagentStatus } from "./contracts.js";
5
6
 
6
7
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
@@ -113,11 +114,14 @@ function stalledDuration(status: SubagentStatus, now: number): number | undefine
113
114
  return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
114
115
  }
115
116
 
116
- function accounting(status: SubagentStatus): string | undefined {
117
+ /** Tokens and cost as the workflow header shows them. */
118
+ function usageStats(status: SubagentStatus): string {
117
119
  const value = status.progress?.accounting;
118
- if (!value) return undefined;
119
- const total = value.input + value.output + value.cacheRead + value.cacheWrite;
120
- return `tokens=${String(total)} cost=${formatCost(value.cost) || "$0.00"}`;
120
+ return value ? [formatTokens(value.input + value.output), formatCost(value.cost)].filter(Boolean).join(" · ") : "";
121
+ }
122
+ function modelName(status: SubagentStatus): string {
123
+ const model = status.progress?.state?.model ?? status.attemptDetails?.at(-1)?.setup.model;
124
+ return model ? `${model.model}${model.thinking ? `:${model.thinking}` : ""}` : "";
121
125
  }
122
126
 
123
127
  function timestamp(value: number | undefined): string | undefined {
@@ -141,8 +145,9 @@ function formatSubagentProgress(status: SubagentStatus, args: SubagentRenderArgs
141
145
  const color = stateColor(status.state);
142
146
  const elapsed = runtime(status.startedAt, status.finishedAt, now);
143
147
  const metadata = requestMetadata(args, args.id === undefined);
148
+ const stats = usageStats(status);
144
149
  const lines = [
145
- `${theme.fg(color, stateGlyph(status.state, spinner))} ${theme.bold(theme.fg("accent", `Subagent: ${label({ ...args, id: status.id })}`))} ${theme.fg(color, `[${status.state}]`)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}${elapsed ? ` runtime=${elapsed}` : ""}`,
150
+ `${theme.fg(color, stateGlyph(status.state, spinner))} ${theme.bold(theme.fg("accent", `Subagent: ${label({ ...args, id: status.id })}`))} ${theme.fg(color, `[${status.state}]`)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}${stats ? ` ${stats}` : ""}${elapsed ? ` runtime=${elapsed}` : ""}`,
146
151
  ];
147
152
  const current = activity(status);
148
153
  const stalled = stalledDuration(status, now);
@@ -153,8 +158,6 @@ function formatSubagentProgress(status: SubagentStatus, args: SubagentRenderArgs
153
158
  lines.push(` ${theme.fg("dim", `id=${status.id}`)}`);
154
159
  const model = status.progress?.state?.model;
155
160
  if (model) lines.push(` ${theme.fg("dim", `model=${model.provider}/${model.model}${model.thinking ? `:${model.thinking}` : ""}`)}`);
156
- const usage = accounting(status);
157
- if (usage) lines.push(` ${theme.fg("dim", usage)}`);
158
161
  if (status.worktree) lines.push(` ${theme.fg("dim", `worktree=${status.worktree.path} branch=${status.worktree.branch}`)}`);
159
162
  }
160
163
  return lines.join("\n");
@@ -257,9 +260,77 @@ export function renderSubagentResult(result: AgentToolResult<unknown>, options:
257
260
 
258
261
  type WidgetRun = { status: Readonly<SubagentStatus>; request: Readonly<SubagentRunRequest> };
259
262
 
260
- export function createSubagentBackgroundWidget() {
263
+ /** One run as a workflow agent row: a quiet or stalled warning on the left, model and figures against the right rule. */
264
+ function widgetRow({ status, request }: WidgetRun, now: number, paint: Paint): { text: string; right: string } {
265
+ const lastEventAt = status.progress?.lastEventAt;
266
+ const silent = lastEventAt === undefined || !Number.isFinite(lastEventAt) || now - lastEventAt < QUIET_MS ? undefined : now - lastEventAt;
267
+ const role = silent !== undefined && silent >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? "error" : "warning";
268
+ const icon = silent === undefined ? mark(status.state, now, paint) : paint(role, "⚠");
269
+ const warning = silent === undefined ? "" : ` ${paint(role, `${role === "error" ? "stalled" : "quiet"} ${formatElapsed(silent)}`)}`;
270
+ const accounting = status.progress?.accounting;
271
+ const attempts = status.attempts ?? 1;
272
+ const right = [
273
+ modelName(status),
274
+ formatTokens(accounting === undefined ? undefined : accounting.input + accounting.output),
275
+ formatCost(accounting?.cost),
276
+ status.startedAt === undefined ? "" : formatElapsed(now - status.startedAt),
277
+ attempts > 1 ? `attempt ${String(attempts)}` : "",
278
+ ].filter(Boolean).join(" · ");
279
+ return { text: `${icon} ${sanitizeDisplayText(label({ ...request, id: status.id }))}${warning}`, right };
280
+ }
281
+
282
+ const RECEIPT_ENTRY_TYPE = "piewf-subagent-receipt";
283
+ type SubagentReceipt = { readonly id: string; readonly label: string; readonly state: string; readonly model?: string; readonly role?: string; readonly tools?: readonly string[]; readonly input: number; readonly output: number; readonly cacheRead: number; readonly costUsd: number; readonly durationMs: number; readonly attempts: number; readonly error?: string };
284
+
285
+ function receiptFor(status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>): SubagentReceipt {
286
+ const accounting = status.progress?.accounting;
287
+ const model = modelName(status);
288
+ const role = roleName(request.role);
289
+ const tools = status.progress?.state?.tools ?? status.attemptDetails?.at(-1)?.setup.tools;
290
+ return {
291
+ id: status.id,
292
+ label: label({ ...request, id: status.id }),
293
+ state: status.state,
294
+ ...(model ? { model } : {}),
295
+ ...(role === undefined ? {} : { role }),
296
+ ...(tools === undefined ? {} : { tools: [...tools] }),
297
+ input: accounting?.input ?? 0,
298
+ output: accounting?.output ?? 0,
299
+ cacheRead: accounting?.cacheRead ?? 0,
300
+ costUsd: accounting?.cost ?? 0,
301
+ durationMs: status.startedAt === undefined ? 0 : Math.max(0, (status.finishedAt ?? Date.now()) - status.startedAt),
302
+ attempts: status.attempts ?? 1,
303
+ ...(status.error === undefined ? {} : { error: `${status.error.code}: ${status.error.message}` }),
304
+ };
305
+ }
306
+
307
+ /** The transcript line a finished background run leaves behind, in the shape of the workflow receipt. */
308
+ export function renderSubagentReceipt(data: SubagentReceipt, expanded: boolean, theme: Theme): string[] {
309
+ const glyph = data.state === "completed" ? theme.fg("success", "✓") : data.state === "failed" ? theme.fg("error", "✗") : theme.fg("muted", "·");
310
+ const headline = [formatTokens(data.input + data.output), formatCost(data.costUsd) || "$0.00", formatElapsed(data.durationMs), data.state].filter(Boolean).join(" · ");
311
+ const lines = [`${glyph} ${theme.bold(sanitizeDisplayText(data.label))} ${theme.fg("muted", headline)}`];
312
+ if (!expanded) return lines;
313
+ const meta = [data.model ?? "", data.role ? `role ${data.role}` : "", data.attempts > 1 ? `${String(data.attempts)} attempts` : "", data.tools?.join(" ") ?? ""].filter(Boolean);
314
+ if (meta.length > 0) lines.push(theme.fg("muted", ` ${meta.join(" · ")}`));
315
+ const split = [data.input ? `in ${formatTokens(data.input)}` : "", data.output ? `out ${formatTokens(data.output)}` : "", data.cacheRead ? `cache ${formatTokens(data.cacheRead)}` : ""].filter(Boolean);
316
+ if (split.length > 0) lines.push(theme.fg("muted", ` ${split.join(" · ")}`));
317
+ if (data.error !== undefined) lines.push(theme.fg("error", ` ${sanitizeDisplayText(data.error)}`));
318
+ lines.push(theme.fg("muted", ` run ${data.id}`));
319
+ return lines;
320
+ }
321
+
322
+ type ReceiptHost = { appendEntry?: (customType: string, data: SubagentReceipt) => void; registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"] };
323
+
324
+ export function createSubagentBackgroundWidget(host: ReceiptHost = {}) {
261
325
  const key = "piewf-subagents-background";
262
326
  const runs = new Map<string, WidgetRun>();
327
+ const receipted = new Set<string>();
328
+ host.registerEntryRenderer?.<SubagentReceipt>(RECEIPT_ENTRY_TYPE, (entry, options, theme) => {
329
+ if (!entry.data) return undefined;
330
+ const lines = renderSubagentReceipt(entry.data, options.expanded, theme);
331
+ // Pi treats an over-wide line as fatal, and an error message has no bound.
332
+ return { render: (width: number): string[] => lines.map((line) => truncateToWidth(line, Math.max(1, width), "…")), invalidate() {} };
333
+ });
263
334
  let context: ExtensionContext | undefined;
264
335
  let requestRender: (() => void) | undefined;
265
336
  let timer: ReturnType<typeof setInterval> | undefined;
@@ -289,23 +360,14 @@ export function createSubagentBackgroundWidget() {
289
360
  return {
290
361
  render(width: number): string[] {
291
362
  const now = Date.now();
292
- const frame = SPINNER[Math.floor(now / 80) % SPINNER.length] ?? "◇";
293
- const title = theme.bold(theme.fg("accent", `Subagents (${String(runs.size)} running)`));
294
- const blocks = [...runs.values()].map(({ status, request }) => formatSubagentProgress(status, request, theme, frame, now, false).split("\n"));
295
- const allRunsFit = blocks.reduce((rows, block) => rows + block.length, 1) <= MAX_BACKGROUND_WIDGET_ROWS;
296
- const runRowBudget = allRunsFit ? MAX_BACKGROUND_WIDGET_ROWS - 1 : MAX_BACKGROUND_WIDGET_ROWS - 2;
297
- const lines = [title];
298
- let runRows = 0;
299
- let displayedRuns = 0;
300
- for (const block of blocks) {
301
- if (runRows + block.length > runRowBudget) break;
302
- lines.push(...block);
303
- runRows += block.length;
304
- displayedRuns += 1;
305
- }
306
- const hiddenRuns = blocks.length - displayedRuns;
307
- if (hiddenRuns > 0) lines.push(`… ${String(hiddenRuns)} more`);
308
- return lines.map((line) => truncateToWidth(line, Math.max(1, width), "…"));
363
+ const paintRole: Paint = (role, text) => theme.fg(role, text);
364
+ // One row per run, so the default concurrency of eight fills the frame exactly.
365
+ // ponytail: no Alt+O scrolling as in the workflow widget; add it if runs routinely exceed the frame.
366
+ const budget = MAX_BACKGROUND_WIDGET_ROWS - 2;
367
+ const rows = [...runs.values()].map((run) => widgetRow(run, now, paintRole));
368
+ const body = rows.length <= budget ? rows : [...rows.slice(0, budget - 1), { text: theme.fg("muted", `… ${String(rows.length - budget + 1)} more`) }];
369
+ const title = runs.size === 1 ? "Subagent" : `Subagents · ${String(runs.size)} runs`;
370
+ return drawFrame(title, "", body, width, paintRole, (text) => theme.fg("accent", text)).map((line) => truncateToWidth(line, Math.max(1, width), ""));
309
371
  },
310
372
  invalidate() {},
311
373
  };
@@ -322,12 +384,19 @@ export function createSubagentBackgroundWidget() {
322
384
  start(next: ExtensionContext): void {
323
385
  hide();
324
386
  runs.clear();
387
+ receipted.clear();
325
388
  context = next.mode === "tui" ? next : undefined;
326
389
  },
327
390
  update(status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>): void {
328
391
  if (request.mode !== "background" || context === undefined) return;
329
- if (TERMINAL_STATES.has(status.state)) runs.delete(status.id);
330
- else runs.set(status.id, { status, request });
392
+ if (TERMINAL_STATES.has(status.state)) {
393
+ runs.delete(status.id);
394
+ // A foreground run already has its tool block; a background one leaves a receipt, once, like a workflow run.
395
+ if (!receipted.has(status.id)) {
396
+ receipted.add(status.id);
397
+ try { host.appendEntry?.(RECEIPT_ENTRY_TYPE, receiptFor(status, request)); } catch { /* A receipt is display-only. */ }
398
+ }
399
+ } else runs.set(status.id, { status, request });
331
400
  paint();
332
401
  },
333
402
  dispose(): void {