@zachwill/pi-orchestrate 0.1.1 → 0.2.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/README.md +3 -0
- package/extension/catalog.ts +176 -157
- package/extension/domain.ts +5 -1
- package/extension/host.ts +2 -2
- package/extension/presentation.ts +139 -104
- package/extension/runtime.ts +150 -63
- package/extension/scheduler.ts +44 -25
- package/extension/tools.ts +63 -23
- package/extension/worker-session.ts +488 -295
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
visibleWidth,
|
|
15
15
|
type Component,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { Result, Schema } from "effect";
|
|
17
18
|
import type { WorkerDeliveryDetails } from "./delivery.js";
|
|
18
19
|
import type { WorkerOutcome, WorkerRecord, WorkerStatus, WorkerUsage } from "./domain.js";
|
|
19
20
|
import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js";
|
|
@@ -22,33 +23,97 @@ export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
|
22
23
|
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
23
24
|
export const MAX_WIDGET_WORKERS = 8;
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const WORKER_ANIMATIONS = {
|
|
27
|
+
starting: {
|
|
28
|
+
frames: ["⠂", "⠌", "⡑", "⢕", "⣫", "⣿", "⣫", "⢕"],
|
|
29
|
+
color: "muted",
|
|
30
|
+
},
|
|
31
|
+
running: {
|
|
32
|
+
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
33
|
+
color: "accent",
|
|
34
|
+
},
|
|
35
|
+
stopping: {
|
|
36
|
+
frames: ["⣿", "⣶", "⣤", "⣀", "⠠", "⠐", "⠈", "⠁"],
|
|
37
|
+
color: "dim",
|
|
38
|
+
},
|
|
39
|
+
} as const;
|
|
40
|
+
const ANIMATION_CYCLE_TICKS = 40;
|
|
26
41
|
const SPINNER_INTERVAL_MS = 140;
|
|
27
42
|
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
28
43
|
|
|
29
44
|
export type PresentationRuntime = Pick<OrchestratorRuntime, "snapshot" | "subscribeState">;
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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>;
|
|
52
117
|
|
|
53
118
|
interface StatusBinding {
|
|
54
119
|
readonly ownerSessionId: string;
|
|
@@ -96,7 +161,7 @@ export function formatWorkerUsage(usage: Partial<WorkerUsage> | undefined): stri
|
|
|
96
161
|
}
|
|
97
162
|
|
|
98
163
|
export function formatWorkerStatusLine(worker: WorkerRecord): string {
|
|
99
|
-
return `${worker.worker} → ${worker.title} · ${workerStateLabel(worker)} · ${
|
|
164
|
+
return `${worker.worker} → ${worker.title} · ${workerStateLabel(worker)} · ${formatTurnMarker(worker)} · ${formatCompactNumber(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
100
165
|
}
|
|
101
166
|
|
|
102
167
|
export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
|
|
@@ -229,7 +294,7 @@ export class WorkerStatusComponent implements Component {
|
|
|
229
294
|
private startTimer(): void {
|
|
230
295
|
if (this.timer || activeWorkers(this.snapshot).length === 0) return;
|
|
231
296
|
this.timer = setInterval(() => {
|
|
232
|
-
this.frameIndex = (this.frameIndex + 1) %
|
|
297
|
+
this.frameIndex = (this.frameIndex + 1) % ANIMATION_CYCLE_TICKS;
|
|
233
298
|
this.tui?.requestRender();
|
|
234
299
|
}, SPINNER_INTERVAL_MS);
|
|
235
300
|
const timer = this.timer;
|
|
@@ -243,13 +308,19 @@ export class WorkerStatusComponent implements Component {
|
|
|
243
308
|
}
|
|
244
309
|
|
|
245
310
|
private workerLine(worker: WorkerRecord, width: number): string {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
311
|
+
const animation = worker.status === "starting"
|
|
312
|
+
? WORKER_ANIMATIONS.starting
|
|
313
|
+
: worker.status === "stopping"
|
|
314
|
+
? WORKER_ANIMATIONS.stopping
|
|
315
|
+
: WORKER_ANIMATIONS.running;
|
|
316
|
+
const glyph = this.theme.fg(
|
|
317
|
+
animation.color,
|
|
318
|
+
animation.frames[this.frameIndex % animation.frames.length] ?? animation.frames[0],
|
|
319
|
+
);
|
|
320
|
+
const turns = formatTurnMarker(worker);
|
|
321
|
+
const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
249
322
|
const suffixFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
|
|
250
|
-
const activity = workerStateLabel(worker);
|
|
251
323
|
const requiredWidth = (fields: readonly string[]) => visibleWidth(`⠋ · ${fields.join(" · ")}`) + 10;
|
|
252
|
-
if (width >= 42 && requiredWidth([activity, ...suffixFields]) <= width) suffixFields.unshift(activity);
|
|
253
324
|
const showWorker = width >= 72 && requiredWidth([worker.worker, ...suffixFields]) <= width;
|
|
254
325
|
const prefix = showWorker ? `${glyph} ${this.theme.fg("muted", worker.worker)} → ` : `${glyph} `;
|
|
255
326
|
const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : "";
|
|
@@ -308,9 +379,19 @@ export class WorkerResultComponent implements Component {
|
|
|
308
379
|
? " · could not start"
|
|
309
380
|
: "";
|
|
310
381
|
const header = `${statusIcon(details)} ${details.worker} · ${details.title}${status}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
382
|
+
const outcome = presentedOutcome(details);
|
|
311
383
|
box.addChild(new Text(this.theme.fg(color, this.theme.bold(header)), 0, 0));
|
|
312
384
|
box.addChild(new Spacer(1));
|
|
313
|
-
|
|
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) {
|
|
390
|
+
box.addChild(new WidthBoundComponent(
|
|
391
|
+
new Markdown(outcome.body, 0, 0, getMarkdownTheme()),
|
|
392
|
+
this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES,
|
|
393
|
+
));
|
|
394
|
+
}
|
|
314
395
|
if (!this.expanded) {
|
|
315
396
|
box.addChild(new Spacer(1));
|
|
316
397
|
box.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to expand")), 0, 0));
|
|
@@ -324,80 +405,11 @@ export class WorkerResultComponent implements Component {
|
|
|
324
405
|
}
|
|
325
406
|
|
|
326
407
|
function readSettlement(value: unknown): SafeSettlement | undefined {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const lifecycle = enumField(candidate.lifecycle, ["one-shot", "reusable"]);
|
|
331
|
-
const status = enumField(candidate.status, ["completed", "ready", "failed", "aborted"]);
|
|
332
|
-
const outcome = readOutcome(candidate.outcome);
|
|
333
|
-
const usage = readUsage(candidate.usage);
|
|
334
|
-
const generation = nonnegativeInteger(candidate.generation);
|
|
335
|
-
if (!mode || !lifecycle || !status || !outcome || outcome.status !== status || !usage || generation === undefined) return undefined;
|
|
336
|
-
const ownerSessionId = requiredString(candidate.ownerSessionId);
|
|
337
|
-
const waveId = requiredString(candidate.waveId);
|
|
338
|
-
const workerId = requiredString(candidate.workerId);
|
|
339
|
-
const worker = requiredString(candidate.worker);
|
|
340
|
-
const title = requiredString(candidate.title);
|
|
341
|
-
if (ownerSessionId === undefined || waveId === undefined || workerId === undefined || worker === undefined || title === undefined) return undefined;
|
|
342
|
-
const eventId = optionalString(candidate.eventId);
|
|
343
|
-
const sessionFile = optionalString(candidate.sessionFile);
|
|
344
|
-
const sequence = optionalInteger(candidate.sequence);
|
|
345
|
-
const startedAt = optionalInteger(candidate.startedAt);
|
|
346
|
-
const settledAt = optionalInteger(candidate.settledAt);
|
|
347
|
-
const remainingActive = optionalInteger(candidate.remainingActive);
|
|
348
|
-
const waveComplete = optionalBoolean(candidate.waveComplete);
|
|
349
|
-
const failureStage = optionalEnum(candidate.failureStage, ["startup", "prompt", "workflow", "cancellation"]);
|
|
350
|
-
if (eventId === INVALID || sessionFile === INVALID || sequence === INVALID || startedAt === INVALID || settledAt === INVALID ||
|
|
351
|
-
remainingActive === INVALID || waveComplete === INVALID || failureStage === INVALID) return undefined;
|
|
352
|
-
if (startedAt === undefined || settledAt === undefined || settledAt < startedAt) return undefined;
|
|
353
|
-
if (failureStage !== undefined && status !== "failed" && status !== "aborted") return undefined;
|
|
354
|
-
return {
|
|
355
|
-
ownerSessionId, waveId, workerId, generation, mode, worker, title, lifecycle, status, outcome, usage,
|
|
356
|
-
...(eventId === undefined ? {} : { eventId }),
|
|
357
|
-
...(sequence === undefined ? {} : { sequence }),
|
|
358
|
-
...(startedAt === undefined ? {} : { startedAt }),
|
|
359
|
-
...(settledAt === undefined ? {} : { settledAt }),
|
|
360
|
-
...(remainingActive === undefined ? {} : { remainingActive }),
|
|
361
|
-
...(waveComplete === undefined ? {} : { waveComplete }),
|
|
362
|
-
...(sessionFile === undefined ? {} : { sessionFile }),
|
|
363
|
-
...(failureStage === undefined ? {} : { failureStage }),
|
|
364
|
-
};
|
|
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;
|
|
365
411
|
}
|
|
366
412
|
|
|
367
|
-
function readOutcome(value: unknown): WorkerOutcome | undefined {
|
|
368
|
-
if (!isRecord(value) || typeof value.status !== "string") return undefined;
|
|
369
|
-
if ((value.status === "completed" || value.status === "ready") && typeof value.assistantText === "string") return { status: value.status, assistantText: value.assistantText };
|
|
370
|
-
const message = optionalString(value.message);
|
|
371
|
-
const assistantText = optionalString(value.assistantText);
|
|
372
|
-
if (message === INVALID || assistantText === INVALID) return undefined;
|
|
373
|
-
if (value.status === "failed" && message !== undefined) return { status: "failed", message, ...(assistantText === undefined ? {} : { assistantText }) };
|
|
374
|
-
if (value.status === "aborted") return { status: "aborted", ...(message === undefined ? {} : { message }), ...(assistantText === undefined ? {} : { assistantText }) };
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const INVALID = Symbol("invalid");
|
|
379
|
-
type Invalid = typeof INVALID;
|
|
380
|
-
function requiredString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; }
|
|
381
|
-
function optionalString(value: unknown): string | undefined | Invalid { return value === undefined ? undefined : typeof value === "string" ? value : INVALID; }
|
|
382
|
-
function nonnegativeInteger(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 ? value : undefined; }
|
|
383
|
-
function optionalInteger(value: unknown): number | undefined | Invalid { return value === undefined ? undefined : nonnegativeInteger(value) ?? INVALID; }
|
|
384
|
-
function optionalBoolean(value: unknown): boolean | undefined | Invalid { return value === undefined ? undefined : typeof value === "boolean" ? value : INVALID; }
|
|
385
|
-
function enumField<const T extends string>(value: unknown, values: readonly T[]): T | undefined { return typeof value === "string" && values.includes(value as T) ? value as T : undefined; }
|
|
386
|
-
function optionalEnum<const T extends string>(value: unknown, values: readonly T[]): T | undefined | Invalid { return value === undefined ? undefined : enumField(value, values) ?? INVALID; }
|
|
387
|
-
function readUsage(value: unknown): WorkerUsage | undefined {
|
|
388
|
-
if (!isRecord(value)) return undefined;
|
|
389
|
-
const input = nonnegativeNumber(value.input);
|
|
390
|
-
const output = nonnegativeNumber(value.output);
|
|
391
|
-
const cacheRead = nonnegativeNumber(value.cacheRead);
|
|
392
|
-
const cacheWrite = nonnegativeNumber(value.cacheWrite);
|
|
393
|
-
const cost = nonnegativeNumber(value.cost);
|
|
394
|
-
const contextTokens = nonnegativeNumber(value.contextTokens);
|
|
395
|
-
const turns = nonnegativeInteger(value.turns);
|
|
396
|
-
if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || cost === undefined || contextTokens === undefined || turns === undefined) return undefined;
|
|
397
|
-
return { input, output, cacheRead, cacheWrite, cost, contextTokens, turns };
|
|
398
|
-
}
|
|
399
|
-
function nonnegativeNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; }
|
|
400
|
-
|
|
401
413
|
function statusHeading(result: SafeSettlement): string {
|
|
402
414
|
if (result.status === "failed") return result.failureStage === "startup" ? "could not start" : "failed";
|
|
403
415
|
if (result.status === "aborted") return "aborted";
|
|
@@ -418,6 +430,24 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
418
430
|
return "Worker session closed.";
|
|
419
431
|
}
|
|
420
432
|
|
|
433
|
+
function presentedOutcome(result: SafeSettlement): { heading?: string; body: string } {
|
|
434
|
+
const body = outcomeText(result.outcome);
|
|
435
|
+
if (result.status !== "completed" && result.status !== "ready") return { body };
|
|
436
|
+
|
|
437
|
+
const lines = body.split("\n");
|
|
438
|
+
const headingIndex = lines.findIndex((line) => line.trim() !== "");
|
|
439
|
+
if (headingIndex < 0 || !/^#{1,6}\s+(?:completed|complete|done)\s*#*\s*$/i.test(lines[headingIndex]!)) {
|
|
440
|
+
return { body };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
lines.splice(headingIndex, 1);
|
|
444
|
+
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
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
421
451
|
function settlementMetadata(result: SafeSettlement): string[] {
|
|
422
452
|
return [
|
|
423
453
|
`worker ID ${result.workerId} · wave ID ${result.waveId}`,
|
|
@@ -446,8 +476,9 @@ function workerStateLabel(worker: Pick<WorkerRecord, "status" | "activity">): st
|
|
|
446
476
|
return TOOL_ACTIVITY[worker.activity] ?? worker.activity;
|
|
447
477
|
}
|
|
448
478
|
|
|
449
|
-
function
|
|
450
|
-
|
|
479
|
+
function formatTurnMarker(worker: Pick<WorkerRecord, "messageDirection" | "usage">): string {
|
|
480
|
+
const direction = worker.messageDirection === "from-model" ? "↓" : "↑";
|
|
481
|
+
return `${numberOrZero(worker.usage?.turns)}${direction}`;
|
|
451
482
|
}
|
|
452
483
|
function elapsedBetween(start?: number, end?: number): string | undefined {
|
|
453
484
|
return start !== undefined && end !== undefined && end >= start ? formatElapsed(end - start) : undefined;
|
|
@@ -462,6 +493,10 @@ function firstNonEmptyLines(text: string, limit: number): string[] {
|
|
|
462
493
|
return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, limit);
|
|
463
494
|
}
|
|
464
495
|
function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
496
|
+
function formatContextTokens(value: number): string {
|
|
497
|
+
if (value < 1_000) return String(Math.round(value));
|
|
498
|
+
return `${Math.round(value / 1_000)}k`;
|
|
499
|
+
}
|
|
465
500
|
function formatCompactNumber(value: number): string {
|
|
466
501
|
if (value >= 1_000_000) return `${trimDecimal(value / 1_000_000)}m`;
|
|
467
502
|
if (value >= 1_000) return `${trimDecimal(value / 1_000)}k`;
|
package/extension/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
3
4
|
import {
|
|
4
5
|
CANCELLATION_GRACE_MS,
|
|
5
6
|
EMPTY_WORKER_USAGE,
|
|
@@ -201,6 +202,7 @@ interface RuntimeEntry {
|
|
|
201
202
|
session?: WorkerSessionHandle;
|
|
202
203
|
unsubscribeUsage?: () => void;
|
|
203
204
|
unsubscribeActivity?: () => void;
|
|
205
|
+
unsubscribeMessageDirection?: () => void;
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
interface WaveWaiter {
|
|
@@ -212,17 +214,16 @@ interface WaveWaiter {
|
|
|
212
214
|
|
|
213
215
|
const defaultBestEffortDeadline: BestEffortDeadline = {
|
|
214
216
|
wait(promise, timeoutMs) {
|
|
215
|
-
|
|
216
|
-
const settled = promise.then(
|
|
217
|
+
const settled = Effect.promise(() => promise.then(
|
|
217
218
|
() => "settled" as const,
|
|
218
219
|
() => "settled" as const,
|
|
220
|
+
));
|
|
221
|
+
return Effect.runPromise(
|
|
222
|
+
settled.pipe(
|
|
223
|
+
Effect.timeoutOption(timeoutMs),
|
|
224
|
+
Effect.map((result) => Option.getOrElse(result, () => "timed-out" as const)),
|
|
225
|
+
),
|
|
219
226
|
);
|
|
220
|
-
const timedOut = new Promise<DeadlineResult>((resolve) => {
|
|
221
|
-
timer = setTimeout(() => resolve("timed-out"), timeoutMs);
|
|
222
|
-
});
|
|
223
|
-
return Promise.race([settled, timedOut]).finally(() => {
|
|
224
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
225
|
-
});
|
|
226
227
|
},
|
|
227
228
|
};
|
|
228
229
|
|
|
@@ -316,6 +317,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
316
317
|
lifecycle: definition.lifecycle,
|
|
317
318
|
status: "starting",
|
|
318
319
|
usage: copyUsage(EMPTY_WORKER_USAGE),
|
|
320
|
+
messageDirection: "to-model",
|
|
319
321
|
startedAt: this.clock(),
|
|
320
322
|
};
|
|
321
323
|
});
|
|
@@ -396,11 +398,12 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
396
398
|
state: "running",
|
|
397
399
|
createdAt: this.clock(),
|
|
398
400
|
};
|
|
399
|
-
const running = {
|
|
401
|
+
const running: WorkerRecord = {
|
|
400
402
|
...transitionWorkerStatus(current, "running"),
|
|
401
403
|
waveId,
|
|
402
404
|
instructions,
|
|
403
405
|
activity: undefined,
|
|
406
|
+
messageDirection: "to-model",
|
|
404
407
|
startedAt: this.clock(),
|
|
405
408
|
settledAt: undefined,
|
|
406
409
|
};
|
|
@@ -558,7 +561,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
558
561
|
try {
|
|
559
562
|
this.scheduler.start(
|
|
560
563
|
workerId,
|
|
561
|
-
|
|
564
|
+
this.bootstrapAndPrompt(workerId, generation),
|
|
562
565
|
(error) => this.settleWorkflowDefect(workerId, generation, error),
|
|
563
566
|
);
|
|
564
567
|
} catch (error) {
|
|
@@ -575,7 +578,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
575
578
|
try {
|
|
576
579
|
this.scheduler.start(
|
|
577
580
|
workerId,
|
|
578
|
-
|
|
581
|
+
this.executePrompt(workerId, generation, session, instructions),
|
|
579
582
|
(error) => this.settleWorkflowDefect(workerId, generation, error),
|
|
580
583
|
);
|
|
581
584
|
} catch (error) {
|
|
@@ -583,44 +586,90 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
583
586
|
}
|
|
584
587
|
}
|
|
585
588
|
|
|
586
|
-
private
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
private bootstrapAndPrompt(
|
|
590
|
+
workerId: WorkerId,
|
|
591
|
+
generation: number,
|
|
592
|
+
): Effect.Effect<void, never> {
|
|
593
|
+
const runtime = this;
|
|
594
|
+
return Effect.gen(function* () {
|
|
595
|
+
const session = yield* runtime.bootstrap(workerId, generation);
|
|
596
|
+
if (!session) return;
|
|
597
|
+
const current = runtime.workers.get(workerId);
|
|
598
|
+
if (!current) return;
|
|
599
|
+
yield* runtime.executePrompt(workerId, generation, session, current.instructions);
|
|
600
|
+
});
|
|
592
601
|
}
|
|
593
602
|
|
|
594
|
-
private
|
|
603
|
+
private bootstrap(
|
|
595
604
|
workerId: WorkerId,
|
|
596
605
|
generation: number,
|
|
597
|
-
):
|
|
598
|
-
|
|
599
|
-
|
|
606
|
+
): Effect.Effect<WorkerSessionHandle | undefined, never> {
|
|
607
|
+
return Effect.suspend(() => {
|
|
608
|
+
const entry = this.entries.get(workerId);
|
|
609
|
+
if (!entry) return Effect.succeed(undefined);
|
|
600
610
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
611
|
+
let creation: Promise<WorkerSessionHandle>;
|
|
612
|
+
try {
|
|
613
|
+
creation = this.workerSessionFactory.create({
|
|
614
|
+
cwd: entry.context.cwd,
|
|
615
|
+
agentDir: entry.context.agentDir,
|
|
616
|
+
parentSessionFile: entry.context.parentSessionFile,
|
|
617
|
+
projectTrusted: entry.context.projectTrusted,
|
|
618
|
+
definition: entry.definition,
|
|
619
|
+
parentModel: entry.context.parentModel,
|
|
620
|
+
modelRegistry: entry.context.modelRegistry,
|
|
621
|
+
});
|
|
622
|
+
} catch (error) {
|
|
623
|
+
this.settleCreationFailure(workerId, generation, error);
|
|
624
|
+
return Effect.succeed(undefined);
|
|
625
|
+
}
|
|
616
626
|
|
|
627
|
+
this.trackCleanup(creation.then(() => undefined, () => undefined));
|
|
628
|
+
void creation.then((session) => {
|
|
629
|
+
if (!this.canAdoptCreatedSession(workerId, generation, entry)) {
|
|
630
|
+
this.disposeSession(session);
|
|
631
|
+
}
|
|
632
|
+
}, () => undefined);
|
|
633
|
+
|
|
634
|
+
return Effect.tryPromise({
|
|
635
|
+
try: () => creation,
|
|
636
|
+
catch: (error) => error,
|
|
637
|
+
}).pipe(
|
|
638
|
+
Effect.match({
|
|
639
|
+
onFailure: (error) => {
|
|
640
|
+
this.settleCreationFailure(workerId, generation, error);
|
|
641
|
+
return undefined;
|
|
642
|
+
},
|
|
643
|
+
onSuccess: (session) => this.adoptCreatedSession(
|
|
644
|
+
workerId,
|
|
645
|
+
generation,
|
|
646
|
+
entry,
|
|
647
|
+
session,
|
|
648
|
+
),
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private canAdoptCreatedSession(
|
|
655
|
+
workerId: WorkerId,
|
|
656
|
+
generation: number,
|
|
657
|
+
entry: RuntimeEntry,
|
|
658
|
+
): boolean {
|
|
617
659
|
const current = this.workers.get(workerId);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
660
|
+
return !this.shuttingDown &&
|
|
661
|
+
current?.status === "starting" &&
|
|
662
|
+
entry.generation === generation;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private adoptCreatedSession(
|
|
666
|
+
workerId: WorkerId,
|
|
667
|
+
generation: number,
|
|
668
|
+
entry: RuntimeEntry,
|
|
669
|
+
session: WorkerSessionHandle,
|
|
670
|
+
): WorkerSessionHandle | undefined {
|
|
671
|
+
const current = this.workers.get(workerId);
|
|
672
|
+
if (!this.canAdoptCreatedSession(workerId, generation, entry) || !current) {
|
|
624
673
|
this.disposeSession(session);
|
|
625
674
|
return undefined;
|
|
626
675
|
}
|
|
@@ -641,31 +690,54 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
641
690
|
}
|
|
642
691
|
}
|
|
643
692
|
|
|
644
|
-
private
|
|
693
|
+
private executePrompt(
|
|
645
694
|
workerId: WorkerId,
|
|
646
695
|
generation: number,
|
|
647
696
|
session: WorkerSessionHandle,
|
|
648
697
|
instructions: string,
|
|
649
|
-
):
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
698
|
+
): Effect.Effect<void, never> {
|
|
699
|
+
return Effect.suspend(() => {
|
|
700
|
+
const before = this.workers.get(workerId);
|
|
701
|
+
const entry = this.entries.get(workerId);
|
|
702
|
+
if (
|
|
703
|
+
!before ||
|
|
704
|
+
before.status !== "running" ||
|
|
705
|
+
!entry ||
|
|
706
|
+
entry.generation !== generation ||
|
|
707
|
+
entry.session !== session
|
|
708
|
+
) {
|
|
709
|
+
return Effect.void;
|
|
710
|
+
}
|
|
661
711
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
712
|
+
let prompt: Promise<WorkerOutcome>;
|
|
713
|
+
try {
|
|
714
|
+
prompt = session.prompt(instructions);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
this.settleOutcome(workerId, generation, session, {
|
|
717
|
+
status: "failed",
|
|
718
|
+
message: describeError(error, "Worker prompt failed"),
|
|
719
|
+
});
|
|
720
|
+
return Effect.void;
|
|
721
|
+
}
|
|
722
|
+
this.trackCleanup(prompt.then(() => undefined, () => undefined));
|
|
723
|
+
|
|
724
|
+
return Effect.tryPromise({
|
|
725
|
+
try: () => prompt,
|
|
726
|
+
catch: (error) => error,
|
|
727
|
+
}).pipe(
|
|
728
|
+
Effect.match({
|
|
729
|
+
onFailure: (error): WorkerOutcome => ({
|
|
730
|
+
status: "failed",
|
|
731
|
+
message: describeError(error, "Worker prompt failed"),
|
|
732
|
+
}),
|
|
733
|
+
onSuccess: (outcome) => outcome,
|
|
734
|
+
}),
|
|
735
|
+
Effect.tap((outcome) => Effect.sync(() => {
|
|
736
|
+
this.settleOutcome(workerId, generation, session, outcome);
|
|
737
|
+
})),
|
|
738
|
+
Effect.asVoid,
|
|
739
|
+
);
|
|
740
|
+
});
|
|
669
741
|
}
|
|
670
742
|
|
|
671
743
|
private settleCreationFailure(
|
|
@@ -1137,6 +1209,14 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
1137
1209
|
this.workers.set(workerId, { ...latest, activity });
|
|
1138
1210
|
this.emitState(latest.ownerSessionId);
|
|
1139
1211
|
});
|
|
1212
|
+
entry.unsubscribeMessageDirection = session.subscribeMessageDirection((messageDirection) => {
|
|
1213
|
+
const latest = this.workers.get(workerId);
|
|
1214
|
+
if (!latest || entry.session !== session || entry.generation !== generation) return;
|
|
1215
|
+
if (latest.status !== "starting" && latest.status !== "running") return;
|
|
1216
|
+
if (latest.messageDirection === messageDirection) return;
|
|
1217
|
+
this.workers.set(workerId, { ...latest, messageDirection });
|
|
1218
|
+
this.emitState(latest.ownerSessionId);
|
|
1219
|
+
});
|
|
1140
1220
|
}
|
|
1141
1221
|
|
|
1142
1222
|
private emitState(ownerSessionId: string): void {
|
|
@@ -1166,6 +1246,8 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
1166
1246
|
entry.unsubscribeUsage = undefined;
|
|
1167
1247
|
safelyCall(entry.unsubscribeActivity);
|
|
1168
1248
|
entry.unsubscribeActivity = undefined;
|
|
1249
|
+
safelyCall(entry.unsubscribeMessageDirection);
|
|
1250
|
+
entry.unsubscribeMessageDirection = undefined;
|
|
1169
1251
|
}
|
|
1170
1252
|
|
|
1171
1253
|
private disposeEntrySession(entry: RuntimeEntry): void {
|
|
@@ -1177,7 +1259,12 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
1177
1259
|
private disposeSession(session: WorkerSessionHandle): void {
|
|
1178
1260
|
if (this.disposedSessions.has(session)) return;
|
|
1179
1261
|
this.disposedSessions.add(session);
|
|
1180
|
-
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
this.trackCleanup(session.dispose());
|
|
1265
|
+
} catch {
|
|
1266
|
+
// Cleanup is best-effort and cannot leave lifecycle state unsettled.
|
|
1267
|
+
}
|
|
1181
1268
|
}
|
|
1182
1269
|
|
|
1183
1270
|
private trackCleanup(operation: Promise<void>): Promise<void> {
|