@zachwill/pi-orchestrate 0.5.0 → 0.6.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 CHANGED
@@ -18,9 +18,9 @@ Pi Orchestrate adds exactly five tools:
18
18
  | --- | --- | --- |
19
19
  | `orchestrate` | `orchestrate({ worker, title, instructions })` | Start one worker task |
20
20
  | `orchestration_status` | `orchestration_status({})` | Inspect the trusted catalog, diagnostics, runs, and worker states |
21
- | `worker_send` | `worker_send({ worker_id, instructions })` | Send a follow-up to a ready reusable worker |
21
+ | `interactive_send` | `interactive_send({ worker_id, instructions })` | Send a follow-up to a ready interactive worker |
22
22
  | `worker_abort` | `worker_abort({ worker_ids })` or `worker_abort({ all: true })` | Stop active owned work |
23
- | `worker_close` | `worker_close({ worker_id })` | Close a ready reusable worker |
23
+ | `interactive_close` | `interactive_close({ worker_id })` | Close a ready interactive worker |
24
24
 
25
25
  `title` is a label. `instructions` is the complete worker brief. Collapsed tool calls preview those instructions; expanded calls show them in full.
26
26
 
@@ -28,14 +28,16 @@ Pi Orchestrate adds exactly five tools:
28
28
 
29
29
  Each `orchestrate` call validates its input, worker definition, and model before allocating IDs or starting a session. Calls are admitted independently: a rejected sibling does not block valid siblings. After admission, a startup or prompt failure settles only that worker as `failed`.
30
30
 
31
- Pi executes native sibling tool calls concurrently. There is no extension-level sibling-group cap or hidden throttle. Before dispatching, enumerate the full wave. If an intended asynchronous wave has N workers, the next assistant response must contain exactly N separate, fully briefed `orchestrate` calls; one call is valid only when N=1. Form all N calls before emitting or finalizing the response because a successfully admitted sole async call returns `terminate: true` and ends the parent turn, so omitted siblings cannot be added afterward. To run the wave asynchronously, its tool-call group must contain exactly those N orchestration calls and no other tool calls. Harmless response text does not affect runtime classification. For example, a three-worker wave is one assistant response containing three native sibling `orchestrate({ worker, title, instructions })` calls.
31
+ Pi executes sibling tool calls concurrently. There is no extension-level sibling-group cap or hidden throttle. Before dispatching, enumerate the full wave. If an intended asynchronous wave has N workers, the next assistant response must contain exactly N separate, fully briefed `orchestrate` calls; one call is valid only when N=1. Form all N calls before emitting or finalizing the response because a successfully admitted sole async call returns `terminate: true` and ends the parent turn, so omitted siblings cannot be added afterward. Never emit one call and wait for its result before forming the rest of the wave.
32
+
33
+ When the host provides a parallel tool dispatcher, use it to submit the complete 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. Without a dispatcher, emit N native sibling `orchestrate` calls in one assistant response. In either form, the resulting expanded tool-call group must contain exactly those N orchestration calls and no other tool calls. Harmless response text does not affect runtime classification. A three-worker wave is one assistant response that submits all three `orchestrate({ worker, title, instructions })` calls together.
32
34
 
33
35
  Execution mode depends on the complete tool-call group:
34
36
 
35
37
  - Pi Orchestrate treats a successfully admitted sole `orchestrate` call as async.
36
38
  - Pi Orchestrate treats a successfully admitted pure group of sibling `orchestrate` calls as async; Pi executes the siblings concurrently.
37
39
  - Mixing `orchestrate` with any other tool makes the orchestration calls inline and blocking.
38
- - `worker_send` is asynchronous only when it is the sole tool call in the message.
40
+ - `interactive_send` is asynchronous only when it is the sole tool call in the message.
39
41
 
40
42
  Inline work follows the parent turn's cancellation signal. Accepted asynchronous work detaches from that signal and continues independently.
41
43
 
@@ -47,19 +49,19 @@ All state and delivery are owner-scoped. If an owning session is busy or inactiv
47
49
 
48
50
  `orchestration_status` is for diagnostics and recovery, not completion polling. It exposes bounded owner-scoped state without full task instructions or worker prompts.
49
51
 
50
- The bottom widget shows active work only. Completed, failed, aborted, and reusable ready workers disappear immediately. Inline work shows its current response in the live tool output while it blocks.
52
+ The bottom widget shows active work only. Completed, failed, aborted, and interactive ready workers disappear immediately. Inline work shows its current response in the live tool output while it blocks.
51
53
 
52
54
  ## Lifecycle
53
55
 
54
- A run represents one worker generation. A worker ID identifies the live worker session.
56
+ A run represents one worker generation. A worker ID identifies its worker session. Completed one-shot IDs may remain in bounded diagnostics history, but their sessions have already terminated.
55
57
 
56
- - A **one-shot** worker succeeds as `completed` and terminates.
57
- - A **reusable** worker succeeds as `ready` and keeps the same worker ID.
58
- - `worker_send` starts a new run on that ready reusable worker.
59
- - `worker_close` closes a ready reusable worker.
60
- - `worker_abort` stops active work only; `{ all: true }` does not close ready workers.
58
+ - A **one-shot** worker is the default. It automatically terminates after settling and requires no cleanup.
59
+ - An **interactive** worker is explicitly retained after a successful response as `ready`, keeping the same worker ID for follow-up work.
60
+ - `interactive_send` starts a new run on that ready interactive worker.
61
+ - `interactive_close` closes a ready interactive worker.
62
+ - `worker_abort` stops active work only; `{ all: true }` does not close ready interactive workers.
61
63
 
62
- Workers, runs, and queued delivery survive extension reloads and session switches within the same Pi process. Reusable workers do not survive process exit, so close them when continuity is no longer needed.
64
+ Workers, runs, and queued delivery survive extension reloads and session switches within the same Pi process. Runtime shutdown releases retained interactive workers automatically; use `interactive_close` earlier only when their continuity is no longer needed.
63
65
 
64
66
  ## Parent contract
65
67
 
@@ -67,7 +69,7 @@ Pi Orchestrate injects the authoritative orchestration contract and trusted cata
67
69
 
68
70
  1. Keep trivial or tightly coupled work in the parent. For broad work, spin up as many workers as needed to cover every useful bounded independent scope and distinct validation perspective; never use a small default or the number of roles the user names. Named workers and counts are a floor unless the user explicitly states an exact cap; the same role can be instantiated for multiple scopes.
69
71
  2. Give every worker a thorough, self-contained brief with the objective, paths and scope, context, success criteria, and expected output. Distinct validation perspectives can intentionally overlap, but avoid accidental duplicate work. State forbidden actions explicitly.
70
- 3. For an intended asynchronous wave of N workers, make the next assistant response's tool-call group exactly N fully briefed native sibling `orchestrate` calls and no other tool calls. Harmless response text does not affect runtime classification. A single call is valid only for N=1. Form the complete group before emitting it because a successfully admitted sole async call returns `terminate: true` and ends the turn; never wait for one sibling before dispatching the rest.
72
+ 3. For an intended asynchronous wave of N workers, submit exactly N fully briefed `orchestrate` calls together and no other tool calls. Prefer one parallel dispatcher call containing N orchestration entries when a dispatcher such as `multi_tool_use.parallel` is available; otherwise emit N native siblings in one assistant response. Harmless response text does not affect runtime classification. A single call is valid only for N=1. Form the complete group before emitting it because a successfully admitted sole async call returns `terminate: true` and ends the turn; never emit one call and wait for its result before forming the rest.
71
73
  4. As results expose new independent work, dispatch each full adaptive wave in parallel and continue until the whole task is complete.
72
74
  5. Review evidence and changes, resolve conflicts, integrate deliberately, and verify the result.
73
75
  6. Produce the final answer from the parent session.
@@ -86,6 +88,8 @@ A higher-precedence definition replaces a lower one with the same `name`. Pi per
86
88
 
87
89
  The package includes `scout`, `investigator`, `web`, and `worker` fallbacks. `scout`, `investigator`, and `worker` omit `model`, so they inherit the parent's active model at dispatch. `web` uses an installed, authenticated Codex CLI for public-web research and pins its Pi session and searches to `gpt-5.6-sol`. To customize one, copy its definition to the user or project directory and keep the same filename and `name`. Add an explicit model only when that worker needs one.
88
90
 
91
+ A catalog definition is dispatch configuration, not a retained session. The same definition can be dispatched repeatedly; each one-shot dispatch creates a fresh session that terminates automatically without cleanup.
92
+
89
93
  ## Worker definitions
90
94
 
91
95
  A worker is a regular Markdown file whose basename matches its `name`:
@@ -95,7 +99,7 @@ A worker is a regular Markdown file whose basename matches its `name`:
95
99
  name: reviewer
96
100
  description: Reviews a bounded change and returns evidence.
97
101
  tools: read, grep, find, ls, bash
98
- lifecycle: reusable
102
+ lifecycle: interactive
99
103
  ---
100
104
 
101
105
  Inspect the assigned scope and return concise findings with file paths.
@@ -106,7 +110,7 @@ Inspect the assigned scope and return concise findings with file paths.
106
110
  | `name` | Required; must match the filename |
107
111
  | `description` | Required; used by the parent to choose a worker |
108
112
  | `tools` | Required, nonempty list using `read`, `bash`, `edit`, `write`, `grep`, `find`, or `ls` |
109
- | `lifecycle` | Required; exactly `one-shot` or `reusable` |
113
+ | `lifecycle` | Required; exactly `one-shot` or `interactive` |
110
114
  | `model` | Optional `provider/model`; omitted inherits the parent model |
111
115
  | `thinking` | Optional Pi thinking level |
112
116
  | `skills` | Optional; omitted uses normal discovery, a list is an exact allowlist, and `[]` disables skills |
@@ -76,7 +76,7 @@ const WorkerFrontmatter = Schema.Struct({
76
76
  tools: commaList(Schema.Literals(SUPPORTED_TOOL_NAMES)),
77
77
  skills: Schema.optionalKey(commaList(Schema.NonEmptyString, true)),
78
78
  compaction: Schema.optionalKey(Compaction),
79
- lifecycle: Schema.Literals(["one-shot", "reusable"]),
79
+ lifecycle: Schema.Literals(["one-shot", "interactive"]),
80
80
  });
81
81
  const decodeWorkerFrontmatter = Schema.decodeUnknownResult(WorkerFrontmatter, {
82
82
  errors: "all",
@@ -267,7 +267,7 @@ function schemaDiagnostic(issue: SchemaIssue.Issue, frontmatter: unknown): strin
267
267
  return "frontmatter field 'compaction' must be a mapping";
268
268
  }
269
269
  if (field === "lifecycle") {
270
- return "frontmatter field 'lifecycle' must be 'one-shot' or 'reusable'";
270
+ return "frontmatter field 'lifecycle' must be 'one-shot' or 'interactive'";
271
271
  }
272
272
  return "invalid worker definition";
273
273
  }
@@ -30,21 +30,22 @@ function buildContract(catalog: WorkerCatalog): string {
30
30
  You are the parent orchestrator and own the task end to end.
31
31
 
32
32
  - 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.
33
- - 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. A worker role is reusable: dispatch the same catalog worker in many calls when it fits separate scopes or perspectives.
33
+ - 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.
34
34
  - Before dispatching, enumerate the full first parallel wave from the work itself.
35
- - **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.
36
- - **Asynchronous response shape:** To run that wave asynchronously, its 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 native sibling tool calls concurrently. For N=3, emit three native sibling calls in one response: \`orchestrate({ worker, title, instructions })\`, \`orchestrate({ worker, title, instructions })\`, and \`orchestrate({ worker, title, instructions })\`.
35
+ - **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.
36
+ - **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.
37
+ - **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 })\`.
37
38
  - 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.
38
39
  - 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.
39
40
  - Give every worker a thorough, self-contained brief with the objective, paths and scope, context, success criteria, and expected output. State forbidden actions explicitly.
40
41
  - 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.
41
- - 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. \`worker_send\` is asynchronous only as the sole tool call in its assistant message.
42
+ - 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.
42
43
  - Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
43
44
  - 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 \`orchestration_status\` or use it as a normal completion mechanism.
44
45
  - 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.
45
46
  - The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion.
46
- - Prefer one-shot workers. Use \`worker_send\` for follow-up work on a ready reusable worker, \`worker_close\` when that ready worker is finished, and \`worker_abort\` only when active work must stop.
47
- - The public tools are \`orchestrate\`, \`orchestration_status\`, \`worker_send\`, \`worker_abort\`, and \`worker_close\`.
47
+ - 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.
48
+ - The public tools are \`orchestrate\`, \`orchestration_status\`, \`interactive_send\`, \`worker_abort\`, and \`interactive_close\`.
48
49
 
49
50
  ### Trusted worker catalog
50
51
 
@@ -198,10 +198,12 @@ export class DeliveryCoordinator {
198
198
  byteLimit: number,
199
199
  ): WorkerDeliveryMessage {
200
200
  const heading = `## Worker result — ${settlement.title} · ${settlement.worker}`;
201
+ const disposition = renderDisposition(settlement);
201
202
  const metadata = [
202
203
  `Worker \`${settlement.workerId}\``,
203
204
  `run \`${settlement.runId}\``,
204
205
  `status \`${settlement.status}\``,
206
+ ...(disposition ? [disposition] : []),
205
207
  ].join(" · ");
206
208
  const body = renderOutcome(settlement.outcome);
207
209
  const content = body.length > 0
@@ -283,6 +285,16 @@ function truncateUtf8(content: string, byteLimit: number): string {
283
285
  return bytes.subarray(0, end).toString("utf8");
284
286
  }
285
287
 
288
+ function renderDisposition(settlement: WorkerSettlement): string | undefined {
289
+ if (settlement.status === "completed" && settlement.lifecycle === "one-shot") {
290
+ return "one-shot session ended automatically; no close needed";
291
+ }
292
+ if (settlement.status === "ready" && settlement.lifecycle === "interactive") {
293
+ return "interactive session retained; use `interactive_send` or `interactive_close`";
294
+ }
295
+ return undefined;
296
+ }
297
+
286
298
  function renderOutcome(outcome: WorkerSettlement["outcome"]): string {
287
299
  switch (outcome.status) {
288
300
  case "completed":
@@ -36,7 +36,7 @@ export interface WorkerCompaction {
36
36
  readonly keepRecentTokens?: number;
37
37
  }
38
38
 
39
- export type WorkerLifecycle = "one-shot" | "reusable";
39
+ export type WorkerLifecycle = "one-shot" | "interactive";
40
40
 
41
41
  export interface WorkerDefinition {
42
42
  readonly name: string;
@@ -278,11 +278,11 @@ export function canTransitionWorkerStatus(
278
278
  case "starting":
279
279
  return to === "running" || to === "stopping" || to === "failed" || to === "aborted";
280
280
  case "running":
281
- if (to === "ready") return lifecycle === "reusable";
281
+ if (to === "ready") return lifecycle === "interactive";
282
282
  if (to === "completed") return lifecycle === "one-shot";
283
283
  return to === "stopping" || to === "failed" || to === "aborted";
284
284
  case "ready":
285
- if (lifecycle !== "reusable") return false;
285
+ if (lifecycle !== "interactive") return false;
286
286
  return to === "running" || to === "stopping" || to === "closed";
287
287
  case "stopping":
288
288
  return to === "aborted" || to === "failed";
@@ -29,7 +29,7 @@ import {
29
29
 
30
30
  const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
31
31
  "orchestrate",
32
- "worker_send",
32
+ "interactive_send",
33
33
  ]);
34
34
 
35
35
  interface StoredDispatchDecision extends DispatchDecision {
@@ -66,7 +66,7 @@ export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
66
66
 
67
67
  export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
68
68
  const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
69
- return ready > 0 ? `${ready} available for follow-up` : undefined;
69
+ return ready > 0 ? `${ready} interactive ready` : undefined;
70
70
  }
71
71
 
72
72
  export class StatusController {
@@ -331,7 +331,8 @@ function resultQualifier(result: SafeSettlement): string | undefined {
331
331
  return "could not start";
332
332
  }
333
333
  if (result.status === "failed") return "failed";
334
- if (result.status === "ready") return "ready for follow-up";
334
+ if (result.status === "ready") return "interactive ready";
335
+ if (result.status === "completed") return "one-shot ended";
335
336
  return undefined;
336
337
  }
337
338
 
@@ -140,7 +140,7 @@ export interface OrchestratorRuntime {
140
140
  signal?: AbortSignal,
141
141
  onSettlement?: SettlementListener,
142
142
  ): Promise<AcceptedRun | CompletedRun>;
143
- send(
143
+ sendInteractive(
144
144
  context: OrchestrationContext,
145
145
  workerId: WorkerId,
146
146
  instructions: string,
@@ -148,7 +148,7 @@ export interface OrchestratorRuntime {
148
148
  signal?: AbortSignal,
149
149
  onSettlement?: SettlementListener,
150
150
  ): Promise<AcceptedRun>;
151
- send(
151
+ sendInteractive(
152
152
  context: OrchestrationContext,
153
153
  workerId: WorkerId,
154
154
  instructions: string,
@@ -156,7 +156,7 @@ export interface OrchestratorRuntime {
156
156
  signal?: AbortSignal,
157
157
  onSettlement?: SettlementListener,
158
158
  ): Promise<CompletedRun>;
159
- send(
159
+ sendInteractive(
160
160
  context: OrchestrationContext,
161
161
  workerId: WorkerId,
162
162
  instructions: string,
@@ -165,7 +165,7 @@ export interface OrchestratorRuntime {
165
165
  onSettlement?: SettlementListener,
166
166
  ): Promise<AcceptedRun | CompletedRun>;
167
167
  abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
168
- close(ownerSessionId: string, workerId: WorkerId): Promise<void>;
168
+ closeInteractive(ownerSessionId: string, workerId: WorkerId): Promise<void>;
169
169
  snapshot(ownerSessionId: string): Promise<RuntimeSnapshot>;
170
170
  subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
171
171
  subscribeState(listener: StateListener): () => void;
@@ -314,7 +314,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
314
314
  return freezeAcceptedRun(runId, workerId);
315
315
  }
316
316
 
317
- send(
317
+ sendInteractive(
318
318
  context: OrchestrationContext,
319
319
  workerId: WorkerId,
320
320
  instructions: string,
@@ -322,7 +322,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
322
322
  signal?: AbortSignal,
323
323
  onSettlement?: SettlementListener,
324
324
  ): Promise<AcceptedRun>;
325
- send(
325
+ sendInteractive(
326
326
  context: OrchestrationContext,
327
327
  workerId: WorkerId,
328
328
  instructions: string,
@@ -330,7 +330,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
330
330
  signal?: AbortSignal,
331
331
  onSettlement?: SettlementListener,
332
332
  ): Promise<CompletedRun>;
333
- send(
333
+ sendInteractive(
334
334
  context: OrchestrationContext,
335
335
  workerId: WorkerId,
336
336
  instructions: string,
@@ -338,7 +338,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
338
338
  signal?: AbortSignal,
339
339
  onSettlement?: SettlementListener,
340
340
  ): Promise<AcceptedRun | CompletedRun>;
341
- async send(
341
+ async sendInteractive(
342
342
  context: OrchestrationContext,
343
343
  workerId: WorkerId,
344
344
  instructions: string,
@@ -353,11 +353,11 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
353
353
  validateText("instructions", instructions, MAX_WORKER_INSTRUCTIONS_LENGTH);
354
354
 
355
355
  const current = this.ownedWorker(context.ownerSessionId, workerId);
356
- if (current.lifecycle !== "reusable" || current.status !== "ready") {
357
- throw new Error("worker_send requires an owned ready reusable worker");
356
+ if (current.lifecycle !== "interactive" || current.status !== "ready") {
357
+ throw new Error("interactive_send requires an owned ready interactive worker");
358
358
  }
359
359
  const entry = this.entries.get(workerId);
360
- if (!entry?.session) throw new Error("Ready reusable worker has no session handle");
360
+ if (!entry?.session) throw new Error("Ready interactive worker has no session handle");
361
361
 
362
362
  const runId = this.idFactories.runId();
363
363
  if (this.runs.has(runId)) throw new Error(`Duplicate run ID: ${runId}`);
@@ -403,14 +403,14 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
403
403
  await this.cancelWorkers(targets);
404
404
  }
405
405
 
406
- async close(ownerSessionId: string, workerId: WorkerId): Promise<void> {
406
+ async closeInteractive(ownerSessionId: string, workerId: WorkerId): Promise<void> {
407
407
  this.assertOpen();
408
408
  validateContextOwner(ownerSessionId);
409
409
  const current = this.ownedWorker(ownerSessionId, workerId);
410
- if (current.lifecycle !== "reusable" || current.status !== "ready") {
411
- throw new Error("worker_close requires an owned ready reusable worker");
410
+ if (current.lifecycle !== "interactive" || current.status !== "ready") {
411
+ throw new Error("interactive_close requires an owned ready interactive worker");
412
412
  }
413
- this.closeReadyWorker(current);
413
+ this.closeReadyInteractiveWorker(current);
414
414
  }
415
415
 
416
416
  async snapshot(ownerSessionId: string): Promise<RuntimeSnapshot> {
@@ -458,7 +458,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
458
458
 
459
459
  private async performShutdown(): Promise<void> {
460
460
  try {
461
- this.closeReadyWorkersForShutdown();
461
+ this.closeReadyInteractiveWorkersForShutdown();
462
462
  const active = [...this.workers.values()]
463
463
  .filter((worker) => isActiveWorkerStatus(worker.status))
464
464
  .map((worker) => worker.id);
@@ -748,7 +748,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
748
748
  let status: "ready" | "completed" | "failed" | "aborted";
749
749
  if (outcome.status === "failed" || outcome.status === "aborted") {
750
750
  status = outcome.status;
751
- } else if (current.lifecycle === "reusable" && outcome.status === "ready") {
751
+ } else if (current.lifecycle === "interactive" && outcome.status === "ready") {
752
752
  status = "ready";
753
753
  } else if (current.lifecycle === "one-shot" && outcome.status === "completed") {
754
754
  status = "completed";
@@ -928,7 +928,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
928
928
  for (const workerId of unique) {
929
929
  const worker = this.ownedWorker(ownerSessionId, workerId);
930
930
  if (worker.status === "ready") {
931
- throw new Error("Ready reusable workers are not active; use worker_close");
931
+ throw new Error("Ready interactive workers are not active; use interactive_close");
932
932
  }
933
933
  if (!isActiveWorkerStatus(worker.status)) {
934
934
  throw new Error("worker_abort requires owned active workers");
@@ -1068,7 +1068,7 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
1068
1068
  });
1069
1069
  }
1070
1070
 
1071
- private closeReadyWorker(current: WorkerRecord): void {
1071
+ private closeReadyInteractiveWorker(current: WorkerRecord): void {
1072
1072
  const entry = this.entries.get(current.id);
1073
1073
  if (entry) this.disposeEntrySession(entry);
1074
1074
  this.workers.set(current.id, {
@@ -1083,9 +1083,13 @@ class DefaultOrchestratorRuntime implements OrchestratorRuntime {
1083
1083
  this.emitStateForOwners(affectedOwners);
1084
1084
  }
1085
1085
 
1086
- private closeReadyWorkersForShutdown(): void {
1087
- const ready = [...this.workers.values()].filter((worker) => worker.status === "ready");
1088
- for (const worker of ready) this.closeReadyWorker(worker);
1086
+ private closeReadyInteractiveWorkersForShutdown(): void {
1087
+ const readyInteractiveWorkers = [...this.workers.values()].filter(
1088
+ (worker) => worker.lifecycle === "interactive" && worker.status === "ready",
1089
+ );
1090
+ for (const worker of readyInteractiveWorkers) {
1091
+ this.closeReadyInteractiveWorker(worker);
1092
+ }
1089
1093
  }
1090
1094
 
1091
1095
  private subscribeEntryObservability(
@@ -59,7 +59,7 @@ const orchestrateSchema = taskSchema;
59
59
 
60
60
  const statusSchema = Type.Object({}, STRICT_OBJECT);
61
61
 
62
- const workerSendSchema = Type.Object(
62
+ const interactiveSendSchema = Type.Object(
63
63
  {
64
64
  worker_id: Type.String({ minLength: 1 }),
65
65
  instructions: Type.String(),
@@ -82,7 +82,7 @@ const workerAbortSchema = Type.Union([
82
82
  ),
83
83
  ]);
84
84
 
85
- const workerCloseSchema = Type.Object(
85
+ const interactiveCloseSchema = Type.Object(
86
86
  {
87
87
  worker_id: Type.String({ minLength: 1 }),
88
88
  },
@@ -116,7 +116,8 @@ export function registerOrchestrationTools(
116
116
  promptGuidelines: [
117
117
  "Spin up as many workers as needed to cover every useful parallel scope and distinct validation perspective. Treat user-named workers or counts as a floor unless explicitly capped, and reuse the same worker role across multiple calls when useful.",
118
118
  "For an intended async wave of N workers, the next assistant response must contain exactly N separate, fully briefed orchestrate calls; one call is valid only when N=1. To run it asynchronously, include no other tool calls; harmless response text does not affect runtime classification.",
119
- "Form all N native sibling calls before emitting or finalizing the response. A successfully admitted sole async orchestrate call returns terminate=true and ends the turn, so omitted siblings cannot be added afterward; never wait for one sibling's acceptance or completion.",
119
+ "When a parallel tool dispatcher is available, use it once with exactly N orchestrate entries and no other tools; for example, put N functions.orchestrate entries in multi_tool_use.parallel. Otherwise emit N native sibling orchestrate calls in one assistant response.",
120
+ "Form all N calls before emitting or finalizing the response. Never emit one call and wait for its result before forming the rest of the wave: a successfully admitted sole async orchestrate call returns terminate=true and ends the turn.",
120
121
  ],
121
122
  executionMode: "parallel",
122
123
  parameters: orchestrateSchema,
@@ -212,17 +213,17 @@ export function registerOrchestrationTools(
212
213
  });
213
214
 
214
215
  pi.registerTool({
215
- name: "worker_send",
216
- label: "Worker Send",
216
+ name: "interactive_send",
217
+ label: "Interactive Send",
217
218
  description:
218
- "Send follow-up instructions to an owned ready reusable worker. A sole tool call runs asynchronously; sibling tool calls make it inline and blocking.",
219
- promptSnippet: "Send follow-up work to an owned ready reusable worker",
219
+ "Send follow-up instructions only to an owned lifecycle interactive worker whose status is ready. Never use for one-shot or completed workers; one-shot sessions terminate automatically. A sole tool call runs asynchronously; sibling tool calls make it inline and blocking.",
220
+ promptSnippet: "Use only for an owned lifecycle interactive worker with status ready; never one-shot/completed because one-shot sessions terminate automatically",
220
221
  promptGuidelines: [
221
- "Use worker_send only for follow-up work on an owned ready reusable worker.",
222
+ "Use interactive_send only for an owned lifecycle interactive worker whose status is ready; never use it for one-shot or completed workers because one-shot sessions terminate automatically.",
222
223
  ],
223
- parameters: workerSendSchema,
224
+ parameters: interactiveSendSchema,
224
225
  renderCall(args, theme, { expanded }) {
225
- return renderWorkerMessageCall(theme, "worker_send", args.worker_id, args.instructions, expanded);
226
+ return renderInteractiveMessageCall(theme, "interactive_send", args.worker_id, args.instructions, expanded);
226
227
  },
227
228
  renderResult(result, { isPartial, expanded }, theme, context) {
228
229
  return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
@@ -232,7 +233,7 @@ export function registerOrchestrationTools(
232
233
  const mode = deps.getDispatchDecision(toolCallId).mode;
233
234
  const runtimeContext = await buildRuntimeContext(ctx, deps);
234
235
  if (mode === "async") {
235
- const acceptedRun = await deps.runtime.send(
236
+ const acceptedRun = await deps.runtime.sendInteractive(
236
237
  runtimeContext,
237
238
  workerId,
238
239
  params.instructions,
@@ -252,7 +253,7 @@ export function registerOrchestrationTools(
252
253
  };
253
254
  }
254
255
 
255
- const completedRun = await deps.runtime.send(
256
+ const completedRun = await deps.runtime.sendInteractive(
256
257
  runtimeContext,
257
258
  workerId,
258
259
  params.instructions,
@@ -280,10 +281,10 @@ export function registerOrchestrationTools(
280
281
  name: "worker_abort",
281
282
  label: "Worker Abort",
282
283
  description:
283
- "Abort owned active work by worker IDs or all active owned workers. Use worker_close for ready reusable workers.",
284
+ "Abort owned active work by worker IDs or all active owned workers. Use interactive_close for owned lifecycle interactive workers whose status is ready.",
284
285
  promptSnippet: "Abort active owned workers by worker IDs or all",
285
286
  promptGuidelines: [
286
- "Use worker_abort only for active work; use worker_close for a ready reusable worker.",
287
+ "Use worker_abort only for active work; use interactive_close only for an owned lifecycle interactive worker whose status is ready, never for one-shot or completed workers because one-shot sessions terminate automatically.",
287
288
  ],
288
289
  parameters: workerAbortSchema,
289
290
  renderCall(args, theme) {
@@ -316,16 +317,16 @@ export function registerOrchestrationTools(
316
317
  });
317
318
 
318
319
  pi.registerTool({
319
- name: "worker_close",
320
- label: "Worker Close",
321
- description: "Close an owned ready reusable worker that no longer needs follow-up work.",
322
- promptSnippet: "Close an owned ready reusable worker",
320
+ name: "interactive_close",
321
+ label: "Interactive Close",
322
+ description: "Close only an owned lifecycle interactive worker whose status is ready. Never use for one-shot or completed workers; one-shot sessions terminate automatically.",
323
+ promptSnippet: "Use only for an owned lifecycle interactive worker with status ready; never one-shot/completed because one-shot sessions terminate automatically",
323
324
  promptGuidelines: [
324
- "Use worker_close when an owned ready reusable worker is finished.",
325
+ "Use interactive_close only for an owned lifecycle interactive worker whose status is ready; never use it for one-shot or completed workers because one-shot sessions terminate automatically.",
325
326
  ],
326
- parameters: workerCloseSchema,
327
+ parameters: interactiveCloseSchema,
327
328
  renderCall(args, theme) {
328
- return renderCompactCall(theme, "worker_close", args.worker_id);
329
+ return renderCompactCall(theme, "interactive_close", args.worker_id);
329
330
  },
330
331
  renderResult(result, { isPartial }, theme) {
331
332
  return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
@@ -336,7 +337,7 @@ export function registerOrchestrationTools(
336
337
  ctx.sessionManager.getSessionId(),
337
338
  );
338
339
  const workerId = asWorkerId(params.worker_id);
339
- await deps.runtime.close(ownerSessionId, workerId);
340
+ await deps.runtime.closeInteractive(ownerSessionId, workerId);
340
341
  const readable = { worker_id: workerId };
341
342
  return {
342
343
  content: [
@@ -641,7 +642,7 @@ class InstructionPreview implements Component {
641
642
  invalidate(): void {}
642
643
  }
643
644
 
644
- function renderWorkerMessageCall(
645
+ function renderInteractiveMessageCall(
645
646
  theme: Theme,
646
647
  tool: string,
647
648
  workerId: unknown,
@@ -348,7 +348,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
348
348
 
349
349
  constructor(
350
350
  private readonly runtime: OwnedWorkerRuntime,
351
- private readonly reusable: boolean,
351
+ private readonly interactive: boolean,
352
352
  sessionFile: string,
353
353
  private readonly scope: Scope.Closeable,
354
354
  private readonly cleanupReporter: WorkerSessionCleanupReporter,
@@ -429,7 +429,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
429
429
  return { status: "failed", message: message.errorMessage ?? failureMessage ?? "Worker assistant reported a failure", ...assistantPayload };
430
430
  }
431
431
  if (failureMessage) return { status: "failed", message: failureMessage, ...assistantPayload };
432
- return { status: this.reusable ? "ready" : "completed", assistantText: text ?? "" };
432
+ return { status: this.interactive ? "ready" : "completed", assistantText: text ?? "" };
433
433
  }
434
434
 
435
435
  async abort(): Promise<void> {
@@ -852,7 +852,7 @@ const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
852
852
 
853
853
  const handle = new DefaultWorkerSessionHandle(
854
854
  runtime,
855
- definition.lifecycle === "reusable",
855
+ definition.lifecycle === "interactive",
856
856
  sessionFile,
857
857
  scope,
858
858
  dependencies.reportCleanupFailure,
@@ -53,7 +53,7 @@ export const WorkerSettlementDetails = Schema.Struct({
53
53
  mode: Schema.Literals(["async", "inline"]),
54
54
  worker: Schema.String,
55
55
  title: Schema.String,
56
- lifecycle: Schema.Literals(["one-shot", "reusable"]),
56
+ lifecycle: Schema.Literals(["one-shot", "interactive"]),
57
57
  status: Schema.Literals(["completed", "ready", "failed", "aborted"]),
58
58
  outcome: WorkerOutcome,
59
59
  usage: WorkerUsage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachwill/pi-orchestrate",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Concurrent worker orchestration for Pi",
6
6
  "files": ["extension/", "examples/", "README.md", "LICENSE"],