@zachwill/pi-orchestrate 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/examples/workers/investigator.md +41 -0
- package/examples/workers/scout.md +36 -0
- package/examples/workers/worker.md +44 -0
- package/extension/catalog.ts +372 -0
- package/extension/contract.ts +70 -0
- package/extension/delivery.ts +196 -0
- package/extension/domain.ts +335 -0
- package/extension/host.ts +107 -0
- package/extension/index.ts +176 -0
- package/extension/presentation.ts +629 -0
- package/extension/runtime.ts +1193 -0
- package/extension/scheduler.ts +66 -0
- package/extension/tools.ts +559 -0
- package/extension/worker-session.ts +526 -0
- package/package.json +46 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
createAgentSession,
|
|
6
|
+
DefaultResourceLoader,
|
|
7
|
+
ModelRuntime,
|
|
8
|
+
type ModelRegistry,
|
|
9
|
+
type ResourceLoader,
|
|
10
|
+
SessionManager,
|
|
11
|
+
SettingsManager,
|
|
12
|
+
type AgentSessionEvent,
|
|
13
|
+
type CreateAgentSessionOptions,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { WorkerDefinition, WorkerOutcome, WorkerUsage } from "./domain.js";
|
|
16
|
+
|
|
17
|
+
const DIRECT_CHILD_BOUNDARY =
|
|
18
|
+
"You are a direct child worker session. Do not spawn, delegate to, or orchestrate other workers or child sessions. Complete the assigned task yourself and return the result directly to the parent orchestrator.";
|
|
19
|
+
|
|
20
|
+
interface WorkerAgentSession {
|
|
21
|
+
readonly sessionFile: string | undefined;
|
|
22
|
+
readonly messages: AgentMessage[];
|
|
23
|
+
prompt(instructions: string): Promise<void>;
|
|
24
|
+
abort(): Promise<void>;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
subscribe(listener: (event: AgentSessionEvent) => void): () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ResourceLoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0];
|
|
30
|
+
type InMemorySettings = Parameters<typeof SettingsManager.inMemory>[0];
|
|
31
|
+
|
|
32
|
+
export interface WorkerSessionFactoryOptions {
|
|
33
|
+
cwd: string;
|
|
34
|
+
agentDir: string;
|
|
35
|
+
parentSessionFile: string | undefined;
|
|
36
|
+
projectTrusted: boolean;
|
|
37
|
+
definition: WorkerDefinition;
|
|
38
|
+
/** The model selected by the parent, used only when the worker omits a model. */
|
|
39
|
+
parentModel?: Model<Api>;
|
|
40
|
+
modelRegistry: ModelRegistry;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkerSessionHandle {
|
|
44
|
+
readonly sessionFile: string;
|
|
45
|
+
prompt(instructions: string): Promise<WorkerOutcome>;
|
|
46
|
+
abort(): Promise<void>;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
subscribeUsage(listener: (usage: WorkerUsage) => void): () => void;
|
|
49
|
+
subscribeActivity(listener: (activity: string | undefined) => void): () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkerSessionFactory {
|
|
53
|
+
create(options: WorkerSessionFactoryOptions): Promise<WorkerSessionHandle>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface SessionManagerInput {
|
|
57
|
+
cwd: string;
|
|
58
|
+
parentSessionFile: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ModelRuntimeInput {
|
|
62
|
+
authPath: string;
|
|
63
|
+
modelsPath: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AgentSessionInput {
|
|
67
|
+
cwd: string;
|
|
68
|
+
agentDir: string;
|
|
69
|
+
model: Model<Api>;
|
|
70
|
+
modelRuntime: ModelRuntime;
|
|
71
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
72
|
+
tools: string[];
|
|
73
|
+
resourceLoader: ResourceLoader;
|
|
74
|
+
sessionManager: unknown;
|
|
75
|
+
settingsManager: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface WorkerSessionDependencies {
|
|
79
|
+
createResourceLoader(options: ResourceLoaderOptions): ResourceLoader;
|
|
80
|
+
createSessionManager(input: SessionManagerInput): unknown;
|
|
81
|
+
createSettingsManager(
|
|
82
|
+
settings: InMemorySettings,
|
|
83
|
+
options: { projectTrusted: boolean },
|
|
84
|
+
): unknown;
|
|
85
|
+
createModelRuntime(input: ModelRuntimeInput): Promise<ModelRuntime>;
|
|
86
|
+
createAgentSession(input: AgentSessionInput): Promise<{ session: WorkerAgentSession }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultDependencies: WorkerSessionDependencies = {
|
|
90
|
+
createResourceLoader: (options) => new DefaultResourceLoader(options),
|
|
91
|
+
createSessionManager: ({ cwd, parentSessionFile }) =>
|
|
92
|
+
SessionManager.create(cwd, undefined, { parentSession: parentSessionFile }),
|
|
93
|
+
createSettingsManager: (settings, options) =>
|
|
94
|
+
SettingsManager.inMemory(settings, { projectTrusted: options.projectTrusted }),
|
|
95
|
+
createModelRuntime: (input) => ModelRuntime.create(input),
|
|
96
|
+
createAgentSession: (input) =>
|
|
97
|
+
createAgentSession(input as CreateAgentSessionOptions) as Promise<{ session: WorkerAgentSession }>,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function resolveWorkerModel(
|
|
101
|
+
definition: WorkerDefinition,
|
|
102
|
+
parentModel: Model<Api> | undefined,
|
|
103
|
+
modelRegistry: ModelRegistry,
|
|
104
|
+
): Model<Api> {
|
|
105
|
+
const configured = definition.model;
|
|
106
|
+
if (!configured) {
|
|
107
|
+
if (parentModel) return parentModel;
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Worker "${definition.name}" has no configured model and no parent model is available`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const model = modelRegistry.find(configured.provider, configured.modelId);
|
|
114
|
+
if (!model) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Worker "${definition.name}" configured model "${configured.provider}/${configured.modelId}" was not found`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return model;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface DisposableResource {
|
|
123
|
+
dispose(): void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isDisposableResource(resource: ResourceLoader): resource is ResourceLoader & DisposableResource {
|
|
127
|
+
return "dispose" in resource && typeof resource.dispose === "function";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function disposeResourceLoader(resourceLoader: ResourceLoader): void {
|
|
131
|
+
if (!isDisposableResource(resourceLoader)) return;
|
|
132
|
+
try {
|
|
133
|
+
resourceLoader.dispose();
|
|
134
|
+
} catch {
|
|
135
|
+
// Cleanup is best-effort and must not hide the original failure.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function disposeSession(session: WorkerAgentSession): void {
|
|
140
|
+
try {
|
|
141
|
+
session.dispose();
|
|
142
|
+
} catch {
|
|
143
|
+
// Other owned resources still need to be released.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function describePromptError(error: unknown): string {
|
|
148
|
+
if (error instanceof Error) return error.message;
|
|
149
|
+
if (typeof error === "string" && error !== "") return error;
|
|
150
|
+
return "Worker prompt failed";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface MutableWorkerUsage {
|
|
154
|
+
input: number;
|
|
155
|
+
output: number;
|
|
156
|
+
cacheRead: number;
|
|
157
|
+
cacheWrite: number;
|
|
158
|
+
cost: number;
|
|
159
|
+
contextTokens: number;
|
|
160
|
+
turns: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function emptyUsage(): MutableWorkerUsage {
|
|
164
|
+
return {
|
|
165
|
+
input: 0,
|
|
166
|
+
output: 0,
|
|
167
|
+
cacheRead: 0,
|
|
168
|
+
cacheWrite: 0,
|
|
169
|
+
cost: 0,
|
|
170
|
+
contextTokens: 0,
|
|
171
|
+
turns: 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assistantText(message: AssistantMessage | undefined): string | undefined {
|
|
176
|
+
if (!message) return undefined;
|
|
177
|
+
const text = message.content
|
|
178
|
+
.filter((part) => part.type === "text")
|
|
179
|
+
.map((part) => part.text);
|
|
180
|
+
return text.length > 0 ? text.join("\n") : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function lastAssistant(messages: AgentMessage[]): AssistantMessage | undefined {
|
|
184
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
185
|
+
const message = messages[index];
|
|
186
|
+
if (message?.role === "assistant") return message;
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
192
|
+
readonly sessionFile: string;
|
|
193
|
+
|
|
194
|
+
private readonly usage = emptyUsage();
|
|
195
|
+
private readonly usageListeners = new Set<(usage: WorkerUsage) => void>();
|
|
196
|
+
private readonly activityListeners = new Set<
|
|
197
|
+
(activity: string | undefined) => void
|
|
198
|
+
>();
|
|
199
|
+
private readonly unsubscribeSession: () => void;
|
|
200
|
+
private disposed = false;
|
|
201
|
+
private activity: string | undefined;
|
|
202
|
+
private readonly activeToolCalls = new Map<string, string>();
|
|
203
|
+
private prompting = false;
|
|
204
|
+
private abortRequested = false;
|
|
205
|
+
private promptAssistant: AssistantMessage | undefined;
|
|
206
|
+
|
|
207
|
+
constructor(
|
|
208
|
+
private readonly session: WorkerAgentSession,
|
|
209
|
+
private readonly resourceLoader: ResourceLoader,
|
|
210
|
+
private readonly reusable: boolean,
|
|
211
|
+
) {
|
|
212
|
+
this.sessionFile = session.sessionFile!;
|
|
213
|
+
this.unsubscribeSession = session.subscribe((event) => this.onSessionEvent(event));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private onSessionEvent(event: AgentSessionEvent): void {
|
|
217
|
+
if (event.type === "tool_execution_start") {
|
|
218
|
+
this.activeToolCalls.delete(event.toolCallId);
|
|
219
|
+
this.activeToolCalls.set(event.toolCallId, event.toolName);
|
|
220
|
+
this.setActivity(event.toolName);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (event.type === "tool_execution_end") {
|
|
225
|
+
if (this.activeToolCalls.get(event.toolCallId) !== event.toolName) return;
|
|
226
|
+
this.activeToolCalls.delete(event.toolCallId);
|
|
227
|
+
this.setActivity(this.mostRecentActiveTool());
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (event.type !== "turn_end" || event.message.role !== "assistant") return;
|
|
232
|
+
|
|
233
|
+
const message = event.message;
|
|
234
|
+
this.promptAssistant = message;
|
|
235
|
+
this.usage.input += message.usage.input ?? 0;
|
|
236
|
+
this.usage.output += message.usage.output ?? 0;
|
|
237
|
+
this.usage.cacheRead += message.usage.cacheRead ?? 0;
|
|
238
|
+
this.usage.cacheWrite += message.usage.cacheWrite ?? 0;
|
|
239
|
+
this.usage.cost += message.usage.cost?.total ?? 0;
|
|
240
|
+
this.usage.contextTokens = message.usage.totalTokens ?? 0;
|
|
241
|
+
this.usage.turns += 1;
|
|
242
|
+
|
|
243
|
+
for (const listener of [...this.usageListeners]) {
|
|
244
|
+
try {
|
|
245
|
+
listener({ ...this.usage });
|
|
246
|
+
} catch {
|
|
247
|
+
// One subscriber cannot prevent other subscribers from receiving usage.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private mostRecentActiveTool(): string | undefined {
|
|
253
|
+
let latest: string | undefined;
|
|
254
|
+
for (const toolName of this.activeToolCalls.values()) latest = toolName;
|
|
255
|
+
return latest;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private setActivity(activity: string | undefined): void {
|
|
259
|
+
if (this.activity === activity) return;
|
|
260
|
+
this.activity = activity;
|
|
261
|
+
for (const listener of [...this.activityListeners]) {
|
|
262
|
+
try {
|
|
263
|
+
listener(activity);
|
|
264
|
+
} catch {
|
|
265
|
+
// One subscriber cannot prevent other subscribers from receiving activity.
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async prompt(instructions: string): Promise<WorkerOutcome> {
|
|
271
|
+
if (this.disposed) throw new Error("Worker session has been disposed");
|
|
272
|
+
if (this.prompting) throw new Error("Worker session is already processing a prompt");
|
|
273
|
+
|
|
274
|
+
this.prompting = true;
|
|
275
|
+
this.abortRequested = false;
|
|
276
|
+
this.promptAssistant = undefined;
|
|
277
|
+
const previousAssistant = lastAssistant(this.session.messages);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await this.session.prompt(instructions);
|
|
281
|
+
const latestAssistant = lastAssistant(this.session.messages);
|
|
282
|
+
const message = this.promptAssistant ?? (latestAssistant !== previousAssistant ? latestAssistant : undefined);
|
|
283
|
+
return this.outcomeFrom(message);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const latestAssistant = lastAssistant(this.session.messages);
|
|
286
|
+
const message = this.promptAssistant ?? (latestAssistant !== previousAssistant ? latestAssistant : undefined);
|
|
287
|
+
return this.outcomeFrom(message, describePromptError(error));
|
|
288
|
+
} finally {
|
|
289
|
+
this.prompting = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private outcomeFrom(
|
|
294
|
+
message: AssistantMessage | undefined,
|
|
295
|
+
failureMessage?: string,
|
|
296
|
+
): WorkerOutcome {
|
|
297
|
+
const text = assistantText(message);
|
|
298
|
+
const assistantPayload = text === undefined ? {} : { assistantText: text };
|
|
299
|
+
|
|
300
|
+
if (this.abortRequested || message?.stopReason === "aborted") {
|
|
301
|
+
const abortMessage = message?.errorMessage ?? failureMessage;
|
|
302
|
+
return {
|
|
303
|
+
status: "aborted",
|
|
304
|
+
...(abortMessage === undefined ? {} : { message: abortMessage }),
|
|
305
|
+
...assistantPayload,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (message?.stopReason === "error") {
|
|
310
|
+
return {
|
|
311
|
+
status: "failed",
|
|
312
|
+
message: message.errorMessage ?? failureMessage ?? "Worker assistant reported a failure",
|
|
313
|
+
...assistantPayload,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (failureMessage !== undefined) {
|
|
318
|
+
return { status: "failed", message: failureMessage, ...assistantPayload };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
status: this.reusable ? "ready" : "completed",
|
|
323
|
+
assistantText: text ?? "",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async abort(): Promise<void> {
|
|
328
|
+
if (this.prompting) this.abortRequested = true;
|
|
329
|
+
await this.session.abort();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
dispose(): void {
|
|
333
|
+
if (this.disposed) return;
|
|
334
|
+
this.disposed = true;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
this.unsubscribeSession();
|
|
338
|
+
} catch {
|
|
339
|
+
// Continue releasing independently owned resources.
|
|
340
|
+
}
|
|
341
|
+
this.activeToolCalls.clear();
|
|
342
|
+
this.usageListeners.clear();
|
|
343
|
+
this.activityListeners.clear();
|
|
344
|
+
disposeSession(this.session);
|
|
345
|
+
disposeResourceLoader(this.resourceLoader);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
subscribeUsage(listener: (usage: WorkerUsage) => void): () => void {
|
|
349
|
+
this.usageListeners.add(listener);
|
|
350
|
+
let subscribed = true;
|
|
351
|
+
return () => {
|
|
352
|
+
if (!subscribed) return;
|
|
353
|
+
subscribed = false;
|
|
354
|
+
this.usageListeners.delete(listener);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
subscribeActivity(
|
|
359
|
+
listener: (activity: string | undefined) => void,
|
|
360
|
+
): () => void {
|
|
361
|
+
this.activityListeners.add(listener);
|
|
362
|
+
let subscribed = true;
|
|
363
|
+
return () => {
|
|
364
|
+
if (!subscribed) return;
|
|
365
|
+
subscribed = false;
|
|
366
|
+
this.activityListeners.delete(listener);
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function selectedModelCoordinates(
|
|
372
|
+
definition: WorkerDefinition,
|
|
373
|
+
parentModel: Model<Api> | undefined,
|
|
374
|
+
): { provider: string; modelId: string } {
|
|
375
|
+
if (definition.model) return definition.model;
|
|
376
|
+
if (parentModel) return { provider: parentModel.provider, modelId: parentModel.id };
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Worker "${definition.name}" has no configured model and no parent model is available`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function refreshChildModelRuntime(
|
|
383
|
+
modelRuntime: ModelRuntime,
|
|
384
|
+
definition: WorkerDefinition,
|
|
385
|
+
): Promise<void> {
|
|
386
|
+
const result = await modelRuntime.refresh({ allowNetwork: false });
|
|
387
|
+
if (result.errors.size > 0) {
|
|
388
|
+
const errors = [...result.errors]
|
|
389
|
+
.map(([providerId, error]) => `${providerId}: ${error.message}`)
|
|
390
|
+
.join("; ");
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Worker "${definition.name}" failed to refresh child model providers: ${errors}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (result.aborted) {
|
|
396
|
+
throw new Error(`Worker "${definition.name}" child model refresh was aborted`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function prepareChildModelRuntime(
|
|
401
|
+
options: WorkerSessionFactoryOptions,
|
|
402
|
+
dependencies: WorkerSessionDependencies,
|
|
403
|
+
): Promise<{ model: Model<Api>; modelRuntime: ModelRuntime }> {
|
|
404
|
+
const selected = selectedModelCoordinates(options.definition, options.parentModel);
|
|
405
|
+
const modelRuntime = await dependencies.createModelRuntime({
|
|
406
|
+
authPath: join(options.agentDir, "auth.json"),
|
|
407
|
+
modelsPath: join(options.agentDir, "models.json"),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
for (const providerId of options.modelRegistry.getRegisteredProviderIds()) {
|
|
411
|
+
const providerConfig = options.modelRegistry.getRegisteredProviderConfig(providerId);
|
|
412
|
+
if (providerConfig) modelRuntime.registerProvider(providerId, { ...providerConfig });
|
|
413
|
+
}
|
|
414
|
+
await refreshChildModelRuntime(modelRuntime, options.definition);
|
|
415
|
+
|
|
416
|
+
const initiallyResolvedModel = modelRuntime.getModel(selected.provider, selected.modelId);
|
|
417
|
+
if (!initiallyResolvedModel) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Worker "${options.definition.name}" configured model "${selected.provider}/${selected.modelId}" was not found`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const resolvedAuth = await options.modelRegistry.getApiKeyAndHeaders(initiallyResolvedModel);
|
|
424
|
+
if (resolvedAuth.ok) {
|
|
425
|
+
if (resolvedAuth.headers) {
|
|
426
|
+
modelRuntime.registerProvider(selected.provider, {
|
|
427
|
+
headers: { ...resolvedAuth.headers },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (resolvedAuth.apiKey) {
|
|
431
|
+
await modelRuntime.setRuntimeApiKey(selected.provider, resolvedAuth.apiKey);
|
|
432
|
+
}
|
|
433
|
+
// Pi 0.80.10 exposes resolved provider env here but has no public ModelRuntime
|
|
434
|
+
// runtime-env setter. Provider registrations, resolved headers, and API keys are copied.
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await refreshChildModelRuntime(modelRuntime, options.definition);
|
|
438
|
+
const model = modelRuntime.getModel(selected.provider, selected.modelId);
|
|
439
|
+
if (!model) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Worker "${options.definition.name}" configured model "${selected.provider}/${selected.modelId}" was not found`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return { model, modelRuntime };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function createWorkerSessionFactory(
|
|
448
|
+
dependencies: WorkerSessionDependencies = defaultDependencies,
|
|
449
|
+
): WorkerSessionFactory {
|
|
450
|
+
return {
|
|
451
|
+
async create(options) {
|
|
452
|
+
const definition = options.definition;
|
|
453
|
+
const { model, modelRuntime } = await prepareChildModelRuntime(options, dependencies);
|
|
454
|
+
const selectedSkills = new Set(definition.skills);
|
|
455
|
+
const settingsManager = dependencies.createSettingsManager(
|
|
456
|
+
definition.compaction === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: { compaction: { ...definition.compaction } },
|
|
459
|
+
{ projectTrusted: options.projectTrusted },
|
|
460
|
+
);
|
|
461
|
+
const resourceLoader = dependencies.createResourceLoader({
|
|
462
|
+
cwd: options.cwd,
|
|
463
|
+
agentDir: options.agentDir,
|
|
464
|
+
settingsManager: settingsManager as SettingsManager,
|
|
465
|
+
noExtensions: true,
|
|
466
|
+
noSkills: selectedSkills.size === 0,
|
|
467
|
+
noPromptTemplates: true,
|
|
468
|
+
noThemes: true,
|
|
469
|
+
noContextFiles: !options.projectTrusted,
|
|
470
|
+
skillsOverride: (base) => ({
|
|
471
|
+
skills: base.skills.filter((skill) => selectedSkills.has(skill.name)),
|
|
472
|
+
diagnostics: base.diagnostics,
|
|
473
|
+
}),
|
|
474
|
+
appendSystemPrompt: [definition.systemPrompt, DIRECT_CHILD_BOUNDARY],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
let session: WorkerAgentSession | undefined;
|
|
478
|
+
try {
|
|
479
|
+
await resourceLoader.reload();
|
|
480
|
+
|
|
481
|
+
const loadedSkillNames = new Set(
|
|
482
|
+
resourceLoader.getSkills().skills.map((skill) => skill.name),
|
|
483
|
+
);
|
|
484
|
+
const missingSkills = [...selectedSkills].filter(
|
|
485
|
+
(skillName) => !loadedSkillNames.has(skillName),
|
|
486
|
+
);
|
|
487
|
+
if (missingSkills.length > 0) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Worker "${definition.name}" selected skills were not loaded: ${missingSkills.join(", ")}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const sessionManager = dependencies.createSessionManager({
|
|
494
|
+
cwd: options.cwd,
|
|
495
|
+
parentSessionFile: options.parentSessionFile,
|
|
496
|
+
});
|
|
497
|
+
({ session } = await dependencies.createAgentSession({
|
|
498
|
+
cwd: options.cwd,
|
|
499
|
+
agentDir: options.agentDir,
|
|
500
|
+
model,
|
|
501
|
+
modelRuntime,
|
|
502
|
+
thinkingLevel: definition.thinking,
|
|
503
|
+
tools: [...definition.tools],
|
|
504
|
+
resourceLoader,
|
|
505
|
+
sessionManager,
|
|
506
|
+
settingsManager,
|
|
507
|
+
}));
|
|
508
|
+
if (!session.sessionFile) {
|
|
509
|
+
throw new Error("Worker session was not created with durable storage");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const handle = new DefaultWorkerSessionHandle(
|
|
513
|
+
session,
|
|
514
|
+
resourceLoader,
|
|
515
|
+
definition.lifecycle === "reusable",
|
|
516
|
+
);
|
|
517
|
+
session = undefined;
|
|
518
|
+
return handle;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (session) disposeSession(session);
|
|
521
|
+
disposeResourceLoader(resourceLoader);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zachwill/pi-orchestrate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Concurrent worker orchestration for Pi",
|
|
6
|
+
"files": ["extension/", "examples/", "README.md", "LICENSE"],
|
|
7
|
+
"keywords": ["pi-package", "pi", "workers", "orchestration"],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/zachwill/pi-orchestrate.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/zachwill/pi-orchestrate#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/zachwill/pi-orchestrate/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": ["./extension/index.ts"]
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"effect": "4.0.0-beta.99"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@earendil-works/pi-agent-core": "^0.80.10",
|
|
32
|
+
"@earendil-works/pi-ai": "^0.80.10",
|
|
33
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
34
|
+
"@earendil-works/pi-tui": "^0.80.10",
|
|
35
|
+
"typebox": "*"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@earendil-works/pi-agent-core": "0.80.10",
|
|
39
|
+
"@earendil-works/pi-ai": "0.80.10",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
41
|
+
"@earendil-works/pi-tui": "0.80.10",
|
|
42
|
+
"@types/node": "^22.19.17",
|
|
43
|
+
"typebox": "^1.1.37",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
}
|
|
46
|
+
}
|