@zachwill/pi-orchestrate 0.7.2 → 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/contract.ts +2 -2
- package/extension/delivery.ts +49 -7
- package/extension/domain.ts +83 -58
- package/extension/host.ts +321 -31
- package/extension/index.ts +36 -29
- package/extension/presentation.ts +42 -89
- package/extension/runtime.ts +1705 -887
- package/extension/tools.ts +146 -246
- package/extension/tui.ts +50 -0
- package/extension/worker-session.ts +517 -191
- package/extension/worker-settlement.ts +127 -47
- package/package.json +2 -2
- package/extension/scheduler.ts +0 -85
package/extension/host.ts
CHANGED
|
@@ -1,27 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Delivery,
|
|
4
|
+
deliveryLayer,
|
|
5
|
+
type DeliveryService,
|
|
6
|
+
} from "./delivery.js";
|
|
7
|
+
import {
|
|
8
|
+
Orchestration,
|
|
9
|
+
orchestrationLayer,
|
|
10
|
+
type AbortTarget,
|
|
11
|
+
type AcceptedRun,
|
|
12
|
+
type CompletedRun,
|
|
13
|
+
type OrchestrationContext,
|
|
14
|
+
type OrchestrationService,
|
|
15
|
+
SHUTDOWN_CLEANUP_GRACE_MS,
|
|
16
|
+
type RuntimeSnapshot,
|
|
17
|
+
type SettlementListener,
|
|
18
|
+
type UnsubscribeSettlement,
|
|
5
19
|
} from "./runtime.js";
|
|
6
|
-
import {
|
|
20
|
+
import { createChildSessionsLayer } from "./worker-session.js";
|
|
21
|
+
import type { OrchestrateTaskInput, RunMode } from "./domain.js";
|
|
7
22
|
|
|
8
23
|
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
9
24
|
|
|
25
|
+
type RunResult<M extends RunMode> = M extends "async"
|
|
26
|
+
? AcceptedRun
|
|
27
|
+
: CompletedRun;
|
|
28
|
+
|
|
29
|
+
export interface OrchestratorRuntime {
|
|
30
|
+
orchestrate<M extends RunMode>(
|
|
31
|
+
context: OrchestrationContext,
|
|
32
|
+
task: OrchestrateTaskInput,
|
|
33
|
+
mode: M,
|
|
34
|
+
signal?: AbortSignal,
|
|
35
|
+
onSettlement?: SettlementListener,
|
|
36
|
+
): Promise<RunResult<M>>;
|
|
37
|
+
sendInteractive<M extends RunMode>(
|
|
38
|
+
context: OrchestrationContext,
|
|
39
|
+
workerId: string,
|
|
40
|
+
instructions: string,
|
|
41
|
+
mode: M,
|
|
42
|
+
signal?: AbortSignal,
|
|
43
|
+
onSettlement?: SettlementListener,
|
|
44
|
+
): Promise<RunResult<M>>;
|
|
45
|
+
abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
|
|
46
|
+
closeInteractive(ownerSessionId: string, workerId: string): Promise<void>;
|
|
47
|
+
snapshot(ownerSessionId: string): Promise<RuntimeSnapshot>;
|
|
48
|
+
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
|
|
49
|
+
subscribeState(
|
|
50
|
+
ownerSessionId: string,
|
|
51
|
+
listener: (snapshot: RuntimeSnapshot) => void,
|
|
52
|
+
): () => void;
|
|
53
|
+
shutdown(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
10
56
|
export interface ProcessHost {
|
|
11
57
|
readonly runtime: OrchestratorRuntime;
|
|
12
|
-
readonly delivery:
|
|
58
|
+
readonly delivery: DeliveryService;
|
|
13
59
|
}
|
|
14
60
|
|
|
15
61
|
export interface ProcessHostAttachment {
|
|
16
62
|
readonly host: ProcessHost;
|
|
17
63
|
}
|
|
18
64
|
|
|
65
|
+
export interface ProcessHostDestructionOptions {
|
|
66
|
+
/** Process-boundary deadline capability; production uses the orchestration cleanup grace. */
|
|
67
|
+
readonly awaitShutdown?: (
|
|
68
|
+
shutdown: Promise<void>,
|
|
69
|
+
graceMs: number,
|
|
70
|
+
) => Promise<void>;
|
|
71
|
+
/** Process-boundary deadline capability; production uses the orchestration cleanup grace. */
|
|
72
|
+
readonly awaitRootDisposal?: (
|
|
73
|
+
disposal: Promise<void>,
|
|
74
|
+
graceMs: number,
|
|
75
|
+
) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
19
78
|
interface AttachmentAwareProcessHost extends ProcessHost {
|
|
20
79
|
attachments?: Set<ProcessHostAttachment>;
|
|
21
80
|
}
|
|
22
81
|
|
|
82
|
+
type ProcessHostLifecycle = "active" | "destroying" | "destroyed";
|
|
83
|
+
|
|
23
84
|
interface OwnedProcessHost extends AttachmentAwareProcessHost {
|
|
24
|
-
readonly
|
|
85
|
+
readonly effectRuntime?: ManagedRuntime.ManagedRuntime<Orchestration | Delivery, never>;
|
|
86
|
+
lifecycle?: ProcessHostLifecycle;
|
|
25
87
|
destroyPromise?: Promise<void>;
|
|
26
88
|
}
|
|
27
89
|
|
|
@@ -37,32 +99,190 @@ export function getProcessHost(): ProcessHost | undefined {
|
|
|
37
99
|
return processGlobal()[PROCESS_HOST_KEY];
|
|
38
100
|
}
|
|
39
101
|
|
|
102
|
+
/** Pi-facing adapter. Every Promise operation executes one complete Orchestration Effect. */
|
|
103
|
+
export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime {
|
|
104
|
+
constructor(
|
|
105
|
+
private readonly effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
106
|
+
private readonly orchestration: OrchestrationService,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
orchestrate(
|
|
110
|
+
context: OrchestrationContext,
|
|
111
|
+
task: OrchestrateTaskInput,
|
|
112
|
+
mode: "async",
|
|
113
|
+
signal?: AbortSignal,
|
|
114
|
+
onSettlement?: SettlementListener,
|
|
115
|
+
): Promise<AcceptedRun>;
|
|
116
|
+
orchestrate(
|
|
117
|
+
context: OrchestrationContext,
|
|
118
|
+
task: OrchestrateTaskInput,
|
|
119
|
+
mode: "inline",
|
|
120
|
+
signal?: AbortSignal,
|
|
121
|
+
onSettlement?: SettlementListener,
|
|
122
|
+
): Promise<CompletedRun>;
|
|
123
|
+
orchestrate(
|
|
124
|
+
context: OrchestrationContext,
|
|
125
|
+
task: OrchestrateTaskInput,
|
|
126
|
+
mode: RunMode,
|
|
127
|
+
signal?: AbortSignal,
|
|
128
|
+
onSettlement?: SettlementListener,
|
|
129
|
+
): Promise<AcceptedRun | CompletedRun>;
|
|
130
|
+
orchestrate(
|
|
131
|
+
context: OrchestrationContext,
|
|
132
|
+
task: OrchestrateTaskInput,
|
|
133
|
+
mode: RunMode,
|
|
134
|
+
signal?: AbortSignal,
|
|
135
|
+
onSettlement?: SettlementListener,
|
|
136
|
+
): Promise<AcceptedRun | CompletedRun> {
|
|
137
|
+
return this.run(
|
|
138
|
+
this.orchestration.orchestrate(context, task, mode, onSettlement),
|
|
139
|
+
signal,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sendInteractive(
|
|
144
|
+
context: OrchestrationContext,
|
|
145
|
+
workerId: string,
|
|
146
|
+
instructions: string,
|
|
147
|
+
mode: "async",
|
|
148
|
+
signal?: AbortSignal,
|
|
149
|
+
onSettlement?: SettlementListener,
|
|
150
|
+
): Promise<AcceptedRun>;
|
|
151
|
+
sendInteractive(
|
|
152
|
+
context: OrchestrationContext,
|
|
153
|
+
workerId: string,
|
|
154
|
+
instructions: string,
|
|
155
|
+
mode: "inline",
|
|
156
|
+
signal?: AbortSignal,
|
|
157
|
+
onSettlement?: SettlementListener,
|
|
158
|
+
): Promise<CompletedRun>;
|
|
159
|
+
sendInteractive(
|
|
160
|
+
context: OrchestrationContext,
|
|
161
|
+
workerId: string,
|
|
162
|
+
instructions: string,
|
|
163
|
+
mode: RunMode,
|
|
164
|
+
signal?: AbortSignal,
|
|
165
|
+
onSettlement?: SettlementListener,
|
|
166
|
+
): Promise<AcceptedRun | CompletedRun>;
|
|
167
|
+
sendInteractive(
|
|
168
|
+
context: OrchestrationContext,
|
|
169
|
+
workerId: string,
|
|
170
|
+
instructions: string,
|
|
171
|
+
mode: RunMode,
|
|
172
|
+
signal?: AbortSignal,
|
|
173
|
+
onSettlement?: SettlementListener,
|
|
174
|
+
): Promise<AcceptedRun | CompletedRun> {
|
|
175
|
+
return this.run(
|
|
176
|
+
this.orchestration.sendInteractive(
|
|
177
|
+
context,
|
|
178
|
+
workerId,
|
|
179
|
+
instructions,
|
|
180
|
+
mode,
|
|
181
|
+
onSettlement,
|
|
182
|
+
),
|
|
183
|
+
signal,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
abort(ownerSessionId: string, target: AbortTarget): Promise<void> {
|
|
188
|
+
return this.run(this.orchestration.abort(ownerSessionId, target));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
closeInteractive(ownerSessionId: string, workerId: string): Promise<void> {
|
|
192
|
+
return this.run(this.orchestration.closeInteractive(ownerSessionId, workerId));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
snapshot(ownerSessionId: string): Promise<RuntimeSnapshot> {
|
|
196
|
+
// Snapshot is dependency-free and remains readable from a retained host reference
|
|
197
|
+
// after the process root has been disposed.
|
|
198
|
+
return Effect.runPromise(this.orchestration.snapshot(ownerSessionId));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement {
|
|
202
|
+
return this.orchestration.subscribeSettlement(listener);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
subscribeState(
|
|
206
|
+
ownerSessionId: string,
|
|
207
|
+
listener: (snapshot: RuntimeSnapshot) => void,
|
|
208
|
+
): () => void {
|
|
209
|
+
return this.orchestration.subscribeState(ownerSessionId, listener);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
shutdown(): Promise<void> {
|
|
213
|
+
return this.run(this.orchestration.shutdown());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async run<A, E>(
|
|
217
|
+
effect: Effect.Effect<A, E>,
|
|
218
|
+
signal?: AbortSignal,
|
|
219
|
+
): Promise<A> {
|
|
220
|
+
if (signal?.aborted) throw abortSignalReason(signal);
|
|
221
|
+
if (!signal) return this.effectRuntime.runPromise(effect);
|
|
222
|
+
|
|
223
|
+
const signalInterruption = {};
|
|
224
|
+
const exit = await this.effectRuntime.runPromiseExit(
|
|
225
|
+
Effect.raceFirst(effect, abortSignalEffect(signal, signalInterruption)),
|
|
226
|
+
);
|
|
227
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
228
|
+
|
|
229
|
+
const error = Cause.squash(exit.cause);
|
|
230
|
+
if (error === signalInterruption) throw abortSignalReason(signal);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function createProcessHostRuntimeAdapter<R>(
|
|
236
|
+
effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
237
|
+
): OrchestratorRuntime {
|
|
238
|
+
// Orchestration acquisition is synchronous; subscriptions must remain reentrant.
|
|
239
|
+
const orchestration = effectRuntime.runSync(Orchestration);
|
|
240
|
+
return new ProcessHostRuntimeAdapter(effectRuntime, orchestration);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function createProcessApplicationLayer(): Layer.Layer<Orchestration | Delivery> {
|
|
244
|
+
const orchestration = orchestrationLayer().pipe(
|
|
245
|
+
Layer.provide(createChildSessionsLayer()),
|
|
246
|
+
);
|
|
247
|
+
return deliveryLayer.pipe(Layer.provideMerge(orchestration));
|
|
248
|
+
}
|
|
249
|
+
|
|
40
250
|
export function createProcessHost(): ProcessHost {
|
|
41
251
|
const global = processGlobal();
|
|
42
252
|
const existing = global[PROCESS_HOST_KEY];
|
|
43
|
-
if (existing)
|
|
253
|
+
if (existing?.lifecycle === "destroying") {
|
|
254
|
+
throw new Error("Cannot create a process host while the current host is being destroyed");
|
|
255
|
+
}
|
|
256
|
+
if (existing?.lifecycle === "destroyed") {
|
|
257
|
+
delete global[PROCESS_HOST_KEY];
|
|
258
|
+
} else if (existing) {
|
|
259
|
+
existing.lifecycle = "active";
|
|
260
|
+
return existing;
|
|
261
|
+
}
|
|
44
262
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const delivery = new DeliveryCoordinator();
|
|
263
|
+
const effectRuntime = ManagedRuntime.make(createProcessApplicationLayer());
|
|
264
|
+
const runtime = createProcessHostRuntimeAdapter(effectRuntime);
|
|
265
|
+
const delivery = effectRuntime.runSync(Delivery);
|
|
49
266
|
const host: OwnedProcessHost = {
|
|
50
267
|
runtime,
|
|
51
268
|
delivery,
|
|
269
|
+
effectRuntime,
|
|
52
270
|
attachments: new Set(),
|
|
53
|
-
|
|
54
|
-
delivery.accept(settlement);
|
|
55
|
-
}),
|
|
271
|
+
lifecycle: "active",
|
|
56
272
|
};
|
|
57
273
|
global[PROCESS_HOST_KEY] = host;
|
|
58
274
|
return host;
|
|
59
275
|
}
|
|
60
276
|
|
|
61
277
|
export function attachProcessHost(host: ProcessHost): ProcessHostAttachment {
|
|
278
|
+
const ownedHost = host as OwnedProcessHost;
|
|
279
|
+
if (ownedHost.lifecycle === "destroying" || ownedHost.lifecycle === "destroyed") {
|
|
280
|
+
throw new Error(`Cannot attach to a ${ownedHost.lifecycle} process host`);
|
|
281
|
+
}
|
|
282
|
+
|
|
62
283
|
const attachment: ProcessHostAttachment = { host };
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
attachmentAwareHost.attachments.add(attachment);
|
|
284
|
+
ownedHost.attachments ??= new Set();
|
|
285
|
+
ownedHost.attachments.add(attachment);
|
|
66
286
|
return attachment;
|
|
67
287
|
}
|
|
68
288
|
|
|
@@ -77,27 +297,65 @@ export function detachProcessHost(
|
|
|
77
297
|
return attachments.size === 0;
|
|
78
298
|
}
|
|
79
299
|
|
|
80
|
-
export
|
|
300
|
+
export function destroyProcessHost(
|
|
301
|
+
host: ProcessHost,
|
|
302
|
+
options: ProcessHostDestructionOptions = {},
|
|
303
|
+
): Promise<void> {
|
|
81
304
|
const ownedHost = host as OwnedProcessHost;
|
|
82
|
-
if (
|
|
83
|
-
if (ownedHost.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
305
|
+
if (ownedHost.destroyPromise) return ownedHost.destroyPromise;
|
|
306
|
+
if ((ownedHost.attachments?.size ?? 0) > 0) return Promise.resolve();
|
|
307
|
+
if (ownedHost.lifecycle === "destroyed") return Promise.resolve();
|
|
308
|
+
|
|
309
|
+
ownedHost.lifecycle = "destroying";
|
|
310
|
+
let resolveDestruction!: () => void;
|
|
311
|
+
let rejectDestruction!: (error: unknown) => void;
|
|
312
|
+
const destroyPromise = new Promise<void>((resolve, reject) => {
|
|
313
|
+
resolveDestruction = resolve;
|
|
314
|
+
rejectDestruction = reject;
|
|
315
|
+
});
|
|
316
|
+
// Publish the exact shared result before shutdown can synchronously reenter destruction.
|
|
317
|
+
ownedHost.destroyPromise = destroyPromise;
|
|
87
318
|
|
|
88
|
-
|
|
319
|
+
let shutdown: Promise<void>;
|
|
320
|
+
try {
|
|
321
|
+
// shutdown() closes Orchestration admission before returning its bounded teardown Promise.
|
|
322
|
+
shutdown = ownedHost.runtime.shutdown();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
shutdown = Promise.reject(error);
|
|
325
|
+
}
|
|
326
|
+
const awaitShutdown =
|
|
327
|
+
options.awaitShutdown ?? awaitPromiseWithinCleanupGrace;
|
|
328
|
+
const awaitRootDisposal =
|
|
329
|
+
options.awaitRootDisposal ?? awaitPromiseWithinCleanupGrace;
|
|
330
|
+
const teardown = (async () => {
|
|
89
331
|
try {
|
|
90
|
-
|
|
332
|
+
// A timed-out shutdown keeps physical best-effort finalizers alive. Observe
|
|
333
|
+
// either late outcome while proceeding to the separately bounded root disposal.
|
|
334
|
+
void shutdown.catch(() => {});
|
|
335
|
+
await awaitShutdown(shutdown, SHUTDOWN_CLEANUP_GRACE_MS);
|
|
91
336
|
} finally {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
337
|
+
try {
|
|
338
|
+
const disposal = ownedHost.effectRuntime?.dispose();
|
|
339
|
+
if (disposal) {
|
|
340
|
+
// ManagedRuntime disposal keeps ownership of uninterruptible finalizers after timeout.
|
|
341
|
+
// Observe its eventual rejection while host destruction proceeds best-effort.
|
|
342
|
+
void disposal.catch(() => {});
|
|
343
|
+
await awaitRootDisposal(disposal, SHUTDOWN_CLEANUP_GRACE_MS);
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
// After either deadline the host is logically destroyed and detached even
|
|
347
|
+
// though abandoned physical finalizers may still settle. A replacement may
|
|
348
|
+
// overlap only that cleanup; identity guards keep stale completion harmless.
|
|
349
|
+
ownedHost.lifecycle = "destroyed";
|
|
350
|
+
const global = processGlobal();
|
|
351
|
+
if (global[PROCESS_HOST_KEY] === ownedHost) {
|
|
352
|
+
delete global[PROCESS_HOST_KEY];
|
|
353
|
+
}
|
|
97
354
|
}
|
|
98
355
|
}
|
|
99
356
|
})();
|
|
100
|
-
|
|
357
|
+
void teardown.then(resolveDestruction, rejectDestruction);
|
|
358
|
+
return destroyPromise;
|
|
101
359
|
}
|
|
102
360
|
|
|
103
361
|
export async function quitProcessHost(): Promise<void> {
|
|
@@ -105,3 +363,35 @@ export async function quitProcessHost(): Promise<void> {
|
|
|
105
363
|
if (!host) return;
|
|
106
364
|
await destroyProcessHost(host);
|
|
107
365
|
}
|
|
366
|
+
|
|
367
|
+
function awaitPromiseWithinCleanupGrace(
|
|
368
|
+
promise: Promise<void>,
|
|
369
|
+
graceMs: number,
|
|
370
|
+
): Promise<void> {
|
|
371
|
+
return Effect.runPromise(
|
|
372
|
+
Effect.promise(() => promise).pipe(
|
|
373
|
+
Effect.timeoutOption(graceMs),
|
|
374
|
+
Effect.asVoid,
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function abortSignalEffect(
|
|
380
|
+
signal: AbortSignal,
|
|
381
|
+
interruption: object,
|
|
382
|
+
): Effect.Effect<never, object> {
|
|
383
|
+
return Effect.callback((resume) => {
|
|
384
|
+
const onAbort = () => resume(Effect.fail(interruption));
|
|
385
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
386
|
+
if (signal.aborted) {
|
|
387
|
+
signal.removeEventListener("abort", onAbort);
|
|
388
|
+
onAbort();
|
|
389
|
+
}
|
|
390
|
+
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function abortSignalReason(signal: AbortSignal): unknown {
|
|
395
|
+
if ("reason" in signal) return signal.reason;
|
|
396
|
+
return new DOMException("This operation was aborted", "AbortError");
|
|
397
|
+
}
|
package/extension/index.ts
CHANGED
|
@@ -52,36 +52,31 @@ export function createOrchestrationExtension(
|
|
|
52
52
|
dependencies: OrchestrationExtensionDependencies = {},
|
|
53
53
|
): ExtensionFactory {
|
|
54
54
|
return (pi) => {
|
|
55
|
-
const host = dependencies.getHost?.() ?? createProcessHost();
|
|
56
55
|
const discoverCatalog = dependencies.discoverCatalog ?? discoverWorkerCatalog;
|
|
57
|
-
const statusController =
|
|
58
|
-
dependencies.createStatusController?.(host.runtime) ??
|
|
59
|
-
createStatusController(host.runtime);
|
|
60
56
|
const dispatchDecisions = new Map<string, StoredDispatchDecision>();
|
|
57
|
+
let host: ProcessHost | undefined;
|
|
61
58
|
let hostAttachment: ProcessHostAttachment | undefined;
|
|
59
|
+
let statusController: StatusController | undefined;
|
|
60
|
+
let toolsRegistered = false;
|
|
62
61
|
let activeBinding: OwnerBinding | undefined;
|
|
63
62
|
let cachedCatalog: WorkerCatalog | undefined;
|
|
64
63
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
cachedCatalog = discoverCatalog({
|
|
64
|
+
const discoverCatalogFor = (ctx: ExtensionContext): WorkerCatalog =>
|
|
65
|
+
discoverCatalog({
|
|
68
66
|
cwd: ctx.cwd,
|
|
69
67
|
projectTrusted: ctx.isProjectTrusted(),
|
|
70
68
|
});
|
|
69
|
+
|
|
70
|
+
const catalogFor = (ctx: ExtensionContext): WorkerCatalog => {
|
|
71
|
+
if (cachedCatalog) return cachedCatalog;
|
|
72
|
+
cachedCatalog = discoverCatalogFor(ctx);
|
|
71
73
|
return cachedCatalog;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
|
-
registerOrchestrationTools(pi, {
|
|
75
|
-
runtime: host.runtime,
|
|
76
|
-
getCatalog: catalogFor,
|
|
77
|
-
getDispatchDecision: (toolCallId) =>
|
|
78
|
-
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
79
|
-
});
|
|
80
76
|
registerOrchestrationPresentation(pi);
|
|
81
77
|
|
|
82
78
|
pi.on("session_start", (_event, ctx) => {
|
|
83
|
-
|
|
84
|
-
if (activeBinding) {
|
|
79
|
+
if (activeBinding && host && statusController) {
|
|
85
80
|
host.delivery.unbind(
|
|
86
81
|
activeBinding.ownerSessionId,
|
|
87
82
|
activeBinding.generation,
|
|
@@ -89,6 +84,21 @@ export function createOrchestrationExtension(
|
|
|
89
84
|
statusController.unbind(activeBinding.ownerSessionId);
|
|
90
85
|
}
|
|
91
86
|
|
|
87
|
+
host ??= dependencies.getHost?.() ?? createProcessHost();
|
|
88
|
+
statusController ??=
|
|
89
|
+
dependencies.createStatusController?.(host.runtime) ??
|
|
90
|
+
createStatusController(host.runtime);
|
|
91
|
+
if (!toolsRegistered) {
|
|
92
|
+
registerOrchestrationTools(pi, {
|
|
93
|
+
runtime: host.runtime,
|
|
94
|
+
getCatalog: catalogFor,
|
|
95
|
+
getDispatchDecision: (toolCallId) =>
|
|
96
|
+
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
97
|
+
});
|
|
98
|
+
toolsRegistered = true;
|
|
99
|
+
}
|
|
100
|
+
hostAttachment ??= attachProcessHost(host);
|
|
101
|
+
|
|
92
102
|
dispatchDecisions.clear();
|
|
93
103
|
cachedCatalog = undefined;
|
|
94
104
|
const binding: OwnerBinding = {
|
|
@@ -106,10 +116,7 @@ export function createOrchestrationExtension(
|
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
pi.on("before_agent_start", (event, ctx) => {
|
|
109
|
-
cachedCatalog =
|
|
110
|
-
cwd: ctx.cwd,
|
|
111
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
112
|
-
});
|
|
119
|
+
cachedCatalog = discoverCatalogFor(ctx);
|
|
113
120
|
return {
|
|
114
121
|
systemPrompt: appendOrchestratorContract(
|
|
115
122
|
event.systemPrompt,
|
|
@@ -125,16 +132,16 @@ export function createOrchestrationExtension(
|
|
|
125
132
|
);
|
|
126
133
|
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
127
134
|
if (!ownerSessionId) return;
|
|
128
|
-
const
|
|
135
|
+
const isOrchestrateGroup =
|
|
129
136
|
toolCalls.length > 1 &&
|
|
130
137
|
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
131
|
-
const synthesisGroup =
|
|
138
|
+
const synthesisGroup = isOrchestrateGroup
|
|
132
139
|
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
133
140
|
: undefined;
|
|
134
141
|
|
|
135
142
|
for (const toolCall of toolCalls) {
|
|
136
143
|
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
|
|
137
|
-
const mode =
|
|
144
|
+
const mode = isOrchestrateGroup || toolCalls.length === 1
|
|
138
145
|
? "async"
|
|
139
146
|
: "inline";
|
|
140
147
|
dispatchDecisions.set(toolCall.id, {
|
|
@@ -151,7 +158,7 @@ export function createOrchestrationExtension(
|
|
|
151
158
|
const decision = dispatchDecisions.get(event.toolCallId);
|
|
152
159
|
dispatchDecisions.delete(event.toolCallId);
|
|
153
160
|
if (!event.isError || !decision?.synthesisGroup) return;
|
|
154
|
-
host
|
|
161
|
+
host?.delivery.skipSynthesisGroupMember(
|
|
155
162
|
decision.ownerSessionId,
|
|
156
163
|
decision.synthesisGroup.id,
|
|
157
164
|
decision.synthesisGroup.size,
|
|
@@ -161,7 +168,7 @@ export function createOrchestrationExtension(
|
|
|
161
168
|
pi.on("agent_start", () => {
|
|
162
169
|
const binding = activeBinding;
|
|
163
170
|
if (!binding) return;
|
|
164
|
-
host
|
|
171
|
+
host?.delivery.markAgentStarted(
|
|
165
172
|
binding.ownerSessionId,
|
|
166
173
|
binding.generation,
|
|
167
174
|
);
|
|
@@ -170,7 +177,7 @@ export function createOrchestrationExtension(
|
|
|
170
177
|
pi.on("agent_settled", () => {
|
|
171
178
|
const binding = activeBinding;
|
|
172
179
|
if (!binding) return;
|
|
173
|
-
host
|
|
180
|
+
host?.delivery.markAgentSettled(
|
|
174
181
|
binding.ownerSessionId,
|
|
175
182
|
binding.generation,
|
|
176
183
|
);
|
|
@@ -182,17 +189,17 @@ export function createOrchestrationExtension(
|
|
|
182
189
|
cachedCatalog = undefined;
|
|
183
190
|
dispatchDecisions.clear();
|
|
184
191
|
|
|
185
|
-
if (binding) {
|
|
192
|
+
if (binding && host) {
|
|
186
193
|
host.delivery.unbind(binding.ownerSessionId, binding.generation);
|
|
187
194
|
}
|
|
188
|
-
statusController
|
|
195
|
+
statusController?.dispose();
|
|
189
196
|
|
|
190
197
|
const attachment = hostAttachment;
|
|
191
198
|
hostAttachment = undefined;
|
|
192
|
-
const wasLastAttachment = attachment
|
|
199
|
+
const wasLastAttachment = host && attachment
|
|
193
200
|
? detachProcessHost(host, attachment)
|
|
194
201
|
: false;
|
|
195
|
-
if (event.reason === "quit" && wasLastAttachment) {
|
|
202
|
+
if (event.reason === "quit" && wasLastAttachment && host) {
|
|
196
203
|
await (dependencies.destroyHost ?? destroyProcessHost)(host);
|
|
197
204
|
}
|
|
198
205
|
});
|