@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
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
WorkerReadyOutcome,
|
|
9
9
|
WorkerResponseOutcome,
|
|
10
10
|
WorkerUsage,
|
|
11
|
-
|
|
11
|
+
type RunRecord,
|
|
12
|
+
type SettledWorkerRecord,
|
|
13
|
+
} from "./model.ts";
|
|
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(
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { WorkerCatalog } from "../catalog/definition.ts";
|
|
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
|
+
### Delegation
|
|
102
|
+
|
|
103
|
+
- Keep trivial or tightly coupled work in the parent. Delegate work that can proceed independently or benefit from independent judgment.
|
|
104
|
+
- Choose worker scopes and counts from the task. Treat workers or counts named by the user as a floor unless the user sets an exact cap.
|
|
105
|
+
- Each \`orchestrate\` call creates a fresh worker session. Multiple calls may use the same worker definition and identical instructions when independent judgments are useful. Do not vary briefs merely to make them appear different. Interactive follow-up instead continues one worker ID with its existing context.
|
|
106
|
+
- Give each worker a self-contained brief with its objective, context, paths and scope, forbidden actions, success criteria, and expected output. Workers do not receive the parent conversation.
|
|
107
|
+
|
|
108
|
+
### Parallel dispatch
|
|
109
|
+
|
|
110
|
+
- Form the complete wave before emitting any tool call.
|
|
111
|
+
- For one worker, make one fully briefed \`orchestrate\` call.
|
|
112
|
+
- For N workers where N > 1, make exactly one \`multi_tool_use.parallel\` call. Its \`tool_uses\` must contain exactly N \`functions.orchestrate\` entries and no other tools.
|
|
113
|
+
- If \`multi_tool_use.parallel\` is not present, emit all N \`orchestrate\` calls as native siblings in one assistant response.
|
|
114
|
+
- Never dispatch a multi-worker wave as separate assistant responses. An admitted sole asynchronous \`orchestrate\` call ends the parent turn, so omitted workers cannot be added afterward.
|
|
115
|
+
- The expanded tool-call group must contain only the intended \`orchestrate\` calls. Mixing another tool into the group makes orchestration inline and blocking.
|
|
116
|
+
|
|
117
|
+
### Completion and lifecycle
|
|
118
|
+
|
|
119
|
+
- Calls are admitted independently; a rejected call does not stop its siblings.
|
|
120
|
+
- After dispatching, wait for automatic result delivery instead of polling \`worker_status\`. When results expose more independent work, dispatch another complete wave.
|
|
121
|
+
- The parent reviews and synthesizes worker results, resolves conflicts, integrates changes, and runs the relevant verification.
|
|
122
|
+
- Prefer one-shot workers. Use interactive workers only when retained context is useful, and follow the ownership and status requirements in the lifecycle tool descriptions.
|
|
123
|
+
|
|
124
|
+
### Trusted worker catalog
|
|
125
|
+
|
|
126
|
+
${formatCatalog(catalog)}
|
|
127
|
+
${CONTRACT_END}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyOrchestratorContract(
|
|
131
|
+
systemPrompt: string,
|
|
132
|
+
catalog: WorkerCatalog,
|
|
133
|
+
): string {
|
|
134
|
+
const section = buildContract(catalog);
|
|
135
|
+
const cleaned = removeContractMarkers(systemPrompt);
|
|
136
|
+
if (cleaned.insertionOffset !== undefined) {
|
|
137
|
+
return `${cleaned.prompt.slice(0, cleaned.insertionOffset)}${section}${cleaned.prompt.slice(cleaned.insertionOffset)}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const separator =
|
|
141
|
+
systemPrompt.length === 0 || systemPrompt.endsWith("\n\n")
|
|
142
|
+
? ""
|
|
143
|
+
: systemPrompt.endsWith("\n")
|
|
144
|
+
? "\n"
|
|
145
|
+
: "\n\n";
|
|
146
|
+
return `${systemPrompt}${separator}${section}`;
|
|
147
|
+
}
|
|
@@ -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.ts";
|
|
3
|
+
import type { WorkerSettlement } from "../orchestration/settlement.ts";
|
|
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,8 +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>();
|
|
70
|
-
//
|
|
71
|
-
// so one watermark is valid.
|
|
67
|
+
// Orchestration settlement sequences are process-scoped and monotonic across
|
|
68
|
+
// owners, so one watermark is valid.
|
|
72
69
|
private highestAcceptedSequence = 0;
|
|
73
70
|
|
|
74
71
|
bind(binding: ParentBinding): void {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SynthesisGroup } from "../orchestration/model.ts";
|
|
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
|
+
}
|
|
@@ -3,37 +3,42 @@ import {
|
|
|
3
3
|
Delivery,
|
|
4
4
|
deliveryLayer,
|
|
5
5
|
type DeliveryService,
|
|
6
|
-
} from "./delivery.
|
|
6
|
+
} from "./delivery.ts";
|
|
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
|
-
|
|
22
|
-
|
|
17
|
+
} from "../orchestration/service.ts";
|
|
18
|
+
import type {
|
|
19
|
+
AbortTarget,
|
|
20
|
+
OrchestrationContext,
|
|
21
|
+
} from "../orchestration/admission.ts";
|
|
22
|
+
import { createChildSessionsLayer } from "../worker/child-sessions.ts";
|
|
23
|
+
import type { OrchestrateTaskInput, RunMode } from "../orchestration/model.ts";
|
|
24
|
+
|
|
25
|
+
// Bump this key when ProcessHost/OrchestrationClient changes incompatibly.
|
|
26
|
+
// Multiple package copies can coexist in one Pi process and adopt the same host.
|
|
23
27
|
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
24
28
|
|
|
25
|
-
type
|
|
29
|
+
type DispatchResult<M extends RunMode> = M extends "async"
|
|
26
30
|
? AcceptedRun
|
|
27
31
|
: CompletedRun;
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
/** Promise-facing orchestration API used by Pi tools and presentation. */
|
|
34
|
+
export interface OrchestrationClient {
|
|
30
35
|
orchestrate<M extends RunMode>(
|
|
31
36
|
context: OrchestrationContext,
|
|
32
37
|
task: OrchestrateTaskInput,
|
|
33
38
|
mode: M,
|
|
34
39
|
signal?: AbortSignal,
|
|
35
40
|
onSettlement?: SettlementListener,
|
|
36
|
-
): Promise<
|
|
41
|
+
): Promise<DispatchResult<M>>;
|
|
37
42
|
sendInteractive<M extends RunMode>(
|
|
38
43
|
context: OrchestrationContext,
|
|
39
44
|
workerId: string,
|
|
@@ -41,20 +46,20 @@ export interface OrchestratorRuntime {
|
|
|
41
46
|
mode: M,
|
|
42
47
|
signal?: AbortSignal,
|
|
43
48
|
onSettlement?: SettlementListener,
|
|
44
|
-
): Promise<
|
|
49
|
+
): Promise<DispatchResult<M>>;
|
|
45
50
|
abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
|
|
46
51
|
closeInteractive(ownerSessionId: string, workerId: string): Promise<void>;
|
|
47
|
-
snapshot(ownerSessionId: string): Promise<
|
|
52
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot>;
|
|
48
53
|
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
|
|
49
54
|
subscribeState(
|
|
50
55
|
ownerSessionId: string,
|
|
51
|
-
listener: (snapshot:
|
|
56
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
52
57
|
): () => void;
|
|
53
58
|
shutdown(): Promise<void>;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export interface ProcessHost {
|
|
57
|
-
readonly
|
|
62
|
+
readonly orchestration: OrchestrationClient;
|
|
58
63
|
readonly delivery: DeliveryService;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -100,7 +105,7 @@ export function getProcessHost(): ProcessHost | undefined {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
/** Pi-facing adapter. Every Promise operation executes one complete Orchestration Effect. */
|
|
103
|
-
export class
|
|
108
|
+
export class ManagedOrchestrationClient<R = never> implements OrchestrationClient {
|
|
104
109
|
constructor(
|
|
105
110
|
private readonly effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
106
111
|
private readonly orchestration: OrchestrationService,
|
|
@@ -192,7 +197,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
192
197
|
return this.run(this.orchestration.closeInteractive(ownerSessionId, workerId));
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
snapshot(ownerSessionId: string): Promise<
|
|
200
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot> {
|
|
196
201
|
// Snapshot is dependency-free and remains readable from a retained host reference
|
|
197
202
|
// after the process root has been disposed.
|
|
198
203
|
return Effect.runPromise(this.orchestration.snapshot(ownerSessionId));
|
|
@@ -204,7 +209,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
204
209
|
|
|
205
210
|
subscribeState(
|
|
206
211
|
ownerSessionId: string,
|
|
207
|
-
listener: (snapshot:
|
|
212
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
208
213
|
): () => void {
|
|
209
214
|
return this.orchestration.subscribeState(ownerSessionId, listener);
|
|
210
215
|
}
|
|
@@ -220,6 +225,8 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
220
225
|
if (signal?.aborted) throw abortSignalReason(signal);
|
|
221
226
|
if (!signal) return this.effectRuntime.runPromise(effect);
|
|
222
227
|
|
|
228
|
+
// Race with a private sentinel because Effect may transform failures; after
|
|
229
|
+
// interruption settles, Pi must receive the caller's exact AbortSignal.reason.
|
|
223
230
|
const signalInterruption = {};
|
|
224
231
|
const exit = await this.effectRuntime.runPromiseExit(
|
|
225
232
|
Effect.raceFirst(effect, abortSignalEffect(signal, signalInterruption)),
|
|
@@ -232,15 +239,15 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
export function
|
|
242
|
+
export function createOrchestrationClient<R>(
|
|
236
243
|
effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
237
|
-
):
|
|
244
|
+
): OrchestrationClient {
|
|
238
245
|
// Orchestration acquisition is synchronous; subscriptions must remain reentrant.
|
|
239
246
|
const orchestration = effectRuntime.runSync(Orchestration);
|
|
240
|
-
return new
|
|
247
|
+
return new ManagedOrchestrationClient(effectRuntime, orchestration);
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
export function
|
|
250
|
+
export function makeProcessHostLayer(): Layer.Layer<Orchestration | Delivery> {
|
|
244
251
|
const orchestration = orchestrationLayer().pipe(
|
|
245
252
|
Layer.provide(createChildSessionsLayer()),
|
|
246
253
|
);
|
|
@@ -258,11 +265,11 @@ export function createProcessHost(): ProcessHost {
|
|
|
258
265
|
}
|
|
259
266
|
if (existing) return existing;
|
|
260
267
|
|
|
261
|
-
const effectRuntime = ManagedRuntime.make(
|
|
262
|
-
const
|
|
268
|
+
const effectRuntime = ManagedRuntime.make(makeProcessHostLayer());
|
|
269
|
+
const orchestration = createOrchestrationClient(effectRuntime);
|
|
263
270
|
const delivery = effectRuntime.runSync(Delivery);
|
|
264
271
|
const host: OwnedProcessHost = {
|
|
265
|
-
|
|
272
|
+
orchestration,
|
|
266
273
|
delivery,
|
|
267
274
|
effectRuntime,
|
|
268
275
|
attachments: new Set(),
|
|
@@ -315,7 +322,7 @@ export function destroyProcessHost(
|
|
|
315
322
|
let shutdown: Promise<void>;
|
|
316
323
|
try {
|
|
317
324
|
// shutdown() closes Orchestration admission before returning its bounded teardown Promise.
|
|
318
|
-
shutdown = ownedHost.
|
|
325
|
+
shutdown = ownedHost.orchestration.shutdown();
|
|
319
326
|
} catch (error) {
|
|
320
327
|
shutdown = Promise.reject(error);
|
|
321
328
|
}
|