@zachwill/pi-orchestrate 0.2.0 → 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 +241 -332
- package/extension/tools.ts +206 -214
- package/extension/worker-session.ts +190 -46
- package/extension/worker-settlement.ts +106 -0
- package/package.json +3 -2
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type CreateAgentSessionResult,
|
|
19
19
|
type DefaultResourceLoader,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import { Effect, Exit, Schema, Scope } from "effect";
|
|
21
|
+
import { Cause, Effect, Exit, Schema, Scope } from "effect";
|
|
22
22
|
import type {
|
|
23
23
|
WorkerDefinition,
|
|
24
24
|
WorkerMessageDirection,
|
|
@@ -75,21 +75,66 @@ export interface WorkerSessionFactory {
|
|
|
75
75
|
create(options: WorkerSessionFactoryOptions): Promise<WorkerSessionHandle>;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const WorkerModelAcquisitionOperation = Schema.Literals([
|
|
79
|
+
"select-model",
|
|
80
|
+
"create-model-runtime",
|
|
81
|
+
"register-providers",
|
|
82
|
+
"refresh-providers",
|
|
83
|
+
"resolve-model",
|
|
84
|
+
"authenticate-model",
|
|
85
|
+
"configure-runtime-auth",
|
|
86
|
+
]);
|
|
87
|
+
type WorkerModelAcquisitionOperation = typeof WorkerModelAcquisitionOperation.Type;
|
|
88
|
+
|
|
89
|
+
const WorkerResourceAcquisitionOperation = Schema.Literals([
|
|
90
|
+
"create-settings",
|
|
91
|
+
"resolve-extensions",
|
|
92
|
+
"create-services",
|
|
93
|
+
"validate-skills",
|
|
94
|
+
]);
|
|
95
|
+
type WorkerResourceAcquisitionOperation = typeof WorkerResourceAcquisitionOperation.Type;
|
|
96
|
+
|
|
97
|
+
const WorkerAgentSessionAcquisitionOperation = Schema.Literals([
|
|
98
|
+
"create-session-manager",
|
|
99
|
+
"create-session",
|
|
100
|
+
"create-runtime",
|
|
101
|
+
"bind-extensions",
|
|
102
|
+
"verify-durability",
|
|
103
|
+
"subscribe-events",
|
|
104
|
+
]);
|
|
105
|
+
type WorkerAgentSessionAcquisitionOperation = typeof WorkerAgentSessionAcquisitionOperation.Type;
|
|
106
|
+
|
|
78
107
|
export class WorkerModelAcquisitionError extends Schema.TaggedErrorClass<WorkerModelAcquisitionError>()(
|
|
79
108
|
"WorkerSession.ModelAcquisitionError",
|
|
80
|
-
{ message: Schema.String, cause: Schema.Defect() },
|
|
109
|
+
{ operation: WorkerModelAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
81
110
|
) {}
|
|
82
111
|
|
|
83
112
|
export class WorkerResourceAcquisitionError extends Schema.TaggedErrorClass<WorkerResourceAcquisitionError>()(
|
|
84
113
|
"WorkerSession.ResourceAcquisitionError",
|
|
85
|
-
{ message: Schema.String, cause: Schema.Defect() },
|
|
114
|
+
{ operation: WorkerResourceAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
86
115
|
) {}
|
|
87
116
|
|
|
88
117
|
export class WorkerAgentSessionAcquisitionError extends Schema.TaggedErrorClass<WorkerAgentSessionAcquisitionError>()(
|
|
89
118
|
"WorkerSession.AgentSessionAcquisitionError",
|
|
90
|
-
{ message: Schema.String, cause: Schema.Defect() },
|
|
119
|
+
{ operation: WorkerAgentSessionAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
91
120
|
) {}
|
|
92
121
|
|
|
122
|
+
const WorkerSessionCleanupOperation = Schema.Literals([
|
|
123
|
+
"unsubscribe",
|
|
124
|
+
"runtime",
|
|
125
|
+
"raw-session",
|
|
126
|
+
"resource-loader",
|
|
127
|
+
"scope-close",
|
|
128
|
+
]);
|
|
129
|
+
export type WorkerSessionCleanupOperation = typeof WorkerSessionCleanupOperation.Type;
|
|
130
|
+
|
|
131
|
+
export interface WorkerSessionCleanupFailure {
|
|
132
|
+
readonly operation: WorkerSessionCleanupOperation;
|
|
133
|
+
readonly cause: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type WorkerSessionCleanupReporter = (failure: WorkerSessionCleanupFailure) => void;
|
|
137
|
+
|
|
93
138
|
interface SettingsManagerInput {
|
|
94
139
|
cwd: string;
|
|
95
140
|
agentDir: string;
|
|
@@ -137,6 +182,7 @@ export interface WorkerSessionDependencies {
|
|
|
137
182
|
createServices(input: ServicesInput): Promise<AgentSessionServices>;
|
|
138
183
|
createAgentSession(input: AgentSessionInput): Promise<{ session: WorkerAgentSession }>;
|
|
139
184
|
createRuntime(input: RuntimeInput): OwnedWorkerRuntime;
|
|
185
|
+
reportCleanupFailure(failure: WorkerSessionCleanupFailure): void;
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
const defaultDependencies: WorkerSessionDependencies = {
|
|
@@ -163,6 +209,12 @@ const defaultDependencies: WorkerSessionDependencies = {
|
|
|
163
209
|
throw new Error("Worker sessions do not support runtime replacement");
|
|
164
210
|
},
|
|
165
211
|
) as unknown as OwnedWorkerRuntime,
|
|
212
|
+
reportCleanupFailure: ({ operation }) => {
|
|
213
|
+
process.emitWarning("Worker session cleanup failed", {
|
|
214
|
+
code: "PI_ORCHESTRATE_WORKER_CLEANUP",
|
|
215
|
+
detail: `Operation: ${operation}`,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
166
218
|
};
|
|
167
219
|
|
|
168
220
|
export function resolveWorkerModel(
|
|
@@ -204,11 +256,41 @@ export function isOrchestrationExtensionPath(path: string): boolean {
|
|
|
204
256
|
return isWithin(canonicalPath(path), ORCHESTRATE_PACKAGE_ROOT);
|
|
205
257
|
}
|
|
206
258
|
|
|
207
|
-
function
|
|
259
|
+
function reportCleanupFailure(
|
|
260
|
+
reporter: WorkerSessionCleanupReporter,
|
|
261
|
+
operation: WorkerSessionCleanupOperation,
|
|
262
|
+
cause: unknown,
|
|
263
|
+
): Effect.Effect<void> {
|
|
208
264
|
return Effect.sync(() => {
|
|
265
|
+
try {
|
|
266
|
+
reporter({ operation, cause });
|
|
267
|
+
} catch {
|
|
268
|
+
// Reporting must never make best-effort cleanup fail.
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function bestEffortCleanup(
|
|
274
|
+
reporter: WorkerSessionCleanupReporter,
|
|
275
|
+
operation: WorkerSessionCleanupOperation,
|
|
276
|
+
cleanup: () => void | Promise<void>,
|
|
277
|
+
): Effect.Effect<void> {
|
|
278
|
+
return Effect.tryPromise({
|
|
279
|
+
try: async () => cleanup(),
|
|
280
|
+
catch: (cause) => cause,
|
|
281
|
+
}).pipe(
|
|
282
|
+
Effect.catch((cause) => reportCleanupFailure(reporter, operation, cause)),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function resourceLoaderFinalizer(
|
|
287
|
+
resourceLoader: ResourceLoader,
|
|
288
|
+
reporter: WorkerSessionCleanupReporter,
|
|
289
|
+
): Effect.Effect<void> {
|
|
290
|
+
return bestEffortCleanup(reporter, "resource-loader", () => {
|
|
209
291
|
if (!("dispose" in resourceLoader) || typeof resourceLoader.dispose !== "function") return;
|
|
210
292
|
resourceLoader.dispose();
|
|
211
|
-
})
|
|
293
|
+
});
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
function describeError(error: unknown, fallback: string): string {
|
|
@@ -267,6 +349,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
|
267
349
|
private readonly reusable: boolean,
|
|
268
350
|
sessionFile: string,
|
|
269
351
|
private readonly scope: Scope.Closeable,
|
|
352
|
+
private readonly cleanupReporter: WorkerSessionCleanupReporter,
|
|
270
353
|
) {
|
|
271
354
|
this.sessionFile = sessionFile;
|
|
272
355
|
}
|
|
@@ -357,7 +440,9 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
|
357
440
|
this.usageListeners.clear();
|
|
358
441
|
this.activityListeners.clear();
|
|
359
442
|
this.messageDirectionListeners.clear();
|
|
360
|
-
this.disposePromise = Effect.runPromise(
|
|
443
|
+
this.disposePromise = Effect.runPromise(
|
|
444
|
+
disposeWorkerSession(this.scope, this.cleanupReporter),
|
|
445
|
+
);
|
|
361
446
|
return this.disposePromise;
|
|
362
447
|
}
|
|
363
448
|
|
|
@@ -395,19 +480,32 @@ function selectedModelCoordinates(
|
|
|
395
480
|
throw new Error(`Worker "${definition.name}" has no configured model and no parent model is available`);
|
|
396
481
|
}
|
|
397
482
|
|
|
398
|
-
function modelAcquisitionError(
|
|
399
|
-
|
|
483
|
+
function modelAcquisitionError(
|
|
484
|
+
operation: WorkerModelAcquisitionOperation,
|
|
485
|
+
fallback: string,
|
|
486
|
+
): (cause: unknown) => WorkerModelAcquisitionError {
|
|
487
|
+
return (cause) => new WorkerModelAcquisitionError({
|
|
488
|
+
operation,
|
|
489
|
+
message: describeError(cause, fallback),
|
|
490
|
+
cause,
|
|
491
|
+
});
|
|
400
492
|
}
|
|
401
493
|
|
|
402
|
-
function resourceAcquisitionError(
|
|
403
|
-
|
|
494
|
+
function resourceAcquisitionError(
|
|
495
|
+
operation: WorkerResourceAcquisitionOperation,
|
|
496
|
+
): (cause: unknown) => WorkerResourceAcquisitionError {
|
|
497
|
+
return (cause) => new WorkerResourceAcquisitionError({
|
|
498
|
+
operation,
|
|
404
499
|
message: describeError(cause, "Worker resource acquisition failed"),
|
|
405
500
|
cause,
|
|
406
501
|
});
|
|
407
502
|
}
|
|
408
503
|
|
|
409
|
-
function agentSessionAcquisitionError(
|
|
410
|
-
|
|
504
|
+
function agentSessionAcquisitionError(
|
|
505
|
+
operation: WorkerAgentSessionAcquisitionOperation,
|
|
506
|
+
): (cause: unknown) => WorkerAgentSessionAcquisitionError {
|
|
507
|
+
return (cause) => new WorkerAgentSessionAcquisitionError({
|
|
508
|
+
operation,
|
|
411
509
|
message: describeError(cause, "Worker agent session acquisition failed"),
|
|
412
510
|
cause,
|
|
413
511
|
});
|
|
@@ -417,20 +515,24 @@ const refreshModelRuntime = Effect.fn("WorkerSession.refreshModelRuntime")(funct
|
|
|
417
515
|
modelRuntime: ModelRuntime,
|
|
418
516
|
definition: WorkerDefinition,
|
|
419
517
|
) {
|
|
518
|
+
const refreshError = modelAcquisitionError(
|
|
519
|
+
"refresh-providers",
|
|
520
|
+
"Worker model refresh failed",
|
|
521
|
+
);
|
|
420
522
|
const result = yield* Effect.tryPromise({
|
|
421
523
|
try: () => modelRuntime.refresh({ allowNetwork: false }),
|
|
422
|
-
catch:
|
|
524
|
+
catch: refreshError,
|
|
423
525
|
});
|
|
424
526
|
if (result.errors.size > 0) {
|
|
425
527
|
const errors = [...result.errors].map(([id, error]) => `${id}: ${error.message}`).join("; ");
|
|
426
528
|
const cause = new Error(
|
|
427
529
|
`Worker "${definition.name}" failed to refresh child model providers: ${errors}`,
|
|
428
530
|
);
|
|
429
|
-
return yield* Effect.fail(
|
|
531
|
+
return yield* Effect.fail(refreshError(cause));
|
|
430
532
|
}
|
|
431
533
|
if (result.aborted) {
|
|
432
534
|
const cause = new Error(`Worker "${definition.name}" child model refresh was aborted`);
|
|
433
|
-
return yield* Effect.fail(
|
|
535
|
+
return yield* Effect.fail(refreshError(cause));
|
|
434
536
|
}
|
|
435
537
|
});
|
|
436
538
|
|
|
@@ -440,14 +542,14 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
|
|
|
440
542
|
) {
|
|
441
543
|
const selected = yield* Effect.try({
|
|
442
544
|
try: () => selectedModelCoordinates(options.definition, options.parentModel),
|
|
443
|
-
catch:
|
|
545
|
+
catch: modelAcquisitionError("select-model", "Worker model selection failed"),
|
|
444
546
|
});
|
|
445
547
|
const modelRuntime = yield* Effect.tryPromise({
|
|
446
548
|
try: () => dependencies.createModelRuntime({
|
|
447
549
|
authPath: join(options.agentDir, "auth.json"),
|
|
448
550
|
modelsPath: join(options.agentDir, "models.json"),
|
|
449
551
|
}),
|
|
450
|
-
catch:
|
|
552
|
+
catch: modelAcquisitionError("create-model-runtime", "Worker model runtime creation failed"),
|
|
451
553
|
});
|
|
452
554
|
|
|
453
555
|
yield* Effect.try({
|
|
@@ -457,18 +559,20 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
|
|
|
457
559
|
if (config) modelRuntime.registerProvider(providerId, { ...config });
|
|
458
560
|
}
|
|
459
561
|
},
|
|
460
|
-
catch:
|
|
562
|
+
catch: modelAcquisitionError("register-providers", "Worker model provider registration failed"),
|
|
461
563
|
});
|
|
564
|
+
// Worker-session owns this pre-service refresh so dynamic parent providers exist
|
|
565
|
+
// before selected-model authentication is copied.
|
|
462
566
|
yield* refreshModelRuntime(modelRuntime, options.definition);
|
|
463
567
|
|
|
464
568
|
const model = yield* Effect.try({
|
|
465
569
|
try: () => modelRuntime.getModel(selected.provider, selected.modelId),
|
|
466
|
-
catch:
|
|
570
|
+
catch: modelAcquisitionError("resolve-model", "Worker model resolution failed"),
|
|
467
571
|
});
|
|
468
572
|
if (model) {
|
|
469
573
|
const auth = yield* Effect.tryPromise({
|
|
470
574
|
try: () => options.modelRegistry.getApiKeyAndHeaders(model),
|
|
471
|
-
catch:
|
|
575
|
+
catch: modelAcquisitionError("authenticate-model", "Worker model authentication failed"),
|
|
472
576
|
});
|
|
473
577
|
if (auth.ok) {
|
|
474
578
|
yield* Effect.tryPromise({
|
|
@@ -480,7 +584,7 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
|
|
|
480
584
|
await modelRuntime.setRuntimeApiKey(selected.provider, auth.apiKey);
|
|
481
585
|
}
|
|
482
586
|
},
|
|
483
|
-
catch:
|
|
587
|
+
catch: modelAcquisitionError("configure-runtime-auth", "Worker model runtime key setup failed"),
|
|
484
588
|
});
|
|
485
589
|
}
|
|
486
590
|
}
|
|
@@ -515,8 +619,13 @@ function contextLoaderOptions(
|
|
|
515
619
|
|
|
516
620
|
const disposeWorkerSession = Effect.fn("WorkerSession.dispose")(function* (
|
|
517
621
|
scope: Scope.Closeable,
|
|
622
|
+
reporter: WorkerSessionCleanupReporter,
|
|
518
623
|
) {
|
|
519
|
-
yield* Scope.close(scope, Exit.void).pipe(
|
|
624
|
+
yield* Scope.close(scope, Exit.void).pipe(
|
|
625
|
+
Effect.catchCause((cause) =>
|
|
626
|
+
reportCleanupFailure(reporter, "scope-close", Cause.squash(cause))
|
|
627
|
+
),
|
|
628
|
+
);
|
|
520
629
|
});
|
|
521
630
|
|
|
522
631
|
const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(function* (
|
|
@@ -532,7 +641,7 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
|
|
|
532
641
|
projectTrusted: options.projectTrusted,
|
|
533
642
|
compaction: definition.compaction,
|
|
534
643
|
}),
|
|
535
|
-
catch: resourceAcquisitionError,
|
|
644
|
+
catch: resourceAcquisitionError("create-settings"),
|
|
536
645
|
});
|
|
537
646
|
const extensionPaths = yield* Effect.tryPromise({
|
|
538
647
|
try: () => dependencies.resolveExtensionPaths({
|
|
@@ -540,7 +649,7 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
|
|
|
540
649
|
agentDir: options.agentDir,
|
|
541
650
|
settingsManager,
|
|
542
651
|
}),
|
|
543
|
-
catch: resourceAcquisitionError,
|
|
652
|
+
catch: resourceAcquisitionError("resolve-extensions"),
|
|
544
653
|
}).pipe(Effect.map((paths) => paths.filter((path) => !isOrchestrationExtensionPath(path))));
|
|
545
654
|
|
|
546
655
|
return yield* Effect.acquireRelease(
|
|
@@ -562,9 +671,12 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
|
|
|
562
671
|
appendSystemPrompt: [definition.systemPrompt, DIRECT_CHILD_BOUNDARY],
|
|
563
672
|
},
|
|
564
673
|
}),
|
|
565
|
-
catch: resourceAcquisitionError,
|
|
674
|
+
catch: resourceAcquisitionError("create-services"),
|
|
566
675
|
}),
|
|
567
|
-
(services) => resourceLoaderFinalizer(
|
|
676
|
+
(services) => resourceLoaderFinalizer(
|
|
677
|
+
services.resourceLoader,
|
|
678
|
+
dependencies.reportCleanupFailure,
|
|
679
|
+
),
|
|
568
680
|
);
|
|
569
681
|
});
|
|
570
682
|
|
|
@@ -573,16 +685,28 @@ interface AgentSessionOwnership {
|
|
|
573
685
|
runtime: OwnedWorkerRuntime | undefined;
|
|
574
686
|
}
|
|
575
687
|
|
|
576
|
-
function rawSessionFinalizer(
|
|
577
|
-
|
|
688
|
+
function rawSessionFinalizer(
|
|
689
|
+
session: WorkerAgentSession,
|
|
690
|
+
reporter: WorkerSessionCleanupReporter,
|
|
691
|
+
): Effect.Effect<void> {
|
|
692
|
+
return bestEffortCleanup(reporter, "raw-session", () => session.dispose());
|
|
578
693
|
}
|
|
579
694
|
|
|
580
|
-
function agentSessionFinalizer(
|
|
695
|
+
function agentSessionFinalizer(
|
|
696
|
+
ownership: AgentSessionOwnership,
|
|
697
|
+
reporter: WorkerSessionCleanupReporter,
|
|
698
|
+
): Effect.Effect<void> {
|
|
581
699
|
const runtime = ownership.runtime;
|
|
582
|
-
if (!runtime) return rawSessionFinalizer(ownership.session);
|
|
583
|
-
return Effect.tryPromise(
|
|
584
|
-
|
|
585
|
-
|
|
700
|
+
if (!runtime) return rawSessionFinalizer(ownership.session, reporter);
|
|
701
|
+
return Effect.tryPromise({
|
|
702
|
+
try: () => runtime.dispose(),
|
|
703
|
+
catch: (cause) => cause,
|
|
704
|
+
}).pipe(
|
|
705
|
+
Effect.catch((cause) =>
|
|
706
|
+
reportCleanupFailure(reporter, "runtime", cause).pipe(
|
|
707
|
+
Effect.andThen(rawSessionFinalizer(ownership.session, reporter)),
|
|
708
|
+
)
|
|
709
|
+
),
|
|
586
710
|
);
|
|
587
711
|
}
|
|
588
712
|
|
|
@@ -593,12 +717,15 @@ const acquireAgentSession = Effect.fn("WorkerSession.acquireAgentSession")(funct
|
|
|
593
717
|
return yield* Effect.acquireRelease(
|
|
594
718
|
Effect.tryPromise({
|
|
595
719
|
try: () => dependencies.createAgentSession(input),
|
|
596
|
-
catch: agentSessionAcquisitionError,
|
|
720
|
+
catch: agentSessionAcquisitionError("create-session"),
|
|
597
721
|
}).pipe(Effect.map(({ session }) => {
|
|
598
722
|
const ownership: AgentSessionOwnership = { session, runtime: undefined };
|
|
599
723
|
return ownership;
|
|
600
724
|
})),
|
|
601
|
-
agentSessionFinalizer
|
|
725
|
+
(ownership) => agentSessionFinalizer(
|
|
726
|
+
ownership,
|
|
727
|
+
dependencies.reportCleanupFailure,
|
|
728
|
+
),
|
|
602
729
|
);
|
|
603
730
|
});
|
|
604
731
|
|
|
@@ -609,22 +736,27 @@ const acquireAgentSessionRuntime = Effect.fn("WorkerSession.acquireAgentSessionR
|
|
|
609
736
|
) {
|
|
610
737
|
const runtime = yield* Effect.try({
|
|
611
738
|
try: () => dependencies.createRuntime({ session: ownership.session, services }),
|
|
612
|
-
catch: agentSessionAcquisitionError,
|
|
739
|
+
catch: agentSessionAcquisitionError("create-runtime"),
|
|
613
740
|
});
|
|
614
741
|
ownership.runtime = runtime;
|
|
615
742
|
return runtime;
|
|
616
743
|
});
|
|
617
744
|
|
|
618
745
|
const acquireSessionSubscription = Effect.fn("WorkerSession.acquireSubscription")(function* (
|
|
746
|
+
dependencies: WorkerSessionDependencies,
|
|
619
747
|
runtime: OwnedWorkerRuntime,
|
|
620
748
|
handle: DefaultWorkerSessionHandle,
|
|
621
749
|
) {
|
|
622
750
|
return yield* Effect.acquireRelease(
|
|
623
751
|
Effect.try({
|
|
624
752
|
try: () => runtime.session.subscribe((event) => handle.receiveSessionEvent(event)),
|
|
625
|
-
catch: agentSessionAcquisitionError,
|
|
753
|
+
catch: agentSessionAcquisitionError("subscribe-events"),
|
|
626
754
|
}),
|
|
627
|
-
(unsubscribe) =>
|
|
755
|
+
(unsubscribe) => bestEffortCleanup(
|
|
756
|
+
dependencies.reportCleanupFailure,
|
|
757
|
+
"unsubscribe",
|
|
758
|
+
unsubscribe,
|
|
759
|
+
),
|
|
628
760
|
);
|
|
629
761
|
});
|
|
630
762
|
|
|
@@ -638,6 +770,9 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
638
770
|
const { selected, modelRuntime } = yield* prepareChildModelRuntime(options, dependencies);
|
|
639
771
|
const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
|
|
640
772
|
|
|
773
|
+
// createAgentSessionServices owns extension-provider registration and refresh, but
|
|
774
|
+
// Pi 0.80.10 discards that refresh result. Keep this worker-owned probe so provider
|
|
775
|
+
// errors and aborts remain typed acquisition failures before session creation.
|
|
641
776
|
yield* refreshModelRuntime(modelRuntime, definition);
|
|
642
777
|
const model = yield* Effect.try({
|
|
643
778
|
try: () => {
|
|
@@ -649,14 +784,14 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
649
784
|
}
|
|
650
785
|
return resolvedModel;
|
|
651
786
|
},
|
|
652
|
-
catch:
|
|
787
|
+
catch: modelAcquisitionError("resolve-model", "Worker model resolution failed"),
|
|
653
788
|
});
|
|
654
789
|
const sessionManager = yield* Effect.try({
|
|
655
790
|
try: () => dependencies.createSessionManager({
|
|
656
791
|
cwd: options.cwd,
|
|
657
792
|
parentSessionFile: options.parentSessionFile,
|
|
658
793
|
}),
|
|
659
|
-
catch: agentSessionAcquisitionError,
|
|
794
|
+
catch: agentSessionAcquisitionError("create-session-manager"),
|
|
660
795
|
});
|
|
661
796
|
const ownership = yield* acquireAgentSession(dependencies, {
|
|
662
797
|
services,
|
|
@@ -668,7 +803,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
668
803
|
const runtime = yield* acquireAgentSessionRuntime(dependencies, ownership, services);
|
|
669
804
|
yield* Effect.tryPromise({
|
|
670
805
|
try: () => runtime.session.bindExtensions({ mode: "print" }),
|
|
671
|
-
catch: agentSessionAcquisitionError,
|
|
806
|
+
catch: agentSessionAcquisitionError("bind-extensions"),
|
|
672
807
|
});
|
|
673
808
|
|
|
674
809
|
yield* Effect.try({
|
|
@@ -684,7 +819,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
684
819
|
);
|
|
685
820
|
}
|
|
686
821
|
},
|
|
687
|
-
catch: resourceAcquisitionError,
|
|
822
|
+
catch: resourceAcquisitionError("validate-skills"),
|
|
688
823
|
});
|
|
689
824
|
const sessionFile = yield* Effect.try({
|
|
690
825
|
try: () => {
|
|
@@ -693,7 +828,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
693
828
|
}
|
|
694
829
|
return runtime.session.sessionFile;
|
|
695
830
|
},
|
|
696
|
-
catch: agentSessionAcquisitionError,
|
|
831
|
+
catch: agentSessionAcquisitionError("verify-durability"),
|
|
697
832
|
});
|
|
698
833
|
|
|
699
834
|
const handle = new DefaultWorkerSessionHandle(
|
|
@@ -701,15 +836,24 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
701
836
|
definition.lifecycle === "reusable",
|
|
702
837
|
sessionFile,
|
|
703
838
|
scope,
|
|
839
|
+
dependencies.reportCleanupFailure,
|
|
704
840
|
);
|
|
705
|
-
yield* acquireSessionSubscription(runtime, handle);
|
|
841
|
+
yield* acquireSessionSubscription(dependencies, runtime, handle);
|
|
706
842
|
return handle;
|
|
707
843
|
}).pipe(Scope.provide(scope));
|
|
708
844
|
|
|
709
845
|
return yield* acquisition.pipe(
|
|
710
846
|
Effect.catchCause((cause) =>
|
|
711
847
|
Effect.gen(function* () {
|
|
712
|
-
yield* Scope.close(scope, Exit.failCause(cause)).pipe(
|
|
848
|
+
yield* Scope.close(scope, Exit.failCause(cause)).pipe(
|
|
849
|
+
Effect.catchCause((cleanupCause) =>
|
|
850
|
+
reportCleanupFailure(
|
|
851
|
+
dependencies.reportCleanupFailure,
|
|
852
|
+
"scope-close",
|
|
853
|
+
Cause.squash(cleanupCause),
|
|
854
|
+
)
|
|
855
|
+
),
|
|
856
|
+
);
|
|
713
857
|
return yield* Effect.failCause(cause);
|
|
714
858
|
})
|
|
715
859
|
),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { RunId, WorkerId } from "./domain.js";
|
|
3
|
+
|
|
4
|
+
const NonnegativeFinite = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0));
|
|
5
|
+
const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
6
|
+
|
|
7
|
+
const WorkerUsage = Schema.Struct({
|
|
8
|
+
input: NonnegativeFinite,
|
|
9
|
+
output: NonnegativeFinite,
|
|
10
|
+
cacheRead: NonnegativeFinite,
|
|
11
|
+
cacheWrite: NonnegativeFinite,
|
|
12
|
+
cost: NonnegativeFinite,
|
|
13
|
+
contextTokens: NonnegativeFinite,
|
|
14
|
+
turns: NonnegativeInteger,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const WorkerOutcome = Schema.Union([
|
|
18
|
+
Schema.Struct({
|
|
19
|
+
status: Schema.Literal("completed"),
|
|
20
|
+
assistantText: Schema.String,
|
|
21
|
+
}),
|
|
22
|
+
Schema.Struct({
|
|
23
|
+
status: Schema.Literal("ready"),
|
|
24
|
+
assistantText: Schema.String,
|
|
25
|
+
}),
|
|
26
|
+
Schema.Struct({
|
|
27
|
+
status: Schema.Literal("failed"),
|
|
28
|
+
message: Schema.String,
|
|
29
|
+
assistantText: Schema.optionalKey(Schema.String),
|
|
30
|
+
}),
|
|
31
|
+
Schema.Struct({
|
|
32
|
+
status: Schema.Literal("aborted"),
|
|
33
|
+
message: Schema.optionalKey(Schema.String),
|
|
34
|
+
assistantText: Schema.optionalKey(Schema.String),
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const FailureStage = Schema.Literals([
|
|
39
|
+
"startup",
|
|
40
|
+
"prompt",
|
|
41
|
+
"workflow",
|
|
42
|
+
"cancellation",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Canonical schema for settlement details written by the current runtime. */
|
|
46
|
+
export const WorkerSettlementDetails = Schema.Struct({
|
|
47
|
+
eventId: Schema.String,
|
|
48
|
+
sequence: NonnegativeInteger,
|
|
49
|
+
ownerSessionId: Schema.String,
|
|
50
|
+
runId: Schema.String,
|
|
51
|
+
workerId: Schema.String,
|
|
52
|
+
generation: NonnegativeInteger,
|
|
53
|
+
mode: Schema.Literals(["async", "inline"]),
|
|
54
|
+
worker: Schema.String,
|
|
55
|
+
title: Schema.String,
|
|
56
|
+
lifecycle: Schema.Literals(["one-shot", "reusable"]),
|
|
57
|
+
status: Schema.Literals(["completed", "ready", "failed", "aborted"]),
|
|
58
|
+
outcome: WorkerOutcome,
|
|
59
|
+
usage: WorkerUsage,
|
|
60
|
+
startedAt: NonnegativeInteger,
|
|
61
|
+
settledAt: NonnegativeInteger,
|
|
62
|
+
synthesisGroupId: Schema.optionalKey(Schema.String),
|
|
63
|
+
synthesisGroupSize: Schema.optionalKey(NonnegativeInteger),
|
|
64
|
+
sessionFile: Schema.optionalKey(Schema.String),
|
|
65
|
+
failureStage: Schema.optionalKey(FailureStage),
|
|
66
|
+
}).check(
|
|
67
|
+
Schema.makeFilter((settlement) => {
|
|
68
|
+
if (settlement.outcome.status !== settlement.status) {
|
|
69
|
+
return "outcome status must match settlement status";
|
|
70
|
+
}
|
|
71
|
+
if (settlement.settledAt < settlement.startedAt) {
|
|
72
|
+
return "settlement timestamp must not precede start timestamp";
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
settlement.failureStage !== undefined &&
|
|
76
|
+
settlement.status !== "failed" &&
|
|
77
|
+
settlement.status !== "aborted"
|
|
78
|
+
) {
|
|
79
|
+
return "failure stage requires a failed or aborted settlement";
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export interface WorkerSettlementDetails
|
|
85
|
+
extends Schema.Schema.Type<typeof WorkerSettlementDetails> {}
|
|
86
|
+
|
|
87
|
+
export type SettlementFailureStage = NonNullable<
|
|
88
|
+
WorkerSettlementDetails["failureStage"]
|
|
89
|
+
>;
|
|
90
|
+
|
|
91
|
+
/** Runtime refinement preserves the nominal IDs already allocated by the domain. */
|
|
92
|
+
export type WorkerSettlement = Omit<
|
|
93
|
+
WorkerSettlementDetails,
|
|
94
|
+
"runId" | "workerId"
|
|
95
|
+
> & {
|
|
96
|
+
readonly runId: RunId;
|
|
97
|
+
readonly workerId: WorkerId;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const decodeCurrentWorkerSettlement = Schema.decodeUnknownResult(
|
|
101
|
+
WorkerSettlementDetails,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export function decodePersistedWorkerSettlementDetails(value: unknown) {
|
|
105
|
+
return decodeCurrentWorkerSettlement(value);
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachwill/pi-orchestrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Concurrent worker orchestration for Pi",
|
|
6
6
|
"files": ["extension/", "examples/", "README.md", "LICENSE"],
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"extensions": ["./extension/index.ts"]
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"typecheck": "tsc --noEmit",
|
|
24
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
25
25
|
"test": "bun test"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@earendil-works/pi-ai": "0.80.10",
|
|
40
40
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
41
41
|
"@earendil-works/pi-tui": "0.80.10",
|
|
42
|
+
"@types/bun": "^1.3.14",
|
|
42
43
|
"@types/node": "^22.19.17",
|
|
43
44
|
"typebox": "^1.1.37",
|
|
44
45
|
"typescript": "^5.9.3"
|