@zachwill/pi-orchestrate 0.15.0 → 0.16.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 -1
- package/examples/workers/investigator.md +4 -6
- package/examples/workers/scout.md +5 -7
- package/examples/workers/web.md +3 -14
- package/examples/workers/worker.md +8 -9
- package/extension/index.ts +50 -0
- package/extension/parent/contract.ts +1 -0
- package/extension/parent/delivery.ts +72 -8
- package/extension/parent/worker-context.ts +196 -0
- package/extension/worker/session.ts +10 -3
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pi Orchestrate
|
|
2
2
|
|
|
3
|
-
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi session delegate work to direct child sessions.
|
|
3
|
+
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi session delegate work to direct child sessions. It requires Pi 0.85.0 or newer.
|
|
4
4
|
|
|
5
5
|
- Each worker gets a focused brief and a separate conversation.
|
|
6
6
|
- Workers run independently and return their results to the parent.
|
|
@@ -23,6 +23,14 @@ A **worker ID** identifies a worker session. A **run ID** identifies one generat
|
|
|
23
23
|
|
|
24
24
|
Interactive workers remain available across session switches and extension reloads within the same Pi process. Closing one releases its retained session; process shutdown releases any that remain.
|
|
25
25
|
|
|
26
|
+
## Compaction
|
|
27
|
+
|
|
28
|
+
Parent compaction does not stop workers. Results that settle during compaction remain queued for the owning session and resume automatic delivery when that session becomes idle, including after failed or cancelled compaction. Grouped work still triggers synthesis only after the group settles.
|
|
29
|
+
|
|
30
|
+
Before each parent model request, the extension adds a fresh, owner-scoped snapshot of active workers and ready interactive sessions. This transient context survives compaction by being rebuilt from live state; it is not appended to the transcript. The snapshot is capped at 12 KiB and reports omitted workers or truncated assignments, with `worker_status` available for recovery rather than polling.
|
|
31
|
+
|
|
32
|
+
This state belongs to the running Pi process. Compaction does not require restarting workers, and persisted transcripts do not restore running workers after a process restart.
|
|
33
|
+
|
|
26
34
|
## Agent interface
|
|
27
35
|
|
|
28
36
|
Pi Orchestrate gives the parent five model-facing tools:
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: investigator
|
|
3
|
-
description: Investigates cross-file questions
|
|
3
|
+
description: Investigates cross-file questions through read-only inspection and evidence-based synthesis.
|
|
4
4
|
thinking: medium
|
|
5
5
|
tools: read, grep, find, ls, bash
|
|
6
6
|
lifecycle: one-shot
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Investigate the assigned
|
|
9
|
+
Investigate the assigned question, tracing relevant relationships and comparing evidence across files. Stay within scope and stop when the evidence supports an answer; do not keep exploring for completeness.
|
|
10
10
|
|
|
11
|
-
Do not modify files or run builds, tests, or
|
|
11
|
+
Do not modify files or run builds, tests, or other state-changing commands. Use bash only for read-only inspection.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Return concise **Findings** and **Synthesis** sections. Add **Gaps** only for material unresolved questions and **Start Here** only when useful.
|
|
13
|
+
Lead with the answer. Support material findings and requested recommendations with exact paths, line ranges, or symbols. Explain how the evidence supports the conclusion, distinguish facts from inference, and identify unresolved gaps that could change the answer.
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scout
|
|
3
|
-
description: Answers
|
|
4
|
-
thinking:
|
|
3
|
+
description: Answers a small factual repository question through shallow, read-only inspection.
|
|
4
|
+
thinking: low
|
|
5
5
|
tools: read, grep, find, ls, bash
|
|
6
6
|
lifecycle: one-shot
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Answer
|
|
9
|
+
Answer the assigned factual question using direct repository evidence. Keep the inspection shallow and stop as soon as the question is answerable. If it requires deeper investigation, return what you found and explain what remains rather than expanding the task.
|
|
10
10
|
|
|
11
|
-
Do not modify files or run builds, tests, or
|
|
11
|
+
Do not modify files or run builds, tests, or other state-changing commands. Use bash only for read-only inspection.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Return a short **Answer** and **Evidence** grounded in paths, line ranges, symbols, or command output. Add **Gaps** only when material.
|
|
13
|
+
Lead with a short answer and cite the paths, line ranges, symbols, or command output needed to support it. Distinguish confirmed facts from inference and identify missing evidence that affects the answer.
|
package/examples/workers/web.md
CHANGED
|
@@ -8,19 +8,13 @@ skills: []
|
|
|
8
8
|
lifecycle: one-shot
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Answer the assigned question using public web sources.
|
|
12
12
|
|
|
13
13
|
Use the installed, authenticated `codex` CLI as your web-search backend. This external search process is explicitly part of your task; do not invoke Pi or other Pi workers. Do not modify project files or install anything. Use fresh temporary directories and clean them up.
|
|
14
14
|
|
|
15
15
|
## Strategy
|
|
16
16
|
|
|
17
|
-
Use
|
|
18
|
-
|
|
19
|
-
- For a narrow lookup, run one focused Codex search.
|
|
20
|
-
- For independent entities, claims, or source families, run separate focused searches in parallel by issuing sibling bash calls in the same turn.
|
|
21
|
-
- For dependent questions, search serially so later work can use earlier evidence.
|
|
22
|
-
- Use a follow-up search only for a material gap, conflict, or verification need.
|
|
23
|
-
- Stop when the objective is adequately answered. Do not multiply searches for cosmetic coverage.
|
|
17
|
+
Use one focused search for a narrow lookup. Search independent angles in parallel and dependent questions serially. Follow up only to resolve a material gap, conflict, or verification need. Stop when the question is adequately answered.
|
|
24
18
|
|
|
25
19
|
Tell each Codex process to use at most four actual web searches unless the assignment justifies a different bound. Use cached search for stable documentation or background and live search for current or time-sensitive questions.
|
|
26
20
|
|
|
@@ -74,11 +68,6 @@ Never use `--dangerously-bypass-approvals-and-sandbox`. Retry only when diagnost
|
|
|
74
68
|
|
|
75
69
|
## Response
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- the answer or strongest supported conclusion;
|
|
80
|
-
- material findings and conflicts;
|
|
81
|
-
- source titles with exact URLs and relevance;
|
|
82
|
-
- unresolved gaps or cautions when they matter.
|
|
71
|
+
Lead with the strongest supported answer. Cite exact source URLs alongside material factual claims. Identify conflicts, uncertainty, and freshness limits that affect the conclusion.
|
|
83
72
|
|
|
84
73
|
Do not dump search transcripts or raw temporary paths. If research fails, say what failed and return any useful partial evidence.
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: worker
|
|
3
|
-
description: Implements
|
|
3
|
+
description: Implements bounded code changes, fixes, and refactors.
|
|
4
4
|
thinking: medium
|
|
5
5
|
tools: read, bash, edit, write, grep, find, ls
|
|
6
6
|
lifecycle: one-shot
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Complete the assigned change within its scope. Follow project instructions, inspect nearby code, and preserve changes you do not own. Report relevant out-of-scope findings rather than fixing them.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- Change only what the assignment requires. Do not fix, refactor, or investigate unrelated work.
|
|
13
|
-
- Do not commit, push, or perform destructive actions unless assigned.
|
|
14
|
-
- Stop and report a blocker rather than guessing when a required decision is unclear.
|
|
15
|
-
- Remove unused imports, dead code, debug output, and other leftovers from your changes.
|
|
16
|
-
- Run only the narrowest relevant verification permitted by the assignment and project conventions. Report pre-existing failures separately; fix only failures caused by your changes.
|
|
11
|
+
Resolve uncertainty through inspection where possible. Make reasonable, reversible decisions within the assignment; report a blocker when progress requires missing authority or a material decision the assignment does not resolve.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
Run checks that establish whether the change works, following the assignment and project requirements. Fix failures introduced by your work and remove leftovers from your changes. Report pre-existing failures separately.
|
|
14
|
+
|
|
15
|
+
Lead with the result. Include changed paths, verification performed, and anything unresolved. Use the structure the handoff needs rather than a fixed template.
|
|
16
|
+
|
|
17
|
+
Do not commit, push, deploy, or take destructive action unless explicitly authorized.
|
package/extension/index.ts
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from "./catalog/discovery.ts";
|
|
10
10
|
import type { WorkerCatalog } from "./catalog/definition.ts";
|
|
11
11
|
import { applyOrchestratorContract } from "./parent/contract.ts";
|
|
12
|
+
import {
|
|
13
|
+
projectLiveWorkerContext,
|
|
14
|
+
replaceLiveWorkerContext,
|
|
15
|
+
} from "./parent/worker-context.ts";
|
|
12
16
|
import {
|
|
13
17
|
attachProcessHost,
|
|
14
18
|
createProcessHost,
|
|
@@ -110,6 +114,52 @@ export function createOrchestrationExtension(
|
|
|
110
114
|
};
|
|
111
115
|
});
|
|
112
116
|
|
|
117
|
+
pi.on("context", async (event, ctx) => {
|
|
118
|
+
const messages = replaceLiveWorkerContext(event.messages, undefined);
|
|
119
|
+
const binding = activeBinding;
|
|
120
|
+
const boundHost = host;
|
|
121
|
+
const attachment = hostAttachment;
|
|
122
|
+
|
|
123
|
+
if (!binding || !boundHost || !attachment) return { messages };
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (
|
|
127
|
+
attachment.host !== boundHost ||
|
|
128
|
+
ctx.sessionManager.getSessionId() !== binding.ownerSessionId
|
|
129
|
+
) {
|
|
130
|
+
return { messages };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const snapshot = await boundHost.orchestration.snapshot(
|
|
134
|
+
binding.ownerSessionId,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Session replacement and reload can race the awaited snapshot. Only the
|
|
138
|
+
// exact attachment generation that requested it may add parent context.
|
|
139
|
+
if (
|
|
140
|
+
activeBinding !== binding ||
|
|
141
|
+
host !== boundHost ||
|
|
142
|
+
hostAttachment !== attachment ||
|
|
143
|
+
attachment.host !== boundHost ||
|
|
144
|
+
ctx.sessionManager.getSessionId() !== binding.ownerSessionId
|
|
145
|
+
) {
|
|
146
|
+
return { messages };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const context = projectLiveWorkerContext(snapshot, {
|
|
150
|
+
ownerSessionId: binding.ownerSessionId,
|
|
151
|
+
pendingResultCount: boundHost.delivery.pendingCount(
|
|
152
|
+
binding.ownerSessionId,
|
|
153
|
+
),
|
|
154
|
+
});
|
|
155
|
+
return { messages: replaceLiveWorkerContext(messages, context) };
|
|
156
|
+
} catch {
|
|
157
|
+
// A context projection is advisory. On snapshot or stale-context failure,
|
|
158
|
+
// omit it rather than retaining an older authority-bearing projection.
|
|
159
|
+
return { messages };
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
113
163
|
pi.on("message_end", (event) => {
|
|
114
164
|
if (event.message.role !== "assistant") return;
|
|
115
165
|
const toolCalls = event.message.content.filter(
|
|
@@ -125,6 +125,7 @@ You are responsible for delivering the user’s requested outcome. Work directly
|
|
|
125
125
|
### Completion
|
|
126
126
|
|
|
127
127
|
- After dispatching, wait for automatic result delivery instead of polling \`worker_status\`. Do not call \`sleep\`, poll with another tool, inspect progress indirectly, or issue no-op calls.
|
|
128
|
+
- Compaction does not stop workers. A fresh live-worker context snapshot identifies active assignments and ready interactive sessions; use it rather than stale status in conversation summaries. Do not redispatch work because its dispatch was compacted away. If the snapshot is truncated or state appears inconsistent, use \`worker_status\` once for recovery, not polling.
|
|
128
129
|
- While waiting, perform only already-admitted independent work from the current change; otherwise end the turn.
|
|
129
130
|
- Classify findings before acting: fix or remove defects introduced by the current change, complete unfinished requirements inside its boundary, and record adjacent or pre-existing concerns without admitting them.
|
|
130
131
|
- Dispatch another wave only for admitted work inside the current change. Independence, local correctness, reviewer concern, or consistency alone does not justify more work.
|
|
@@ -10,6 +10,7 @@ export const DELIVERY_PARENT_INSTRUCTIONS =
|
|
|
10
10
|
"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.";
|
|
11
11
|
|
|
12
12
|
export type ParentBindingGeneration = string | number | symbol;
|
|
13
|
+
export type ScheduleIdleRecheck = (recheck: () => void) => () => void;
|
|
13
14
|
|
|
14
15
|
export interface WorkerDeliveryMessage {
|
|
15
16
|
readonly customType: "pi-orchestrate-worker-result";
|
|
@@ -34,6 +35,11 @@ interface BoundParent {
|
|
|
34
35
|
agentRunning: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
interface ScheduledIdleRecheck {
|
|
39
|
+
readonly generation: ParentBindingGeneration;
|
|
40
|
+
cancel(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
interface SynthesisGroupState {
|
|
38
44
|
expected: number;
|
|
39
45
|
readonly acceptedEventIds: string[];
|
|
@@ -63,21 +69,30 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
63
69
|
private readonly boundParents = new Map<string, BoundParent>();
|
|
64
70
|
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
65
71
|
private readonly flushingOwners = new Set<string>();
|
|
72
|
+
private readonly idleRechecks = new Map<string, ScheduledIdleRecheck>();
|
|
66
73
|
private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
|
|
67
74
|
// Orchestration settlement sequences are process-scoped and monotonic across
|
|
68
75
|
// owners, so one watermark is valid.
|
|
69
76
|
private highestAcceptedSequence = 0;
|
|
70
77
|
|
|
78
|
+
constructor(
|
|
79
|
+
private readonly scheduleIdleRecheck: ScheduleIdleRecheck = scheduleDeliveryIdleRecheck,
|
|
80
|
+
) {}
|
|
81
|
+
|
|
71
82
|
bind(binding: ParentBinding): void {
|
|
83
|
+
this.cancelIdleRecheck(binding.ownerSessionId);
|
|
72
84
|
this.boundParents.set(binding.ownerSessionId, {
|
|
73
85
|
binding,
|
|
74
|
-
|
|
86
|
+
// Non-idle also covers manual compaction. Agent lifecycle events, rather
|
|
87
|
+
// than the broader idle flag, own this state.
|
|
88
|
+
agentRunning: false,
|
|
75
89
|
});
|
|
76
90
|
this.flush(binding.ownerSessionId, binding.generation);
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
unbind(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
80
94
|
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
95
|
+
this.cancelIdleRecheck(ownerSessionId, generation);
|
|
81
96
|
this.boundParents.delete(ownerSessionId);
|
|
82
97
|
}
|
|
83
98
|
|
|
@@ -85,6 +100,7 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
85
100
|
const parent = this.boundParents.get(ownerSessionId);
|
|
86
101
|
if (parent?.binding.generation !== generation) return;
|
|
87
102
|
parent.agentRunning = true;
|
|
103
|
+
this.cancelIdleRecheck(ownerSessionId, generation);
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
@@ -130,6 +146,8 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
clear(): void {
|
|
149
|
+
for (const recheck of this.idleRechecks.values()) recheck.cancel();
|
|
150
|
+
this.idleRechecks.clear();
|
|
133
151
|
this.boundParents.clear();
|
|
134
152
|
this.pendingSettlements.length = 0;
|
|
135
153
|
this.flushingOwners.clear();
|
|
@@ -146,18 +164,28 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
146
164
|
|
|
147
165
|
private canDeliver(ownerSessionId: string, generation: ParentBindingGeneration): boolean {
|
|
148
166
|
const parent = this.boundParents.get(ownerSessionId);
|
|
149
|
-
|
|
150
|
-
parent
|
|
151
|
-
parent.binding.generation
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
if (
|
|
168
|
+
parent === undefined ||
|
|
169
|
+
parent.binding.generation !== generation ||
|
|
170
|
+
parent.agentRunning
|
|
171
|
+
) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (!parent.binding.isIdle()) {
|
|
175
|
+
this.ensureIdleRecheck(ownerSessionId, generation);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
this.cancelIdleRecheck(ownerSessionId, generation);
|
|
179
|
+
return true;
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
private flush(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
158
|
-
if (this.flushingOwners.has(ownerSessionId)
|
|
183
|
+
if (this.flushingOwners.has(ownerSessionId)) return;
|
|
184
|
+
if (this.pendingCount(ownerSessionId) === 0) {
|
|
185
|
+
this.cancelIdleRecheck(ownerSessionId, generation);
|
|
159
186
|
return;
|
|
160
187
|
}
|
|
188
|
+
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
161
189
|
|
|
162
190
|
this.flushingOwners.add(ownerSessionId);
|
|
163
191
|
try {
|
|
@@ -216,6 +244,36 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
216
244
|
}
|
|
217
245
|
}
|
|
218
246
|
|
|
247
|
+
private ensureIdleRecheck(
|
|
248
|
+
ownerSessionId: string,
|
|
249
|
+
generation: ParentBindingGeneration,
|
|
250
|
+
): void {
|
|
251
|
+
const current = this.idleRechecks.get(ownerSessionId);
|
|
252
|
+
if (current?.generation === generation) return;
|
|
253
|
+
current?.cancel();
|
|
254
|
+
|
|
255
|
+
const scheduled: ScheduledIdleRecheck = {
|
|
256
|
+
generation,
|
|
257
|
+
cancel: () => {},
|
|
258
|
+
};
|
|
259
|
+
this.idleRechecks.set(ownerSessionId, scheduled);
|
|
260
|
+
scheduled.cancel = this.scheduleIdleRecheck(() => {
|
|
261
|
+
if (this.idleRechecks.get(ownerSessionId) !== scheduled) return;
|
|
262
|
+
this.idleRechecks.delete(ownerSessionId);
|
|
263
|
+
this.flush(ownerSessionId, generation);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private cancelIdleRecheck(
|
|
268
|
+
ownerSessionId: string,
|
|
269
|
+
generation?: ParentBindingGeneration,
|
|
270
|
+
): void {
|
|
271
|
+
const scheduled = this.idleRechecks.get(ownerSessionId);
|
|
272
|
+
if (!scheduled || (generation !== undefined && scheduled.generation !== generation)) return;
|
|
273
|
+
this.idleRechecks.delete(ownerSessionId);
|
|
274
|
+
scheduled.cancel();
|
|
275
|
+
}
|
|
276
|
+
|
|
219
277
|
private renderWorkerMessage(
|
|
220
278
|
settlement: WorkerSettlement,
|
|
221
279
|
byteLimit: number,
|
|
@@ -302,6 +360,12 @@ export const deliveryLayer: Layer.Layer<Delivery, never, Orchestration> = Layer.
|
|
|
302
360
|
}),
|
|
303
361
|
);
|
|
304
362
|
|
|
363
|
+
function scheduleDeliveryIdleRecheck(recheck: () => void): () => void {
|
|
364
|
+
const timeout = setTimeout(recheck, 100);
|
|
365
|
+
timeout.unref();
|
|
366
|
+
return () => clearTimeout(timeout);
|
|
367
|
+
}
|
|
368
|
+
|
|
305
369
|
function synthesisGroupKey(ownerSessionId: string, synthesisGroupId: string): string {
|
|
306
370
|
return `${ownerSessionId}\u0000${synthesisGroupId}`;
|
|
307
371
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type {} from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { WorkerRecord } from "../orchestration/model.ts";
|
|
4
|
+
import type { OwnerSnapshot } from "../orchestration/service.ts";
|
|
5
|
+
|
|
6
|
+
export const LIVE_WORKER_CONTEXT_TYPE = "pi-orchestrate-live-worker-context";
|
|
7
|
+
export const MAX_LIVE_WORKER_CONTEXT_BYTES = 12 * 1024;
|
|
8
|
+
export const MAX_LIVE_WORKER_ITEM_BYTES = 1024;
|
|
9
|
+
export const MAX_LIVE_WORKER_ASSIGNMENT_BYTES = 480;
|
|
10
|
+
|
|
11
|
+
const ACTIVE_STATUSES = new Set<WorkerRecord["status"]>([
|
|
12
|
+
"starting",
|
|
13
|
+
"running",
|
|
14
|
+
"stopping",
|
|
15
|
+
]);
|
|
16
|
+
const ITEM_TRUNCATION_MARKER =
|
|
17
|
+
"\n [entry truncated; use worker_status once for diagnostics/recovery]";
|
|
18
|
+
const ASSIGNMENT_TRUNCATION_MARKER = " … [assignment excerpt truncated]";
|
|
19
|
+
|
|
20
|
+
type CustomMessage = Extract<AgentMessage, { readonly role: "custom" }>;
|
|
21
|
+
|
|
22
|
+
export interface LiveWorkerContextOptions {
|
|
23
|
+
readonly ownerSessionId: string;
|
|
24
|
+
readonly pendingResultCount: number;
|
|
25
|
+
readonly timestamp?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Project only current actionable process state; terminal history and outcomes stay out. */
|
|
29
|
+
export function projectLiveWorkerContext(
|
|
30
|
+
snapshot: OwnerSnapshot,
|
|
31
|
+
options: LiveWorkerContextOptions,
|
|
32
|
+
): CustomMessage | undefined {
|
|
33
|
+
const workers = snapshot.workers.filter((worker) =>
|
|
34
|
+
worker.ownerSessionId === options.ownerSessionId && isActionable(worker)
|
|
35
|
+
);
|
|
36
|
+
const pendingResultCount = normalizeCount(options.pendingResultCount);
|
|
37
|
+
if (workers.length === 0 && pendingResultCount === 0) return undefined;
|
|
38
|
+
|
|
39
|
+
const activeCount = workers.filter((worker) => ACTIVE_STATUSES.has(worker.status)).length;
|
|
40
|
+
const readyCount = workers.length - activeCount;
|
|
41
|
+
const items = workers.map(renderWorkerItem);
|
|
42
|
+
const content = renderBoundedContext(
|
|
43
|
+
items,
|
|
44
|
+
activeCount,
|
|
45
|
+
readyCount,
|
|
46
|
+
pendingResultCount,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
role: "custom",
|
|
51
|
+
customType: LIVE_WORKER_CONTEXT_TYPE,
|
|
52
|
+
content,
|
|
53
|
+
display: false,
|
|
54
|
+
timestamp: options.timestamp ?? Date.now(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Replace this extension's transient projection while preserving every unrelated message. */
|
|
59
|
+
export function replaceLiveWorkerContext(
|
|
60
|
+
messages: readonly AgentMessage[],
|
|
61
|
+
context: CustomMessage | undefined,
|
|
62
|
+
): AgentMessage[] {
|
|
63
|
+
const retained = messages.filter((message) =>
|
|
64
|
+
message.role !== "custom" || message.customType !== LIVE_WORKER_CONTEXT_TYPE
|
|
65
|
+
);
|
|
66
|
+
return context ? [...retained, context] : retained;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isActionable(worker: WorkerRecord): boolean {
|
|
70
|
+
return ACTIVE_STATUSES.has(worker.status) ||
|
|
71
|
+
(worker.status === "ready" && worker.lifecycle === "interactive");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderWorkerItem(worker: WorkerRecord): string {
|
|
75
|
+
const metadata = [
|
|
76
|
+
`worker_id=${quote(worker.id)}`,
|
|
77
|
+
`run_id=${quote(worker.runId)}`,
|
|
78
|
+
`definition=${quote(normalizeText(worker.worker))}`,
|
|
79
|
+
`title=${quote(normalizeText(worker.title))}`,
|
|
80
|
+
`lifecycle=${worker.lifecycle}`,
|
|
81
|
+
`status=${worker.status}`,
|
|
82
|
+
].join(" | ");
|
|
83
|
+
const assignment = capUtf8(
|
|
84
|
+
normalizeText(worker.instructions),
|
|
85
|
+
MAX_LIVE_WORKER_ASSIGNMENT_BYTES,
|
|
86
|
+
ASSIGNMENT_TRUNCATION_MARKER,
|
|
87
|
+
);
|
|
88
|
+
const item = `- ${metadata}\n assignment=${quote(assignment)}`;
|
|
89
|
+
return capUtf8(item, MAX_LIVE_WORKER_ITEM_BYTES, ITEM_TRUNCATION_MARKER);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderBoundedContext(
|
|
93
|
+
items: readonly string[],
|
|
94
|
+
activeCount: number,
|
|
95
|
+
readyCount: number,
|
|
96
|
+
pendingResultCount: number,
|
|
97
|
+
): string {
|
|
98
|
+
for (let shown = items.length; shown >= 0; shown -= 1) {
|
|
99
|
+
const content = renderContext(
|
|
100
|
+
items.slice(0, shown),
|
|
101
|
+
items.length,
|
|
102
|
+
activeCount,
|
|
103
|
+
readyCount,
|
|
104
|
+
pendingResultCount,
|
|
105
|
+
);
|
|
106
|
+
if (utf8Bytes(content) <= MAX_LIVE_WORKER_CONTEXT_BYTES) return content;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fixed guidance is intentionally far below the global limit. Keep a defensive
|
|
110
|
+
// cap so future copy changes cannot violate the public bound.
|
|
111
|
+
return capUtf8(
|
|
112
|
+
renderContext([], items.length, activeCount, readyCount, pendingResultCount),
|
|
113
|
+
MAX_LIVE_WORKER_CONTEXT_BYTES,
|
|
114
|
+
"\n[Live worker context truncated; use worker_status once for recovery.]",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderContext(
|
|
119
|
+
shownItems: readonly string[],
|
|
120
|
+
totalWorkers: number,
|
|
121
|
+
activeCount: number,
|
|
122
|
+
readyCount: number,
|
|
123
|
+
pendingResultCount: number,
|
|
124
|
+
): string {
|
|
125
|
+
const sections = [
|
|
126
|
+
"## Pi Orchestrate live worker context",
|
|
127
|
+
"Authoritative transient process snapshot for this parent provider call; it is not conversation history.",
|
|
128
|
+
`Relevant owned workers: ${totalWorkers} (${activeCount} active, ${readyCount} ready interactive).`,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (shownItems.length > 0) {
|
|
132
|
+
sections.push(shownItems.join("\n"));
|
|
133
|
+
}
|
|
134
|
+
if (shownItems.length < totalWorkers) {
|
|
135
|
+
const omitted = totalWorkers - shownItems.length;
|
|
136
|
+
sections.push(
|
|
137
|
+
`Snapshot overflow: showing ${shownItems.length} of ${totalWorkers}; ${omitted} worker${omitted === 1 ? "" : "s"} omitted by the ${MAX_LIVE_WORKER_CONTEXT_BYTES}-byte limit. Use worker_status once for diagnostics/recovery to inspect omitted worker IDs and current state; do not poll.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (pendingResultCount > 0) {
|
|
141
|
+
sections.push(
|
|
142
|
+
`Pending delivery: ${pendingResultCount} settled worker result${pendingResultCount === 1 ? "" : "s"} await automatic delivery. Do not redispatch that work.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const guidance: string[] = [];
|
|
147
|
+
if (activeCount > 0) {
|
|
148
|
+
guidance.push(
|
|
149
|
+
"Do not duplicate active assignments. Wait for automatic result delivery; do not poll worker_status for completion.",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (readyCount > 0) {
|
|
153
|
+
guidance.push(
|
|
154
|
+
"Ready interactive sessions are retained: use interactive_send with the worker_id for follow-up, or interactive_close when finished.",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
guidance.push(
|
|
158
|
+
"Use worker_status only for diagnostics or recovery when this snapshot reports overflow or state appears inconsistent.",
|
|
159
|
+
);
|
|
160
|
+
sections.push(`Guidance:\n- ${guidance.join("\n- ")}`);
|
|
161
|
+
|
|
162
|
+
return sections.join("\n\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeText(value: string): string {
|
|
166
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function quote(value: string): string {
|
|
170
|
+
return JSON.stringify(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeCount(value: number): number {
|
|
174
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function utf8Bytes(value: string): number {
|
|
178
|
+
return Buffer.byteLength(value, "utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function capUtf8(value: string, byteLimit: number, marker: string): string {
|
|
182
|
+
if (utf8Bytes(value) <= byteLimit) return value;
|
|
183
|
+
const markerBytes = utf8Bytes(marker);
|
|
184
|
+
if (markerBytes >= byteLimit) return truncateUtf8(marker, byteLimit);
|
|
185
|
+
return `${truncateUtf8(value, byteLimit - markerBytes)}${marker}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function truncateUtf8(value: string, byteLimit: number): string {
|
|
189
|
+
if (byteLimit <= 0) return "";
|
|
190
|
+
const bytes = Buffer.from(value, "utf8");
|
|
191
|
+
if (bytes.byteLength <= byteLimit) return value;
|
|
192
|
+
|
|
193
|
+
let end = byteLimit;
|
|
194
|
+
while (end > 0 && (bytes[end] ?? 0) >> 6 === 0b10) end -= 1;
|
|
195
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
196
|
+
}
|
|
@@ -674,7 +674,14 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
|
|
|
674
674
|
yield* Effect.tryPromise({
|
|
675
675
|
try: async () => {
|
|
676
676
|
if (auth.headers) {
|
|
677
|
-
|
|
677
|
+
// Provider config accepts resolved values, not request-only null suppressions;
|
|
678
|
+
// the child runtime derives those again from the provider's auth implementation.
|
|
679
|
+
const headers = Object.fromEntries(
|
|
680
|
+
Object.entries(auth.headers).filter(
|
|
681
|
+
(entry): entry is [string, string] => entry[1] !== null,
|
|
682
|
+
),
|
|
683
|
+
);
|
|
684
|
+
modelRuntime.registerProvider(selected.provider, { headers });
|
|
678
685
|
}
|
|
679
686
|
if (auth.apiKey && !options.modelRegistry.isUsingOAuth(model)) {
|
|
680
687
|
await modelRuntime.setRuntimeApiKey(selected.provider, auth.apiKey);
|
|
@@ -869,8 +876,8 @@ export const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
869
876
|
const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
|
|
870
877
|
|
|
871
878
|
// createAgentSessionServices registers extension providers and refreshes, but
|
|
872
|
-
//
|
|
873
|
-
// errors and aborts remain typed acquisition failures
|
|
879
|
+
// does not surface the refresh result. Keep this worker-owned probe so provider
|
|
880
|
+
// errors and aborts remain typed acquisition failures.
|
|
874
881
|
yield* refreshModelRuntime(modelRuntime, definition);
|
|
875
882
|
const model = yield* Effect.try({
|
|
876
883
|
try: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachwill/pi-orchestrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Concurrent worker orchestration for Pi",
|
|
6
6
|
"exports": {},
|
|
@@ -29,17 +29,18 @@
|
|
|
29
29
|
"effect": "4.0.0-rc.111"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
33
|
-
"@earendil-works/pi-ai": "^0.
|
|
34
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
35
|
-
"@earendil-works/pi-tui": "^0.
|
|
32
|
+
"@earendil-works/pi-agent-core": "^0.85.0",
|
|
33
|
+
"@earendil-works/pi-ai": "^0.85.0",
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
35
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
36
36
|
"typebox": "*"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@earendil-works/pi-agent-core": "0.
|
|
40
|
-
"@earendil-works/pi-ai": "0.
|
|
41
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
42
|
-
"@earendil-works/pi-
|
|
39
|
+
"@earendil-works/pi-agent-core": "0.85.0",
|
|
40
|
+
"@earendil-works/pi-ai": "0.85.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
42
|
+
"@earendil-works/pi-server": "0.85.0",
|
|
43
|
+
"@earendil-works/pi-tui": "0.85.0",
|
|
43
44
|
"@types/bun": "^1.3.14",
|
|
44
45
|
"@types/node": "^22.19.17",
|
|
45
46
|
"typebox": "^1.1.37",
|