@zachwill/pi-orchestrate 0.9.0 → 0.10.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/extension/catalog/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +22 -51
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +222 -523
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/{contract.ts → parent/contract.ts} +75 -9
- package/extension/{delivery.ts → parent/delivery.ts} +19 -16
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +30 -31
- package/extension/{presentation.ts → pi/presentation.ts} +41 -38
- 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} +23 -287
- package/package.json +2 -1
- package/extension/tools.ts +0 -771
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
WorkerReadyOutcome,
|
|
9
9
|
WorkerResponseOutcome,
|
|
10
10
|
WorkerUsage,
|
|
11
|
-
|
|
11
|
+
type RunRecord,
|
|
12
|
+
type SettledWorkerRecord,
|
|
13
|
+
} from "./model.js";
|
|
12
14
|
|
|
13
15
|
const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
14
16
|
const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
|
|
@@ -20,8 +22,8 @@ const FailureStage = Schema.Literals([
|
|
|
20
22
|
"cancellation",
|
|
21
23
|
]);
|
|
22
24
|
|
|
23
|
-
/** Canonical schema for
|
|
24
|
-
export const
|
|
25
|
+
/** Canonical schema for settlements written and persisted by orchestration. */
|
|
26
|
+
export const WorkerSettlement = Schema.Struct({
|
|
25
27
|
eventId: Schema.String,
|
|
26
28
|
sequence: PositiveInteger,
|
|
27
29
|
ownerSessionId: Schema.String,
|
|
@@ -77,25 +79,69 @@ export const WorkerSettlementDetails = Schema.Struct({
|
|
|
77
79
|
}),
|
|
78
80
|
);
|
|
79
81
|
|
|
80
|
-
export interface
|
|
81
|
-
extends Schema.Schema.Type<typeof
|
|
82
|
+
export interface WorkerSettlement
|
|
83
|
+
extends Schema.Schema.Type<typeof WorkerSettlement> {}
|
|
82
84
|
|
|
83
85
|
export type SettlementFailureStage = NonNullable<
|
|
84
|
-
|
|
85
|
-
>;
|
|
86
|
-
|
|
87
|
-
export type WorkerSettlement = Schema.Schema.Type<
|
|
88
|
-
typeof WorkerSettlementDetails
|
|
86
|
+
WorkerSettlement["failureStage"]
|
|
89
87
|
>;
|
|
90
88
|
|
|
91
89
|
const decodeCurrentWorkerSettlement = Schema.decodeUnknownResult(
|
|
92
|
-
|
|
90
|
+
WorkerSettlement,
|
|
93
91
|
);
|
|
94
92
|
|
|
95
|
-
export function
|
|
93
|
+
export function decodePersistedWorkerSettlement(value: unknown) {
|
|
96
94
|
return decodeCurrentWorkerSettlement(value);
|
|
97
95
|
}
|
|
98
96
|
|
|
97
|
+
/** Every input a settlement needs; orchestration state entries stay private. */
|
|
98
|
+
export interface WorkerSettlementInput {
|
|
99
|
+
readonly sequence: number;
|
|
100
|
+
readonly generation: number;
|
|
101
|
+
readonly run: RunRecord;
|
|
102
|
+
readonly worker: SettledWorkerRecord;
|
|
103
|
+
readonly settledAt: number;
|
|
104
|
+
readonly failureStage?: SettlementFailureStage;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Builds the canonical settlement published to listeners and persisted by Pi. */
|
|
108
|
+
export function createWorkerSettlement({
|
|
109
|
+
sequence,
|
|
110
|
+
generation,
|
|
111
|
+
run,
|
|
112
|
+
worker,
|
|
113
|
+
settledAt,
|
|
114
|
+
failureStage,
|
|
115
|
+
}: WorkerSettlementInput): WorkerSettlement {
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
eventId: `${sequence}:${run.id}:${worker.id}:${generation}`,
|
|
118
|
+
sequence,
|
|
119
|
+
ownerSessionId: worker.ownerSessionId,
|
|
120
|
+
runId: run.id,
|
|
121
|
+
workerId: worker.id,
|
|
122
|
+
generation,
|
|
123
|
+
mode: run.mode,
|
|
124
|
+
worker: worker.worker,
|
|
125
|
+
title: worker.title,
|
|
126
|
+
lifecycle: worker.lifecycle,
|
|
127
|
+
status: worker.status,
|
|
128
|
+
outcome: Object.freeze({ ...worker.outcome }),
|
|
129
|
+
...(failureStage ? { failureStage } : {}),
|
|
130
|
+
usage: Object.freeze({ ...worker.usage }),
|
|
131
|
+
startedAt: worker.startedAt,
|
|
132
|
+
settledAt,
|
|
133
|
+
...(run.synthesisGroupId && run.synthesisGroupSize
|
|
134
|
+
? {
|
|
135
|
+
synthesisGroupId: run.synthesisGroupId,
|
|
136
|
+
synthesisGroupSize: run.synthesisGroupSize,
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
...(worker.sessionFile !== undefined
|
|
140
|
+
? { sessionFile: worker.sessionFile }
|
|
141
|
+
: {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
const InlineWorkerOutcome = Schema.Union([
|
|
100
146
|
WorkerCompletedOutcome.pipe(
|
|
101
147
|
Schema.encodeKeys({ assistantText: "assistant_text" }),
|
|
@@ -121,15 +167,15 @@ const InlineWorkerUsage = WorkerUsage.pipe(
|
|
|
121
167
|
|
|
122
168
|
/** Tool transport projection derived from the canonical settlement field schemas. */
|
|
123
169
|
export const InlineWorkerSettlementDetails = Schema.Struct({
|
|
124
|
-
workerId:
|
|
125
|
-
worker:
|
|
126
|
-
title:
|
|
127
|
-
status:
|
|
170
|
+
workerId: WorkerSettlement.fields.workerId,
|
|
171
|
+
worker: WorkerSettlement.fields.worker,
|
|
172
|
+
title: WorkerSettlement.fields.title,
|
|
173
|
+
status: WorkerSettlement.fields.status,
|
|
128
174
|
outcome: InlineWorkerOutcome,
|
|
129
175
|
usage: InlineWorkerUsage,
|
|
130
|
-
startedAt:
|
|
131
|
-
settledAt:
|
|
132
|
-
sessionFile:
|
|
176
|
+
startedAt: WorkerSettlement.fields.startedAt,
|
|
177
|
+
settledAt: WorkerSettlement.fields.settledAt,
|
|
178
|
+
sessionFile: WorkerSettlement.fields.sessionFile,
|
|
133
179
|
}).pipe(
|
|
134
180
|
Schema.encodeKeys({
|
|
135
181
|
workerId: "worker_id",
|
|
@@ -153,9 +199,9 @@ export interface InlineWorkerSettlementDetails
|
|
|
153
199
|
|
|
154
200
|
export const InlineWorkerToolDetails = Schema.Struct({
|
|
155
201
|
mode: Schema.Literal("inline"),
|
|
156
|
-
runId: Schema.optionalKey(
|
|
202
|
+
runId: Schema.optionalKey(WorkerSettlement.fields.runId),
|
|
157
203
|
ownerSessionId: Schema.optionalKey(
|
|
158
|
-
|
|
204
|
+
WorkerSettlement.fields.ownerSessionId,
|
|
159
205
|
),
|
|
160
206
|
result: InlineWorkerSettlementDetails,
|
|
161
207
|
}).pipe(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkerCatalog } from "
|
|
1
|
+
import type { WorkerCatalog } from "../catalog/definition.js";
|
|
2
2
|
|
|
3
3
|
const CONTRACT_START = "<!-- pi-orchestrate:contract:start -->";
|
|
4
4
|
const CONTRACT_END = "<!-- pi-orchestrate:contract:end -->";
|
|
@@ -11,6 +11,12 @@ function sortedWorkers(catalog: WorkerCatalog) {
|
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
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
|
+
|
|
14
20
|
function formatCatalog(catalog: WorkerCatalog): string {
|
|
15
21
|
const workers = sortedWorkers(catalog);
|
|
16
22
|
if (workers.length === 0) return "- No trusted workers are available for this session.";
|
|
@@ -18,11 +24,74 @@ function formatCatalog(catalog: WorkerCatalog): string {
|
|
|
18
24
|
return workers
|
|
19
25
|
.map(
|
|
20
26
|
(worker) =>
|
|
21
|
-
`- \`${worker.name}\` [${worker.source.kind}] (${worker.lifecycle}): ${worker.description}`,
|
|
27
|
+
`- \`${escapeContractMarkers(worker.name)}\` [${worker.source.kind}] (${worker.lifecycle}): ${escapeContractMarkers(worker.description)}`,
|
|
22
28
|
)
|
|
23
29
|
.join("\n");
|
|
24
30
|
}
|
|
25
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
|
+
|
|
26
95
|
function buildContract(catalog: WorkerCatalog): string {
|
|
27
96
|
return `${CONTRACT_START}
|
|
28
97
|
## Pi Orchestrate Contract
|
|
@@ -55,17 +124,14 @@ ${formatCatalog(catalog)}
|
|
|
55
124
|
${CONTRACT_END}`;
|
|
56
125
|
}
|
|
57
126
|
|
|
58
|
-
export function
|
|
127
|
+
export function applyOrchestratorContract(
|
|
59
128
|
systemPrompt: string,
|
|
60
129
|
catalog: WorkerCatalog,
|
|
61
130
|
): string {
|
|
62
131
|
const section = buildContract(catalog);
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
if (end >= 0) {
|
|
67
|
-
return `${systemPrompt.slice(0, start)}${section}${systemPrompt.slice(end + CONTRACT_END.length)}`;
|
|
68
|
-
}
|
|
132
|
+
const cleaned = removeContractMarkers(systemPrompt);
|
|
133
|
+
if (cleaned.insertionOffset !== undefined) {
|
|
134
|
+
return `${cleaned.prompt.slice(0, cleaned.insertionOffset)}${section}${cleaned.prompt.slice(cleaned.insertionOffset)}`;
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
const separator =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
import { Orchestration } from "
|
|
3
|
-
import type { WorkerSettlement } from "
|
|
2
|
+
import { Orchestration } from "../orchestration/service.js";
|
|
3
|
+
import type { WorkerSettlement } from "../orchestration/settlement.js";
|
|
4
4
|
|
|
5
5
|
export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
|
|
6
6
|
export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
|
|
@@ -11,14 +11,11 @@ export const DELIVERY_PARENT_INSTRUCTIONS =
|
|
|
11
11
|
|
|
12
12
|
export type ParentBindingGeneration = string | number | symbol;
|
|
13
13
|
|
|
14
|
-
/** Complete, immutable worker output for presentation and history consumers. */
|
|
15
|
-
export type WorkerDeliveryDetails = WorkerSettlement;
|
|
16
|
-
|
|
17
14
|
export interface WorkerDeliveryMessage {
|
|
18
15
|
readonly customType: "pi-orchestrate-worker-result";
|
|
19
16
|
readonly content: string;
|
|
20
17
|
readonly display: true;
|
|
21
|
-
readonly details:
|
|
18
|
+
readonly details: WorkerSettlement;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
export interface WorkerDeliveryOptions {
|
|
@@ -67,6 +64,8 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
67
64
|
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
68
65
|
private readonly flushingOwners = new Set<string>();
|
|
69
66
|
private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
|
|
67
|
+
// Orchestration settlement sequences are process-scoped and monotonic across
|
|
68
|
+
// owners, so one watermark is valid.
|
|
70
69
|
private highestAcceptedSequence = 0;
|
|
71
70
|
|
|
72
71
|
bind(binding: ParentBinding): void {
|
|
@@ -83,15 +82,14 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
86
|
-
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
87
85
|
const parent = this.boundParents.get(ownerSessionId);
|
|
88
|
-
if (parent
|
|
86
|
+
if (parent?.binding.generation !== generation) return;
|
|
87
|
+
parent.agentRunning = true;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
92
|
-
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
93
91
|
const parent = this.boundParents.get(ownerSessionId);
|
|
94
|
-
if (
|
|
92
|
+
if (parent?.binding.generation !== generation) return;
|
|
95
93
|
parent.agentRunning = false;
|
|
96
94
|
this.flush(ownerSessionId, generation);
|
|
97
95
|
}
|
|
@@ -163,21 +161,23 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
163
161
|
|
|
164
162
|
this.flushingOwners.add(ownerSessionId);
|
|
165
163
|
try {
|
|
164
|
+
// Deliver a stable owner-ordered prefix, stopping at the latest complete synthesis boundary.
|
|
166
165
|
const queued = this.pendingSettlements.filter(
|
|
167
166
|
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
168
167
|
);
|
|
169
168
|
let latestFinalIndex = -1;
|
|
170
|
-
for (
|
|
171
|
-
|
|
172
|
-
if (settlement && this.isFinalBoundary(settlement)) latestFinalIndex = index;
|
|
169
|
+
for (const [index, settlement] of queued.entries()) {
|
|
170
|
+
if (this.isFinalBoundary(settlement)) latestFinalIndex = index;
|
|
173
171
|
}
|
|
174
172
|
const flushThrough = latestFinalIndex >= 0 ? latestFinalIndex : queued.length - 1;
|
|
175
173
|
let flushBytesRemaining = MAX_DELIVERY_MARKDOWN_BYTES;
|
|
176
174
|
|
|
177
|
-
for (
|
|
175
|
+
for (const [index, settlement] of queued.entries()) {
|
|
176
|
+
if (index > flushThrough) break;
|
|
177
|
+
// Synchronous delivery callbacks can change owner, generation, or idle
|
|
178
|
+
// state before the next send.
|
|
178
179
|
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
179
|
-
|
|
180
|
-
if (!settlement || !this.pendingSettlements.includes(settlement)) continue;
|
|
180
|
+
if (!this.pendingSettlements.includes(settlement)) continue;
|
|
181
181
|
|
|
182
182
|
const messagesRemaining = flushThrough - index + 1;
|
|
183
183
|
const fairFlushBytes = Math.floor(flushBytesRemaining / messagesRemaining);
|
|
@@ -186,6 +186,8 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
186
186
|
fairFlushBytes,
|
|
187
187
|
flushBytesRemaining,
|
|
188
188
|
));
|
|
189
|
+
// Intermediate results add context; only the completed boundary
|
|
190
|
+
// transfers work to a parent turn.
|
|
189
191
|
const triggerTurn = latestFinalIndex >= 0 && index === flushThrough;
|
|
190
192
|
const message = this.renderWorkerMessage(settlement, byteLimit);
|
|
191
193
|
const parent = this.boundParents.get(ownerSessionId);
|
|
@@ -194,6 +196,7 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
194
196
|
try {
|
|
195
197
|
parent.binding.sendMessage(message, { triggerTurn });
|
|
196
198
|
} catch {
|
|
199
|
+
// Keep this settlement and the remaining prefix queued for a later retry.
|
|
197
200
|
return;
|
|
198
201
|
}
|
|
199
202
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SynthesisGroup } from "../orchestration/model.js";
|
|
2
|
+
|
|
3
|
+
export interface ParentToolCall {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DispatchDecision {
|
|
9
|
+
readonly mode: "async" | "inline";
|
|
10
|
+
readonly synthesisGroup?: SynthesisGroup;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClassifiedParentDispatch {
|
|
14
|
+
readonly toolCallId: string;
|
|
15
|
+
readonly decision: DispatchDecision;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
19
|
+
"orchestrate",
|
|
20
|
+
"interactive_send",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Sole dispatches and homogeneous orchestrate waves detach so the parent turn can
|
|
24
|
+
// end while work continues. Mixed tools stay inline because their shared parent
|
|
25
|
+
// turn still has sibling work; one wave boundary defers one synthesis turn until
|
|
26
|
+
// every admitted member has settled.
|
|
27
|
+
export function classifyParentDispatches(
|
|
28
|
+
toolCalls: readonly ParentToolCall[],
|
|
29
|
+
): readonly ClassifiedParentDispatch[] {
|
|
30
|
+
const isOrchestrateGroup =
|
|
31
|
+
toolCalls.length > 1 &&
|
|
32
|
+
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
33
|
+
const synthesisGroup = isOrchestrateGroup
|
|
34
|
+
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
return toolCalls.flatMap((toolCall): ClassifiedParentDispatch[] => {
|
|
38
|
+
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) return [];
|
|
39
|
+
return [{
|
|
40
|
+
toolCallId: toolCall.id,
|
|
41
|
+
decision: {
|
|
42
|
+
mode: isOrchestrateGroup || toolCalls.length === 1 ? "async" : "inline",
|
|
43
|
+
...(toolCall.name === "orchestrate" && synthesisGroup
|
|
44
|
+
? { synthesisGroup }
|
|
45
|
+
: {}),
|
|
46
|
+
},
|
|
47
|
+
}];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -7,33 +7,36 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
Orchestration,
|
|
9
9
|
orchestrationLayer,
|
|
10
|
-
type AbortTarget,
|
|
11
10
|
type AcceptedRun,
|
|
12
11
|
type CompletedRun,
|
|
13
|
-
type OrchestrationContext,
|
|
14
12
|
type OrchestrationService,
|
|
15
13
|
SHUTDOWN_CLEANUP_GRACE_MS,
|
|
16
|
-
type
|
|
14
|
+
type OwnerSnapshot,
|
|
17
15
|
type SettlementListener,
|
|
18
16
|
type UnsubscribeSettlement,
|
|
19
|
-
} from "
|
|
20
|
-
import {
|
|
21
|
-
|
|
17
|
+
} from "../orchestration/service.js";
|
|
18
|
+
import type {
|
|
19
|
+
AbortTarget,
|
|
20
|
+
OrchestrationContext,
|
|
21
|
+
} from "../orchestration/admission.js";
|
|
22
|
+
import { createChildSessionsLayer } from "../worker/child-sessions.js";
|
|
23
|
+
import type { OrchestrateTaskInput, RunMode } from "../orchestration/model.js";
|
|
22
24
|
|
|
23
25
|
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
24
26
|
|
|
25
|
-
type
|
|
27
|
+
type DispatchResult<M extends RunMode> = M extends "async"
|
|
26
28
|
? AcceptedRun
|
|
27
29
|
: CompletedRun;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
/** Promise-facing orchestration API used by Pi tools and presentation. */
|
|
32
|
+
export interface OrchestrationClient {
|
|
30
33
|
orchestrate<M extends RunMode>(
|
|
31
34
|
context: OrchestrationContext,
|
|
32
35
|
task: OrchestrateTaskInput,
|
|
33
36
|
mode: M,
|
|
34
37
|
signal?: AbortSignal,
|
|
35
38
|
onSettlement?: SettlementListener,
|
|
36
|
-
): Promise<
|
|
39
|
+
): Promise<DispatchResult<M>>;
|
|
37
40
|
sendInteractive<M extends RunMode>(
|
|
38
41
|
context: OrchestrationContext,
|
|
39
42
|
workerId: string,
|
|
@@ -41,20 +44,20 @@ export interface OrchestratorRuntime {
|
|
|
41
44
|
mode: M,
|
|
42
45
|
signal?: AbortSignal,
|
|
43
46
|
onSettlement?: SettlementListener,
|
|
44
|
-
): Promise<
|
|
47
|
+
): Promise<DispatchResult<M>>;
|
|
45
48
|
abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
|
|
46
49
|
closeInteractive(ownerSessionId: string, workerId: string): Promise<void>;
|
|
47
|
-
snapshot(ownerSessionId: string): Promise<
|
|
50
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot>;
|
|
48
51
|
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
|
|
49
52
|
subscribeState(
|
|
50
53
|
ownerSessionId: string,
|
|
51
|
-
listener: (snapshot:
|
|
54
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
52
55
|
): () => void;
|
|
53
56
|
shutdown(): Promise<void>;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export interface ProcessHost {
|
|
57
|
-
readonly
|
|
60
|
+
readonly orchestration: OrchestrationClient;
|
|
58
61
|
readonly delivery: DeliveryService;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -79,7 +82,7 @@ interface AttachmentAwareProcessHost extends ProcessHost {
|
|
|
79
82
|
attachments?: Set<ProcessHostAttachment>;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
type ProcessHostLifecycle = "
|
|
85
|
+
type ProcessHostLifecycle = "destroying" | "destroyed";
|
|
83
86
|
|
|
84
87
|
interface OwnedProcessHost extends AttachmentAwareProcessHost {
|
|
85
88
|
readonly effectRuntime?: ManagedRuntime.ManagedRuntime<Orchestration | Delivery, never>;
|
|
@@ -100,7 +103,7 @@ export function getProcessHost(): ProcessHost | undefined {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/** Pi-facing adapter. Every Promise operation executes one complete Orchestration Effect. */
|
|
103
|
-
export class
|
|
106
|
+
export class ManagedOrchestrationClient<R = never> implements OrchestrationClient {
|
|
104
107
|
constructor(
|
|
105
108
|
private readonly effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
106
109
|
private readonly orchestration: OrchestrationService,
|
|
@@ -192,7 +195,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
192
195
|
return this.run(this.orchestration.closeInteractive(ownerSessionId, workerId));
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
snapshot(ownerSessionId: string): Promise<
|
|
198
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot> {
|
|
196
199
|
// Snapshot is dependency-free and remains readable from a retained host reference
|
|
197
200
|
// after the process root has been disposed.
|
|
198
201
|
return Effect.runPromise(this.orchestration.snapshot(ownerSessionId));
|
|
@@ -204,7 +207,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
204
207
|
|
|
205
208
|
subscribeState(
|
|
206
209
|
ownerSessionId: string,
|
|
207
|
-
listener: (snapshot:
|
|
210
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
208
211
|
): () => void {
|
|
209
212
|
return this.orchestration.subscribeState(ownerSessionId, listener);
|
|
210
213
|
}
|
|
@@ -232,15 +235,15 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
export function
|
|
238
|
+
export function createOrchestrationClient<R>(
|
|
236
239
|
effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
237
|
-
):
|
|
240
|
+
): OrchestrationClient {
|
|
238
241
|
// Orchestration acquisition is synchronous; subscriptions must remain reentrant.
|
|
239
242
|
const orchestration = effectRuntime.runSync(Orchestration);
|
|
240
|
-
return new
|
|
243
|
+
return new ManagedOrchestrationClient(effectRuntime, orchestration);
|
|
241
244
|
}
|
|
242
245
|
|
|
243
|
-
export function
|
|
246
|
+
export function makeProcessHostLayer(): Layer.Layer<Orchestration | Delivery> {
|
|
244
247
|
const orchestration = orchestrationLayer().pipe(
|
|
245
248
|
Layer.provide(createChildSessionsLayer()),
|
|
246
249
|
);
|
|
@@ -254,21 +257,18 @@ export function createProcessHost(): ProcessHost {
|
|
|
254
257
|
throw new Error("Cannot create a process host while the current host is being destroyed");
|
|
255
258
|
}
|
|
256
259
|
if (existing?.lifecycle === "destroyed") {
|
|
257
|
-
|
|
258
|
-
} else if (existing) {
|
|
259
|
-
existing.lifecycle = "active";
|
|
260
|
-
return existing;
|
|
260
|
+
throw new Error("Cannot create a process host after the current host was destroyed");
|
|
261
261
|
}
|
|
262
|
+
if (existing) return existing;
|
|
262
263
|
|
|
263
|
-
const effectRuntime = ManagedRuntime.make(
|
|
264
|
-
const
|
|
264
|
+
const effectRuntime = ManagedRuntime.make(makeProcessHostLayer());
|
|
265
|
+
const orchestration = createOrchestrationClient(effectRuntime);
|
|
265
266
|
const delivery = effectRuntime.runSync(Delivery);
|
|
266
267
|
const host: OwnedProcessHost = {
|
|
267
|
-
|
|
268
|
+
orchestration,
|
|
268
269
|
delivery,
|
|
269
270
|
effectRuntime,
|
|
270
271
|
attachments: new Set(),
|
|
271
|
-
lifecycle: "active",
|
|
272
272
|
};
|
|
273
273
|
global[PROCESS_HOST_KEY] = host;
|
|
274
274
|
return host;
|
|
@@ -304,7 +304,6 @@ export function destroyProcessHost(
|
|
|
304
304
|
const ownedHost = host as OwnedProcessHost;
|
|
305
305
|
if (ownedHost.destroyPromise) return ownedHost.destroyPromise;
|
|
306
306
|
if ((ownedHost.attachments?.size ?? 0) > 0) return Promise.resolve();
|
|
307
|
-
if (ownedHost.lifecycle === "destroyed") return Promise.resolve();
|
|
308
307
|
|
|
309
308
|
ownedHost.lifecycle = "destroying";
|
|
310
309
|
let resolveDestruction!: () => void;
|
|
@@ -319,7 +318,7 @@ export function destroyProcessHost(
|
|
|
319
318
|
let shutdown: Promise<void>;
|
|
320
319
|
try {
|
|
321
320
|
// shutdown() closes Orchestration admission before returning its bounded teardown Promise.
|
|
322
|
-
shutdown = ownedHost.
|
|
321
|
+
shutdown = ownedHost.orchestration.shutdown();
|
|
323
322
|
} catch (error) {
|
|
324
323
|
shutdown = Promise.reject(error);
|
|
325
324
|
}
|