@zachwill/pi-orchestrate 0.8.0 → 0.9.2
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/contract.ts +73 -7
- package/extension/delivery.ts +65 -17
- package/extension/domain.ts +83 -58
- package/extension/host.ts +316 -30
- package/extension/index.ts +28 -31
- package/extension/presentation.ts +48 -98
- package/extension/runtime.ts +1690 -884
- package/extension/tools.ts +236 -270
- 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,86 +80,67 @@ 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;
|
|
79
|
-
private widgetInstalled = false;
|
|
80
|
-
private pendingSnapshot: RuntimeSnapshot | undefined;
|
|
81
86
|
|
|
82
87
|
constructor(private readonly runtime: PresentationRuntime) {}
|
|
83
88
|
|
|
84
89
|
bind(ownerSessionId: string, ctx: ExtensionContext): void {
|
|
85
90
|
if (this.disposed) return;
|
|
86
91
|
this.clearBinding();
|
|
87
|
-
|
|
88
|
-
this.binding =
|
|
89
|
-
this.unsubscribeState = this.runtime.subscribeState((
|
|
90
|
-
if (
|
|
92
|
+
const binding = { ownerSessionId, ctx };
|
|
93
|
+
this.binding = binding;
|
|
94
|
+
this.unsubscribeState = this.runtime.subscribeState(ownerSessionId, (snapshot) => {
|
|
95
|
+
if (this.binding !== binding) return;
|
|
96
|
+
this.present(binding.ctx, snapshot);
|
|
91
97
|
});
|
|
92
|
-
void this.refresh();
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
unbind(ownerSessionId?: string): void {
|
|
96
101
|
if (ownerSessionId !== undefined && ownerSessionId !== this.binding?.ownerSessionId) return;
|
|
97
102
|
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
103
|
}
|
|
112
104
|
|
|
113
105
|
dispose(): void {
|
|
114
106
|
if (this.disposed) return;
|
|
115
107
|
this.disposed = true;
|
|
116
108
|
this.clearBinding();
|
|
117
|
-
this.bindingGeneration += 1;
|
|
118
|
-
this.refreshGeneration += 1;
|
|
119
109
|
}
|
|
120
110
|
|
|
121
111
|
private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void {
|
|
122
112
|
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
|
|
123
113
|
if (ctx.mode !== "tui") return;
|
|
124
114
|
const active = activeWorkers(snapshot);
|
|
125
|
-
this.pendingSnapshot = snapshot;
|
|
126
115
|
if (active.length === 0) {
|
|
127
|
-
if (this.
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
if (this.widget !== undefined) {
|
|
117
|
+
ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
118
|
+
this.widget = undefined;
|
|
119
|
+
}
|
|
130
120
|
return;
|
|
131
121
|
}
|
|
132
|
-
if (this.widget) {
|
|
122
|
+
if (this.widget !== undefined) {
|
|
133
123
|
this.widget.update(snapshot);
|
|
134
124
|
return;
|
|
135
125
|
}
|
|
136
|
-
if (this.widgetInstalled) return;
|
|
137
126
|
ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, (tui, theme) => {
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
const widget = new WorkerStatusComponent(snapshot, theme, tui);
|
|
128
|
+
this.widget = widget;
|
|
129
|
+
return widget;
|
|
140
130
|
}, { placement: "aboveEditor" });
|
|
141
|
-
this.widgetInstalled = true;
|
|
142
131
|
}
|
|
143
132
|
|
|
144
133
|
private clearBinding(): void {
|
|
145
|
-
this.unsubscribeState
|
|
134
|
+
const unsubscribeState = this.unsubscribeState;
|
|
146
135
|
this.unsubscribeState = undefined;
|
|
147
|
-
|
|
148
|
-
this.pendingSnapshot = undefined;
|
|
136
|
+
unsubscribeState?.();
|
|
149
137
|
const current = this.binding;
|
|
150
138
|
if (!current) return;
|
|
151
139
|
current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
152
|
-
if (current.ctx.mode === "tui" && this.
|
|
153
|
-
|
|
140
|
+
if (current.ctx.mode === "tui" && this.widget !== undefined) {
|
|
141
|
+
current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
142
|
+
this.widget = undefined;
|
|
143
|
+
}
|
|
154
144
|
this.binding = undefined;
|
|
155
145
|
}
|
|
156
146
|
}
|
|
@@ -235,18 +225,6 @@ export class WorkerStatusComponent implements Component {
|
|
|
235
225
|
}
|
|
236
226
|
}
|
|
237
227
|
|
|
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
228
|
export class WorkerResultComponent implements Component {
|
|
251
229
|
private child: Component;
|
|
252
230
|
|
|
@@ -261,10 +239,10 @@ export class WorkerResultComponent implements Component {
|
|
|
261
239
|
|
|
262
240
|
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
263
241
|
invalidate(): void {
|
|
264
|
-
(this.child
|
|
242
|
+
disposeComponent(this.child);
|
|
265
243
|
this.child = this.build();
|
|
266
244
|
}
|
|
267
|
-
dispose(): void { (this.child
|
|
245
|
+
dispose(): void { disposeComponent(this.child); }
|
|
268
246
|
|
|
269
247
|
private build(): Component {
|
|
270
248
|
const details = readSettlement(this.rawDetails);
|
|
@@ -276,16 +254,19 @@ export class WorkerResultComponent implements Component {
|
|
|
276
254
|
return box;
|
|
277
255
|
}
|
|
278
256
|
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
257
|
+
const appearance = resultAppearance(
|
|
258
|
+
details.status,
|
|
259
|
+
"interactive ready",
|
|
260
|
+
details.failureStage === "startup" ? "could not start" : "failed",
|
|
261
|
+
);
|
|
262
|
+
const elapsed = formatElapsed(details.settledAt - details.startedAt);
|
|
282
263
|
const title = this.theme.bold(details.title);
|
|
283
264
|
const workerName = this.theme.fg("muted", this.theme.italic(details.worker));
|
|
284
|
-
const suffix = [qualifier, elapsed].filter(Boolean).join(" · ");
|
|
265
|
+
const suffix = [appearance.qualifier, elapsed].filter(Boolean).join(" · ");
|
|
285
266
|
const header = [
|
|
286
|
-
this.theme.fg(color, `${
|
|
267
|
+
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
287
268
|
workerName,
|
|
288
|
-
...(suffix ? [this.theme.fg(color, suffix)] : []),
|
|
269
|
+
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
289
270
|
].join(" · ");
|
|
290
271
|
const outcome = presentedOutcome(details);
|
|
291
272
|
box.addChild(new Text(header, 0, 0));
|
|
@@ -308,7 +289,7 @@ export class WorkerResultComponent implements Component {
|
|
|
308
289
|
}
|
|
309
290
|
}
|
|
310
291
|
|
|
311
|
-
function readSettlement(value: unknown):
|
|
292
|
+
function readSettlement(value: unknown): WorkerSettlementDetails | undefined {
|
|
312
293
|
const decoded = decodePersistedWorkerSettlementDetails(value);
|
|
313
294
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
314
295
|
}
|
|
@@ -319,28 +300,6 @@ function workerAnimation(status: WorkerStatus) {
|
|
|
319
300
|
return WORKER_ANIMATIONS.running;
|
|
320
301
|
}
|
|
321
302
|
|
|
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
303
|
function outcomeText(outcome: WorkerOutcome): string {
|
|
345
304
|
if (outcome.status === "completed" || outcome.status === "ready") return outcome.assistantText;
|
|
346
305
|
if (outcome.status === "failed") return outcome.assistantText ? `${outcome.message}\n\n${outcome.assistantText}` : outcome.message;
|
|
@@ -348,7 +307,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
348
307
|
return "Worker session closed.";
|
|
349
308
|
}
|
|
350
309
|
|
|
351
|
-
function presentedOutcome(result:
|
|
310
|
+
function presentedOutcome(result: WorkerSettlementDetails): string {
|
|
352
311
|
const body = outcomeText(result.outcome);
|
|
353
312
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
354
313
|
|
|
@@ -363,12 +322,12 @@ function presentedOutcome(result: DecodedSettlement): string {
|
|
|
363
322
|
return lines.join("\n").trimEnd();
|
|
364
323
|
}
|
|
365
324
|
|
|
366
|
-
function settlementMetadata(result:
|
|
325
|
+
function settlementMetadata(result: WorkerSettlementDetails): string[] {
|
|
367
326
|
return [
|
|
368
327
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
369
328
|
`status ${result.status} · generation ${result.generation}`,
|
|
370
|
-
`turns ${
|
|
371
|
-
`input ${
|
|
329
|
+
`turns ${result.usage.turns} · current context ${formatCompactNumber(result.usage.contextTokens)}`,
|
|
330
|
+
`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
331
|
`session ${result.sessionFile ?? "unavailable"}`,
|
|
373
332
|
];
|
|
374
333
|
}
|
|
@@ -387,15 +346,6 @@ function formatTurnMarker(worker: Pick<WorkerRecord, "messageDirection" | "usage
|
|
|
387
346
|
const direction = worker.messageDirection === "from-model" ? "↓" : "↑";
|
|
388
347
|
return `${numberOrZero(worker.usage?.turns)}${direction}`;
|
|
389
348
|
}
|
|
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
349
|
function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
400
350
|
function formatContextTokens(value: number): string {
|
|
401
351
|
if (value < 1_000) return String(Math.round(value));
|