@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
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 = "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,187 @@ 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];
|
|
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
|
+
throw new Error("Cannot create a process host after the current host was destroyed");
|
|
258
|
+
}
|
|
43
259
|
if (existing) return existing;
|
|
44
260
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const delivery = new DeliveryCoordinator();
|
|
261
|
+
const effectRuntime = ManagedRuntime.make(createProcessApplicationLayer());
|
|
262
|
+
const runtime = createProcessHostRuntimeAdapter(effectRuntime);
|
|
263
|
+
const delivery = effectRuntime.runSync(Delivery);
|
|
49
264
|
const host: OwnedProcessHost = {
|
|
50
265
|
runtime,
|
|
51
266
|
delivery,
|
|
267
|
+
effectRuntime,
|
|
52
268
|
attachments: new Set(),
|
|
53
|
-
unsubscribeSettlement: runtime.subscribeSettlement((settlement) => {
|
|
54
|
-
delivery.accept(settlement);
|
|
55
|
-
}),
|
|
56
269
|
};
|
|
57
270
|
global[PROCESS_HOST_KEY] = host;
|
|
58
271
|
return host;
|
|
59
272
|
}
|
|
60
273
|
|
|
61
274
|
export function attachProcessHost(host: ProcessHost): ProcessHostAttachment {
|
|
275
|
+
const ownedHost = host as OwnedProcessHost;
|
|
276
|
+
if (ownedHost.lifecycle === "destroying" || ownedHost.lifecycle === "destroyed") {
|
|
277
|
+
throw new Error(`Cannot attach to a ${ownedHost.lifecycle} process host`);
|
|
278
|
+
}
|
|
279
|
+
|
|
62
280
|
const attachment: ProcessHostAttachment = { host };
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
attachmentAwareHost.attachments.add(attachment);
|
|
281
|
+
ownedHost.attachments ??= new Set();
|
|
282
|
+
ownedHost.attachments.add(attachment);
|
|
66
283
|
return attachment;
|
|
67
284
|
}
|
|
68
285
|
|
|
@@ -77,27 +294,64 @@ export function detachProcessHost(
|
|
|
77
294
|
return attachments.size === 0;
|
|
78
295
|
}
|
|
79
296
|
|
|
80
|
-
export
|
|
297
|
+
export function destroyProcessHost(
|
|
298
|
+
host: ProcessHost,
|
|
299
|
+
options: ProcessHostDestructionOptions = {},
|
|
300
|
+
): Promise<void> {
|
|
81
301
|
const ownedHost = host as OwnedProcessHost;
|
|
82
|
-
if (
|
|
83
|
-
if (ownedHost.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
302
|
+
if (ownedHost.destroyPromise) return ownedHost.destroyPromise;
|
|
303
|
+
if ((ownedHost.attachments?.size ?? 0) > 0) return Promise.resolve();
|
|
304
|
+
|
|
305
|
+
ownedHost.lifecycle = "destroying";
|
|
306
|
+
let resolveDestruction!: () => void;
|
|
307
|
+
let rejectDestruction!: (error: unknown) => void;
|
|
308
|
+
const destroyPromise = new Promise<void>((resolve, reject) => {
|
|
309
|
+
resolveDestruction = resolve;
|
|
310
|
+
rejectDestruction = reject;
|
|
311
|
+
});
|
|
312
|
+
// Publish the exact shared result before shutdown can synchronously reenter destruction.
|
|
313
|
+
ownedHost.destroyPromise = destroyPromise;
|
|
87
314
|
|
|
88
|
-
|
|
315
|
+
let shutdown: Promise<void>;
|
|
316
|
+
try {
|
|
317
|
+
// shutdown() closes Orchestration admission before returning its bounded teardown Promise.
|
|
318
|
+
shutdown = ownedHost.runtime.shutdown();
|
|
319
|
+
} catch (error) {
|
|
320
|
+
shutdown = Promise.reject(error);
|
|
321
|
+
}
|
|
322
|
+
const awaitShutdown =
|
|
323
|
+
options.awaitShutdown ?? awaitPromiseWithinCleanupGrace;
|
|
324
|
+
const awaitRootDisposal =
|
|
325
|
+
options.awaitRootDisposal ?? awaitPromiseWithinCleanupGrace;
|
|
326
|
+
const teardown = (async () => {
|
|
89
327
|
try {
|
|
90
|
-
|
|
328
|
+
// A timed-out shutdown keeps physical best-effort finalizers alive. Observe
|
|
329
|
+
// either late outcome while proceeding to the separately bounded root disposal.
|
|
330
|
+
void shutdown.catch(() => {});
|
|
331
|
+
await awaitShutdown(shutdown, SHUTDOWN_CLEANUP_GRACE_MS);
|
|
91
332
|
} finally {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
333
|
+
try {
|
|
334
|
+
const disposal = ownedHost.effectRuntime?.dispose();
|
|
335
|
+
if (disposal) {
|
|
336
|
+
// ManagedRuntime disposal keeps ownership of uninterruptible finalizers after timeout.
|
|
337
|
+
// Observe its eventual rejection while host destruction proceeds best-effort.
|
|
338
|
+
void disposal.catch(() => {});
|
|
339
|
+
await awaitRootDisposal(disposal, SHUTDOWN_CLEANUP_GRACE_MS);
|
|
340
|
+
}
|
|
341
|
+
} finally {
|
|
342
|
+
// After either deadline the host is logically destroyed and detached even
|
|
343
|
+
// though abandoned physical finalizers may still settle. A replacement may
|
|
344
|
+
// overlap only that cleanup; identity guards keep stale completion harmless.
|
|
345
|
+
ownedHost.lifecycle = "destroyed";
|
|
346
|
+
const global = processGlobal();
|
|
347
|
+
if (global[PROCESS_HOST_KEY] === ownedHost) {
|
|
348
|
+
delete global[PROCESS_HOST_KEY];
|
|
349
|
+
}
|
|
97
350
|
}
|
|
98
351
|
}
|
|
99
352
|
})();
|
|
100
|
-
|
|
353
|
+
void teardown.then(resolveDestruction, rejectDestruction);
|
|
354
|
+
return destroyPromise;
|
|
101
355
|
}
|
|
102
356
|
|
|
103
357
|
export async function quitProcessHost(): Promise<void> {
|
|
@@ -105,3 +359,35 @@ export async function quitProcessHost(): Promise<void> {
|
|
|
105
359
|
if (!host) return;
|
|
106
360
|
await destroyProcessHost(host);
|
|
107
361
|
}
|
|
362
|
+
|
|
363
|
+
function awaitPromiseWithinCleanupGrace(
|
|
364
|
+
promise: Promise<void>,
|
|
365
|
+
graceMs: number,
|
|
366
|
+
): Promise<void> {
|
|
367
|
+
return Effect.runPromise(
|
|
368
|
+
Effect.promise(() => promise).pipe(
|
|
369
|
+
Effect.timeoutOption(graceMs),
|
|
370
|
+
Effect.asVoid,
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function abortSignalEffect(
|
|
376
|
+
signal: AbortSignal,
|
|
377
|
+
interruption: object,
|
|
378
|
+
): Effect.Effect<never, object> {
|
|
379
|
+
return Effect.callback((resume) => {
|
|
380
|
+
const onAbort = () => resume(Effect.fail(interruption));
|
|
381
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
382
|
+
if (signal.aborted) {
|
|
383
|
+
signal.removeEventListener("abort", onAbort);
|
|
384
|
+
onAbort();
|
|
385
|
+
}
|
|
386
|
+
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function abortSignalReason(signal: AbortSignal): unknown {
|
|
391
|
+
if ("reason" in signal) return signal.reason;
|
|
392
|
+
return new DOMException("This operation was aborted", "AbortError");
|
|
393
|
+
}
|
package/extension/index.ts
CHANGED
|
@@ -52,42 +52,40 @@ 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;
|
|
62
60
|
let activeBinding: OwnerBinding | undefined;
|
|
63
61
|
let cachedCatalog: WorkerCatalog | undefined;
|
|
64
62
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
cachedCatalog = discoverCatalog({
|
|
63
|
+
const discoverCatalogFor = (ctx: ExtensionContext): WorkerCatalog =>
|
|
64
|
+
discoverCatalog({
|
|
68
65
|
cwd: ctx.cwd,
|
|
69
66
|
projectTrusted: ctx.isProjectTrusted(),
|
|
70
67
|
});
|
|
68
|
+
|
|
69
|
+
const catalogFor = (ctx: ExtensionContext): WorkerCatalog => {
|
|
70
|
+
if (cachedCatalog) return cachedCatalog;
|
|
71
|
+
cachedCatalog = discoverCatalogFor(ctx);
|
|
71
72
|
return cachedCatalog;
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
registerOrchestrationTools(pi, {
|
|
75
|
-
runtime: host.runtime,
|
|
76
|
-
getCatalog: catalogFor,
|
|
77
|
-
getDispatchDecision: (toolCallId) =>
|
|
78
|
-
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
79
|
-
});
|
|
80
75
|
registerOrchestrationPresentation(pi);
|
|
81
76
|
|
|
82
77
|
pi.on("session_start", (_event, ctx) => {
|
|
78
|
+
host ??= dependencies.getHost?.() ?? createProcessHost();
|
|
79
|
+
statusController ??=
|
|
80
|
+
dependencies.createStatusController?.(host.runtime) ??
|
|
81
|
+
createStatusController(host.runtime);
|
|
82
|
+
registerOrchestrationTools(pi, {
|
|
83
|
+
runtime: host.runtime,
|
|
84
|
+
getCatalog: catalogFor,
|
|
85
|
+
getDispatchDecision: (toolCallId) =>
|
|
86
|
+
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
87
|
+
});
|
|
83
88
|
hostAttachment ??= attachProcessHost(host);
|
|
84
|
-
if (activeBinding) {
|
|
85
|
-
host.delivery.unbind(
|
|
86
|
-
activeBinding.ownerSessionId,
|
|
87
|
-
activeBinding.generation,
|
|
88
|
-
);
|
|
89
|
-
statusController.unbind(activeBinding.ownerSessionId);
|
|
90
|
-
}
|
|
91
89
|
|
|
92
90
|
dispatchDecisions.clear();
|
|
93
91
|
cachedCatalog = undefined;
|
|
@@ -106,10 +104,7 @@ export function createOrchestrationExtension(
|
|
|
106
104
|
});
|
|
107
105
|
|
|
108
106
|
pi.on("before_agent_start", (event, ctx) => {
|
|
109
|
-
cachedCatalog =
|
|
110
|
-
cwd: ctx.cwd,
|
|
111
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
112
|
-
});
|
|
107
|
+
cachedCatalog = discoverCatalogFor(ctx);
|
|
113
108
|
return {
|
|
114
109
|
systemPrompt: appendOrchestratorContract(
|
|
115
110
|
event.systemPrompt,
|
|
@@ -125,6 +120,8 @@ export function createOrchestrationExtension(
|
|
|
125
120
|
);
|
|
126
121
|
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
127
122
|
if (!ownerSessionId) return;
|
|
123
|
+
// Sole dispatches and homogeneous orchestrate waves detach; mixed tools stay with the
|
|
124
|
+
// current parent turn, while a wave shares one boundary for one later synthesis turn.
|
|
128
125
|
const isOrchestrateGroup =
|
|
129
126
|
toolCalls.length > 1 &&
|
|
130
127
|
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
@@ -151,7 +148,7 @@ export function createOrchestrationExtension(
|
|
|
151
148
|
const decision = dispatchDecisions.get(event.toolCallId);
|
|
152
149
|
dispatchDecisions.delete(event.toolCallId);
|
|
153
150
|
if (!event.isError || !decision?.synthesisGroup) return;
|
|
154
|
-
host
|
|
151
|
+
host?.delivery.skipSynthesisGroupMember(
|
|
155
152
|
decision.ownerSessionId,
|
|
156
153
|
decision.synthesisGroup.id,
|
|
157
154
|
decision.synthesisGroup.size,
|
|
@@ -161,7 +158,7 @@ export function createOrchestrationExtension(
|
|
|
161
158
|
pi.on("agent_start", () => {
|
|
162
159
|
const binding = activeBinding;
|
|
163
160
|
if (!binding) return;
|
|
164
|
-
host
|
|
161
|
+
host?.delivery.markAgentStarted(
|
|
165
162
|
binding.ownerSessionId,
|
|
166
163
|
binding.generation,
|
|
167
164
|
);
|
|
@@ -170,7 +167,7 @@ export function createOrchestrationExtension(
|
|
|
170
167
|
pi.on("agent_settled", () => {
|
|
171
168
|
const binding = activeBinding;
|
|
172
169
|
if (!binding) return;
|
|
173
|
-
host
|
|
170
|
+
host?.delivery.markAgentSettled(
|
|
174
171
|
binding.ownerSessionId,
|
|
175
172
|
binding.generation,
|
|
176
173
|
);
|
|
@@ -182,17 +179,17 @@ export function createOrchestrationExtension(
|
|
|
182
179
|
cachedCatalog = undefined;
|
|
183
180
|
dispatchDecisions.clear();
|
|
184
181
|
|
|
185
|
-
if (binding) {
|
|
182
|
+
if (binding && host) {
|
|
186
183
|
host.delivery.unbind(binding.ownerSessionId, binding.generation);
|
|
187
184
|
}
|
|
188
|
-
statusController
|
|
185
|
+
statusController?.dispose();
|
|
189
186
|
|
|
190
187
|
const attachment = hostAttachment;
|
|
191
188
|
hostAttachment = undefined;
|
|
192
|
-
const wasLastAttachment = attachment
|
|
189
|
+
const wasLastAttachment = host && attachment
|
|
193
190
|
? detachProcessHost(host, attachment)
|
|
194
191
|
: false;
|
|
195
|
-
if (event.reason === "quit" && wasLastAttachment) {
|
|
192
|
+
if (event.reason === "quit" && wasLastAttachment && host) {
|
|
196
193
|
await (dependencies.destroyHost ?? destroyProcessHost)(host);
|
|
197
194
|
}
|
|
198
195
|
});
|