@zachwill/pi-orchestrate 0.8.0 → 0.9.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.
- package/extension/catalog.ts +70 -75
- package/extension/delivery.ts +49 -7
- package/extension/domain.ts +83 -58
- package/extension/host.ts +321 -31
- package/extension/index.ts +33 -26
- package/extension/presentation.ts +36 -83
- package/extension/runtime.ts +1702 -884
- package/extension/tools.ts +143 -243
- package/extension/tui.ts +50 -0
- package/extension/worker-session.ts +514 -188
- package/extension/worker-settlement.ts +105 -44
- package/package.json +1 -1
- package/extension/scheduler.ts +0 -85
|
@@ -17,7 +17,13 @@ import {
|
|
|
17
17
|
import { Result } from "effect";
|
|
18
18
|
import type { WorkerDeliveryDetails } from "./delivery.js";
|
|
19
19
|
import type { WorkerOutcome, WorkerRecord, WorkerStatus } from "./domain.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { RuntimeSnapshot } from "./runtime.js";
|
|
21
|
+
import {
|
|
22
|
+
disposeComponent,
|
|
23
|
+
formatElapsed,
|
|
24
|
+
resultAppearance,
|
|
25
|
+
WidthBoundComponent,
|
|
26
|
+
} from "./tui.js";
|
|
21
27
|
import {
|
|
22
28
|
decodePersistedWorkerSettlementDetails,
|
|
23
29
|
type WorkerSettlementDetails,
|
|
@@ -45,9 +51,12 @@ const ANIMATION_CYCLE_TICKS = 40;
|
|
|
45
51
|
const SPINNER_INTERVAL_MS = 140;
|
|
46
52
|
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
47
53
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
export interface PresentationRuntime {
|
|
55
|
+
subscribeState(
|
|
56
|
+
ownerSessionId: string,
|
|
57
|
+
listener: (snapshot: RuntimeSnapshot) => void,
|
|
58
|
+
): () => void;
|
|
59
|
+
}
|
|
51
60
|
|
|
52
61
|
interface StatusBinding {
|
|
53
62
|
readonly ownerSessionId: string;
|
|
@@ -71,8 +80,6 @@ export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefine
|
|
|
71
80
|
|
|
72
81
|
export class StatusController {
|
|
73
82
|
private binding: StatusBinding | undefined;
|
|
74
|
-
private bindingGeneration = 0;
|
|
75
|
-
private refreshGeneration = 0;
|
|
76
83
|
private disposed = false;
|
|
77
84
|
private unsubscribeState: (() => void) | undefined;
|
|
78
85
|
private widget: WorkerStatusComponent | undefined;
|
|
@@ -84,38 +91,23 @@ export class StatusController {
|
|
|
84
91
|
bind(ownerSessionId: string, ctx: ExtensionContext): void {
|
|
85
92
|
if (this.disposed) return;
|
|
86
93
|
this.clearBinding();
|
|
87
|
-
|
|
88
|
-
this.binding =
|
|
89
|
-
this.unsubscribeState = this.runtime.subscribeState((
|
|
90
|
-
if (
|
|
94
|
+
const binding = { ownerSessionId, ctx };
|
|
95
|
+
this.binding = binding;
|
|
96
|
+
this.unsubscribeState = this.runtime.subscribeState(ownerSessionId, (snapshot) => {
|
|
97
|
+
if (this.binding !== binding) return;
|
|
98
|
+
this.present(binding.ctx, snapshot);
|
|
91
99
|
});
|
|
92
|
-
void this.refresh();
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
unbind(ownerSessionId?: string): void {
|
|
96
103
|
if (ownerSessionId !== undefined && ownerSessionId !== this.binding?.ownerSessionId) return;
|
|
97
104
|
this.clearBinding();
|
|
98
|
-
this.bindingGeneration += 1;
|
|
99
|
-
this.refreshGeneration += 1;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async refresh(): Promise<void> {
|
|
103
|
-
const binding = this.binding;
|
|
104
|
-
if (!binding || this.disposed) return;
|
|
105
|
-
const bindingGeneration = this.bindingGeneration;
|
|
106
|
-
const refreshGeneration = ++this.refreshGeneration;
|
|
107
|
-
let snapshot: RuntimeSnapshot;
|
|
108
|
-
try { snapshot = await this.runtime.snapshot(binding.ownerSessionId); } catch { return; }
|
|
109
|
-
if (this.disposed || this.binding !== binding || this.bindingGeneration !== bindingGeneration || this.refreshGeneration !== refreshGeneration) return;
|
|
110
|
-
this.present(binding.ctx, snapshot);
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
dispose(): void {
|
|
114
108
|
if (this.disposed) return;
|
|
115
109
|
this.disposed = true;
|
|
116
110
|
this.clearBinding();
|
|
117
|
-
this.bindingGeneration += 1;
|
|
118
|
-
this.refreshGeneration += 1;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
113
|
private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void {
|
|
@@ -142,8 +134,9 @@ export class StatusController {
|
|
|
142
134
|
}
|
|
143
135
|
|
|
144
136
|
private clearBinding(): void {
|
|
145
|
-
this.unsubscribeState
|
|
137
|
+
const unsubscribeState = this.unsubscribeState;
|
|
146
138
|
this.unsubscribeState = undefined;
|
|
139
|
+
unsubscribeState?.();
|
|
147
140
|
this.widget = undefined;
|
|
148
141
|
this.pendingSnapshot = undefined;
|
|
149
142
|
const current = this.binding;
|
|
@@ -235,18 +228,6 @@ export class WorkerStatusComponent implements Component {
|
|
|
235
228
|
}
|
|
236
229
|
}
|
|
237
230
|
|
|
238
|
-
class WidthBoundComponent implements Component {
|
|
239
|
-
constructor(private readonly child: Component, private readonly maxLines?: number) {}
|
|
240
|
-
render(width: number): string[] {
|
|
241
|
-
const bounded = Math.max(1, Math.floor(width));
|
|
242
|
-
const lines = this.child.render(bounded);
|
|
243
|
-
const selected = this.maxLines === undefined ? lines : lines.slice(0, this.maxLines);
|
|
244
|
-
return selected.map((line) => truncateToWidth(line, bounded, "…"));
|
|
245
|
-
}
|
|
246
|
-
invalidate(): void { this.child.invalidate(); }
|
|
247
|
-
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
248
|
-
}
|
|
249
|
-
|
|
250
231
|
export class WorkerResultComponent implements Component {
|
|
251
232
|
private child: Component;
|
|
252
233
|
|
|
@@ -261,10 +242,10 @@ export class WorkerResultComponent implements Component {
|
|
|
261
242
|
|
|
262
243
|
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
263
244
|
invalidate(): void {
|
|
264
|
-
(this.child
|
|
245
|
+
disposeComponent(this.child);
|
|
265
246
|
this.child = this.build();
|
|
266
247
|
}
|
|
267
|
-
dispose(): void { (this.child
|
|
248
|
+
dispose(): void { disposeComponent(this.child); }
|
|
268
249
|
|
|
269
250
|
private build(): Component {
|
|
270
251
|
const details = readSettlement(this.rawDetails);
|
|
@@ -276,16 +257,19 @@ export class WorkerResultComponent implements Component {
|
|
|
276
257
|
return box;
|
|
277
258
|
}
|
|
278
259
|
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
260
|
+
const appearance = resultAppearance(
|
|
261
|
+
details.status,
|
|
262
|
+
"interactive ready",
|
|
263
|
+
details.failureStage === "startup" ? "could not start" : "failed",
|
|
264
|
+
);
|
|
265
|
+
const elapsed = formatElapsed(details.settledAt - details.startedAt);
|
|
282
266
|
const title = this.theme.bold(details.title);
|
|
283
267
|
const workerName = this.theme.fg("muted", this.theme.italic(details.worker));
|
|
284
|
-
const suffix = [qualifier, elapsed].filter(Boolean).join(" · ");
|
|
268
|
+
const suffix = [appearance.qualifier, elapsed].filter(Boolean).join(" · ");
|
|
285
269
|
const header = [
|
|
286
|
-
this.theme.fg(color, `${
|
|
270
|
+
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
287
271
|
workerName,
|
|
288
|
-
...(suffix ? [this.theme.fg(color, suffix)] : []),
|
|
272
|
+
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
289
273
|
].join(" · ");
|
|
290
274
|
const outcome = presentedOutcome(details);
|
|
291
275
|
box.addChild(new Text(header, 0, 0));
|
|
@@ -308,7 +292,7 @@ export class WorkerResultComponent implements Component {
|
|
|
308
292
|
}
|
|
309
293
|
}
|
|
310
294
|
|
|
311
|
-
function readSettlement(value: unknown):
|
|
295
|
+
function readSettlement(value: unknown): WorkerSettlementDetails | undefined {
|
|
312
296
|
const decoded = decodePersistedWorkerSettlementDetails(value);
|
|
313
297
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
314
298
|
}
|
|
@@ -319,28 +303,6 @@ function workerAnimation(status: WorkerStatus) {
|
|
|
319
303
|
return WORKER_ANIMATIONS.running;
|
|
320
304
|
}
|
|
321
305
|
|
|
322
|
-
function resultColor(status: DecodedSettlement["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: DecodedSettlement): string | undefined {
|
|
329
|
-
if (result.status === "aborted") return "aborted";
|
|
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 "interactive ready";
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function statusIcon(result: DecodedSettlement): string {
|
|
339
|
-
if (result.status === "failed") return "✗";
|
|
340
|
-
if (result.status === "aborted") return "■";
|
|
341
|
-
return "✓";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
306
|
function outcomeText(outcome: WorkerOutcome): string {
|
|
345
307
|
if (outcome.status === "completed" || outcome.status === "ready") return outcome.assistantText;
|
|
346
308
|
if (outcome.status === "failed") return outcome.assistantText ? `${outcome.message}\n\n${outcome.assistantText}` : outcome.message;
|
|
@@ -348,7 +310,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
348
310
|
return "Worker session closed.";
|
|
349
311
|
}
|
|
350
312
|
|
|
351
|
-
function presentedOutcome(result:
|
|
313
|
+
function presentedOutcome(result: WorkerSettlementDetails): string {
|
|
352
314
|
const body = outcomeText(result.outcome);
|
|
353
315
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
354
316
|
|
|
@@ -363,12 +325,12 @@ function presentedOutcome(result: DecodedSettlement): string {
|
|
|
363
325
|
return lines.join("\n").trimEnd();
|
|
364
326
|
}
|
|
365
327
|
|
|
366
|
-
function settlementMetadata(result:
|
|
328
|
+
function settlementMetadata(result: WorkerSettlementDetails): string[] {
|
|
367
329
|
return [
|
|
368
330
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
369
331
|
`status ${result.status} · generation ${result.generation}`,
|
|
370
|
-
`turns ${
|
|
371
|
-
`input ${
|
|
332
|
+
`turns ${result.usage.turns} · current context ${formatCompactNumber(result.usage.contextTokens)}`,
|
|
333
|
+
`input ${result.usage.input} · output ${result.usage.output} · cache read ${result.usage.cacheRead} · cache write ${result.usage.cacheWrite} · cost $${result.usage.cost.toFixed(4)}`,
|
|
372
334
|
`session ${result.sessionFile ?? "unavailable"}`,
|
|
373
335
|
];
|
|
374
336
|
}
|
|
@@ -387,15 +349,6 @@ function formatTurnMarker(worker: Pick<WorkerRecord, "messageDirection" | "usage
|
|
|
387
349
|
const direction = worker.messageDirection === "from-model" ? "↓" : "↑";
|
|
388
350
|
return `${numberOrZero(worker.usage?.turns)}${direction}`;
|
|
389
351
|
}
|
|
390
|
-
function elapsedBetween(start?: number, end?: number): string | undefined {
|
|
391
|
-
return start !== undefined && end !== undefined && end >= start ? formatElapsed(end - start) : undefined;
|
|
392
|
-
}
|
|
393
|
-
function formatElapsed(milliseconds: number): string {
|
|
394
|
-
const seconds = Math.floor(milliseconds / 1000);
|
|
395
|
-
if (seconds < 60) return `${seconds}s`;
|
|
396
|
-
const minutes = Math.floor(seconds / 60);
|
|
397
|
-
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
398
|
-
}
|
|
399
352
|
function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
400
353
|
function formatContextTokens(value: number): string {
|
|
401
354
|
if (value < 1_000) return String(Math.round(value));
|