@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.
- package/README.md +47 -108
- package/extension/catalog/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +19 -36
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +179 -473
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/parent/contract.ts +147 -0
- package/extension/{delivery.ts → parent/delivery.ts} +5 -8
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +33 -26
- package/extension/{presentation.ts → pi/presentation.ts} +39 -27
- package/extension/pi/tool-renderer.ts +422 -0
- package/extension/pi/tools.ts +470 -0
- package/extension/worker/child-sessions.ts +291 -0
- package/extension/{worker-session.ts → worker/session.ts} +33 -291
- package/package.json +2 -1
- package/extension/contract.ts +0 -144
- package/extension/tools.ts +0 -837
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -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 "
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
888
|
-
// Pi 0.80.10
|
|
889
|
-
// errors and aborts remain typed acquisition failures
|
|
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.
|
|
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",
|
package/extension/contract.ts
DELETED
|
@@ -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, "<!-- pi-orchestrate:contract:start -->")
|
|
17
|
-
.replaceAll(CONTRACT_END, "<!-- pi-orchestrate:contract:end -->");
|
|
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
|
-
}
|