@zachwill/pi-orchestrate 0.2.1 → 0.4.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.
@@ -10,6 +10,7 @@ import {
10
10
  DefaultPackageManager,
11
11
  ModelRuntime,
12
12
  type ModelRegistry,
13
+ type PromptOptions,
13
14
  type ResourceLoader,
14
15
  SessionManager,
15
16
  SettingsManager,
@@ -18,7 +19,7 @@ import {
18
19
  type CreateAgentSessionResult,
19
20
  type DefaultResourceLoader,
20
21
  } from "@earendil-works/pi-coding-agent";
21
- import { Effect, Exit, Schema, Scope } from "effect";
22
+ import { Cause, Effect, Exit, Schema, Scope } from "effect";
22
23
  import type {
23
24
  WorkerDefinition,
24
25
  WorkerMessageDirection,
@@ -27,13 +28,14 @@ import type {
27
28
  } from "./domain.js";
28
29
 
29
30
  const DIRECT_CHILD_BOUNDARY =
30
- "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.";
31
+ "You are a direct child worker session. Do not spawn, delegate to, or orchestrate descendant Pi worker sessions. Complete the assigned task yourself and return the result directly to the parent orchestrator.";
31
32
  const ORCHESTRATE_PACKAGE_ROOT = canonicalPath(fileURLToPath(new URL("..", import.meta.url)));
32
33
 
33
34
  interface WorkerAgentSession {
34
35
  readonly sessionFile: string | undefined;
35
36
  readonly messages: AgentMessage[];
36
- prompt(instructions: string): Promise<void>;
37
+ prompt(instructions: string, options?: PromptOptions): Promise<void>;
38
+ abortCompaction(): void;
37
39
  abort(): Promise<void>;
38
40
  dispose(): void;
39
41
  bindExtensions(bindings: { mode: "print" }): Promise<void>;
@@ -75,21 +77,66 @@ export interface WorkerSessionFactory {
75
77
  create(options: WorkerSessionFactoryOptions): Promise<WorkerSessionHandle>;
76
78
  }
77
79
 
80
+ const WorkerModelAcquisitionOperation = Schema.Literals([
81
+ "select-model",
82
+ "create-model-runtime",
83
+ "register-providers",
84
+ "refresh-providers",
85
+ "resolve-model",
86
+ "authenticate-model",
87
+ "configure-runtime-auth",
88
+ ]);
89
+ type WorkerModelAcquisitionOperation = typeof WorkerModelAcquisitionOperation.Type;
90
+
91
+ const WorkerResourceAcquisitionOperation = Schema.Literals([
92
+ "create-settings",
93
+ "resolve-extensions",
94
+ "create-services",
95
+ "validate-skills",
96
+ ]);
97
+ type WorkerResourceAcquisitionOperation = typeof WorkerResourceAcquisitionOperation.Type;
98
+
99
+ const WorkerAgentSessionAcquisitionOperation = Schema.Literals([
100
+ "create-session-manager",
101
+ "create-session",
102
+ "create-runtime",
103
+ "bind-extensions",
104
+ "verify-durability",
105
+ "subscribe-events",
106
+ ]);
107
+ type WorkerAgentSessionAcquisitionOperation = typeof WorkerAgentSessionAcquisitionOperation.Type;
108
+
78
109
  export class WorkerModelAcquisitionError extends Schema.TaggedErrorClass<WorkerModelAcquisitionError>()(
79
110
  "WorkerSession.ModelAcquisitionError",
80
- { message: Schema.String, cause: Schema.Defect() },
111
+ { operation: WorkerModelAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
81
112
  ) {}
82
113
 
83
114
  export class WorkerResourceAcquisitionError extends Schema.TaggedErrorClass<WorkerResourceAcquisitionError>()(
84
115
  "WorkerSession.ResourceAcquisitionError",
85
- { message: Schema.String, cause: Schema.Defect() },
116
+ { operation: WorkerResourceAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
86
117
  ) {}
87
118
 
88
119
  export class WorkerAgentSessionAcquisitionError extends Schema.TaggedErrorClass<WorkerAgentSessionAcquisitionError>()(
89
120
  "WorkerSession.AgentSessionAcquisitionError",
90
- { message: Schema.String, cause: Schema.Defect() },
121
+ { operation: WorkerAgentSessionAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
91
122
  ) {}
92
123
 
124
+ const WorkerSessionCleanupOperation = Schema.Literals([
125
+ "unsubscribe",
126
+ "runtime",
127
+ "raw-session",
128
+ "resource-loader",
129
+ "scope-close",
130
+ ]);
131
+ export type WorkerSessionCleanupOperation = typeof WorkerSessionCleanupOperation.Type;
132
+
133
+ export interface WorkerSessionCleanupFailure {
134
+ readonly operation: WorkerSessionCleanupOperation;
135
+ readonly cause: unknown;
136
+ }
137
+
138
+ export type WorkerSessionCleanupReporter = (failure: WorkerSessionCleanupFailure) => void;
139
+
93
140
  interface SettingsManagerInput {
94
141
  cwd: string;
95
142
  agentDir: string;
@@ -137,6 +184,7 @@ export interface WorkerSessionDependencies {
137
184
  createServices(input: ServicesInput): Promise<AgentSessionServices>;
138
185
  createAgentSession(input: AgentSessionInput): Promise<{ session: WorkerAgentSession }>;
139
186
  createRuntime(input: RuntimeInput): OwnedWorkerRuntime;
187
+ reportCleanupFailure(failure: WorkerSessionCleanupFailure): void;
140
188
  }
141
189
 
142
190
  const defaultDependencies: WorkerSessionDependencies = {
@@ -163,6 +211,12 @@ const defaultDependencies: WorkerSessionDependencies = {
163
211
  throw new Error("Worker sessions do not support runtime replacement");
164
212
  },
165
213
  ) as unknown as OwnedWorkerRuntime,
214
+ reportCleanupFailure: ({ operation }) => {
215
+ process.emitWarning("Worker session cleanup failed", {
216
+ code: "PI_ORCHESTRATE_WORKER_CLEANUP",
217
+ detail: `Operation: ${operation}`,
218
+ });
219
+ },
166
220
  };
167
221
 
168
222
  export function resolveWorkerModel(
@@ -204,11 +258,41 @@ export function isOrchestrationExtensionPath(path: string): boolean {
204
258
  return isWithin(canonicalPath(path), ORCHESTRATE_PACKAGE_ROOT);
205
259
  }
206
260
 
207
- function resourceLoaderFinalizer(resourceLoader: ResourceLoader): Effect.Effect<void> {
261
+ function reportCleanupFailure(
262
+ reporter: WorkerSessionCleanupReporter,
263
+ operation: WorkerSessionCleanupOperation,
264
+ cause: unknown,
265
+ ): Effect.Effect<void> {
208
266
  return Effect.sync(() => {
267
+ try {
268
+ reporter({ operation, cause });
269
+ } catch {
270
+ // Reporting must never make best-effort cleanup fail.
271
+ }
272
+ });
273
+ }
274
+
275
+ function bestEffortCleanup(
276
+ reporter: WorkerSessionCleanupReporter,
277
+ operation: WorkerSessionCleanupOperation,
278
+ cleanup: () => void | Promise<void>,
279
+ ): Effect.Effect<void> {
280
+ return Effect.tryPromise({
281
+ try: async () => cleanup(),
282
+ catch: (cause) => cause,
283
+ }).pipe(
284
+ Effect.catch((cause) => reportCleanupFailure(reporter, operation, cause)),
285
+ );
286
+ }
287
+
288
+ function resourceLoaderFinalizer(
289
+ resourceLoader: ResourceLoader,
290
+ reporter: WorkerSessionCleanupReporter,
291
+ ): Effect.Effect<void> {
292
+ return bestEffortCleanup(reporter, "resource-loader", () => {
209
293
  if (!("dispose" in resourceLoader) || typeof resourceLoader.dispose !== "function") return;
210
294
  resourceLoader.dispose();
211
- }).pipe(Effect.ignoreCause);
295
+ });
212
296
  }
213
297
 
214
298
  function describeError(error: unknown, fallback: string): string {
@@ -267,6 +351,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
267
351
  private readonly reusable: boolean,
268
352
  sessionFile: string,
269
353
  private readonly scope: Scope.Closeable,
354
+ private readonly cleanupReporter: WorkerSessionCleanupReporter,
270
355
  ) {
271
356
  this.sessionFile = sessionFile;
272
357
  }
@@ -323,7 +408,10 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
323
408
  const previousAssistant = lastAssistant(this.runtime.session.messages);
324
409
  let failureMessage: string | undefined;
325
410
  try {
326
- await this.runtime.session.prompt(instructions);
411
+ await this.runtime.session.prompt(instructions, {
412
+ expandPromptTemplates: false,
413
+ source: "extension",
414
+ });
327
415
  } catch (error) {
328
416
  failureMessage = describeError(error, "Worker prompt failed");
329
417
  } finally {
@@ -346,7 +434,21 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
346
434
 
347
435
  async abort(): Promise<void> {
348
436
  if (this.prompting) this.abortRequested = true;
349
- await this.runtime.session.abort();
437
+
438
+ let firstFailure: unknown;
439
+ try {
440
+ this.runtime.session.abortCompaction();
441
+ } catch (error) {
442
+ firstFailure = error;
443
+ }
444
+
445
+ try {
446
+ await this.runtime.session.abort();
447
+ } catch (error) {
448
+ if (firstFailure === undefined) firstFailure = error;
449
+ }
450
+
451
+ if (firstFailure !== undefined) throw firstFailure;
350
452
  }
351
453
 
352
454
  dispose(): Promise<void> {
@@ -357,7 +459,9 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
357
459
  this.usageListeners.clear();
358
460
  this.activityListeners.clear();
359
461
  this.messageDirectionListeners.clear();
360
- this.disposePromise = Effect.runPromise(disposeWorkerSession(this.scope));
462
+ this.disposePromise = Effect.runPromise(
463
+ disposeWorkerSession(this.scope, this.cleanupReporter),
464
+ );
361
465
  return this.disposePromise;
362
466
  }
363
467
 
@@ -395,19 +499,32 @@ function selectedModelCoordinates(
395
499
  throw new Error(`Worker "${definition.name}" has no configured model and no parent model is available`);
396
500
  }
397
501
 
398
- function modelAcquisitionError(cause: unknown, fallback: string): WorkerModelAcquisitionError {
399
- return new WorkerModelAcquisitionError({ message: describeError(cause, fallback), cause });
502
+ function modelAcquisitionError(
503
+ operation: WorkerModelAcquisitionOperation,
504
+ fallback: string,
505
+ ): (cause: unknown) => WorkerModelAcquisitionError {
506
+ return (cause) => new WorkerModelAcquisitionError({
507
+ operation,
508
+ message: describeError(cause, fallback),
509
+ cause,
510
+ });
400
511
  }
401
512
 
402
- function resourceAcquisitionError(cause: unknown): WorkerResourceAcquisitionError {
403
- return new WorkerResourceAcquisitionError({
513
+ function resourceAcquisitionError(
514
+ operation: WorkerResourceAcquisitionOperation,
515
+ ): (cause: unknown) => WorkerResourceAcquisitionError {
516
+ return (cause) => new WorkerResourceAcquisitionError({
517
+ operation,
404
518
  message: describeError(cause, "Worker resource acquisition failed"),
405
519
  cause,
406
520
  });
407
521
  }
408
522
 
409
- function agentSessionAcquisitionError(cause: unknown): WorkerAgentSessionAcquisitionError {
410
- return new WorkerAgentSessionAcquisitionError({
523
+ function agentSessionAcquisitionError(
524
+ operation: WorkerAgentSessionAcquisitionOperation,
525
+ ): (cause: unknown) => WorkerAgentSessionAcquisitionError {
526
+ return (cause) => new WorkerAgentSessionAcquisitionError({
527
+ operation,
411
528
  message: describeError(cause, "Worker agent session acquisition failed"),
412
529
  cause,
413
530
  });
@@ -417,20 +534,24 @@ const refreshModelRuntime = Effect.fn("WorkerSession.refreshModelRuntime")(funct
417
534
  modelRuntime: ModelRuntime,
418
535
  definition: WorkerDefinition,
419
536
  ) {
537
+ const refreshError = modelAcquisitionError(
538
+ "refresh-providers",
539
+ "Worker model refresh failed",
540
+ );
420
541
  const result = yield* Effect.tryPromise({
421
542
  try: () => modelRuntime.refresh({ allowNetwork: false }),
422
- catch: (cause) => modelAcquisitionError(cause, "Worker model refresh failed"),
543
+ catch: refreshError,
423
544
  });
424
545
  if (result.errors.size > 0) {
425
546
  const errors = [...result.errors].map(([id, error]) => `${id}: ${error.message}`).join("; ");
426
547
  const cause = new Error(
427
548
  `Worker "${definition.name}" failed to refresh child model providers: ${errors}`,
428
549
  );
429
- return yield* Effect.fail(modelAcquisitionError(cause, "Worker model refresh failed"));
550
+ return yield* Effect.fail(refreshError(cause));
430
551
  }
431
552
  if (result.aborted) {
432
553
  const cause = new Error(`Worker "${definition.name}" child model refresh was aborted`);
433
- return yield* Effect.fail(modelAcquisitionError(cause, "Worker model refresh failed"));
554
+ return yield* Effect.fail(refreshError(cause));
434
555
  }
435
556
  });
436
557
 
@@ -440,14 +561,14 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
440
561
  ) {
441
562
  const selected = yield* Effect.try({
442
563
  try: () => selectedModelCoordinates(options.definition, options.parentModel),
443
- catch: (cause) => modelAcquisitionError(cause, "Worker model selection failed"),
564
+ catch: modelAcquisitionError("select-model", "Worker model selection failed"),
444
565
  });
445
566
  const modelRuntime = yield* Effect.tryPromise({
446
567
  try: () => dependencies.createModelRuntime({
447
568
  authPath: join(options.agentDir, "auth.json"),
448
569
  modelsPath: join(options.agentDir, "models.json"),
449
570
  }),
450
- catch: (cause) => modelAcquisitionError(cause, "Worker model runtime creation failed"),
571
+ catch: modelAcquisitionError("create-model-runtime", "Worker model runtime creation failed"),
451
572
  });
452
573
 
453
574
  yield* Effect.try({
@@ -457,18 +578,20 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
457
578
  if (config) modelRuntime.registerProvider(providerId, { ...config });
458
579
  }
459
580
  },
460
- catch: (cause) => modelAcquisitionError(cause, "Worker model provider registration failed"),
581
+ catch: modelAcquisitionError("register-providers", "Worker model provider registration failed"),
461
582
  });
583
+ // Worker-session owns this pre-service refresh so dynamic parent providers exist
584
+ // before selected-model authentication is copied.
462
585
  yield* refreshModelRuntime(modelRuntime, options.definition);
463
586
 
464
587
  const model = yield* Effect.try({
465
588
  try: () => modelRuntime.getModel(selected.provider, selected.modelId),
466
- catch: (cause) => modelAcquisitionError(cause, "Worker model resolution failed"),
589
+ catch: modelAcquisitionError("resolve-model", "Worker model resolution failed"),
467
590
  });
468
591
  if (model) {
469
592
  const auth = yield* Effect.tryPromise({
470
593
  try: () => options.modelRegistry.getApiKeyAndHeaders(model),
471
- catch: (cause) => modelAcquisitionError(cause, "Worker model authentication failed"),
594
+ catch: modelAcquisitionError("authenticate-model", "Worker model authentication failed"),
472
595
  });
473
596
  if (auth.ok) {
474
597
  yield* Effect.tryPromise({
@@ -480,7 +603,7 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
480
603
  await modelRuntime.setRuntimeApiKey(selected.provider, auth.apiKey);
481
604
  }
482
605
  },
483
- catch: (cause) => modelAcquisitionError(cause, "Worker model runtime key setup failed"),
606
+ catch: modelAcquisitionError("configure-runtime-auth", "Worker model runtime key setup failed"),
484
607
  });
485
608
  }
486
609
  }
@@ -515,8 +638,13 @@ function contextLoaderOptions(
515
638
 
516
639
  const disposeWorkerSession = Effect.fn("WorkerSession.dispose")(function* (
517
640
  scope: Scope.Closeable,
641
+ reporter: WorkerSessionCleanupReporter,
518
642
  ) {
519
- yield* Scope.close(scope, Exit.void).pipe(Effect.ignoreCause);
643
+ yield* Scope.close(scope, Exit.void).pipe(
644
+ Effect.catchCause((cause) =>
645
+ reportCleanupFailure(reporter, "scope-close", Cause.squash(cause))
646
+ ),
647
+ );
520
648
  });
521
649
 
522
650
  const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(function* (
@@ -532,7 +660,7 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
532
660
  projectTrusted: options.projectTrusted,
533
661
  compaction: definition.compaction,
534
662
  }),
535
- catch: resourceAcquisitionError,
663
+ catch: resourceAcquisitionError("create-settings"),
536
664
  });
537
665
  const extensionPaths = yield* Effect.tryPromise({
538
666
  try: () => dependencies.resolveExtensionPaths({
@@ -540,7 +668,7 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
540
668
  agentDir: options.agentDir,
541
669
  settingsManager,
542
670
  }),
543
- catch: resourceAcquisitionError,
671
+ catch: resourceAcquisitionError("resolve-extensions"),
544
672
  }).pipe(Effect.map((paths) => paths.filter((path) => !isOrchestrationExtensionPath(path))));
545
673
 
546
674
  return yield* Effect.acquireRelease(
@@ -562,9 +690,12 @@ const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(functio
562
690
  appendSystemPrompt: [definition.systemPrompt, DIRECT_CHILD_BOUNDARY],
563
691
  },
564
692
  }),
565
- catch: resourceAcquisitionError,
693
+ catch: resourceAcquisitionError("create-services"),
566
694
  }),
567
- (services) => resourceLoaderFinalizer(services.resourceLoader),
695
+ (services) => resourceLoaderFinalizer(
696
+ services.resourceLoader,
697
+ dependencies.reportCleanupFailure,
698
+ ),
568
699
  );
569
700
  });
570
701
 
@@ -573,16 +704,28 @@ interface AgentSessionOwnership {
573
704
  runtime: OwnedWorkerRuntime | undefined;
574
705
  }
575
706
 
576
- function rawSessionFinalizer(session: WorkerAgentSession): Effect.Effect<void> {
577
- return Effect.sync(() => session.dispose()).pipe(Effect.ignoreCause);
707
+ function rawSessionFinalizer(
708
+ session: WorkerAgentSession,
709
+ reporter: WorkerSessionCleanupReporter,
710
+ ): Effect.Effect<void> {
711
+ return bestEffortCleanup(reporter, "raw-session", () => session.dispose());
578
712
  }
579
713
 
580
- function agentSessionFinalizer(ownership: AgentSessionOwnership): Effect.Effect<void> {
714
+ function agentSessionFinalizer(
715
+ ownership: AgentSessionOwnership,
716
+ reporter: WorkerSessionCleanupReporter,
717
+ ): Effect.Effect<void> {
581
718
  const runtime = ownership.runtime;
582
- if (!runtime) return rawSessionFinalizer(ownership.session);
583
- return Effect.tryPromise(() => runtime.dispose()).pipe(
584
- Effect.catch(() => rawSessionFinalizer(ownership.session)),
585
- Effect.ignoreCause,
719
+ if (!runtime) return rawSessionFinalizer(ownership.session, reporter);
720
+ return Effect.tryPromise({
721
+ try: () => runtime.dispose(),
722
+ catch: (cause) => cause,
723
+ }).pipe(
724
+ Effect.catch((cause) =>
725
+ reportCleanupFailure(reporter, "runtime", cause).pipe(
726
+ Effect.andThen(rawSessionFinalizer(ownership.session, reporter)),
727
+ )
728
+ ),
586
729
  );
587
730
  }
588
731
 
@@ -593,12 +736,15 @@ const acquireAgentSession = Effect.fn("WorkerSession.acquireAgentSession")(funct
593
736
  return yield* Effect.acquireRelease(
594
737
  Effect.tryPromise({
595
738
  try: () => dependencies.createAgentSession(input),
596
- catch: agentSessionAcquisitionError,
739
+ catch: agentSessionAcquisitionError("create-session"),
597
740
  }).pipe(Effect.map(({ session }) => {
598
741
  const ownership: AgentSessionOwnership = { session, runtime: undefined };
599
742
  return ownership;
600
743
  })),
601
- agentSessionFinalizer,
744
+ (ownership) => agentSessionFinalizer(
745
+ ownership,
746
+ dependencies.reportCleanupFailure,
747
+ ),
602
748
  );
603
749
  });
604
750
 
@@ -609,22 +755,27 @@ const acquireAgentSessionRuntime = Effect.fn("WorkerSession.acquireAgentSessionR
609
755
  ) {
610
756
  const runtime = yield* Effect.try({
611
757
  try: () => dependencies.createRuntime({ session: ownership.session, services }),
612
- catch: agentSessionAcquisitionError,
758
+ catch: agentSessionAcquisitionError("create-runtime"),
613
759
  });
614
760
  ownership.runtime = runtime;
615
761
  return runtime;
616
762
  });
617
763
 
618
764
  const acquireSessionSubscription = Effect.fn("WorkerSession.acquireSubscription")(function* (
765
+ dependencies: WorkerSessionDependencies,
619
766
  runtime: OwnedWorkerRuntime,
620
767
  handle: DefaultWorkerSessionHandle,
621
768
  ) {
622
769
  return yield* Effect.acquireRelease(
623
770
  Effect.try({
624
771
  try: () => runtime.session.subscribe((event) => handle.receiveSessionEvent(event)),
625
- catch: agentSessionAcquisitionError,
772
+ catch: agentSessionAcquisitionError("subscribe-events"),
626
773
  }),
627
- (unsubscribe) => Effect.sync(unsubscribe).pipe(Effect.ignoreCause),
774
+ (unsubscribe) => bestEffortCleanup(
775
+ dependencies.reportCleanupFailure,
776
+ "unsubscribe",
777
+ unsubscribe,
778
+ ),
628
779
  );
629
780
  });
630
781
 
@@ -638,6 +789,9 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
638
789
  const { selected, modelRuntime } = yield* prepareChildModelRuntime(options, dependencies);
639
790
  const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
640
791
 
792
+ // createAgentSessionServices owns extension-provider registration and refresh, but
793
+ // Pi 0.80.10 discards that refresh result. Keep this worker-owned probe so provider
794
+ // errors and aborts remain typed acquisition failures before session creation.
641
795
  yield* refreshModelRuntime(modelRuntime, definition);
642
796
  const model = yield* Effect.try({
643
797
  try: () => {
@@ -649,14 +803,14 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
649
803
  }
650
804
  return resolvedModel;
651
805
  },
652
- catch: (cause) => modelAcquisitionError(cause, "Worker model resolution failed"),
806
+ catch: modelAcquisitionError("resolve-model", "Worker model resolution failed"),
653
807
  });
654
808
  const sessionManager = yield* Effect.try({
655
809
  try: () => dependencies.createSessionManager({
656
810
  cwd: options.cwd,
657
811
  parentSessionFile: options.parentSessionFile,
658
812
  }),
659
- catch: agentSessionAcquisitionError,
813
+ catch: agentSessionAcquisitionError("create-session-manager"),
660
814
  });
661
815
  const ownership = yield* acquireAgentSession(dependencies, {
662
816
  services,
@@ -668,7 +822,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
668
822
  const runtime = yield* acquireAgentSessionRuntime(dependencies, ownership, services);
669
823
  yield* Effect.tryPromise({
670
824
  try: () => runtime.session.bindExtensions({ mode: "print" }),
671
- catch: agentSessionAcquisitionError,
825
+ catch: agentSessionAcquisitionError("bind-extensions"),
672
826
  });
673
827
 
674
828
  yield* Effect.try({
@@ -684,7 +838,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
684
838
  );
685
839
  }
686
840
  },
687
- catch: resourceAcquisitionError,
841
+ catch: resourceAcquisitionError("validate-skills"),
688
842
  });
689
843
  const sessionFile = yield* Effect.try({
690
844
  try: () => {
@@ -693,7 +847,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
693
847
  }
694
848
  return runtime.session.sessionFile;
695
849
  },
696
- catch: agentSessionAcquisitionError,
850
+ catch: agentSessionAcquisitionError("verify-durability"),
697
851
  });
698
852
 
699
853
  const handle = new DefaultWorkerSessionHandle(
@@ -701,15 +855,24 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
701
855
  definition.lifecycle === "reusable",
702
856
  sessionFile,
703
857
  scope,
858
+ dependencies.reportCleanupFailure,
704
859
  );
705
- yield* acquireSessionSubscription(runtime, handle);
860
+ yield* acquireSessionSubscription(dependencies, runtime, handle);
706
861
  return handle;
707
862
  }).pipe(Scope.provide(scope));
708
863
 
709
864
  return yield* acquisition.pipe(
710
865
  Effect.catchCause((cause) =>
711
866
  Effect.gen(function* () {
712
- yield* Scope.close(scope, Exit.failCause(cause)).pipe(Effect.ignoreCause);
867
+ yield* Scope.close(scope, Exit.failCause(cause)).pipe(
868
+ Effect.catchCause((cleanupCause) =>
869
+ reportCleanupFailure(
870
+ dependencies.reportCleanupFailure,
871
+ "scope-close",
872
+ Cause.squash(cleanupCause),
873
+ )
874
+ ),
875
+ );
713
876
  return yield* Effect.failCause(cause);
714
877
  })
715
878
  ),
@@ -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.2.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Concurrent worker orchestration for Pi",
6
6
  "files": ["extension/", "examples/", "README.md", "LICENSE"],