@zachwill/pi-orchestrate 0.2.1 → 0.3.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 +9 -9
- 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 +190 -46
- package/extension/worker-settlement.ts +106 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,23 +14,23 @@ Pi packages execute with your system permissions. Review the package and worker
|
|
|
14
14
|
|
|
15
15
|
Pi Orchestrate adds exactly five tools:
|
|
16
16
|
|
|
17
|
-
- `orchestrate` dispatches
|
|
18
|
-
- `orchestration_status` inspects the trusted catalog, catalog diagnostics,
|
|
17
|
+
- `orchestrate` dispatches one task with `orchestrate({ worker, title, instructions })`. Send independent tasks as sibling `orchestrate` calls in the same assistant message.
|
|
18
|
+
- `orchestration_status` inspects the trusted catalog, catalog diagnostics, runs, and worker states without exposing full task instructions.
|
|
19
19
|
- `worker_send` sends follow-up instructions to an owned reusable worker in the `ready` state.
|
|
20
20
|
- `worker_abort` stops owned active work that is no longer needed.
|
|
21
21
|
- `worker_close` closes an owned reusable worker in the `ready` state.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Each `orchestrate` call first performs atomic input, catalog, and model preflight before its worker starts. Sibling calls are admitted independently: one rejected call does not prevent valid siblings from starting. After acceptance, a resource startup failure becomes that worker's `failed` result and does not roll back or stop sibling calls.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Pi executes sibling tool calls concurrently, so independent `orchestrate` calls start concurrently without an extension-level group limit or hidden throttle. Exact unchanged worker instructions remain visible in each tool call: collapsed calls preview the message, and expanded calls show the full brief. Titles are labels, never replacements for instructions.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
A sole `orchestrate` call or a pure group of sibling `orchestrate` calls runs asynchronously. Pi accepts a pure group concurrently and yields the parent turn. Mixing `orchestrate` with another tool makes it inline and blocking. `worker_send` is asynchronous only as the sole tool call in its assistant message. Inline work receives the parent turn's cancellation signal. Accepted async work does not retain that signal and continues independently.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Async worker responses enter the transcript individually as workers finish. An ungrouped response starts the parent's synthesis turn. Responses from sibling calls share one final synthesis turn. The bottom widget is ephemeral and shows active work only; completed, failed, aborted, and reusable ready workers disappear immediately. An inline call shows its single current response in the live tool output while it blocks.
|
|
30
30
|
|
|
31
31
|
`orchestration_status` is for diagnostics and recovery, never a normal completion mechanism. Do not poll it for completion. If the owning session becomes inactive, its workers continue and completed results remain queued. Those results become available only when that exact owning session resumes; they are never delivered to another session.
|
|
32
32
|
|
|
33
|
-
Worker IDs identify live worker sessions. A reusable worker keeps the same worker ID across `worker_send` follow-ups, with each follow-up result belonging to a new
|
|
33
|
+
Each run owns exactly one worker generation. Worker IDs identify live worker sessions. A reusable worker keeps the same worker ID across `worker_send` follow-ups, with each follow-up result belonging to a new run. One-shot workers finish as `completed`. Reusable workers deliver as `ready`, remain available for follow-up, and wait for `worker_send` or `worker_close`. Use `worker_abort` only for active work by `worker_ids` or `all`, not to close a ready worker.
|
|
34
34
|
|
|
35
35
|
## Parent orchestration contract
|
|
36
36
|
|
|
@@ -38,8 +38,8 @@ Pi Orchestrate automatically injects the authoritative orchestration contract an
|
|
|
38
38
|
|
|
39
39
|
1. Keep trivial or tightly coupled work in the parent session.
|
|
40
40
|
2. Give every delegated task a full brief: objective, paths and scope, forbidden actions, context, constraints, observable success, checks, and expected output.
|
|
41
|
-
3. Dispatch every known independent task
|
|
42
|
-
4.
|
|
41
|
+
3. Dispatch every known independent task with a sibling `orchestrate` call in the same assistant message.
|
|
42
|
+
4. Keep an async `orchestrate` call or pure sibling group separate from other tools, then yield after acceptance. Make `worker_send` the sole tool call when it should run asynchronously.
|
|
43
43
|
5. Review delivered evidence and changes, resolve conflicts, integrate deliberately, and run the relevant verification.
|
|
44
44
|
6. Deliver the final answer from the parent session.
|
|
45
45
|
|
package/extension/catalog.ts
CHANGED
|
@@ -13,7 +13,11 @@ import type {
|
|
|
13
13
|
WorkerDefinition,
|
|
14
14
|
WorkerSourceKind,
|
|
15
15
|
} from "./domain.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createWorkerCatalog,
|
|
18
|
+
isSupportedToolName,
|
|
19
|
+
SUPPORTED_TOOL_NAMES,
|
|
20
|
+
} from "./domain.js";
|
|
17
21
|
|
|
18
22
|
const MAX_WORKER_BYTES = 64 * 1024;
|
|
19
23
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
@@ -127,27 +131,34 @@ function isMissingPath(error: unknown): boolean {
|
|
|
127
131
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
interface
|
|
134
|
+
interface IssuePath {
|
|
131
135
|
readonly path: readonly PropertyKey[];
|
|
136
|
+
readonly issue: SchemaIssue.Issue;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
function
|
|
139
|
+
function collectIssuePaths(
|
|
135
140
|
issue: SchemaIssue.Issue,
|
|
136
141
|
parentPath: readonly PropertyKey[] = [],
|
|
137
|
-
):
|
|
142
|
+
): IssuePath[] {
|
|
138
143
|
switch (issue._tag) {
|
|
139
144
|
case "Pointer":
|
|
140
|
-
return
|
|
145
|
+
return collectIssuePaths(issue.issue, [...parentPath, ...issue.path]);
|
|
141
146
|
case "Composite":
|
|
147
|
+
return issue.issues.flatMap((child) => collectIssuePaths(child, parentPath));
|
|
142
148
|
case "AnyOf":
|
|
143
|
-
return issue.issues.
|
|
149
|
+
return issue.issues.length === 0
|
|
150
|
+
? [{ path: parentPath, issue }]
|
|
151
|
+
: issue.issues.flatMap((child) => collectIssuePaths(child, parentPath));
|
|
144
152
|
case "Encoding":
|
|
145
153
|
case "Filter":
|
|
146
|
-
return
|
|
154
|
+
return collectIssuePaths(issue.issue, parentPath);
|
|
155
|
+
case "InvalidType":
|
|
156
|
+
case "InvalidValue":
|
|
157
|
+
case "MissingKey":
|
|
147
158
|
case "UnexpectedKey":
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return [];
|
|
159
|
+
case "Forbidden":
|
|
160
|
+
case "OneOf":
|
|
161
|
+
return [{ path: parentPath, issue }];
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -165,8 +176,9 @@ function listItems(value: unknown): readonly unknown[] {
|
|
|
165
176
|
return Array.isArray(value) ? value : [];
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
function schemaDiagnostic(
|
|
169
|
-
const
|
|
179
|
+
function schemaDiagnostic(issue: SchemaIssue.Issue, frontmatter: unknown): string {
|
|
180
|
+
const issuePaths = collectIssuePaths(issue);
|
|
181
|
+
const unexpected = issuePaths.filter(({ issue }) => issue._tag === "UnexpectedKey");
|
|
170
182
|
const frontmatterFields = unexpected
|
|
171
183
|
.filter(({ path }) => path.length === 1 && typeof path[0] === "string")
|
|
172
184
|
.map(({ path }) => String(path[0]))
|
|
@@ -196,8 +208,9 @@ function schemaDiagnostic(error: Schema.SchemaError, frontmatter: unknown): stri
|
|
|
196
208
|
"compaction",
|
|
197
209
|
"lifecycle",
|
|
198
210
|
];
|
|
199
|
-
const
|
|
200
|
-
|
|
211
|
+
const field = orderedFields.find((candidate) =>
|
|
212
|
+
issuePaths.some(({ path }) => path[0] === candidate)
|
|
213
|
+
);
|
|
201
214
|
const value = field === undefined ? undefined : fieldValue(frontmatter, field);
|
|
202
215
|
|
|
203
216
|
if (field === "name" || field === "description") {
|
|
@@ -234,13 +247,21 @@ function schemaDiagnostic(error: Schema.SchemaError, frontmatter: unknown): stri
|
|
|
234
247
|
if (compactionFields.length > 0) {
|
|
235
248
|
return `unknown compaction field${compactionFields.length === 1 ? "" : "s"}: ${compactionFields.join(", ")}`;
|
|
236
249
|
}
|
|
237
|
-
if (
|
|
250
|
+
if (issuePaths.some(({ path }) => path[0] === "compaction" && path[1] === "enabled")) {
|
|
238
251
|
return "frontmatter field 'compaction.enabled' must be a boolean";
|
|
239
252
|
}
|
|
240
|
-
if (
|
|
253
|
+
if (
|
|
254
|
+
issuePaths.some(({ path }) =>
|
|
255
|
+
path[0] === "compaction" && path[1] === "reserveTokens"
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
241
258
|
return "frontmatter field 'compaction.reserveTokens' must be a non-negative integer";
|
|
242
259
|
}
|
|
243
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
issuePaths.some(({ path }) =>
|
|
262
|
+
path[0] === "compaction" && path[1] === "keepRecentTokens"
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
244
265
|
return "frontmatter field 'compaction.keepRecentTokens' must be a non-negative integer";
|
|
245
266
|
}
|
|
246
267
|
return "frontmatter field 'compaction' must be a mapping";
|
|
@@ -266,7 +287,7 @@ function parseWorker(
|
|
|
266
287
|
const { frontmatter, body } = parsed;
|
|
267
288
|
const decoded = decodeWorkerFrontmatter(frontmatter);
|
|
268
289
|
if (Result.isFailure(decoded)) {
|
|
269
|
-
throw new Error(schemaDiagnostic(decoded.failure, frontmatter));
|
|
290
|
+
throw new Error(schemaDiagnostic(decoded.failure.issue, frontmatter));
|
|
270
291
|
}
|
|
271
292
|
|
|
272
293
|
const worker = decoded.success;
|
|
@@ -381,10 +402,7 @@ export function createWorkerCatalogDiscovery(fileSystem: CatalogFileSystem) {
|
|
|
381
402
|
diagnostics.push(...discovered.diagnostics);
|
|
382
403
|
}
|
|
383
404
|
|
|
384
|
-
|
|
385
|
-
compareText(left.name, right.name),
|
|
386
|
-
);
|
|
387
|
-
return { workers, diagnostics };
|
|
405
|
+
return createWorkerCatalog([...workersByName.values()], diagnostics);
|
|
388
406
|
};
|
|
389
407
|
}
|
|
390
408
|
|
package/extension/contract.ts
CHANGED
|
@@ -30,12 +30,12 @@ function buildContract(catalog: WorkerCatalog): string {
|
|
|
30
30
|
You are the parent orchestrator and own the task end to end.
|
|
31
31
|
|
|
32
32
|
- Keep trivial or tightly coupled work in the parent. Use as many useful workers as independent scopes justify.
|
|
33
|
-
- Delegate
|
|
33
|
+
- Delegate each independent scope with its own \`orchestrate\` call using \`{ worker, title, instructions }\`. Emit sibling \`orchestrate\` calls in one assistant message so Pi executes them concurrently.
|
|
34
34
|
- Give every worker a full brief: objective; paths/scope; forbidden actions; context; constraints; observable success; checks; expected output.
|
|
35
|
-
- Input, catalog, and model preflight is atomic before
|
|
36
|
-
-
|
|
35
|
+
- Input, catalog, and model preflight is atomic per call before that worker starts. Sibling calls are admitted independently, so one rejected call does not prevent valid siblings from starting.
|
|
36
|
+
- A sole \`orchestrate\` call or a pure group of sibling \`orchestrate\` calls runs asynchronously. Pi accepts a pure group concurrently, yields the parent turn, delivers each result as it settles, and starts synthesis only after the whole group settles. Mixing \`orchestrate\` with another tool makes it inline and blocking. \`worker_send\` is asynchronous only as the sole tool call in its assistant message.
|
|
37
37
|
- Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
|
|
38
|
-
- After an accepted async
|
|
38
|
+
- After an accepted async run, yield the parent turn. Worker responses arrive individually as each worker settles, and the final response starts parent synthesis. Do not duplicate delegated work, poll \`orchestration_status\`, or use it as a normal completion mechanism.
|
|
39
39
|
- The active-work widget shows only workers currently starting, running, or stopping. Inline worker responses appear progressively in live tool output.
|
|
40
40
|
- The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion.
|
|
41
41
|
- Prefer one-shot workers. Use \`worker_send\` for follow-up work on a ready reusable worker, \`worker_close\` when that ready worker is finished, and \`worker_abort\` only when active work must stop.
|
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
|
-
}
|