@zachwill/pi-orchestrate 0.2.1 → 0.4.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 +77 -48
- package/examples/workers/investigator.md +5 -31
- package/examples/workers/scout.md +5 -26
- package/examples/workers/web.md +84 -0
- package/examples/workers/worker.md +9 -35
- package/extension/catalog.ts +40 -22
- package/extension/contract.ts +4 -4
- package/extension/delivery.ts +72 -16
- package/extension/domain.ts +21 -55
- package/extension/host.ts +1 -37
- package/extension/index.ts +39 -11
- package/extension/presentation.ts +62 -158
- package/extension/runtime.ts +239 -331
- package/extension/tools.ts +206 -214
- package/extension/worker-session.ts +213 -50
- package/extension/worker-settlement.ts +106 -0
- package/package.json +1 -1
package/extension/delivery.ts
CHANGED
|
@@ -35,10 +35,17 @@ interface BoundParent {
|
|
|
35
35
|
agentRunning: boolean;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface SynthesisGroupState {
|
|
39
|
+
expected: number;
|
|
40
|
+
readonly acceptedEventIds: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
export class DeliveryCoordinator {
|
|
39
44
|
private readonly boundParents = new Map<string, BoundParent>();
|
|
40
45
|
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
41
46
|
private readonly flushingOwners = new Set<string>();
|
|
47
|
+
private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
|
|
48
|
+
private readonly finalSynthesisGroupEvents = new Set<string>();
|
|
42
49
|
private highestAcceptedSequence = 0;
|
|
43
50
|
|
|
44
51
|
bind(binding: ParentBinding): void {
|
|
@@ -75,12 +82,28 @@ export class DeliveryCoordinator {
|
|
|
75
82
|
|
|
76
83
|
this.highestAcceptedSequence = settlement.sequence;
|
|
77
84
|
this.pendingSettlements.push(settlement);
|
|
85
|
+
this.acceptSynthesisGroupSettlement(settlement);
|
|
78
86
|
|
|
79
87
|
const parent = this.boundParents.get(settlement.ownerSessionId);
|
|
80
88
|
if (parent) this.flush(settlement.ownerSessionId, parent.binding.generation);
|
|
81
89
|
return true;
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
skipSynthesisGroupMember(
|
|
93
|
+
ownerSessionId: string,
|
|
94
|
+
synthesisGroupId: string,
|
|
95
|
+
synthesisGroupSize: number,
|
|
96
|
+
): void {
|
|
97
|
+
const key = synthesisGroupKey(ownerSessionId, synthesisGroupId);
|
|
98
|
+
const state = this.synthesisGroups.get(key) ?? {
|
|
99
|
+
expected: synthesisGroupSize,
|
|
100
|
+
acceptedEventIds: [],
|
|
101
|
+
};
|
|
102
|
+
state.expected = Math.max(0, state.expected - 1);
|
|
103
|
+
this.synthesisGroups.set(key, state);
|
|
104
|
+
this.refreshSynthesisGroupBoundary(key, state);
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
pendingCount(ownerSessionId: string): number {
|
|
85
108
|
return this.pendingSettlements.filter(
|
|
86
109
|
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
@@ -91,13 +114,11 @@ export class DeliveryCoordinator {
|
|
|
91
114
|
this.boundParents.clear();
|
|
92
115
|
this.pendingSettlements.length = 0;
|
|
93
116
|
this.flushingOwners.clear();
|
|
117
|
+
this.synthesisGroups.clear();
|
|
118
|
+
this.finalSynthesisGroupEvents.clear();
|
|
94
119
|
this.highestAcceptedSequence = 0;
|
|
95
120
|
}
|
|
96
121
|
|
|
97
|
-
close(): void {
|
|
98
|
-
this.clear();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
122
|
private matchesBinding(
|
|
102
123
|
ownerSessionId: string,
|
|
103
124
|
generation: ParentBindingGeneration,
|
|
@@ -127,7 +148,8 @@ export class DeliveryCoordinator {
|
|
|
127
148
|
);
|
|
128
149
|
let latestFinalIndex = -1;
|
|
129
150
|
for (let index = 0; index < queued.length; index += 1) {
|
|
130
|
-
|
|
151
|
+
const settlement = queued[index];
|
|
152
|
+
if (settlement && this.isFinalBoundary(settlement)) latestFinalIndex = index;
|
|
131
153
|
}
|
|
132
154
|
const flushThrough = latestFinalIndex >= 0 ? latestFinalIndex : queued.length - 1;
|
|
133
155
|
let flushBytesRemaining = MAX_DELIVERY_MARKDOWN_BYTES;
|
|
@@ -139,13 +161,9 @@ export class DeliveryCoordinator {
|
|
|
139
161
|
|
|
140
162
|
const messagesRemaining = flushThrough - index + 1;
|
|
141
163
|
const fairFlushBytes = Math.floor(flushBytesRemaining / messagesRemaining);
|
|
142
|
-
const fairWaveBytes = Math.floor(
|
|
143
|
-
MAX_DELIVERY_MARKDOWN_BYTES / Math.max(1, settlement.waveSize),
|
|
144
|
-
);
|
|
145
164
|
const byteLimit = Math.max(0, Math.min(
|
|
146
165
|
MAX_WORKER_DELIVERY_MARKDOWN_BYTES,
|
|
147
166
|
fairFlushBytes,
|
|
148
|
-
fairWaveBytes,
|
|
149
167
|
flushBytesRemaining,
|
|
150
168
|
));
|
|
151
169
|
const triggerTurn = latestFinalIndex >= 0 && index === flushThrough;
|
|
@@ -165,6 +183,7 @@ export class DeliveryCoordinator {
|
|
|
165
183
|
if (pendingIndex >= 0) this.pendingSettlements.splice(pendingIndex, 1);
|
|
166
184
|
|
|
167
185
|
if (triggerTurn) {
|
|
186
|
+
this.finishSynthesisGroup(settlement);
|
|
168
187
|
parent.agentRunning = true;
|
|
169
188
|
return;
|
|
170
189
|
}
|
|
@@ -178,17 +197,17 @@ export class DeliveryCoordinator {
|
|
|
178
197
|
settlement: WorkerSettlement,
|
|
179
198
|
byteLimit: number,
|
|
180
199
|
): WorkerDeliveryMessage {
|
|
181
|
-
const heading = `## Worker result — ${settlement.worker}`;
|
|
200
|
+
const heading = `## Worker result — ${settlement.title} · ${settlement.worker}`;
|
|
182
201
|
const metadata = [
|
|
183
202
|
`Worker \`${settlement.workerId}\``,
|
|
184
|
-
`
|
|
203
|
+
`run \`${settlement.runId}\``,
|
|
185
204
|
`status \`${settlement.status}\``,
|
|
186
205
|
].join(" · ");
|
|
187
206
|
const body = renderOutcome(settlement.outcome);
|
|
188
207
|
const content = body.length > 0
|
|
189
|
-
? `${heading}\n\n
|
|
190
|
-
: `${heading}\n\n
|
|
191
|
-
const appendix = settlement
|
|
208
|
+
? `${heading}\n\n${metadata}\n\n${body}`
|
|
209
|
+
: `${heading}\n\n${metadata}`;
|
|
210
|
+
const appendix = this.isFinalBoundary(settlement)
|
|
192
211
|
? `\n\n---\n\n${DELIVERY_PARENT_INSTRUCTIONS}`
|
|
193
212
|
: "";
|
|
194
213
|
|
|
@@ -199,6 +218,45 @@ export class DeliveryCoordinator {
|
|
|
199
218
|
details: settlement,
|
|
200
219
|
});
|
|
201
220
|
}
|
|
221
|
+
|
|
222
|
+
private acceptSynthesisGroupSettlement(settlement: WorkerSettlement): void {
|
|
223
|
+
if (!settlement.synthesisGroupId || !settlement.synthesisGroupSize) return;
|
|
224
|
+
const key = synthesisGroupKey(settlement.ownerSessionId, settlement.synthesisGroupId);
|
|
225
|
+
const state = this.synthesisGroups.get(key) ?? {
|
|
226
|
+
expected: settlement.synthesisGroupSize,
|
|
227
|
+
acceptedEventIds: [],
|
|
228
|
+
};
|
|
229
|
+
state.acceptedEventIds.push(settlement.eventId);
|
|
230
|
+
this.synthesisGroups.set(key, state);
|
|
231
|
+
this.refreshSynthesisGroupBoundary(key, state);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private refreshSynthesisGroupBoundary(key: string, state: SynthesisGroupState): void {
|
|
235
|
+
if (state.expected === 0) {
|
|
236
|
+
this.synthesisGroups.delete(key);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (state.acceptedEventIds.length !== state.expected) return;
|
|
240
|
+
const finalEventId = state.acceptedEventIds.at(-1);
|
|
241
|
+
if (finalEventId) this.finalSynthesisGroupEvents.add(finalEventId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private isFinalBoundary(settlement: WorkerSettlement): boolean {
|
|
245
|
+
if (!settlement.synthesisGroupId) return true;
|
|
246
|
+
return this.finalSynthesisGroupEvents.has(settlement.eventId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private finishSynthesisGroup(settlement: WorkerSettlement): void {
|
|
250
|
+
if (!settlement.synthesisGroupId) return;
|
|
251
|
+
this.synthesisGroups.delete(
|
|
252
|
+
synthesisGroupKey(settlement.ownerSessionId, settlement.synthesisGroupId),
|
|
253
|
+
);
|
|
254
|
+
this.finalSynthesisGroupEvents.delete(settlement.eventId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function synthesisGroupKey(ownerSessionId: string, synthesisGroupId: string): string {
|
|
259
|
+
return `${ownerSessionId}\u0000${synthesisGroupId}`;
|
|
202
260
|
}
|
|
203
261
|
|
|
204
262
|
function capMarkdown(content: string, appendix: string, byteLimit: number): string {
|
|
@@ -238,7 +296,5 @@ function renderOutcome(outcome: WorkerSettlement["outcome"]): string {
|
|
|
238
296
|
const reason = outcome.message ? `Aborted: ${outcome.message}` : "Aborted";
|
|
239
297
|
return outcome.assistantText ? `${reason}\n\n${outcome.assistantText}` : reason;
|
|
240
298
|
}
|
|
241
|
-
case "closed":
|
|
242
|
-
return "Closed";
|
|
243
299
|
}
|
|
244
300
|
}
|
package/extension/domain.ts
CHANGED
|
@@ -95,17 +95,17 @@ export interface OrchestrateTaskInput {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
declare const workerIdBrand: unique symbol;
|
|
98
|
-
declare const
|
|
98
|
+
declare const runIdBrand: unique symbol;
|
|
99
99
|
|
|
100
100
|
export type WorkerId = string & { readonly [workerIdBrand]: "WorkerId" };
|
|
101
|
-
export type
|
|
101
|
+
export type RunId = string & { readonly [runIdBrand]: "RunId" };
|
|
102
102
|
|
|
103
103
|
export type WorkerIdFactory = () => WorkerId;
|
|
104
|
-
export type
|
|
104
|
+
export type RunIdFactory = () => RunId;
|
|
105
105
|
|
|
106
106
|
export interface OrchestrateIdFactories {
|
|
107
107
|
readonly workerId: WorkerIdFactory;
|
|
108
|
-
readonly
|
|
108
|
+
readonly runId: RunIdFactory;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export function createRandomWorkerIdFactory(
|
|
@@ -114,10 +114,10 @@ export function createRandomWorkerIdFactory(
|
|
|
114
114
|
return () => `worker-${randomId()}` as WorkerId;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export function
|
|
117
|
+
export function createRandomRunIdFactory(
|
|
118
118
|
randomId: () => string = defaultRandomId,
|
|
119
|
-
):
|
|
120
|
-
return () => `
|
|
119
|
+
): RunIdFactory {
|
|
120
|
+
return () => `run-${randomId()}` as RunId;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export function createRandomIdFactories(
|
|
@@ -125,7 +125,7 @@ export function createRandomIdFactories(
|
|
|
125
125
|
): OrchestrateIdFactories {
|
|
126
126
|
return {
|
|
127
127
|
workerId: createRandomWorkerIdFactory(randomId),
|
|
128
|
-
|
|
128
|
+
runId: createRandomRunIdFactory(randomId),
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -134,15 +134,15 @@ export function createSequentialWorkerIdFactory(startAt = 1): WorkerIdFactory {
|
|
|
134
134
|
return () => `worker-${next++}` as WorkerId;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
export function
|
|
137
|
+
export function createSequentialRunIdFactory(startAt = 1): RunIdFactory {
|
|
138
138
|
let next = startAt;
|
|
139
|
-
return () => `
|
|
139
|
+
return () => `run-${next++}` as RunId;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
export function createSequentialIdFactories(startAt = 1): OrchestrateIdFactories {
|
|
143
143
|
return {
|
|
144
144
|
workerId: createSequentialWorkerIdFactory(startAt),
|
|
145
|
-
|
|
145
|
+
runId: createSequentialRunIdFactory(startAt),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -205,7 +205,6 @@ export type WorkerOutcome =
|
|
|
205
205
|
| WorkerFailedOutcome
|
|
206
206
|
| WorkerAbortedOutcome
|
|
207
207
|
| WorkerClosedOutcome;
|
|
208
|
-
export type TerminalWorkerOutcome = Exclude<WorkerOutcome, WorkerReadyOutcome>;
|
|
209
208
|
export type WorkerStatus =
|
|
210
209
|
| "starting"
|
|
211
210
|
| "running"
|
|
@@ -219,13 +218,12 @@ export type TerminalWorkerStatus = Extract<
|
|
|
219
218
|
WorkerStatus,
|
|
220
219
|
"completed" | "failed" | "aborted" | "closed"
|
|
221
220
|
>;
|
|
222
|
-
export type WaveCompleteWorkerStatus = TerminalWorkerStatus | "ready";
|
|
223
221
|
|
|
224
222
|
export interface WorkerRecord {
|
|
225
223
|
readonly id: WorkerId;
|
|
226
224
|
readonly worker: string;
|
|
227
225
|
readonly ownerSessionId: string;
|
|
228
|
-
readonly
|
|
226
|
+
readonly runId: RunId;
|
|
229
227
|
readonly title: string;
|
|
230
228
|
readonly instructions: string;
|
|
231
229
|
readonly lifecycle: WorkerLifecycle;
|
|
@@ -239,16 +237,18 @@ export interface WorkerRecord {
|
|
|
239
237
|
readonly sessionFile?: string;
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
export type
|
|
243
|
-
export type
|
|
240
|
+
export type RunMode = "async" | "inline";
|
|
241
|
+
export type RunState = "running" | "complete";
|
|
244
242
|
|
|
245
|
-
export interface
|
|
246
|
-
readonly id:
|
|
243
|
+
export interface RunRecord {
|
|
244
|
+
readonly id: RunId;
|
|
247
245
|
readonly ownerSessionId: string;
|
|
248
|
-
readonly
|
|
249
|
-
readonly mode:
|
|
250
|
-
readonly state:
|
|
246
|
+
readonly workerId: WorkerId;
|
|
247
|
+
readonly mode: RunMode;
|
|
248
|
+
readonly state: RunState;
|
|
251
249
|
readonly createdAt: number;
|
|
250
|
+
readonly synthesisGroupId?: string;
|
|
251
|
+
readonly synthesisGroupSize?: number;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export class InvalidTransitionError extends Error {
|
|
@@ -267,10 +267,6 @@ export function isTerminalWorkerStatus(status: WorkerStatus): status is Terminal
|
|
|
267
267
|
return status === "completed" || status === "failed" || status === "aborted" || status === "closed";
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
export function isTerminalWorkerOutcome(outcome: WorkerOutcome): outcome is TerminalWorkerOutcome {
|
|
271
|
-
return outcome.status !== "ready";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
270
|
export function canTransitionWorkerStatus(
|
|
275
271
|
from: WorkerStatus,
|
|
276
272
|
to: WorkerStatus,
|
|
@@ -306,36 +302,6 @@ export function transitionWorkerStatus(
|
|
|
306
302
|
return { ...worker, status, outcome: undefined };
|
|
307
303
|
}
|
|
308
304
|
|
|
309
|
-
export function isWorkerCompleteForWave(
|
|
310
|
-
status: WorkerStatus,
|
|
311
|
-
): status is WaveCompleteWorkerStatus {
|
|
312
|
-
return status === "ready" || isTerminalWorkerStatus(status);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export function getWaveWorkersInOrder(
|
|
316
|
-
wave: WaveRecord,
|
|
317
|
-
workersById: ReadonlyMap<WorkerId, WorkerRecord>,
|
|
318
|
-
): readonly WorkerRecord[] | undefined {
|
|
319
|
-
const workers: WorkerRecord[] = [];
|
|
320
|
-
|
|
321
|
-
for (const workerId of wave.workerIds) {
|
|
322
|
-
const worker = workersById.get(workerId);
|
|
323
|
-
if (!worker) return undefined;
|
|
324
|
-
workers.push(worker);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return workers;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export function isWaveComplete(
|
|
331
|
-
wave: WaveRecord,
|
|
332
|
-
workersById: ReadonlyMap<WorkerId, WorkerRecord>,
|
|
333
|
-
): boolean {
|
|
334
|
-
const workers = getWaveWorkersInOrder(wave, workersById);
|
|
335
|
-
return workers !== undefined && workers.every((worker) => isWorkerCompleteForWave(worker.status));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export const MAX_TASKS_PER_WAVE = 12;
|
|
339
305
|
export const MAX_WORKER_TITLE_LENGTH = 200;
|
|
340
306
|
export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
|
|
341
307
|
export const CANCELLATION_GRACE_MS = 5_000;
|
package/extension/host.ts
CHANGED
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
} from "./runtime.js";
|
|
6
6
|
import { createWorkerSessionFactory } from "./worker-session.js";
|
|
7
7
|
|
|
8
|
-
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/
|
|
9
|
-
const LEGACY_PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v1");
|
|
8
|
+
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
10
9
|
|
|
11
10
|
export interface ProcessHost {
|
|
12
11
|
readonly runtime: OrchestratorRuntime;
|
|
@@ -26,14 +25,8 @@ interface OwnedProcessHost extends AttachmentAwareProcessHost {
|
|
|
26
25
|
destroyPromise?: Promise<void>;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
interface LegacyProcessHost extends ProcessHost {
|
|
30
|
-
unsubscribeCompletion?: () => void;
|
|
31
|
-
unsubscribeSettlement?: () => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
28
|
type ProcessGlobal = typeof globalThis & {
|
|
35
29
|
[PROCESS_HOST_KEY]?: OwnedProcessHost;
|
|
36
|
-
[LEGACY_PROCESS_HOST_KEY]?: LegacyProcessHost;
|
|
37
30
|
};
|
|
38
31
|
|
|
39
32
|
function processGlobal(): ProcessGlobal {
|
|
@@ -49,8 +42,6 @@ export function createProcessHost(): ProcessHost {
|
|
|
49
42
|
const existing = global[PROCESS_HOST_KEY];
|
|
50
43
|
if (existing) return existing;
|
|
51
44
|
|
|
52
|
-
retireLegacyProcessHost(global);
|
|
53
|
-
|
|
54
45
|
const runtime = createOrchestratorRuntime({
|
|
55
46
|
workerSessionFactory: createWorkerSessionFactory(),
|
|
56
47
|
});
|
|
@@ -114,30 +105,3 @@ export async function quitProcessHost(): Promise<void> {
|
|
|
114
105
|
if (!host) return;
|
|
115
106
|
await destroyProcessHost(host);
|
|
116
107
|
}
|
|
117
|
-
|
|
118
|
-
function retireLegacyProcessHost(global: ProcessGlobal): void {
|
|
119
|
-
const legacy = global[LEGACY_PROCESS_HOST_KEY];
|
|
120
|
-
if (!legacy) return;
|
|
121
|
-
|
|
122
|
-
delete global[LEGACY_PROCESS_HOST_KEY];
|
|
123
|
-
try {
|
|
124
|
-
legacy.unsubscribeCompletion?.();
|
|
125
|
-
} catch {
|
|
126
|
-
// A stale subscription cannot prevent installation of the current host.
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
legacy.unsubscribeSettlement?.();
|
|
130
|
-
} catch {
|
|
131
|
-
// A stale subscription cannot prevent installation of the current host.
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
legacy.delivery.close();
|
|
135
|
-
} catch {
|
|
136
|
-
// Legacy delivery cleanup is best-effort during reload migration.
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
void legacy.runtime.shutdown().catch(() => undefined);
|
|
140
|
-
} catch {
|
|
141
|
-
// Legacy runtime shutdown is best-effort during reload migration.
|
|
142
|
-
}
|
|
143
|
-
}
|
package/extension/index.ts
CHANGED
|
@@ -22,14 +22,19 @@ import {
|
|
|
22
22
|
registerOrchestrationPresentation,
|
|
23
23
|
type StatusController,
|
|
24
24
|
} from "./presentation.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
registerOrchestrationTools,
|
|
27
|
+
type DispatchDecision,
|
|
28
|
+
} from "./tools.js";
|
|
26
29
|
|
|
27
30
|
const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
28
31
|
"orchestrate",
|
|
29
32
|
"worker_send",
|
|
30
33
|
]);
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
interface StoredDispatchDecision extends DispatchDecision {
|
|
36
|
+
readonly ownerSessionId: string;
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
interface OwnerBinding {
|
|
35
40
|
readonly ownerSessionId: string;
|
|
@@ -52,7 +57,7 @@ export function createOrchestrationExtension(
|
|
|
52
57
|
const statusController =
|
|
53
58
|
dependencies.createStatusController?.(host.runtime) ??
|
|
54
59
|
createStatusController(host.runtime);
|
|
55
|
-
const
|
|
60
|
+
const dispatchDecisions = new Map<string, StoredDispatchDecision>();
|
|
56
61
|
let hostAttachment: ProcessHostAttachment | undefined;
|
|
57
62
|
let activeBinding: OwnerBinding | undefined;
|
|
58
63
|
let cachedCatalog: WorkerCatalog | undefined;
|
|
@@ -69,7 +74,8 @@ export function createOrchestrationExtension(
|
|
|
69
74
|
registerOrchestrationTools(pi, {
|
|
70
75
|
runtime: host.runtime,
|
|
71
76
|
getCatalog: catalogFor,
|
|
72
|
-
|
|
77
|
+
getDispatchDecision: (toolCallId) =>
|
|
78
|
+
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
73
79
|
});
|
|
74
80
|
registerOrchestrationPresentation(pi);
|
|
75
81
|
|
|
@@ -83,7 +89,7 @@ export function createOrchestrationExtension(
|
|
|
83
89
|
statusController.unbind(activeBinding.ownerSessionId);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
dispatchDecisions.clear();
|
|
87
93
|
cachedCatalog = undefined;
|
|
88
94
|
const binding: OwnerBinding = {
|
|
89
95
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -117,17 +123,39 @@ export function createOrchestrationExtension(
|
|
|
117
123
|
const toolCalls = event.message.content.filter(
|
|
118
124
|
(part) => part.type === "toolCall",
|
|
119
125
|
);
|
|
120
|
-
const
|
|
126
|
+
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
127
|
+
if (!ownerSessionId) return;
|
|
128
|
+
const groupedOrchestration =
|
|
129
|
+
toolCalls.length > 1 &&
|
|
130
|
+
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
131
|
+
const synthesisGroup = groupedOrchestration
|
|
132
|
+
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
133
|
+
: undefined;
|
|
121
134
|
|
|
122
135
|
for (const toolCall of toolCalls) {
|
|
123
|
-
if (DISPATCH_TOOL_NAMES.has(toolCall.name))
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
|
|
137
|
+
const mode = groupedOrchestration || toolCalls.length === 1
|
|
138
|
+
? "async"
|
|
139
|
+
: "inline";
|
|
140
|
+
dispatchDecisions.set(toolCall.id, {
|
|
141
|
+
mode,
|
|
142
|
+
ownerSessionId,
|
|
143
|
+
...(toolCall.name === "orchestrate" && synthesisGroup
|
|
144
|
+
? { synthesisGroup }
|
|
145
|
+
: {}),
|
|
146
|
+
});
|
|
126
147
|
}
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
pi.on("tool_execution_end", (event) => {
|
|
130
|
-
|
|
151
|
+
const decision = dispatchDecisions.get(event.toolCallId);
|
|
152
|
+
dispatchDecisions.delete(event.toolCallId);
|
|
153
|
+
if (!event.isError || !decision?.synthesisGroup) return;
|
|
154
|
+
host.delivery.skipSynthesisGroupMember(
|
|
155
|
+
decision.ownerSessionId,
|
|
156
|
+
decision.synthesisGroup.id,
|
|
157
|
+
decision.synthesisGroup.size,
|
|
158
|
+
);
|
|
131
159
|
});
|
|
132
160
|
|
|
133
161
|
pi.on("agent_start", () => {
|
|
@@ -152,7 +180,7 @@ export function createOrchestrationExtension(
|
|
|
152
180
|
const binding = activeBinding;
|
|
153
181
|
activeBinding = undefined;
|
|
154
182
|
cachedCatalog = undefined;
|
|
155
|
-
|
|
183
|
+
dispatchDecisions.clear();
|
|
156
184
|
|
|
157
185
|
if (binding) {
|
|
158
186
|
host.delivery.unbind(binding.ownerSessionId, binding.generation);
|