@zachwill/pi-orchestrate 0.9.2 → 0.11.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.
@@ -1,6 +1,5 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
5
4
  import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
6
5
  import {
@@ -20,26 +19,22 @@ import {
20
19
  } from "@earendil-works/pi-coding-agent";
21
20
  import {
22
21
  Cause,
23
- Context,
24
- Deferred,
25
22
  Effect,
26
23
  Exit,
27
- FiberSet,
28
- Layer,
29
24
  Schema,
30
25
  Scope,
31
- SynchronizedRef,
32
26
  } from "effect";
27
+ import type { WorkerDefinition } from "../catalog/definition.ts";
33
28
  import type {
34
- WorkerDefinition,
35
29
  WorkerMessageDirection,
36
30
  WorkerOutcome,
37
31
  WorkerUsage,
38
- } from "./domain.js";
32
+ } from "../orchestration/model.ts";
33
+ import { PACKAGE_ROOT } from "../package-root.ts";
39
34
 
40
35
  const DIRECT_CHILD_BOUNDARY =
41
36
  "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.";
42
- const ORCHESTRATE_PACKAGE_ROOT = canonicalPath(fileURLToPath(new URL("..", import.meta.url)));
37
+ const ORCHESTRATE_PACKAGE_ROOT = canonicalPath(PACKAGE_ROOT);
43
38
 
44
39
  interface WorkerAgentSession {
45
40
  readonly sessionFile: string | undefined;
@@ -131,11 +126,6 @@ export class WorkerAgentSessionAcquisitionError extends Schema.TaggedError<Worke
131
126
  { operation: WorkerAgentSessionAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
132
127
  ) {}
133
128
 
134
- export class WorkerSessionAcquisitionClosedError extends Schema.TaggedError<WorkerSessionAcquisitionClosedError>()(
135
- "WorkerSession.AcquisitionClosedError",
136
- { message: Schema.String },
137
- ) {}
138
-
139
129
  const WorkerSessionAbortOperation = Schema.Literals([
140
130
  "abort-compaction",
141
131
  "abort-prompt",
@@ -155,29 +145,10 @@ export class WorkerSessionAbortError extends Schema.TaggedError<WorkerSessionAbo
155
145
  },
156
146
  ) {}
157
147
 
158
- export type WorkerSessionAcquisitionError =
148
+ export type WorkerSessionCreationError =
159
149
  | WorkerModelAcquisitionError
160
150
  | WorkerResourceAcquisitionError
161
- | WorkerAgentSessionAcquisitionError
162
- | WorkerSessionAcquisitionClosedError;
163
-
164
- export interface ChildSessionsService {
165
- /**
166
- * Acquires a child session through a process-owned producer. The adopter must
167
- * synchronously install ownership before returning a value. Returning undefined
168
- * rejects adoption and leaves ChildSessions responsible for disposal.
169
- */
170
- readonly acquire: <Adopted>(
171
- options: ChildSessionOptions,
172
- adopt: (session: WorkerSessionHandle) => Adopted | undefined,
173
- ) => Effect.Effect<Adopted | undefined, WorkerSessionAcquisitionError>;
174
- /** Closes every handoff without waiting for uncancellable Pi calls. */
175
- readonly shutdown: () => Effect.Effect<void>;
176
- }
177
-
178
- export class ChildSessions extends Context.Service<ChildSessions, ChildSessionsService>()(
179
- "@zachwill/pi-orchestrate/ChildSessions",
180
- ) {}
151
+ | WorkerAgentSessionAcquisitionError;
181
152
 
182
153
  const WorkerSessionCleanupOperation = Schema.Literals([
183
154
  "unsubscribe",
@@ -249,7 +220,12 @@ export interface WorkerSessionDependencies {
249
220
  onReclamationOpenObserved(): Effect.Effect<void>;
250
221
  }
251
222
 
252
- const defaultDependencies: WorkerSessionDependencies = {
223
+ type WorkerSessionAcquisitionDependencies = Omit<
224
+ WorkerSessionDependencies,
225
+ "beforeAdoptionReservation" | "onReclamationOpenObserved"
226
+ >;
227
+
228
+ const defaultWorkerSessionDependencies: WorkerSessionAcquisitionDependencies = {
253
229
  createSettingsManager: ({ cwd, agentDir, projectTrusted, compaction }) => {
254
230
  const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
255
231
  if (compaction !== undefined) settingsManager.applyOverrides({ compaction: { ...compaction } });
@@ -279,8 +255,6 @@ const defaultDependencies: WorkerSessionDependencies = {
279
255
  detail: `Operation: ${operation}`,
280
256
  });
281
257
  },
282
- beforeAdoptionReservation: () => Effect.void,
283
- onReclamationOpenObserved: () => Effect.void,
284
258
  };
285
259
 
286
260
  function canonicalPath(path: string): string {
@@ -411,6 +385,8 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
411
385
  ): Effect.Effect<DefaultWorkerSessionHandle> {
412
386
  return Effect.gen(function* () {
413
387
  const handle = new DefaultWorkerSessionHandle(runtime, interactive, sessionFile);
388
+ // Cache the disposal Effect so repeated or concurrent callers join the same
389
+ // uninterruptible Scope.close rather than skipping an in-progress disposal.
414
390
  handle.disposeOperation = yield* Effect.cached(
415
391
  Effect.sync(() => handle.beginDispose()).pipe(
416
392
  Effect.andThen(disposeWorkerSession(scope, cleanupReporter)),
@@ -483,6 +459,8 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
483
459
  prompt(instructions: string): Effect.Effect<WorkerOutcome, never> {
484
460
  return Effect.fn("WorkerSession.prompt")(function* (this: DefaultWorkerSessionHandle) {
485
461
  const completion = yield* Effect.sync(() => this.startPrompt(instructions));
462
+ // Interrupting this waiter does not cancel Pi's prompt. abort() owns physical
463
+ // cancellation, and completePrompt releases Active state when it settles.
486
464
  const { failureMessage, message, prompt } = yield* Effect.promise(() => completion);
487
465
  const text = assistantText(message);
488
466
  const assistantPayload = text === undefined ? {} : { assistantText: text };
@@ -582,7 +560,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
582
560
  }
583
561
 
584
562
  function safelyNotify(callback: () => void): void {
585
- try { callback(); } catch { /* One cleanup/listener cannot block another. */ }
563
+ try { callback(); } catch { /* One observer cannot block the others. */ }
586
564
  }
587
565
 
588
566
  function subscribe<T>(listeners: Set<(value: T) => void>, listener: (value: T) => void): () => void {
@@ -656,7 +634,7 @@ const refreshModelRuntime = Effect.fn("WorkerSession.refreshModelRuntime")(funct
656
634
 
657
635
  const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRuntime")(function* (
658
636
  options: ChildSessionOptions,
659
- dependencies: WorkerSessionDependencies,
637
+ dependencies: WorkerSessionAcquisitionDependencies,
660
638
  ) {
661
639
  const selected = yield* Effect.try({
662
640
  try: () => selectedModelCoordinates(options.definition, options.parentModel),
@@ -749,7 +727,7 @@ const disposeWorkerSession = Effect.fn("WorkerSession.dispose")(function* (
749
727
 
750
728
  const acquireWorkerServices = Effect.fn("WorkerSession.acquireServices")(function* (
751
729
  options: ChildSessionOptions,
752
- dependencies: WorkerSessionDependencies,
730
+ dependencies: WorkerSessionAcquisitionDependencies,
753
731
  modelRuntime: ModelRuntime,
754
732
  ) {
755
733
  const definition = options.definition;
@@ -819,13 +797,15 @@ function agentSessionFinalizer(
819
797
  () => ownership.session.dispose(),
820
798
  );
821
799
  const runtime = ownership.runtime;
800
+ // The runtime owns normal raw-session disposal after creation. Before that
801
+ // handoff, or if runtime disposal fails, close the raw session directly.
822
802
  return runtime
823
803
  ? bestEffortCleanup(reporter, "runtime", () => runtime.dispose(), disposeRawSession)
824
804
  : disposeRawSession;
825
805
  }
826
806
 
827
807
  const acquireAgentSession = Effect.fn("WorkerSession.acquireAgentSession")(function* (
828
- dependencies: WorkerSessionDependencies,
808
+ dependencies: WorkerSessionAcquisitionDependencies,
829
809
  input: AgentSessionInput,
830
810
  ) {
831
811
  return yield* Effect.acquireRelease(
@@ -844,7 +824,7 @@ const acquireAgentSession = Effect.fn("WorkerSession.acquireAgentSession")(funct
844
824
  });
845
825
 
846
826
  const acquireAgentSessionRuntime = Effect.fn("WorkerSession.acquireAgentSessionRuntime")(function* (
847
- dependencies: WorkerSessionDependencies,
827
+ dependencies: WorkerSessionAcquisitionDependencies,
848
828
  ownership: AgentSessionOwnership,
849
829
  services: AgentSessionServices,
850
830
  ) {
@@ -857,7 +837,7 @@ const acquireAgentSessionRuntime = Effect.fn("WorkerSession.acquireAgentSessionR
857
837
  });
858
838
 
859
839
  const acquireSessionSubscription = Effect.fn("WorkerSession.acquireSubscription")(function* (
860
- dependencies: WorkerSessionDependencies,
840
+ dependencies: WorkerSessionAcquisitionDependencies,
861
841
  runtime: OwnedWorkerRuntime,
862
842
  handle: DefaultWorkerSessionHandle,
863
843
  ) {
@@ -874,19 +854,23 @@ const acquireSessionSubscription = Effect.fn("WorkerSession.acquireSubscription"
874
854
  );
875
855
  });
876
856
 
877
- const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
857
+ export const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
878
858
  options: ChildSessionOptions,
879
- dependencies: WorkerSessionDependencies,
859
+ overrides: Partial<WorkerSessionDependencies>,
880
860
  ) {
861
+ const dependencies: WorkerSessionAcquisitionDependencies = {
862
+ ...defaultWorkerSessionDependencies,
863
+ ...overrides,
864
+ };
881
865
  const scope = yield* Scope.make("sequential");
882
866
  const acquisition = Effect.gen(function* () {
883
867
  const definition = options.definition;
884
868
  const { selected, modelRuntime } = yield* prepareChildModelRuntime(options, dependencies);
885
869
  const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
886
870
 
887
- // createAgentSessionServices owns extension-provider registration and refresh, but
888
- // Pi 0.80.10 discards that refresh result. Keep this worker-owned probe so provider
889
- // errors and aborts remain typed acquisition failures before session creation.
871
+ // createAgentSessionServices registers extension providers and refreshes, but
872
+ // discards that result in Pi 0.80.10. Keep this worker-owned probe so provider
873
+ // errors and aborts remain typed acquisition failures; remove it when Pi surfaces them.
890
874
  yield* refreshModelRuntime(modelRuntime, definition);
891
875
  const model = yield* Effect.try({
892
876
  try: () => {
@@ -969,245 +953,3 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
969
953
  ),
970
954
  );
971
955
  });
972
-
973
- type AcquisitionHandoffState =
974
- | { readonly _tag: "Pending" }
975
- | { readonly _tag: "Offered"; readonly session: WorkerSessionHandle }
976
- | { readonly _tag: "Adopting"; readonly session: WorkerSessionHandle }
977
- | { readonly _tag: "Adopted" }
978
- | { readonly _tag: "Abandoned"; readonly error?: WorkerSessionAcquisitionClosedError }
979
- | { readonly _tag: "Failed" };
980
-
981
- interface AcquisitionHandoff {
982
- readonly state: SynchronizedRef.SynchronizedRef<AcquisitionHandoffState>;
983
- readonly result: Deferred.Deferred<WorkerSessionHandle, WorkerSessionAcquisitionError>;
984
- }
985
-
986
- type ChildSessionsState =
987
- | { readonly _tag: "Open"; readonly handoffs: ReadonlySet<AcquisitionHandoff> }
988
- | { readonly _tag: "Closed"; readonly error: WorkerSessionAcquisitionClosedError };
989
-
990
- type AdoptionReservation =
991
- | { readonly _tag: "Reserved"; readonly session: WorkerSessionHandle }
992
- | { readonly _tag: "Closed"; readonly error: WorkerSessionAcquisitionClosedError }
993
- | { readonly _tag: "Abandoned" };
994
-
995
- function disposeLateSession(session: WorkerSessionHandle): Effect.Effect<void> {
996
- return session.dispose().pipe(Effect.catchCause(() => Effect.void));
997
- }
998
-
999
- function admitReclamationOrJoinAfterClosure(
1000
- fibers: FiberSet.FiberSet<void, never>,
1001
- reclamation: Effect.Effect<void>,
1002
- onOpenObserved: () => Effect.Effect<void>,
1003
- ): Effect.Effect<void> {
1004
- return Effect.suspend(() => {
1005
- if (fibers.state._tag === "Closed") return reclamation;
1006
- return Effect.gen(function* () {
1007
- yield* onOpenObserved().pipe(Effect.catchCause(() => Effect.void));
1008
- yield* FiberSet.run(fibers, reclamation, { startImmediately: true });
1009
- if (fibers.state._tag === "Closed") {
1010
- // FiberSet.run returns an interrupted sentinel when closure wins admission.
1011
- // If admission won, closure interrupts the admitted fiber instead. Real
1012
- // session disposal is cached and uninterruptible, so this fallback safely
1013
- // joins that same disposal in both cases rather than starting cleanup twice.
1014
- yield* reclamation;
1015
- }
1016
- });
1017
- });
1018
- }
1019
-
1020
- export function createChildSessionsLayer(
1021
- overrides: Partial<WorkerSessionDependencies> = {},
1022
- ): Layer.Layer<ChildSessions> {
1023
- return Layer.effect(
1024
- ChildSessions,
1025
- Effect.gen(function* () {
1026
- const dependencies: WorkerSessionDependencies = { ...defaultDependencies, ...overrides };
1027
- const fibers = yield* FiberSet.make<void, never>();
1028
- const serviceState = yield* SynchronizedRef.make<ChildSessionsState>({
1029
- _tag: "Open",
1030
- handoffs: new Set(),
1031
- });
1032
-
1033
- const withoutHandoff = (
1034
- state: ChildSessionsState,
1035
- handoff: AcquisitionHandoff,
1036
- ): ChildSessionsState => {
1037
- if (state._tag === "Closed" || !state.handoffs.has(handoff)) return state;
1038
- const handoffs = new Set(state.handoffs);
1039
- handoffs.delete(handoff);
1040
- return { _tag: "Open", handoffs };
1041
- };
1042
-
1043
- const removeHandoff = (handoff: AcquisitionHandoff): Effect.Effect<void> =>
1044
- SynchronizedRef.update(serviceState, (state) => withoutHandoff(state, handoff));
1045
-
1046
- const runReclamation = (session: WorkerSessionHandle): Effect.Effect<void> =>
1047
- admitReclamationOrJoinAfterClosure(
1048
- fibers,
1049
- disposeLateSession(session).pipe(Effect.uninterruptible),
1050
- dependencies.onReclamationOpenObserved,
1051
- );
1052
-
1053
- const abandonHandoff = Effect.fn("ChildSessions.abandonHandoff")(function* (
1054
- handoff: AcquisitionHandoff,
1055
- ) {
1056
- const session = yield* SynchronizedRef.modifyEffect(serviceState, (service) => {
1057
- const nextService = withoutHandoff(service, handoff);
1058
- return SynchronizedRef.modify(handoff.state, (state): readonly [
1059
- { readonly session: WorkerSessionHandle | undefined },
1060
- AcquisitionHandoffState,
1061
- ] => {
1062
- if (state._tag === "Pending") {
1063
- return [{ session: undefined }, { _tag: "Abandoned" }];
1064
- }
1065
- if (state._tag === "Offered") {
1066
- return [{ session: state.session }, { _tag: "Abandoned" }];
1067
- }
1068
- return [{ session: undefined }, state];
1069
- }).pipe(Effect.map(({ session }) => [session, nextService] as const));
1070
- });
1071
- if (session) yield* runReclamation(session);
1072
- });
1073
-
1074
- const shutdown = Effect.fn("ChildSessions.shutdown")(() =>
1075
- Effect.gen(function* () {
1076
- const closed = yield* SynchronizedRef.modifyEffect(serviceState, (state) => {
1077
- if (state._tag === "Closed") return Effect.succeed([undefined, state] as const);
1078
- const error = new WorkerSessionAcquisitionClosedError({
1079
- message: "Child sessions are shutting down",
1080
- });
1081
- return Effect.gen(function* () {
1082
- const sessions: WorkerSessionHandle[] = [];
1083
- for (const handoff of state.handoffs) {
1084
- const session = yield* SynchronizedRef.modifyEffect(handoff.state, (handoffState) => {
1085
- if (handoffState._tag === "Pending") {
1086
- return Deferred.fail(handoff.result, error).pipe(
1087
- Effect.as([undefined, { _tag: "Abandoned", error }] as const),
1088
- );
1089
- }
1090
- if (handoffState._tag === "Offered") {
1091
- return Effect.succeed([
1092
- handoffState.session,
1093
- { _tag: "Abandoned", error },
1094
- ] as const);
1095
- }
1096
- // Failed already settled its Deferred. Adopting is an ownership
1097
- // reservation whose synchronous winner must be allowed to commit.
1098
- return Effect.succeed([undefined, handoffState] as const);
1099
- });
1100
- if (session) sessions.push(session);
1101
- }
1102
- return [{ sessions }, { _tag: "Closed", error }] as const;
1103
- });
1104
- });
1105
- if (!closed) return;
1106
- for (const session of closed.sessions) yield* runReclamation(session);
1107
- }).pipe(Effect.uninterruptible)
1108
- );
1109
-
1110
- yield* Effect.addFinalizer(() => shutdown());
1111
-
1112
- const acquire = Effect.fn("ChildSessions.acquire")(function* <Adopted>(
1113
- options: ChildSessionOptions,
1114
- adopt: (session: WorkerSessionHandle) => Adopted | undefined,
1115
- ) {
1116
- const handoff: AcquisitionHandoff = {
1117
- state: yield* SynchronizedRef.make<AcquisitionHandoffState>({ _tag: "Pending" }),
1118
- result: yield* Deferred.make<WorkerSessionHandle, WorkerSessionAcquisitionError>(),
1119
- };
1120
- return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1121
- const admissionError = yield* SynchronizedRef.modify(serviceState, (state) => {
1122
- if (state._tag === "Closed") return [state.error, state] as const;
1123
- return [undefined, {
1124
- _tag: "Open",
1125
- handoffs: new Set([...state.handoffs, handoff]),
1126
- }] as const;
1127
- });
1128
- if (admissionError) return yield* Effect.fail(admissionError);
1129
-
1130
- const producer = createWorkerSession(options, dependencies).pipe(
1131
- Effect.matchCauseEffect({
1132
- onFailure: (cause) => Effect.gen(function* () {
1133
- const failed = yield* SynchronizedRef.modifyEffect(handoff.state, (state) =>
1134
- state._tag === "Pending"
1135
- ? Deferred.failCause(handoff.result, cause).pipe(
1136
- Effect.as([true, { _tag: "Failed" }] as const),
1137
- )
1138
- : Effect.succeed([false, state] as const));
1139
- if (failed) yield* removeHandoff(handoff);
1140
- }),
1141
- onSuccess: (session) => Effect.gen(function* () {
1142
- const offered = yield* SynchronizedRef.modifyEffect(handoff.state, (state) =>
1143
- state._tag === "Pending"
1144
- ? Deferred.succeed(handoff.result, session).pipe(
1145
- Effect.as([true, { _tag: "Offered", session }] as const),
1146
- )
1147
- : Effect.succeed([false, state] as const));
1148
- if (!offered) {
1149
- yield* removeHandoff(handoff);
1150
- yield* runReclamation(session);
1151
- }
1152
- }),
1153
- }),
1154
- Effect.uninterruptible,
1155
- );
1156
- yield* FiberSet.run(fibers, producer, { startImmediately: true });
1157
-
1158
- const session = yield* restore(
1159
- Deferred.await(handoff.result).pipe(
1160
- Effect.tap(() => dependencies.beforeAdoptionReservation()),
1161
- ),
1162
- ).pipe(Effect.onInterrupt(() => abandonHandoff(handoff)));
1163
- const reservation = yield* SynchronizedRef.modifyEffect(serviceState, (service) => {
1164
- if (service._tag === "Closed") {
1165
- return Effect.succeed([
1166
- { _tag: "Closed", error: service.error } satisfies AdoptionReservation,
1167
- service,
1168
- ] as const);
1169
- }
1170
- return SynchronizedRef.modify(
1171
- handoff.state,
1172
- (state): readonly [AdoptionReservation, AcquisitionHandoffState] => {
1173
- if (state._tag === "Abandoned") {
1174
- return state.error
1175
- ? [{ _tag: "Closed", error: state.error }, state]
1176
- : [{ _tag: "Abandoned" }, state];
1177
- }
1178
- if (state._tag !== "Offered" || state.session !== session) {
1179
- return [{ _tag: "Abandoned" }, state];
1180
- }
1181
- return [
1182
- { _tag: "Reserved", session },
1183
- { _tag: "Adopting", session },
1184
- ];
1185
- },
1186
- ).pipe(Effect.map((result) => [result, service] as const));
1187
- });
1188
- if (reservation._tag === "Closed") return yield* Effect.fail(reservation.error);
1189
- if (reservation._tag === "Abandoned") {
1190
- return yield* Effect.die(new Error("Child session acquisition handoff was abandoned"));
1191
- }
1192
-
1193
- // The adopter is arbitrary synchronous runtime code. The reservation
1194
- // protects it from shutdown, but no SynchronizedRef semaphore is held.
1195
- const adopted = yield* Effect.exit(Effect.sync(() => adopt(reservation.session)));
1196
- const transferred = Exit.isSuccess(adopted) && adopted.value !== undefined;
1197
- yield* SynchronizedRef.update(handoff.state, (state): AcquisitionHandoffState => {
1198
- if (state._tag !== "Adopting" || state.session !== reservation.session) return state;
1199
- return transferred ? { _tag: "Adopted" } : { _tag: "Abandoned" };
1200
- });
1201
- yield* removeHandoff(handoff);
1202
- if (transferred) return adopted.value;
1203
-
1204
- yield* runReclamation(reservation.session);
1205
- if (Exit.isFailure(adopted)) return yield* Effect.failCause(adopted.cause);
1206
- return undefined;
1207
- }));
1208
- });
1209
-
1210
- return ChildSessions.of({ acquire, shutdown });
1211
- }),
1212
- );
1213
- }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@zachwill/pi-orchestrate",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Concurrent worker orchestration for Pi",
6
+ "exports": {},
6
7
  "files": ["extension/", "examples/", "README.md", "LICENSE"],
7
8
  "keywords": ["pi-package", "pi", "workers", "orchestration"],
8
9
  "license": "MIT",
@@ -1,144 +0,0 @@
1
- import type { WorkerCatalog } from "./domain.js";
2
-
3
- const CONTRACT_START = "<!-- pi-orchestrate:contract:start -->";
4
- const CONTRACT_END = "<!-- pi-orchestrate:contract:end -->";
5
-
6
- function sortedWorkers(catalog: WorkerCatalog) {
7
- return [...catalog.workers].sort((left, right) => {
8
- if (left.name < right.name) return -1;
9
- if (left.name > right.name) return 1;
10
- return 0;
11
- });
12
- }
13
-
14
- function escapeContractMarkers(value: string): string {
15
- return value
16
- .replaceAll(CONTRACT_START, "&lt;!-- pi-orchestrate:contract:start --&gt;")
17
- .replaceAll(CONTRACT_END, "&lt;!-- pi-orchestrate:contract:end --&gt;");
18
- }
19
-
20
- function formatCatalog(catalog: WorkerCatalog): string {
21
- const workers = sortedWorkers(catalog);
22
- if (workers.length === 0) return "- No trusted workers are available for this session.";
23
-
24
- return workers
25
- .map(
26
- (worker) =>
27
- `- \`${escapeContractMarkers(worker.name)}\` [${worker.source.kind}] (${worker.lifecycle}): ${escapeContractMarkers(worker.description)}`,
28
- )
29
- .join("\n");
30
- }
31
-
32
- interface ContractMarker {
33
- readonly start: number;
34
- readonly end: number;
35
- readonly kind: "start" | "end";
36
- }
37
-
38
- function contractMarkers(prompt: string): ContractMarker[] {
39
- const markers: ContractMarker[] = [];
40
- for (const [value, kind] of [
41
- [CONTRACT_START, "start"],
42
- [CONTRACT_END, "end"],
43
- ] as const) {
44
- let offset = 0;
45
- while (offset < prompt.length) {
46
- const start = prompt.indexOf(value, offset);
47
- if (start < 0) break;
48
- markers.push({ start, end: start + value.length, kind });
49
- offset = start + value.length;
50
- }
51
- }
52
- return markers.sort((left, right) => left.start - right.start);
53
- }
54
-
55
- function removeContractMarkers(prompt: string): {
56
- readonly prompt: string;
57
- readonly insertionOffset?: number;
58
- } {
59
- const markers = contractMarkers(prompt);
60
- if (markers.length === 0) return { prompt };
61
-
62
- const removed: Array<{ start: number; end: number }> = [];
63
- const stack: ContractMarker[] = [];
64
- for (const marker of markers) {
65
- if (marker.kind === "start") {
66
- stack.push(marker);
67
- continue;
68
- }
69
- const start = stack.pop();
70
- if (start && stack.length === 0) removed.push({ start: start.start, end: marker.end });
71
- }
72
-
73
- for (const marker of markers) {
74
- if (!removed.some((range) => marker.start >= range.start && marker.end <= range.end)) {
75
- removed.push({ start: marker.start, end: marker.end });
76
- }
77
- }
78
- removed.sort((left, right) => left.start - right.start);
79
-
80
- const insertionPoint = markers[0]!.start;
81
- let insertionOffset = 0;
82
- let cursor = 0;
83
- let cleaned = "";
84
- for (const range of removed) {
85
- if (range.start < cursor) continue;
86
- const retained = prompt.slice(cursor, range.start);
87
- cleaned += retained;
88
- if (range.start <= insertionPoint) insertionOffset = cleaned.length;
89
- cursor = range.end;
90
- }
91
- cleaned += prompt.slice(cursor);
92
- return { prompt: cleaned, insertionOffset };
93
- }
94
-
95
- function buildContract(catalog: WorkerCatalog): string {
96
- return `${CONTRACT_START}
97
- ## Pi Orchestrate Contract
98
-
99
- You are the parent orchestrator and own the task end to end.
100
-
101
- - Keep trivial or tightly coupled work in the parent. For broad work, proactively identify every useful bounded independent scope and every materially distinct evidence, hypothesis, or validation perspective. Spin up as many workers as needed to cover them; never use a small fixed default.
102
- - Treat worker roles and counts named by the user as minimum requirements, not ceilings. Exceed them when additional useful independent scopes or materially distinct perspectives exist, unless the user explicitly sets an exact cap. The same worker definition can be dispatched in multiple independent calls when it fits separate scopes or perspectives; each call creates an independent worker session. This is distinct from interactive session continuity, which keeps one worker ID for explicit follow-up work.
103
- - Before dispatching, enumerate the full first parallel wave from the work itself.
104
- - **Mandatory asynchronous-wave cardinality:** If an intended asynchronous wave has N workers, your next assistant response must contain exactly N separate, fully briefed \`orchestrate\` invocations. A single invocation is valid only when N=1. Form all N invocations before emitting or finalizing the response: a successfully admitted sole async invocation returns \`terminate: true\` and ends the parent turn, so omitted siblings cannot be added afterward. Do not emit one invocation and wait for its result before forming the rest of the wave.
105
- - **Parallel-dispatch mechanism:** When a parallel tool dispatcher is available, use it to submit the entire wave as one tool-call group. For example, with \`multi_tool_use.parallel\`, make one dispatcher call whose \`tool_uses\` contains exactly N \`functions.orchestrate\` entries and no other tools. If no parallel dispatcher is available, emit N native sibling \`orchestrate\` calls in the same assistant response. Never represent an N-worker wave as N sequential assistant responses.
106
- - **Asynchronous response shape:** To run that wave asynchronously, the resulting expanded tool-call group must contain exactly those N \`orchestrate\` invocations and no other tool calls. Harmless response text does not affect runtime classification. Pi executes sibling tool calls concurrently. For N=3, submit together three calls: \`orchestrate({ worker, title, instructions })\`, \`orchestrate({ worker, title, instructions })\`, and \`orchestrate({ worker, title, instructions })\`.
107
- - Delegate each independent scope or distinct perspective with its own fully briefed \`orchestrate\` call. Do not wait for one sibling's acceptance or completion before dispatching the rest.
108
- - Deliberate overlap is allowed only when calls pursue materially distinct evidence sources, competing hypotheses, or validation perspectives. Encode that distinction in each brief; accidental duplicate assignments are forbidden.
109
- - Give every worker a thorough, self-contained brief with the objective, paths and scope, context, success criteria, and expected output. State forbidden actions explicitly.
110
- - Input, catalog, and model preflight is atomic per call before that worker starts. Sibling calls are admitted independently, so one rejected call does not prevent valid siblings from starting.
111
- - Pi Orchestrate treats a successfully admitted sole \`orchestrate\` call or pure sibling group as async. Pi executes native sibling tools concurrently. A pure group yields the parent turn, delivers each result as it settles, and starts synthesis only after the whole group settles. Mixing \`orchestrate\` with another tool makes it inline and blocking. \`interactive_send\` is asynchronous only as the sole tool call in its assistant message.
112
- - Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
113
- - After the full current wave has been dispatched, yield the parent turn once its admissions have resolved; a rejected sibling does not block yielding. Worker responses arrive individually as each worker settles, and the final response starts parent synthesis. Do not poll \`worker_status\` or use it as a normal completion mechanism.
114
- - As results expose more useful independent scopes or materially distinct perspectives, enumerate and dispatch another full parallel wave before yielding. Continue adaptive full waves until the whole task is complete.
115
- - The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion.
116
- - Prefer one-shot workers. Use \`interactive_send\` only for follow-up work on an owned lifecycle interactive worker whose status is ready, and \`interactive_close\` only when that ready interactive worker is finished. Never use either tool for one-shot or completed workers because one-shot sessions terminate automatically. Use \`worker_abort\` only when active work must stop.
117
- - The public tools are \`orchestrate\`, \`worker_status\`, \`interactive_send\`, \`worker_abort\`, and \`interactive_close\`.
118
-
119
- ### Trusted worker catalog
120
-
121
- Source labels show where each trusted definition came from; later catalog sources have already overridden earlier definitions with the same name.
122
-
123
- ${formatCatalog(catalog)}
124
- ${CONTRACT_END}`;
125
- }
126
-
127
- export function appendOrchestratorContract(
128
- systemPrompt: string,
129
- catalog: WorkerCatalog,
130
- ): string {
131
- const section = buildContract(catalog);
132
- const cleaned = removeContractMarkers(systemPrompt);
133
- if (cleaned.insertionOffset !== undefined) {
134
- return `${cleaned.prompt.slice(0, cleaned.insertionOffset)}${section}${cleaned.prompt.slice(cleaned.insertionOffset)}`;
135
- }
136
-
137
- const separator =
138
- systemPrompt.length === 0 || systemPrompt.endsWith("\n\n")
139
- ? ""
140
- : systemPrompt.endsWith("\n")
141
- ? "\n"
142
- : "\n\n";
143
- return `${systemPrompt}${separator}${section}`;
144
- }