@zachwill/pi-orchestrate 0.5.1 → 0.7.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
@@ -17,10 +17,10 @@ Pi Orchestrate adds exactly five tools:
17
17
  | Tool | Call | Purpose |
18
18
  | --- | --- | --- |
19
19
  | `orchestrate` | `orchestrate({ worker, title, instructions })` | Start one worker task |
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 |
20
+ | `worker_status` | `worker_status({})` | Inspect the aggregate worker-system diagnostics and recovery snapshot |
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
 
@@ -37,7 +37,7 @@ Execution mode depends on the complete tool-call group:
37
37
  - Pi Orchestrate treats a successfully admitted sole `orchestrate` call as async.
38
38
  - Pi Orchestrate treats a successfully admitted pure group of sibling `orchestrate` calls as async; Pi executes the siblings concurrently.
39
39
  - Mixing `orchestrate` with any other tool makes the orchestration calls inline and blocking.
40
- - `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.
41
41
 
42
42
  Inline work follows the parent turn's cancellation signal. Accepted asynchronous work detaches from that signal and continues independently.
43
43
 
@@ -47,21 +47,21 @@ Asynchronous worker results enter the transcript individually. An ungrouped resu
47
47
 
48
48
  All state and delivery are owner-scoped. If an owning session is busy or inactive, completed results queue until that exact session is active and idle again. They are never delivered to another session.
49
49
 
50
- `orchestration_status` is for diagnostics and recovery, not completion polling. It exposes bounded owner-scoped state without full task instructions or worker prompts.
50
+ `worker_status` is the aggregate worker-system diagnostics and recovery snapshot, containing the trusted catalog, diagnostics, runs, and worker states. It is not for completion polling and exposes bounded owner-scoped state without full task instructions or worker prompts.
51
51
 
52
- 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.
53
53
 
54
54
  ## Lifecycle
55
55
 
56
- 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.
57
57
 
58
- - A **one-shot** worker succeeds as `completed` and terminates.
59
- - A **reusable** worker succeeds as `ready` and keeps the same worker ID.
60
- - `worker_send` starts a new run on that ready reusable worker.
61
- - `worker_close` closes a ready reusable worker.
62
- - `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.
63
63
 
64
- 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.
65
65
 
66
66
  ## Parent contract
67
67
 
@@ -88,6 +88,8 @@ A higher-precedence definition replaces a lower one with the same `name`. Pi per
88
88
 
89
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.
90
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
+
91
93
  ## Worker definitions
92
94
 
93
95
  A worker is a regular Markdown file whose basename matches its `name`:
@@ -97,7 +99,7 @@ A worker is a regular Markdown file whose basename matches its `name`:
97
99
  name: reviewer
98
100
  description: Reviews a bounded change and returns evidence.
99
101
  tools: read, grep, find, ls, bash
100
- lifecycle: reusable
102
+ lifecycle: interactive
101
103
  ---
102
104
 
103
105
  Inspect the assigned scope and return concise findings with file paths.
@@ -108,7 +110,7 @@ Inspect the assigned scope and return concise findings with file paths.
108
110
  | `name` | Required; must match the filename |
109
111
  | `description` | Required; used by the parent to choose a worker |
110
112
  | `tools` | Required, nonempty list using `read`, `bash`, `edit`, `write`, `grep`, `find`, or `ls` |
111
- | `lifecycle` | Required; exactly `one-shot` or `reusable` |
113
+ | `lifecycle` | Required; exactly `one-shot` or `interactive` |
112
114
  | `model` | Optional `provider/model`; omitted inherits the parent model |
113
115
  | `thinking` | Optional Pi thinking level |
114
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,7 +30,7 @@ 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
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
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.
@@ -39,13 +39,13 @@ You are the parent orchestrator and own the task end to end.
39
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.
40
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.
41
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.
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. \`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.
43
43
  - Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
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
+ - After the full current wave has been dispatched, yield the parent turn once its admissions have resolved; a rejected sibling does not block yielding. Worker responses arrive individually as each worker settles, and the final response starts parent synthesis. Do not poll \`worker_status\` or use it as a normal completion mechanism.
45
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.
46
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.
47
- - 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.
48
- - 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\`, \`worker_status\`, \`interactive_send\`, \`worker_abort\`, and \`interactive_close\`.
49
49
 
50
50
  ### Trusted worker catalog
51
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
  },
@@ -175,17 +175,17 @@ export function registerOrchestrationTools(
175
175
  });
176
176
 
177
177
  pi.registerTool({
178
- name: "orchestration_status",
179
- label: "Orchestration Status",
178
+ name: "worker_status",
179
+ label: "Worker Status",
180
180
  description:
181
181
  "Diagnostics and recovery only: inspect trusted catalog entries, catalog diagnostics, and this session's runtime state. Never poll for completion.",
182
- promptSnippet: "Inspect owned orchestration state for diagnostics or recovery",
182
+ promptSnippet: "Inspect owned worker state for diagnostics or recovery",
183
183
  promptGuidelines: [
184
- "Use orchestration_status only for diagnostics or recovery; never poll it for completion.",
184
+ "Use worker_status only for diagnostics or recovery; never poll it for completion.",
185
185
  ],
186
186
  parameters: statusSchema,
187
187
  renderCall(_args, theme) {
188
- return new Text(theme.fg("toolTitle", theme.bold("orchestration_status")), 0, 0);
188
+ return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
189
189
  },
190
190
  renderResult(result, { isPartial }, theme) {
191
191
  return renderDiagnosticsResult(result, isPartial, theme);
@@ -204,7 +204,7 @@ export function registerOrchestrationTools(
204
204
  content: [
205
205
  {
206
206
  type: "text",
207
- text: readableDetails("Orchestration diagnostics and recovery snapshot.", readable),
207
+ text: readableDetails("Worker diagnostics and recovery snapshot.", readable),
208
208
  },
209
209
  ],
210
210
  details: readable,
@@ -213,17 +213,17 @@ export function registerOrchestrationTools(
213
213
  });
214
214
 
215
215
  pi.registerTool({
216
- name: "worker_send",
217
- label: "Worker Send",
216
+ name: "interactive_send",
217
+ label: "Interactive Send",
218
218
  description:
219
- "Send follow-up instructions to an owned ready reusable worker. A sole tool call runs asynchronously; sibling tool calls make it inline and blocking.",
220
- 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",
221
221
  promptGuidelines: [
222
- "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.",
223
223
  ],
224
- parameters: workerSendSchema,
224
+ parameters: interactiveSendSchema,
225
225
  renderCall(args, theme, { expanded }) {
226
- return renderWorkerMessageCall(theme, "worker_send", args.worker_id, args.instructions, expanded);
226
+ return renderInteractiveMessageCall(theme, "interactive_send", args.worker_id, args.instructions, expanded);
227
227
  },
228
228
  renderResult(result, { isPartial, expanded }, theme, context) {
229
229
  return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
@@ -233,7 +233,7 @@ export function registerOrchestrationTools(
233
233
  const mode = deps.getDispatchDecision(toolCallId).mode;
234
234
  const runtimeContext = await buildRuntimeContext(ctx, deps);
235
235
  if (mode === "async") {
236
- const acceptedRun = await deps.runtime.send(
236
+ const acceptedRun = await deps.runtime.sendInteractive(
237
237
  runtimeContext,
238
238
  workerId,
239
239
  params.instructions,
@@ -253,7 +253,7 @@ export function registerOrchestrationTools(
253
253
  };
254
254
  }
255
255
 
256
- const completedRun = await deps.runtime.send(
256
+ const completedRun = await deps.runtime.sendInteractive(
257
257
  runtimeContext,
258
258
  workerId,
259
259
  params.instructions,
@@ -281,10 +281,10 @@ export function registerOrchestrationTools(
281
281
  name: "worker_abort",
282
282
  label: "Worker Abort",
283
283
  description:
284
- "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.",
285
285
  promptSnippet: "Abort active owned workers by worker IDs or all",
286
286
  promptGuidelines: [
287
- "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.",
288
288
  ],
289
289
  parameters: workerAbortSchema,
290
290
  renderCall(args, theme) {
@@ -317,16 +317,16 @@ export function registerOrchestrationTools(
317
317
  });
318
318
 
319
319
  pi.registerTool({
320
- name: "worker_close",
321
- label: "Worker Close",
322
- description: "Close an owned ready reusable worker that no longer needs follow-up work.",
323
- 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",
324
324
  promptGuidelines: [
325
- "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.",
326
326
  ],
327
- parameters: workerCloseSchema,
327
+ parameters: interactiveCloseSchema,
328
328
  renderCall(args, theme) {
329
- return renderCompactCall(theme, "worker_close", args.worker_id);
329
+ return renderCompactCall(theme, "interactive_close", args.worker_id);
330
330
  },
331
331
  renderResult(result, { isPartial }, theme) {
332
332
  return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
@@ -337,7 +337,7 @@ export function registerOrchestrationTools(
337
337
  ctx.sessionManager.getSessionId(),
338
338
  );
339
339
  const workerId = asWorkerId(params.worker_id);
340
- await deps.runtime.close(ownerSessionId, workerId);
340
+ await deps.runtime.closeInteractive(ownerSessionId, workerId);
341
341
  const readable = { worker_id: workerId };
342
342
  return {
343
343
  content: [
@@ -642,7 +642,7 @@ class InstructionPreview implements Component {
642
642
  invalidate(): void {}
643
643
  }
644
644
 
645
- function renderWorkerMessageCall(
645
+ function renderInteractiveMessageCall(
646
646
  theme: Theme,
647
647
  tool: string,
648
648
  workerId: unknown,
@@ -837,7 +837,7 @@ function formatElapsed(milliseconds: number): string {
837
837
  }
838
838
 
839
839
  function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
840
- if (isPartial) return new Text(theme.fg("muted", "Reading orchestration diagnostics…"), 0, 0);
840
+ if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
841
841
  const details = result.details;
842
842
  if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
843
843
  const workers = details.state.workers.filter(isRecord);
@@ -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.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Concurrent worker orchestration for Pi",
6
6
  "files": ["extension/", "examples/", "README.md", "LICENSE"],