@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.
@@ -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 resourceLoaderFinalizer(resourceLoader: ResourceLoader): Effect.Effect<void> {
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
- }).pipe(Effect.ignoreCause);
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(disposeWorkerSession(this.scope));
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(cause: unknown, fallback: string): WorkerModelAcquisitionError {
399
- return new WorkerModelAcquisitionError({ message: describeError(cause, fallback), cause });
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(cause: unknown): WorkerResourceAcquisitionError {
403
- return new WorkerResourceAcquisitionError({
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(cause: unknown): WorkerAgentSessionAcquisitionError {
410
- return new WorkerAgentSessionAcquisitionError({
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: (cause) => modelAcquisitionError(cause, "Worker model refresh failed"),
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(modelAcquisitionError(cause, "Worker model refresh failed"));
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(modelAcquisitionError(cause, "Worker model refresh failed"));
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: (cause) => modelAcquisitionError(cause, "Worker model selection failed"),
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: (cause) => modelAcquisitionError(cause, "Worker model runtime creation failed"),
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: (cause) => modelAcquisitionError(cause, "Worker model provider registration failed"),
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: (cause) => modelAcquisitionError(cause, "Worker model resolution failed"),
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: (cause) => modelAcquisitionError(cause, "Worker model authentication failed"),
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: (cause) => modelAcquisitionError(cause, "Worker model runtime key setup failed"),
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(Effect.ignoreCause);
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(services.resourceLoader),
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(session: WorkerAgentSession): Effect.Effect<void> {
577
- return Effect.sync(() => session.dispose()).pipe(Effect.ignoreCause);
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(ownership: AgentSessionOwnership): Effect.Effect<void> {
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(() => runtime.dispose()).pipe(
584
- Effect.catch(() => rawSessionFinalizer(ownership.session)),
585
- Effect.ignoreCause,
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) => Effect.sync(unsubscribe).pipe(Effect.ignoreCause),
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: (cause) => modelAcquisitionError(cause, "Worker model resolution failed"),
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(Effect.ignoreCause);
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.2.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"