@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.
@@ -8,7 +8,9 @@ import {
8
8
  WorkerReadyOutcome,
9
9
  WorkerResponseOutcome,
10
10
  WorkerUsage,
11
- } from "./domain.js";
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 settlement details written by the current runtime. */
24
- export const WorkerSettlementDetails = Schema.Struct({
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 WorkerSettlementDetails
81
- extends Schema.Schema.Type<typeof WorkerSettlementDetails> {}
82
+ export interface WorkerSettlement
83
+ extends Schema.Schema.Type<typeof WorkerSettlement> {}
82
84
 
83
85
  export type SettlementFailureStage = NonNullable<
84
- WorkerSettlementDetails["failureStage"]
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
- WorkerSettlementDetails,
90
+ WorkerSettlement,
93
91
  );
94
92
 
95
- export function decodePersistedWorkerSettlementDetails(value: unknown) {
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: WorkerSettlementDetails.fields.workerId,
125
- worker: WorkerSettlementDetails.fields.worker,
126
- title: WorkerSettlementDetails.fields.title,
127
- status: WorkerSettlementDetails.fields.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: WorkerSettlementDetails.fields.startedAt,
131
- settledAt: WorkerSettlementDetails.fields.settledAt,
132
- sessionFile: WorkerSettlementDetails.fields.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(WorkerSettlementDetails.fields.runId),
202
+ runId: Schema.optionalKey(WorkerSettlement.fields.runId),
157
203
  ownerSessionId: Schema.optionalKey(
158
- WorkerSettlementDetails.fields.ownerSessionId,
204
+ WorkerSettlement.fields.ownerSessionId,
159
205
  ),
160
206
  result: InlineWorkerSettlementDetails,
161
207
  }).pipe(
@@ -0,0 +1,3 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ export const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
@@ -1,4 +1,4 @@
1
- import type { WorkerCatalog } from "./domain.js";
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, "&lt;!-- pi-orchestrate:contract:start --&gt;")
17
+ .replaceAll(CONTRACT_END, "&lt;!-- pi-orchestrate:contract:end --&gt;");
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 appendOrchestratorContract(
127
+ export function applyOrchestratorContract(
59
128
  systemPrompt: string,
60
129
  catalog: WorkerCatalog,
61
130
  ): string {
62
131
  const section = buildContract(catalog);
63
- const start = systemPrompt.indexOf(CONTRACT_START);
64
- if (start >= 0) {
65
- const end = systemPrompt.indexOf(CONTRACT_END, start);
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 "./runtime.js";
3
- import type { WorkerSettlement } from "./worker-settlement.js";
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: WorkerDeliveryDetails;
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) parent.agentRunning = true;
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 (!parent) return;
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 (let index = 0; index < queued.length; index += 1) {
171
- const settlement = queued[index];
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 (let index = 0; index <= flushThrough; index += 1) {
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
- const settlement = queued[index];
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 RuntimeSnapshot,
14
+ type OwnerSnapshot,
17
15
  type SettlementListener,
18
16
  type UnsubscribeSettlement,
19
- } from "./runtime.js";
20
- import { createChildSessionsLayer } from "./worker-session.js";
21
- import type { OrchestrateTaskInput, RunMode } from "./domain.js";
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 RunResult<M extends RunMode> = M extends "async"
27
+ type DispatchResult<M extends RunMode> = M extends "async"
26
28
  ? AcceptedRun
27
29
  : CompletedRun;
28
30
 
29
- export interface OrchestratorRuntime {
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<RunResult<M>>;
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<RunResult<M>>;
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<RuntimeSnapshot>;
50
+ snapshot(ownerSessionId: string): Promise<OwnerSnapshot>;
48
51
  subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
49
52
  subscribeState(
50
53
  ownerSessionId: string,
51
- listener: (snapshot: RuntimeSnapshot) => void,
54
+ listener: (snapshot: OwnerSnapshot) => void,
52
55
  ): () => void;
53
56
  shutdown(): Promise<void>;
54
57
  }
55
58
 
56
59
  export interface ProcessHost {
57
- readonly runtime: OrchestratorRuntime;
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 = "active" | "destroying" | "destroyed";
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 ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime {
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<RuntimeSnapshot> {
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: RuntimeSnapshot) => void,
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 createProcessHostRuntimeAdapter<R>(
238
+ export function createOrchestrationClient<R>(
236
239
  effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
237
- ): OrchestratorRuntime {
240
+ ): OrchestrationClient {
238
241
  // Orchestration acquisition is synchronous; subscriptions must remain reentrant.
239
242
  const orchestration = effectRuntime.runSync(Orchestration);
240
- return new ProcessHostRuntimeAdapter(effectRuntime, orchestration);
243
+ return new ManagedOrchestrationClient(effectRuntime, orchestration);
241
244
  }
242
245
 
243
- export function createProcessApplicationLayer(): Layer.Layer<Orchestration | Delivery> {
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
- delete global[PROCESS_HOST_KEY];
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(createProcessApplicationLayer());
264
- const runtime = createProcessHostRuntimeAdapter(effectRuntime);
264
+ const effectRuntime = ManagedRuntime.make(makeProcessHostLayer());
265
+ const orchestration = createOrchestrationClient(effectRuntime);
265
266
  const delivery = effectRuntime.runSync(Delivery);
266
267
  const host: OwnedProcessHost = {
267
- runtime,
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.runtime.shutdown();
321
+ shutdown = ownedHost.orchestration.shutdown();
323
322
  } catch (error) {
324
323
  shutdown = Promise.reject(error);
325
324
  }