@zachwill/pi-orchestrate 0.1.0 → 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 +8 -3
- package/extension/catalog.ts +176 -157
- package/extension/contract.ts +3 -1
- package/extension/delivery.ts +137 -89
- package/extension/domain.ts +7 -1
- package/extension/host.ts +41 -5
- package/extension/presentation.ts +331 -454
- package/extension/runtime.ts +338 -88
- package/extension/scheduler.ts +44 -25
- package/extension/tools.ts +337 -20
- package/extension/worker-session.ts +488 -290
- package/package.json +1 -1
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,
|
|
@@ -72,6 +73,31 @@ export interface CompletedWave {
|
|
|
72
73
|
readonly results: readonly CompletedResult[];
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
export type SettlementFailureStage = "startup" | "prompt" | "workflow" | "cancellation";
|
|
77
|
+
|
|
78
|
+
export interface WorkerSettlement {
|
|
79
|
+
readonly eventId: string;
|
|
80
|
+
readonly sequence: number;
|
|
81
|
+
readonly ownerSessionId: string;
|
|
82
|
+
readonly waveId: WaveId;
|
|
83
|
+
readonly workerId: WorkerId;
|
|
84
|
+
readonly generation: number;
|
|
85
|
+
readonly mode: WaveMode;
|
|
86
|
+
readonly worker: string;
|
|
87
|
+
readonly title: string;
|
|
88
|
+
readonly lifecycle: WorkerRecord["lifecycle"];
|
|
89
|
+
readonly status: Exclude<WaveCompleteWorkerStatus, "closed">;
|
|
90
|
+
readonly outcome: WorkerOutcome;
|
|
91
|
+
readonly failureStage?: SettlementFailureStage;
|
|
92
|
+
readonly usage: WorkerUsage;
|
|
93
|
+
readonly startedAt: number;
|
|
94
|
+
readonly settledAt: number;
|
|
95
|
+
readonly remainingActive: number;
|
|
96
|
+
readonly waveSize: number;
|
|
97
|
+
readonly waveComplete: boolean;
|
|
98
|
+
readonly sessionFile: string | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
export interface RuntimeSnapshot {
|
|
76
102
|
readonly waves: readonly WaveRecord[];
|
|
77
103
|
readonly workers: readonly WorkerRecord[];
|
|
@@ -79,6 +105,8 @@ export interface RuntimeSnapshot {
|
|
|
79
105
|
|
|
80
106
|
export type CompletionListener = (wave: CompletedWave) => void;
|
|
81
107
|
export type UnsubscribeCompletion = () => void;
|
|
108
|
+
export type SettlementListener = (settlement: WorkerSettlement) => void;
|
|
109
|
+
export type UnsubscribeSettlement = () => void;
|
|
82
110
|
export type StateListener = (ownerSessionId: string) => void;
|
|
83
111
|
|
|
84
112
|
export type AbortTarget =
|
|
@@ -118,18 +146,21 @@ export interface OrchestratorRuntime {
|
|
|
118
146
|
tasks: readonly OrchestrateTaskInput[],
|
|
119
147
|
mode: "async",
|
|
120
148
|
signal?: AbortSignal,
|
|
149
|
+
onSettlement?: SettlementListener,
|
|
121
150
|
): Promise<AcceptedWave>;
|
|
122
151
|
orchestrate(
|
|
123
152
|
context: OrchestrationContext,
|
|
124
153
|
tasks: readonly OrchestrateTaskInput[],
|
|
125
154
|
mode: "inline",
|
|
126
155
|
signal?: AbortSignal,
|
|
156
|
+
onSettlement?: SettlementListener,
|
|
127
157
|
): Promise<CompletedWave>;
|
|
128
158
|
orchestrate(
|
|
129
159
|
context: OrchestrationContext,
|
|
130
160
|
tasks: readonly OrchestrateTaskInput[],
|
|
131
161
|
mode: WaveMode,
|
|
132
162
|
signal?: AbortSignal,
|
|
163
|
+
onSettlement?: SettlementListener,
|
|
133
164
|
): Promise<AcceptedWave | CompletedWave>;
|
|
134
165
|
send(
|
|
135
166
|
context: OrchestrationContext,
|
|
@@ -137,6 +168,7 @@ export interface OrchestratorRuntime {
|
|
|
137
168
|
instructions: string,
|
|
138
169
|
mode: "async",
|
|
139
170
|
signal?: AbortSignal,
|
|
171
|
+
onSettlement?: SettlementListener,
|
|
140
172
|
): Promise<AcceptedWave>;
|
|
141
173
|
send(
|
|
142
174
|
context: OrchestrationContext,
|
|
@@ -144,6 +176,7 @@ export interface OrchestratorRuntime {
|
|
|
144
176
|
instructions: string,
|
|
145
177
|
mode: "inline",
|
|
146
178
|
signal?: AbortSignal,
|
|
179
|
+
onSettlement?: SettlementListener,
|
|
147
180
|
): Promise<CompletedWave>;
|
|
148
181
|
send(
|
|
149
182
|
context: OrchestrationContext,
|
|
@@ -151,11 +184,13 @@ export interface OrchestratorRuntime {
|
|
|
151
184
|
instructions: string,
|
|
152
185
|
mode: WaveMode,
|
|
153
186
|
signal?: AbortSignal,
|
|
187
|
+
onSettlement?: SettlementListener,
|
|
154
188
|
): Promise<AcceptedWave | CompletedWave>;
|
|
155
189
|
abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
|
|
156
190
|
close(ownerSessionId: string, workerId: WorkerId): Promise<void>;
|
|
157
191
|
snapshot(ownerSessionId: string): Promise<RuntimeSnapshot>;
|
|
158
192
|
subscribeCompletion(listener: CompletionListener): UnsubscribeCompletion;
|
|
193
|
+
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
|
|
159
194
|
subscribeState(listener: StateListener): () => void;
|
|
160
195
|
shutdown(): Promise<void>;
|
|
161
196
|
}
|
|
@@ -167,6 +202,7 @@ interface RuntimeEntry {
|
|
|
167
202
|
session?: WorkerSessionHandle;
|
|
168
203
|
unsubscribeUsage?: () => void;
|
|
169
204
|
unsubscribeActivity?: () => void;
|
|
205
|
+
unsubscribeMessageDirection?: () => void;
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
interface WaveWaiter {
|
|
@@ -178,17 +214,16 @@ interface WaveWaiter {
|
|
|
178
214
|
|
|
179
215
|
const defaultBestEffortDeadline: BestEffortDeadline = {
|
|
180
216
|
wait(promise, timeoutMs) {
|
|
181
|
-
|
|
182
|
-
const settled = promise.then(
|
|
217
|
+
const settled = Effect.promise(() => promise.then(
|
|
183
218
|
() => "settled" as const,
|
|
184
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
|
+
),
|
|
185
226
|
);
|
|
186
|
-
const timedOut = new Promise<DeadlineResult>((resolve) => {
|
|
187
|
-
timer = setTimeout(() => resolve("timed-out"), timeoutMs);
|
|
188
|
-
});
|
|
189
|
-
return Promise.race([settled, timedOut]).finally(() => {
|
|
190
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
191
|
-
});
|
|
192
227
|
},
|
|
193
228
|
};
|
|
194
229
|
|
|
@@ -208,7 +243,10 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
208
243
|
private readonly terminalWorkerOrder: WorkerId[] = [];
|
|
209
244
|
private readonly completedWaveOrder: WaveId[] = [];
|
|
210
245
|
private readonly completionListeners = new Set<CompletionListener>();
|
|
246
|
+
private readonly settlementListeners = new Set<SettlementListener>();
|
|
247
|
+
private readonly waveSettlementListeners = new Map<WaveId, SettlementListener>();
|
|
211
248
|
private readonly stateListeners = new Set<StateListener>();
|
|
249
|
+
private settlementSequence = 0;
|
|
212
250
|
private readonly disposedSessions = new WeakSet<WorkerSessionHandle>();
|
|
213
251
|
private shuttingDown = false;
|
|
214
252
|
private shutdownPromise: Promise<void> | undefined;
|
|
@@ -226,24 +264,28 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
226
264
|
tasks: readonly OrchestrateTaskInput[],
|
|
227
265
|
mode: "async",
|
|
228
266
|
signal?: AbortSignal,
|
|
267
|
+
onSettlement?: SettlementListener,
|
|
229
268
|
): Promise<AcceptedWave>;
|
|
230
269
|
orchestrate(
|
|
231
270
|
context: OrchestrationContext,
|
|
232
271
|
tasks: readonly OrchestrateTaskInput[],
|
|
233
272
|
mode: "inline",
|
|
234
273
|
signal?: AbortSignal,
|
|
274
|
+
onSettlement?: SettlementListener,
|
|
235
275
|
): Promise<CompletedWave>;
|
|
236
276
|
orchestrate(
|
|
237
277
|
context: OrchestrationContext,
|
|
238
278
|
tasks: readonly OrchestrateTaskInput[],
|
|
239
279
|
mode: WaveMode,
|
|
240
280
|
signal?: AbortSignal,
|
|
281
|
+
onSettlement?: SettlementListener,
|
|
241
282
|
): Promise<AcceptedWave | CompletedWave>;
|
|
242
283
|
async orchestrate(
|
|
243
284
|
context: OrchestrationContext,
|
|
244
285
|
tasks: readonly OrchestrateTaskInput[],
|
|
245
286
|
mode: WaveMode,
|
|
246
287
|
signal?: AbortSignal,
|
|
288
|
+
onSettlement?: SettlementListener,
|
|
247
289
|
): Promise<AcceptedWave | CompletedWave> {
|
|
248
290
|
this.assertOpen();
|
|
249
291
|
throwIfAborted(signal);
|
|
@@ -275,12 +317,15 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
275
317
|
lifecycle: definition.lifecycle,
|
|
276
318
|
status: "starting",
|
|
277
319
|
usage: copyUsage(EMPTY_WORKER_USAGE),
|
|
320
|
+
messageDirection: "to-model",
|
|
321
|
+
startedAt: this.clock(),
|
|
278
322
|
};
|
|
279
323
|
});
|
|
280
324
|
|
|
281
325
|
const waiter = makeWaveWaiter();
|
|
282
326
|
this.waves.set(waveId, wave);
|
|
283
327
|
this.waveWaiters.set(waveId, waiter);
|
|
328
|
+
if (onSettlement) this.waveSettlementListeners.set(waveId, onSettlement);
|
|
284
329
|
for (let index = 0; index < records.length; index += 1) {
|
|
285
330
|
const record = records[index];
|
|
286
331
|
const definition = definitions[index];
|
|
@@ -304,6 +349,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
304
349
|
instructions: string,
|
|
305
350
|
mode: "async",
|
|
306
351
|
signal?: AbortSignal,
|
|
352
|
+
onSettlement?: SettlementListener,
|
|
307
353
|
): Promise<AcceptedWave>;
|
|
308
354
|
send(
|
|
309
355
|
context: OrchestrationContext,
|
|
@@ -311,6 +357,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
311
357
|
instructions: string,
|
|
312
358
|
mode: "inline",
|
|
313
359
|
signal?: AbortSignal,
|
|
360
|
+
onSettlement?: SettlementListener,
|
|
314
361
|
): Promise<CompletedWave>;
|
|
315
362
|
send(
|
|
316
363
|
context: OrchestrationContext,
|
|
@@ -318,6 +365,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
318
365
|
instructions: string,
|
|
319
366
|
mode: WaveMode,
|
|
320
367
|
signal?: AbortSignal,
|
|
368
|
+
onSettlement?: SettlementListener,
|
|
321
369
|
): Promise<AcceptedWave | CompletedWave>;
|
|
322
370
|
async send(
|
|
323
371
|
context: OrchestrationContext,
|
|
@@ -325,6 +373,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
325
373
|
instructions: string,
|
|
326
374
|
mode: WaveMode,
|
|
327
375
|
signal?: AbortSignal,
|
|
376
|
+
onSettlement?: SettlementListener,
|
|
328
377
|
): Promise<AcceptedWave | CompletedWave> {
|
|
329
378
|
this.assertOpen();
|
|
330
379
|
throwIfAborted(signal);
|
|
@@ -349,11 +398,14 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
349
398
|
state: "running",
|
|
350
399
|
createdAt: this.clock(),
|
|
351
400
|
};
|
|
352
|
-
const running = {
|
|
401
|
+
const running: WorkerRecord = {
|
|
353
402
|
...transitionWorkerStatus(current, "running"),
|
|
354
403
|
waveId,
|
|
355
404
|
instructions,
|
|
356
405
|
activity: undefined,
|
|
406
|
+
messageDirection: "to-model",
|
|
407
|
+
startedAt: this.clock(),
|
|
408
|
+
settledAt: undefined,
|
|
357
409
|
};
|
|
358
410
|
const waiter = makeWaveWaiter();
|
|
359
411
|
|
|
@@ -361,6 +413,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
361
413
|
const generation = entry.generation;
|
|
362
414
|
this.waves.set(waveId, wave);
|
|
363
415
|
this.waveWaiters.set(waveId, waiter);
|
|
416
|
+
if (onSettlement) this.waveSettlementListeners.set(waveId, onSettlement);
|
|
364
417
|
this.workers.set(workerId, running);
|
|
365
418
|
this.subscribeEntryObservability(workerId, entry, entry.session, generation);
|
|
366
419
|
this.emitState(context.ownerSessionId);
|
|
@@ -414,6 +467,17 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
414
467
|
};
|
|
415
468
|
}
|
|
416
469
|
|
|
470
|
+
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement {
|
|
471
|
+
if (typeof listener !== "function") throw new Error("Settlement listener must be a function");
|
|
472
|
+
this.settlementListeners.add(listener);
|
|
473
|
+
let subscribed = true;
|
|
474
|
+
return () => {
|
|
475
|
+
if (!subscribed) return;
|
|
476
|
+
subscribed = false;
|
|
477
|
+
this.settlementListeners.delete(listener);
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
417
481
|
subscribeState(listener: StateListener): () => void {
|
|
418
482
|
if (typeof listener !== "function") throw new Error("State listener must be a function");
|
|
419
483
|
this.stateListeners.add(listener);
|
|
@@ -446,6 +510,10 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
446
510
|
// Scheduler closure is best-effort during process shutdown.
|
|
447
511
|
} finally {
|
|
448
512
|
await this.awaitTrackedCleanupBestEffort();
|
|
513
|
+
this.waveSettlementListeners.clear();
|
|
514
|
+
this.completionListeners.clear();
|
|
515
|
+
this.settlementListeners.clear();
|
|
516
|
+
this.stateListeners.clear();
|
|
449
517
|
}
|
|
450
518
|
}
|
|
451
519
|
}
|
|
@@ -490,11 +558,15 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
490
558
|
}
|
|
491
559
|
|
|
492
560
|
private launchBootstrap(workerId: WorkerId, generation: number): void {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
561
|
+
try {
|
|
562
|
+
this.scheduler.start(
|
|
563
|
+
workerId,
|
|
564
|
+
this.bootstrapAndPrompt(workerId, generation),
|
|
565
|
+
(error) => this.settleWorkflowDefect(workerId, generation, error),
|
|
566
|
+
);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
this.settleWorkflowDefect(workerId, generation, error);
|
|
569
|
+
}
|
|
498
570
|
}
|
|
499
571
|
|
|
500
572
|
private launchPrompt(
|
|
@@ -503,51 +575,101 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
503
575
|
session: WorkerSessionHandle,
|
|
504
576
|
instructions: string,
|
|
505
577
|
): void {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
578
|
+
try {
|
|
579
|
+
this.scheduler.start(
|
|
580
|
+
workerId,
|
|
581
|
+
this.executePrompt(workerId, generation, session, instructions),
|
|
582
|
+
(error) => this.settleWorkflowDefect(workerId, generation, error),
|
|
583
|
+
);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.settleWorkflowDefect(workerId, generation, error);
|
|
586
|
+
}
|
|
511
587
|
}
|
|
512
588
|
|
|
513
|
-
private
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
});
|
|
519
601
|
}
|
|
520
602
|
|
|
521
|
-
private
|
|
603
|
+
private bootstrap(
|
|
522
604
|
workerId: WorkerId,
|
|
523
605
|
generation: number,
|
|
524
|
-
):
|
|
525
|
-
|
|
526
|
-
|
|
606
|
+
): Effect.Effect<WorkerSessionHandle | undefined, never> {
|
|
607
|
+
return Effect.suspend(() => {
|
|
608
|
+
const entry = this.entries.get(workerId);
|
|
609
|
+
if (!entry) return Effect.succeed(undefined);
|
|
527
610
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
}
|
|
543
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 {
|
|
544
659
|
const current = this.workers.get(workerId);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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) {
|
|
551
673
|
this.disposeSession(session);
|
|
552
674
|
return undefined;
|
|
553
675
|
}
|
|
@@ -568,31 +690,54 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
568
690
|
}
|
|
569
691
|
}
|
|
570
692
|
|
|
571
|
-
private
|
|
693
|
+
private executePrompt(
|
|
572
694
|
workerId: WorkerId,
|
|
573
695
|
generation: number,
|
|
574
696
|
session: WorkerSessionHandle,
|
|
575
697
|
instructions: string,
|
|
576
|
-
):
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
}
|
|
588
711
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
+
});
|
|
596
741
|
}
|
|
597
742
|
|
|
598
743
|
private settleCreationFailure(
|
|
@@ -606,7 +751,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
606
751
|
this.settleTerminalWorker(current, "failed", {
|
|
607
752
|
status: "failed",
|
|
608
753
|
message: describeError(error, "Worker session creation failed"),
|
|
609
|
-
});
|
|
754
|
+
}, "startup");
|
|
610
755
|
}
|
|
611
756
|
|
|
612
757
|
private settleWorkflowDefect(
|
|
@@ -626,7 +771,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
626
771
|
this.settleTerminalWorker(current, "failed", {
|
|
627
772
|
status: "failed",
|
|
628
773
|
message: describeError(error, "Worker workflow failed"),
|
|
629
|
-
});
|
|
774
|
+
}, "workflow");
|
|
630
775
|
}
|
|
631
776
|
|
|
632
777
|
private settleOutcome(
|
|
@@ -663,17 +808,27 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
663
808
|
};
|
|
664
809
|
}
|
|
665
810
|
|
|
811
|
+
const settledAt = this.clock();
|
|
666
812
|
this.workers.set(workerId, {
|
|
667
813
|
...transitionWorkerStatus(current, status),
|
|
668
814
|
activity: undefined,
|
|
669
815
|
outcome: copyOutcome(settledOutcome),
|
|
816
|
+
settledAt,
|
|
670
817
|
});
|
|
671
818
|
if (status !== "ready") this.disposeEntrySession(entry);
|
|
672
819
|
const affectedOwners = this.maybeCompleteWave(current.waveId);
|
|
820
|
+
affectedOwners.add(current.ownerSessionId);
|
|
821
|
+
this.emitSettlement(
|
|
822
|
+
workerId,
|
|
823
|
+
generation,
|
|
824
|
+
settledAt,
|
|
825
|
+
status === "failed" ? "prompt" : undefined,
|
|
826
|
+
);
|
|
673
827
|
if (isTerminalWorkerStatus(status)) {
|
|
674
828
|
addAll(affectedOwners, this.rememberTerminalWorker(workerId));
|
|
829
|
+
} else {
|
|
830
|
+
addAll(affectedOwners, this.pruneHistory());
|
|
675
831
|
}
|
|
676
|
-
affectedOwners.add(current.ownerSessionId);
|
|
677
832
|
this.emitStateForOwners(affectedOwners);
|
|
678
833
|
}
|
|
679
834
|
|
|
@@ -681,18 +836,91 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
681
836
|
current: WorkerRecord,
|
|
682
837
|
status: "failed" | "aborted",
|
|
683
838
|
outcome: WorkerOutcome,
|
|
839
|
+
failureStage?: SettlementFailureStage,
|
|
684
840
|
): void {
|
|
841
|
+
const settledAt = this.clock();
|
|
685
842
|
this.workers.set(current.id, {
|
|
686
843
|
...transitionWorkerStatus(current, status),
|
|
687
844
|
activity: undefined,
|
|
688
845
|
outcome: copyOutcome(outcome),
|
|
846
|
+
settledAt,
|
|
689
847
|
});
|
|
690
848
|
const affectedOwners = this.maybeCompleteWave(current.waveId);
|
|
691
|
-
addAll(affectedOwners, this.rememberTerminalWorker(current.id));
|
|
692
849
|
affectedOwners.add(current.ownerSessionId);
|
|
850
|
+
const generation = this.entries.get(current.id)?.generation;
|
|
851
|
+
if (generation !== undefined) {
|
|
852
|
+
this.emitSettlement(current.id, generation, settledAt, failureStage);
|
|
853
|
+
}
|
|
854
|
+
addAll(affectedOwners, this.rememberTerminalWorker(current.id));
|
|
693
855
|
this.emitStateForOwners(affectedOwners);
|
|
694
856
|
}
|
|
695
857
|
|
|
858
|
+
private emitSettlement(
|
|
859
|
+
workerId: WorkerId,
|
|
860
|
+
generation: number,
|
|
861
|
+
settledAt: number,
|
|
862
|
+
failureStage?: SettlementFailureStage,
|
|
863
|
+
): void {
|
|
864
|
+
const worker = this.workers.get(workerId);
|
|
865
|
+
const wave = worker ? this.waves.get(worker.waveId) : undefined;
|
|
866
|
+
if (!worker || !wave || !worker.outcome || !isWorkerCompleteForWave(worker.status)) return;
|
|
867
|
+
if (worker.status === "closed") return;
|
|
868
|
+
|
|
869
|
+
let remainingActive = 0;
|
|
870
|
+
let waveComplete = true;
|
|
871
|
+
for (const waveWorkerId of wave.workerIds) {
|
|
872
|
+
const waveWorker = this.workers.get(waveWorkerId);
|
|
873
|
+
if (!waveWorker || waveWorker.waveId !== wave.id || !isWorkerCompleteForWave(waveWorker.status)) {
|
|
874
|
+
waveComplete = false;
|
|
875
|
+
if (waveWorker && isActiveWorkerStatus(waveWorker.status)) remainingActive += 1;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const sequence = ++this.settlementSequence;
|
|
880
|
+
const settlement: WorkerSettlement = Object.freeze({
|
|
881
|
+
eventId: `${sequence}:${wave.id}:${workerId}:${generation}`,
|
|
882
|
+
sequence,
|
|
883
|
+
ownerSessionId: worker.ownerSessionId,
|
|
884
|
+
waveId: wave.id,
|
|
885
|
+
workerId,
|
|
886
|
+
generation,
|
|
887
|
+
mode: wave.mode,
|
|
888
|
+
worker: worker.worker,
|
|
889
|
+
title: worker.title,
|
|
890
|
+
lifecycle: worker.lifecycle,
|
|
891
|
+
status: worker.status,
|
|
892
|
+
outcome: Object.freeze(copyOutcome(worker.outcome)),
|
|
893
|
+
...(failureStage ? { failureStage } : {}),
|
|
894
|
+
usage: Object.freeze(copyUsage(worker.usage)),
|
|
895
|
+
startedAt: worker.startedAt,
|
|
896
|
+
settledAt,
|
|
897
|
+
remainingActive,
|
|
898
|
+
waveSize: wave.workerIds.length,
|
|
899
|
+
waveComplete,
|
|
900
|
+
sessionFile: worker.sessionFile,
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const localListener = this.waveSettlementListeners.get(wave.id);
|
|
904
|
+
if (waveComplete) this.waveSettlementListeners.delete(wave.id);
|
|
905
|
+
notifySettlementListener(localListener, settlement);
|
|
906
|
+
for (const listener of [...this.settlementListeners]) {
|
|
907
|
+
notifySettlementListener(listener, settlement);
|
|
908
|
+
}
|
|
909
|
+
if (waveComplete) this.emitCompletedWave(wave.id);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private emitCompletedWave(waveId: WaveId): void {
|
|
913
|
+
const completed = this.completedWaves.get(waveId);
|
|
914
|
+
if (!completed || completed.mode !== "async") return;
|
|
915
|
+
for (const listener of [...this.completionListeners]) {
|
|
916
|
+
try {
|
|
917
|
+
listener(completed);
|
|
918
|
+
} catch {
|
|
919
|
+
// One subscriber cannot prevent other subscribers from receiving completion.
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
696
924
|
private maybeCompleteWave(waveId: WaveId): Set<string> {
|
|
697
925
|
const affectedOwners = new Set<string>();
|
|
698
926
|
if (this.completedWaves.has(waveId)) return affectedOwners;
|
|
@@ -719,17 +947,6 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
719
947
|
this.waveWaiters.get(waveId)?.resolve(completed);
|
|
720
948
|
this.waveWaiters.delete(waveId);
|
|
721
949
|
|
|
722
|
-
if (wave.mode === "async") {
|
|
723
|
-
for (const listener of [...this.completionListeners]) {
|
|
724
|
-
try {
|
|
725
|
-
listener(completed);
|
|
726
|
-
} catch {
|
|
727
|
-
// One subscriber cannot prevent other subscribers from receiving completion.
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
addAll(affectedOwners, this.pruneHistory());
|
|
733
950
|
return affectedOwners;
|
|
734
951
|
}
|
|
735
952
|
|
|
@@ -884,7 +1101,12 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
884
1101
|
if (entry) this.disposeEntrySession(entry);
|
|
885
1102
|
const current = this.workers.get(workerId);
|
|
886
1103
|
if (current?.status === "stopping") {
|
|
887
|
-
this.settleTerminalWorker(
|
|
1104
|
+
this.settleTerminalWorker(
|
|
1105
|
+
current,
|
|
1106
|
+
"aborted",
|
|
1107
|
+
{ status: "aborted" },
|
|
1108
|
+
"cancellation",
|
|
1109
|
+
);
|
|
888
1110
|
}
|
|
889
1111
|
}
|
|
890
1112
|
}
|
|
@@ -897,7 +1119,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
897
1119
|
this.settleTerminalWorker(current, "failed", {
|
|
898
1120
|
status: "failed",
|
|
899
1121
|
message: describeError(error, "Worker cancellation failed"),
|
|
900
|
-
});
|
|
1122
|
+
}, "cancellation");
|
|
901
1123
|
}
|
|
902
1124
|
|
|
903
1125
|
private async cancelExactWave(wave: WaveRecord): Promise<void> {
|
|
@@ -952,6 +1174,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
952
1174
|
...transitionWorkerStatus(current, "closed"),
|
|
953
1175
|
activity: undefined,
|
|
954
1176
|
outcome: { status: "closed" },
|
|
1177
|
+
settledAt: this.clock(),
|
|
955
1178
|
});
|
|
956
1179
|
const affectedOwners = this.maybeCompleteWave(current.waveId);
|
|
957
1180
|
addAll(affectedOwners, this.rememberTerminalWorker(current.id));
|
|
@@ -986,6 +1209,14 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
986
1209
|
this.workers.set(workerId, { ...latest, activity });
|
|
987
1210
|
this.emitState(latest.ownerSessionId);
|
|
988
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
|
+
});
|
|
989
1220
|
}
|
|
990
1221
|
|
|
991
1222
|
private emitState(ownerSessionId: string): void {
|
|
@@ -1015,6 +1246,8 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
1015
1246
|
entry.unsubscribeUsage = undefined;
|
|
1016
1247
|
safelyCall(entry.unsubscribeActivity);
|
|
1017
1248
|
entry.unsubscribeActivity = undefined;
|
|
1249
|
+
safelyCall(entry.unsubscribeMessageDirection);
|
|
1250
|
+
entry.unsubscribeMessageDirection = undefined;
|
|
1018
1251
|
}
|
|
1019
1252
|
|
|
1020
1253
|
private disposeEntrySession(entry: RuntimeEntry): void {
|
|
@@ -1026,7 +1259,12 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
|
|
|
1026
1259
|
private disposeSession(session: WorkerSessionHandle): void {
|
|
1027
1260
|
if (this.disposedSessions.has(session)) return;
|
|
1028
1261
|
this.disposedSessions.add(session);
|
|
1029
|
-
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
this.trackCleanup(session.dispose());
|
|
1265
|
+
} catch {
|
|
1266
|
+
// Cleanup is best-effort and cannot leave lifecycle state unsettled.
|
|
1267
|
+
}
|
|
1030
1268
|
}
|
|
1031
1269
|
|
|
1032
1270
|
private trackCleanup(operation: Promise<void>): Promise<void> {
|
|
@@ -1114,6 +1352,18 @@ function addAll(target: Set<string>, source: ReadonlySet<string>): void {
|
|
|
1114
1352
|
for (const value of source) target.add(value);
|
|
1115
1353
|
}
|
|
1116
1354
|
|
|
1355
|
+
function notifySettlementListener(
|
|
1356
|
+
listener: SettlementListener | undefined,
|
|
1357
|
+
settlement: WorkerSettlement,
|
|
1358
|
+
): void {
|
|
1359
|
+
if (!listener) return;
|
|
1360
|
+
try {
|
|
1361
|
+
listener(settlement);
|
|
1362
|
+
} catch {
|
|
1363
|
+
// One observer cannot prevent settlement or other observers from being notified.
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1117
1367
|
function makeWaveWaiter(): WaveWaiter {
|
|
1118
1368
|
let complete!: (wave: CompletedWave) => void;
|
|
1119
1369
|
const waiter: WaveWaiter = {
|