@zachwill/pi-orchestrate 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,10 +14,14 @@ import {
14
14
  visibleWidth,
15
15
  type Component,
16
16
  } from "@earendil-works/pi-tui";
17
- import { Result, Schema } from "effect";
17
+ import { Result } from "effect";
18
18
  import type { WorkerDeliveryDetails } from "./delivery.js";
19
- import type { WorkerOutcome, WorkerRecord, WorkerStatus, WorkerUsage } from "./domain.js";
19
+ import type { WorkerOutcome, WorkerRecord, WorkerStatus } from "./domain.js";
20
20
  import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js";
21
+ import {
22
+ decodePersistedWorkerSettlementDetails,
23
+ type WorkerSettlementDetails,
24
+ } from "./worker-settlement.js";
21
25
 
22
26
  export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
23
27
  export const MAX_RESULT_PREVIEW_LINES = 6;
@@ -43,77 +47,7 @@ const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running
43
47
 
44
48
  export type PresentationRuntime = Pick<OrchestratorRuntime, "snapshot" | "subscribeState">;
45
49
 
46
- const NonnegativeFinite = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0));
47
- const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
48
- const legacyOptionalKey = <S extends Schema.Constraint>(schema: S) =>
49
- Schema.optionalKey(Schema.UndefinedOr(schema));
50
-
51
- const WorkerUsageSchema = Schema.Struct({
52
- input: NonnegativeFinite,
53
- output: NonnegativeFinite,
54
- cacheRead: NonnegativeFinite,
55
- cacheWrite: NonnegativeFinite,
56
- cost: NonnegativeFinite,
57
- contextTokens: NonnegativeFinite,
58
- turns: NonnegativeInteger,
59
- });
60
-
61
- const WorkerCompletedOutcomeSchema = Schema.Struct({
62
- status: Schema.Literal("completed"),
63
- assistantText: Schema.String,
64
- });
65
- const WorkerReadyOutcomeSchema = Schema.Struct({
66
- status: Schema.Literal("ready"),
67
- assistantText: Schema.String,
68
- });
69
- const WorkerFailedOutcomeSchema = Schema.Struct({
70
- status: Schema.Literal("failed"),
71
- message: Schema.String,
72
- assistantText: legacyOptionalKey(Schema.String),
73
- });
74
- const WorkerAbortedOutcomeSchema = Schema.Struct({
75
- status: Schema.Literal("aborted"),
76
- message: legacyOptionalKey(Schema.String),
77
- assistantText: legacyOptionalKey(Schema.String),
78
- });
79
- const WorkerOutcomeSchema = Schema.Union([
80
- WorkerCompletedOutcomeSchema,
81
- WorkerReadyOutcomeSchema,
82
- WorkerFailedOutcomeSchema,
83
- WorkerAbortedOutcomeSchema,
84
- ]);
85
-
86
- const SettlementPayloadSchema = Schema.Struct({
87
- eventId: legacyOptionalKey(Schema.String),
88
- sequence: legacyOptionalKey(NonnegativeInteger),
89
- ownerSessionId: Schema.String,
90
- waveId: Schema.String,
91
- workerId: Schema.String,
92
- generation: NonnegativeInteger,
93
- mode: Schema.Literals(["async", "inline"]),
94
- worker: Schema.String,
95
- title: Schema.String,
96
- lifecycle: Schema.Literals(["one-shot", "reusable"]),
97
- status: Schema.Literals(["completed", "ready", "failed", "aborted"]),
98
- outcome: WorkerOutcomeSchema,
99
- usage: WorkerUsageSchema,
100
- startedAt: NonnegativeInteger,
101
- settledAt: NonnegativeInteger,
102
- remainingActive: legacyOptionalKey(NonnegativeInteger),
103
- waveComplete: legacyOptionalKey(Schema.Boolean),
104
- sessionFile: legacyOptionalKey(Schema.String),
105
- failureStage: legacyOptionalKey(Schema.Literals(["startup", "prompt", "workflow", "cancellation"])),
106
- }).check(Schema.makeFilter((settlement) => {
107
- if (settlement.outcome.status !== settlement.status) return "outcome status must match settlement status";
108
- if (settlement.settledAt < settlement.startedAt) return "settlement timestamp must not precede start timestamp";
109
- if (settlement.failureStage !== undefined && settlement.status !== "failed" && settlement.status !== "aborted") {
110
- return "failure stage requires a failed or aborted settlement";
111
- }
112
- }));
113
-
114
- const SettlementEnvelopeSchema = Schema.Struct({ settlement: SettlementPayloadSchema });
115
- const decodeSettlementEnvelope = Schema.decodeUnknownResult(SettlementEnvelopeSchema);
116
- type SafeSettlement = Schema.Schema.Type<typeof SettlementPayloadSchema>;
50
+ type SafeSettlement = WorkerSettlementDetails;
117
51
 
118
52
  interface StatusBinding {
119
53
  readonly ownerSessionId: string;
@@ -130,40 +64,6 @@ export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
130
64
  );
131
65
  }
132
66
 
133
- export function formatResultStatusSummary(details: unknown): string {
134
- const result = readSettlement(details);
135
- return result ? statusHeading(result) : "Worker result details unavailable";
136
- }
137
-
138
- export function formatResultPreviews(
139
- details: unknown,
140
- fallbackContent = "",
141
- limit = MAX_RESULT_PREVIEW_LINES,
142
- ): string[] {
143
- if (limit <= 0) return [];
144
- const result = readSettlement(details);
145
- const body = result ? outcomeText(result.outcome) : fallbackContent;
146
- return firstNonEmptyLines(body, limit);
147
- }
148
-
149
- export function formatWorkerUsage(usage: Partial<WorkerUsage> | undefined): string | undefined {
150
- if (!usage) return undefined;
151
- const parts = [
152
- `${numberOrZero(usage.turns)}t`,
153
- `${formatCompactNumber(numberOrZero(usage.contextTokens))} ctx`,
154
- `↑${formatCompactNumber(numberOrZero(usage.input))}`,
155
- `↓${formatCompactNumber(numberOrZero(usage.output))}`,
156
- `R${formatCompactNumber(numberOrZero(usage.cacheRead))}`,
157
- `W${formatCompactNumber(numberOrZero(usage.cacheWrite))}`,
158
- `$${numberOrZero(usage.cost).toFixed(4)}`,
159
- ];
160
- return parts.join(" · ");
161
- }
162
-
163
- export function formatWorkerStatusLine(worker: WorkerRecord): string {
164
- return `${worker.worker} → ${worker.title} · ${workerStateLabel(worker)} · ${formatTurnMarker(worker)} · ${formatCompactNumber(numberOrZero(worker.usage?.contextTokens))} ctx`;
165
- }
166
-
167
67
  export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
168
68
  const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
169
69
  return ready > 0 ? `${ready} available for follow-up` : undefined;
@@ -308,24 +208,29 @@ export class WorkerStatusComponent implements Component {
308
208
  }
309
209
 
310
210
  private workerLine(worker: WorkerRecord, width: number): string {
311
- const animation = worker.status === "starting"
312
- ? WORKER_ANIMATIONS.starting
313
- : worker.status === "stopping"
314
- ? WORKER_ANIMATIONS.stopping
315
- : WORKER_ANIMATIONS.running;
211
+ const animation = workerAnimation(worker.status);
316
212
  const glyph = this.theme.fg(
317
213
  animation.color,
318
214
  animation.frames[this.frameIndex % animation.frames.length] ?? animation.frames[0],
319
215
  );
320
216
  const turns = formatTurnMarker(worker);
321
217
  const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
322
- const suffixFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
323
- const requiredWidth = (fields: readonly string[]) => visibleWidth(`⠋ · ${fields.join(" · ")}`) + 10;
324
- const showWorker = width >= 72 && requiredWidth([worker.worker, ...suffixFields]) <= width;
325
- const prefix = showWorker ? `${glyph} ${this.theme.fg("muted", worker.worker)} → ` : `${glyph} `;
218
+ const usageFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
219
+ const workerType = this.theme.fg("muted", this.theme.italic(worker.worker));
220
+ const workerTypeFits = visibleWidth(
221
+ `⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
222
+ ) + 10 <= width;
223
+ const suffixFields = width >= 72 && workerTypeFits
224
+ ? [workerType, ...usageFields]
225
+ : usageFields;
226
+ const prefix = `${glyph} `;
326
227
  const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : "";
327
228
  const titleWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(suffix));
328
- const title = truncateToWidth(this.theme.fg("text", this.theme.bold(worker.title)), titleWidth, "…");
229
+ const title = truncateToWidth(
230
+ this.theme.fg("text", this.theme.bold(worker.title)),
231
+ titleWidth,
232
+ "…",
233
+ );
329
234
  return `${prefix}${title}${suffix}`;
330
235
  }
331
236
  }
@@ -371,24 +276,23 @@ export class WorkerResultComponent implements Component {
371
276
  return box;
372
277
  }
373
278
 
374
- const color = details.status === "failed" ? "error" : details.status === "aborted" ? "warning" : "success";
279
+ const color = resultColor(details.status);
375
280
  const elapsed = elapsedBetween(details.startedAt, details.settledAt);
376
- const status = details.status === "aborted"
377
- ? " · aborted"
378
- : details.status === "failed" && details.failureStage === "startup"
379
- ? " · could not start"
380
- : "";
381
- const header = `${statusIcon(details)} ${details.worker} · ${details.title}${status}${elapsed ? ` · ${elapsed}` : ""}`;
281
+ const qualifier = resultQualifier(details);
282
+ const title = this.theme.bold(details.title);
283
+ const workerType = this.theme.fg("muted", this.theme.italic(details.worker));
284
+ const suffix = [qualifier, elapsed].filter(Boolean).join(" · ");
285
+ const header = [
286
+ this.theme.fg(color, `${statusIcon(details)} ${title}`),
287
+ workerType,
288
+ ...(suffix ? [this.theme.fg(color, suffix)] : []),
289
+ ].join(" · ");
382
290
  const outcome = presentedOutcome(details);
383
- box.addChild(new Text(this.theme.fg(color, this.theme.bold(header)), 0, 0));
384
- box.addChild(new Spacer(1));
385
- if (outcome.heading) {
386
- box.addChild(new Text(this.theme.fg("success", this.theme.bold(outcome.heading)), 0, 0));
387
- if (outcome.body) box.addChild(new Spacer(1));
388
- }
389
- if (outcome.body) {
291
+ box.addChild(new Text(header, 0, 0));
292
+ if (outcome) {
293
+ box.addChild(new Spacer(1));
390
294
  box.addChild(new WidthBoundComponent(
391
- new Markdown(outcome.body, 0, 0, getMarkdownTheme()),
295
+ new Markdown(outcome, 0, 0, getMarkdownTheme()),
392
296
  this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES,
393
297
  ));
394
298
  }
@@ -405,16 +309,30 @@ export class WorkerResultComponent implements Component {
405
309
  }
406
310
 
407
311
  function readSettlement(value: unknown): SafeSettlement | undefined {
408
- const envelope = isRecord(value) && isRecord(value.settlement) ? value : { settlement: value };
409
- const decoded = decodeSettlementEnvelope(envelope);
410
- return Result.isSuccess(decoded) ? decoded.success.settlement : undefined;
312
+ const decoded = decodePersistedWorkerSettlementDetails(value);
313
+ return Result.isSuccess(decoded) ? decoded.success : undefined;
314
+ }
315
+
316
+ function workerAnimation(status: WorkerStatus) {
317
+ if (status === "starting") return WORKER_ANIMATIONS.starting;
318
+ if (status === "stopping") return WORKER_ANIMATIONS.stopping;
319
+ return WORKER_ANIMATIONS.running;
411
320
  }
412
321
 
413
- function statusHeading(result: SafeSettlement): string {
414
- if (result.status === "failed") return result.failureStage === "startup" ? "could not start" : "failed";
322
+ function resultColor(status: SafeSettlement["status"]): "success" | "error" | "warning" {
323
+ if (status === "failed") return "error";
324
+ if (status === "aborted") return "warning";
325
+ return "success";
326
+ }
327
+
328
+ function resultQualifier(result: SafeSettlement): string | undefined {
415
329
  if (result.status === "aborted") return "aborted";
416
- if (result.status === "ready") return "response complete";
417
- return "completed";
330
+ if (result.status === "failed" && result.failureStage === "startup") {
331
+ return "could not start";
332
+ }
333
+ if (result.status === "failed") return "failed";
334
+ if (result.status === "ready") return "ready for follow-up";
335
+ return undefined;
418
336
  }
419
337
 
420
338
  function statusIcon(result: SafeSettlement): string {
@@ -430,27 +348,24 @@ function outcomeText(outcome: WorkerOutcome): string {
430
348
  return "Worker session closed.";
431
349
  }
432
350
 
433
- function presentedOutcome(result: SafeSettlement): { heading?: string; body: string } {
351
+ function presentedOutcome(result: SafeSettlement): string {
434
352
  const body = outcomeText(result.outcome);
435
- if (result.status !== "completed" && result.status !== "ready") return { body };
353
+ if (result.status !== "completed" && result.status !== "ready") return body;
436
354
 
437
355
  const lines = body.split("\n");
438
356
  const headingIndex = lines.findIndex((line) => line.trim() !== "");
439
357
  if (headingIndex < 0 || !/^#{1,6}\s+(?:completed|complete|done)\s*#*\s*$/i.test(lines[headingIndex]!)) {
440
- return { body };
358
+ return body;
441
359
  }
442
360
 
443
361
  lines.splice(headingIndex, 1);
444
362
  while (lines[headingIndex]?.trim() === "") lines.splice(headingIndex, 1);
445
- return {
446
- heading: result.status === "ready" ? "✓ Response complete" : "✓ Completed",
447
- body: lines.join("\n").trimEnd(),
448
- };
363
+ return lines.join("\n").trimEnd();
449
364
  }
450
365
 
451
366
  function settlementMetadata(result: SafeSettlement): string[] {
452
367
  return [
453
- `worker ID ${result.workerId} · wave ID ${result.waveId}`,
368
+ `worker ID ${result.workerId} · run ID ${result.runId}`,
454
369
  `status ${result.status} · generation ${result.generation}`,
455
370
  `turns ${numberOrZero(result.usage.turns)} · current context ${formatCompactNumber(numberOrZero(result.usage.contextTokens))}`,
456
371
  `input ${numberOrZero(result.usage.input)} · output ${numberOrZero(result.usage.output)} · cache read ${numberOrZero(result.usage.cacheRead)} · cache write ${numberOrZero(result.usage.cacheWrite)} · cost $${numberOrZero(result.usage.cost).toFixed(4)}`,
@@ -468,14 +383,6 @@ function activeWorkers(snapshot: RuntimeSnapshot): WorkerRecord[] {
468
383
  return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
469
384
  }
470
385
 
471
- const TOOL_ACTIVITY: Readonly<Record<string, string>> = { read: "reading", grep: "searching", find: "finding files", ls: "listing", bash: "running command", edit: "editing", write: "writing" };
472
- function workerStateLabel(worker: Pick<WorkerRecord, "status" | "activity">): string {
473
- if (worker.status === "starting" || worker.status === "stopping") return worker.status;
474
- if (worker.status !== "running") return worker.status;
475
- if (!worker.activity?.trim()) return "working";
476
- return TOOL_ACTIVITY[worker.activity] ?? worker.activity;
477
- }
478
-
479
386
  function formatTurnMarker(worker: Pick<WorkerRecord, "messageDirection" | "usage">): string {
480
387
  const direction = worker.messageDirection === "from-model" ? "↓" : "↑";
481
388
  return `${numberOrZero(worker.usage?.turns)}${direction}`;
@@ -489,9 +396,6 @@ function formatElapsed(milliseconds: number): string {
489
396
  const minutes = Math.floor(seconds / 60);
490
397
  return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
491
398
  }
492
- function firstNonEmptyLines(text: string, limit: number): string[] {
493
- return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, limit);
494
- }
495
399
  function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
496
400
  function formatContextTokens(value: number): string {
497
401
  if (value < 1_000) return String(Math.round(value));