@zachwill/pi-orchestrate 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/extension/catalog.ts +176 -157
- package/extension/contract.ts +3 -1
- package/extension/delivery.ts +137 -89
- package/extension/domain.ts +7 -1
- package/extension/host.ts +41 -5
- package/extension/presentation.ts +331 -454
- package/extension/runtime.ts +338 -88
- package/extension/scheduler.ts +44 -25
- package/extension/tools.ts +337 -20
- package/extension/worker-session.ts +488 -290
- package/package.json +1 -1
package/extension/delivery.ts
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkerSettlement } from "./runtime.js";
|
|
2
2
|
|
|
3
3
|
export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
|
|
4
|
+
export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
|
|
4
5
|
export const DELIVERY_TRUNCATION_MARKER =
|
|
5
|
-
"\n\n[Worker
|
|
6
|
+
"\n\n[Worker result truncated for parent context. Full output remains in structured details.]";
|
|
6
7
|
export const DELIVERY_PARENT_INSTRUCTIONS =
|
|
7
8
|
"Parent: Synthesize all results, resolve conflicts, review changes and evidence, run integration checks, and continue the user's task. Do not merely forward worker reports.";
|
|
8
9
|
|
|
9
10
|
export type ParentBindingGeneration = string | number | symbol;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly ownerSessionId: CompletedWave["ownerSessionId"];
|
|
14
|
-
readonly mode: CompletedWave["mode"];
|
|
15
|
-
readonly results: CompletedWave["results"];
|
|
16
|
-
}
|
|
12
|
+
/** Complete, immutable worker output for presentation and history consumers. */
|
|
13
|
+
export type WorkerDeliveryDetails = WorkerSettlement;
|
|
17
14
|
|
|
18
|
-
export interface
|
|
19
|
-
readonly customType: "pi-orchestrate-
|
|
15
|
+
export interface WorkerDeliveryMessage {
|
|
16
|
+
readonly customType: "pi-orchestrate-worker-result";
|
|
20
17
|
readonly content: string;
|
|
21
18
|
readonly display: true;
|
|
22
|
-
readonly details:
|
|
19
|
+
readonly details: WorkerDeliveryDetails;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
export interface
|
|
22
|
+
export interface WorkerDeliveryOptions {
|
|
26
23
|
readonly triggerTurn: boolean;
|
|
27
24
|
}
|
|
28
25
|
|
|
@@ -30,7 +27,7 @@ export interface ParentBinding {
|
|
|
30
27
|
readonly ownerSessionId: string;
|
|
31
28
|
readonly generation: ParentBindingGeneration;
|
|
32
29
|
isIdle(): boolean;
|
|
33
|
-
sendMessage(message:
|
|
30
|
+
sendMessage(message: WorkerDeliveryMessage, options: WorkerDeliveryOptions): void;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
interface BoundParent {
|
|
@@ -40,7 +37,9 @@ interface BoundParent {
|
|
|
40
37
|
|
|
41
38
|
export class DeliveryCoordinator {
|
|
42
39
|
private readonly boundParents = new Map<string, BoundParent>();
|
|
43
|
-
private readonly
|
|
40
|
+
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
41
|
+
private readonly flushingOwners = new Set<string>();
|
|
42
|
+
private highestAcceptedSequence = 0;
|
|
44
43
|
|
|
45
44
|
bind(binding: ParentBinding): void {
|
|
46
45
|
this.boundParents.set(binding.ownerSessionId, {
|
|
@@ -57,33 +56,42 @@ export class DeliveryCoordinator {
|
|
|
57
56
|
|
|
58
57
|
markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
59
58
|
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
60
|
-
this.boundParents.get(ownerSessionId)
|
|
59
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
60
|
+
if (parent) parent.agentRunning = true;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
64
64
|
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
65
|
-
this.boundParents.get(ownerSessionId)
|
|
65
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
66
|
+
if (!parent) return;
|
|
67
|
+
parent.agentRunning = false;
|
|
66
68
|
this.flush(ownerSessionId, generation);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
accept(
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
this.pendingWaves.push(completedWave);
|
|
73
|
-
const generation = this.boundParents.get(completedWave.ownerSessionId)?.binding.generation;
|
|
74
|
-
if (generation !== undefined) {
|
|
75
|
-
this.flush(completedWave.ownerSessionId, generation);
|
|
71
|
+
accept(settlement: WorkerSettlement): boolean {
|
|
72
|
+
if (settlement.mode === "inline" || settlement.sequence <= this.highestAcceptedSequence) {
|
|
73
|
+
return false;
|
|
76
74
|
}
|
|
75
|
+
|
|
76
|
+
this.highestAcceptedSequence = settlement.sequence;
|
|
77
|
+
this.pendingSettlements.push(settlement);
|
|
78
|
+
|
|
79
|
+
const parent = this.boundParents.get(settlement.ownerSessionId);
|
|
80
|
+
if (parent) this.flush(settlement.ownerSessionId, parent.binding.generation);
|
|
77
81
|
return true;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
pendingCount(ownerSessionId: string): number {
|
|
81
|
-
return this.
|
|
85
|
+
return this.pendingSettlements.filter(
|
|
86
|
+
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
87
|
+
).length;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
clear(): void {
|
|
85
91
|
this.boundParents.clear();
|
|
86
|
-
this.
|
|
92
|
+
this.pendingSettlements.length = 0;
|
|
93
|
+
this.flushingOwners.clear();
|
|
94
|
+
this.highestAcceptedSequence = 0;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
close(): void {
|
|
@@ -94,14 +102,10 @@ export class DeliveryCoordinator {
|
|
|
94
102
|
ownerSessionId: string,
|
|
95
103
|
generation: ParentBindingGeneration,
|
|
96
104
|
): boolean {
|
|
97
|
-
|
|
98
|
-
return binding?.generation === generation;
|
|
105
|
+
return this.boundParents.get(ownerSessionId)?.binding.generation === generation;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
private canDeliver(
|
|
102
|
-
ownerSessionId: string,
|
|
103
|
-
generation: ParentBindingGeneration,
|
|
104
|
-
): boolean {
|
|
108
|
+
private canDeliver(ownerSessionId: string, generation: ParentBindingGeneration): boolean {
|
|
105
109
|
const parent = this.boundParents.get(ownerSessionId);
|
|
106
110
|
return (
|
|
107
111
|
parent !== undefined &&
|
|
@@ -111,73 +115,117 @@ export class DeliveryCoordinator {
|
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
private flush(
|
|
115
|
-
ownerSessionId
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
119
|
-
|
|
120
|
-
const waves = this.pendingWaves.filter((wave) => wave.ownerSessionId === ownerSessionId);
|
|
121
|
-
for (let index = 0; index < waves.length; index += 1) {
|
|
122
|
-
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
123
|
-
|
|
124
|
-
const wave = waves[index];
|
|
125
|
-
if (!wave) return;
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
// Pi 0.80.10 starts an idle turn only through triggerTurn. Omitting deliverAs keeps
|
|
129
|
-
// streaming races as steering; nextTurn would queue without starting a turn.
|
|
130
|
-
this.boundParents.get(ownerSessionId)!.binding.sendMessage(renderWaveMessage(wave), {
|
|
131
|
-
triggerTurn: index === waves.length - 1,
|
|
132
|
-
});
|
|
133
|
-
} catch {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
118
|
+
private flush(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
119
|
+
if (this.flushingOwners.has(ownerSessionId) || !this.canDeliver(ownerSessionId, generation)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
this.flushingOwners.add(ownerSessionId);
|
|
124
|
+
try {
|
|
125
|
+
const queued = this.pendingSettlements.filter(
|
|
126
|
+
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
127
|
+
);
|
|
128
|
+
let latestFinalIndex = -1;
|
|
129
|
+
for (let index = 0; index < queued.length; index += 1) {
|
|
130
|
+
if (queued[index]?.waveComplete) latestFinalIndex = index;
|
|
131
|
+
}
|
|
132
|
+
const flushThrough = latestFinalIndex >= 0 ? latestFinalIndex : queued.length - 1;
|
|
133
|
+
let flushBytesRemaining = MAX_DELIVERY_MARKDOWN_BYTES;
|
|
134
|
+
|
|
135
|
+
for (let index = 0; index <= flushThrough; index += 1) {
|
|
136
|
+
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
137
|
+
const settlement = queued[index];
|
|
138
|
+
if (!settlement || !this.pendingSettlements.includes(settlement)) continue;
|
|
139
|
+
|
|
140
|
+
const messagesRemaining = flushThrough - index + 1;
|
|
141
|
+
const fairFlushBytes = Math.floor(flushBytesRemaining / messagesRemaining);
|
|
142
|
+
const fairWaveBytes = Math.floor(
|
|
143
|
+
MAX_DELIVERY_MARKDOWN_BYTES / Math.max(1, settlement.waveSize),
|
|
144
|
+
);
|
|
145
|
+
const byteLimit = Math.max(0, Math.min(
|
|
146
|
+
MAX_WORKER_DELIVERY_MARKDOWN_BYTES,
|
|
147
|
+
fairFlushBytes,
|
|
148
|
+
fairWaveBytes,
|
|
149
|
+
flushBytesRemaining,
|
|
150
|
+
));
|
|
151
|
+
const triggerTurn = latestFinalIndex >= 0 && index === flushThrough;
|
|
152
|
+
const message = this.renderWorkerMessage(settlement, byteLimit);
|
|
153
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
154
|
+
if (!parent || parent.binding.generation !== generation) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
parent.binding.sendMessage(message, { triggerTurn });
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const deliveredBytes = Buffer.byteLength(message.content, "utf8");
|
|
163
|
+
flushBytesRemaining = Math.max(0, flushBytesRemaining - deliveredBytes);
|
|
164
|
+
const pendingIndex = this.pendingSettlements.indexOf(settlement);
|
|
165
|
+
if (pendingIndex >= 0) this.pendingSettlements.splice(pendingIndex, 1);
|
|
166
|
+
|
|
167
|
+
if (triggerTurn) {
|
|
168
|
+
parent.agentRunning = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
this.flushingOwners.delete(ownerSessionId);
|
|
139
174
|
}
|
|
140
175
|
}
|
|
141
|
-
}
|
|
142
176
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const heading =
|
|
148
|
-
const metadata =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
private renderWorkerMessage(
|
|
178
|
+
settlement: WorkerSettlement,
|
|
179
|
+
byteLimit: number,
|
|
180
|
+
): WorkerDeliveryMessage {
|
|
181
|
+
const heading = `## Worker result — ${settlement.worker}`;
|
|
182
|
+
const metadata = [
|
|
183
|
+
`Worker \`${settlement.workerId}\``,
|
|
184
|
+
`wave \`${settlement.waveId}\``,
|
|
185
|
+
`status \`${settlement.status}\``,
|
|
186
|
+
].join(" · ");
|
|
187
|
+
const body = renderOutcome(settlement.outcome);
|
|
188
|
+
const content = body.length > 0
|
|
189
|
+
? `${heading}\n\n### ${settlement.title}\n${metadata}\n\n${body}`
|
|
190
|
+
: `${heading}\n\n### ${settlement.title}\n${metadata}`;
|
|
191
|
+
const appendix = settlement.waveComplete
|
|
192
|
+
? `\n\n---\n\n${DELIVERY_PARENT_INSTRUCTIONS}`
|
|
193
|
+
: "";
|
|
194
|
+
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
customType: "pi-orchestrate-worker-result",
|
|
197
|
+
content: capMarkdown(content, appendix, byteLimit),
|
|
198
|
+
display: true,
|
|
199
|
+
details: settlement,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
166
202
|
}
|
|
167
203
|
|
|
168
|
-
function capMarkdown(content: string, appendix: string): string {
|
|
204
|
+
function capMarkdown(content: string, appendix: string, byteLimit: number): string {
|
|
169
205
|
const complete = `${content}${appendix}`;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
if (Buffer.byteLength(complete, "utf8") <= byteLimit) return complete;
|
|
207
|
+
|
|
208
|
+
const suffix = `${DELIVERY_TRUNCATION_MARKER}${appendix}`;
|
|
209
|
+
const suffixBytes = Buffer.byteLength(suffix, "utf8");
|
|
210
|
+
if (suffixBytes >= byteLimit) return truncateUtf8(suffix, byteLimit);
|
|
211
|
+
const prefix = truncateUtf8(content, byteLimit - suffixBytes);
|
|
212
|
+
return `${prefix}${suffix}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function truncateUtf8(content: string, byteLimit: number): string {
|
|
216
|
+
if (byteLimit <= 0) return "";
|
|
217
|
+
const bytes = Buffer.from(content, "utf8");
|
|
218
|
+
if (bytes.byteLength <= byteLimit) return content;
|
|
219
|
+
let end = byteLimit;
|
|
220
|
+
while (end > 0) {
|
|
221
|
+
const byte = bytes[end];
|
|
222
|
+
if (byte === undefined || (byte & 0xc0) !== 0x80) break;
|
|
223
|
+
end -= 1;
|
|
224
|
+
}
|
|
225
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
178
226
|
}
|
|
179
227
|
|
|
180
|
-
function renderOutcome(outcome:
|
|
228
|
+
function renderOutcome(outcome: WorkerSettlement["outcome"]): string {
|
|
181
229
|
switch (outcome.status) {
|
|
182
230
|
case "completed":
|
|
183
231
|
case "ready":
|
package/extension/domain.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface WorkerDefinition {
|
|
|
45
45
|
readonly systemPrompt: string;
|
|
46
46
|
readonly lifecycle: WorkerLifecycle;
|
|
47
47
|
readonly tools: readonly SupportedToolName[];
|
|
48
|
-
readonly skills
|
|
48
|
+
readonly skills?: readonly string[];
|
|
49
49
|
readonly model?: WorkerModel;
|
|
50
50
|
readonly thinking?: ThinkingLevel;
|
|
51
51
|
readonly compaction?: WorkerCompaction;
|
|
@@ -160,6 +160,9 @@ export interface WorkerUsage {
|
|
|
160
160
|
readonly turns: number;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Direction of the most recent message across the worker/model boundary. */
|
|
164
|
+
export type WorkerMessageDirection = "to-model" | "from-model";
|
|
165
|
+
|
|
163
166
|
export const EMPTY_WORKER_USAGE: WorkerUsage = Object.freeze({
|
|
164
167
|
input: 0,
|
|
165
168
|
output: 0,
|
|
@@ -228,7 +231,10 @@ export interface WorkerRecord {
|
|
|
228
231
|
readonly lifecycle: WorkerLifecycle;
|
|
229
232
|
readonly status: WorkerStatus;
|
|
230
233
|
readonly usage: WorkerUsage;
|
|
234
|
+
readonly startedAt: number;
|
|
235
|
+
readonly settledAt?: number;
|
|
231
236
|
readonly activity?: string;
|
|
237
|
+
readonly messageDirection?: WorkerMessageDirection;
|
|
232
238
|
readonly outcome?: WorkerOutcome;
|
|
233
239
|
readonly sessionFile?: string;
|
|
234
240
|
}
|
package/extension/host.ts
CHANGED
|
@@ -5,7 +5,8 @@ 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/
|
|
8
|
+
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v2");
|
|
9
|
+
const LEGACY_PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v1");
|
|
9
10
|
|
|
10
11
|
export interface ProcessHost {
|
|
11
12
|
readonly runtime: OrchestratorRuntime;
|
|
@@ -21,12 +22,18 @@ interface AttachmentAwareProcessHost extends ProcessHost {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface OwnedProcessHost extends AttachmentAwareProcessHost {
|
|
24
|
-
readonly
|
|
25
|
+
readonly unsubscribeSettlement?: () => void;
|
|
25
26
|
destroyPromise?: Promise<void>;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface LegacyProcessHost extends ProcessHost {
|
|
30
|
+
unsubscribeCompletion?: () => void;
|
|
31
|
+
unsubscribeSettlement?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
type ProcessGlobal = typeof globalThis & {
|
|
29
35
|
[PROCESS_HOST_KEY]?: OwnedProcessHost;
|
|
36
|
+
[LEGACY_PROCESS_HOST_KEY]?: LegacyProcessHost;
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
function processGlobal(): ProcessGlobal {
|
|
@@ -42,6 +49,8 @@ export function createProcessHost(): ProcessHost {
|
|
|
42
49
|
const existing = global[PROCESS_HOST_KEY];
|
|
43
50
|
if (existing) return existing;
|
|
44
51
|
|
|
52
|
+
retireLegacyProcessHost(global);
|
|
53
|
+
|
|
45
54
|
const runtime = createOrchestratorRuntime({
|
|
46
55
|
workerSessionFactory: createWorkerSessionFactory(),
|
|
47
56
|
});
|
|
@@ -50,8 +59,8 @@ export function createProcessHost(): ProcessHost {
|
|
|
50
59
|
runtime,
|
|
51
60
|
delivery,
|
|
52
61
|
attachments: new Set(),
|
|
53
|
-
|
|
54
|
-
delivery.accept(
|
|
62
|
+
unsubscribeSettlement: runtime.subscribeSettlement((settlement) => {
|
|
63
|
+
delivery.accept(settlement);
|
|
55
64
|
}),
|
|
56
65
|
};
|
|
57
66
|
global[PROCESS_HOST_KEY] = host;
|
|
@@ -90,7 +99,7 @@ export async function destroyProcessHost(host: ProcessHost): Promise<void> {
|
|
|
90
99
|
await ownedHost.runtime.shutdown();
|
|
91
100
|
} finally {
|
|
92
101
|
ownedHost.delivery.clear();
|
|
93
|
-
ownedHost.
|
|
102
|
+
ownedHost.unsubscribeSettlement?.();
|
|
94
103
|
const global = processGlobal();
|
|
95
104
|
if (global[PROCESS_HOST_KEY] === ownedHost) {
|
|
96
105
|
delete global[PROCESS_HOST_KEY];
|
|
@@ -105,3 +114,30 @@ export async function quitProcessHost(): Promise<void> {
|
|
|
105
114
|
if (!host) return;
|
|
106
115
|
await destroyProcessHost(host);
|
|
107
116
|
}
|
|
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
|
+
}
|