@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 +19 -15
- package/extension/catalog.ts +2 -2
- package/extension/contract.ts +7 -6
- package/extension/delivery.ts +12 -0
- package/extension/domain.ts +3 -3
- package/extension/index.ts +1 -1
- package/extension/presentation.ts +3 -2
- package/extension/runtime.ts +26 -22
- package/extension/tools.ts +24 -23
- package/extension/worker-session.ts +3 -3
- package/extension/worker-settlement.ts +1 -1
- package/package.json +1 -1
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
- `
|
|
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
|
|
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
|
|
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
|
|
57
|
-
-
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
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.
|
|
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,
|
|
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:
|
|
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 `
|
|
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 |
|
package/extension/catalog.ts
CHANGED
|
@@ -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", "
|
|
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 '
|
|
270
|
+
return "frontmatter field 'lifecycle' must be 'one-shot' or 'interactive'";
|
|
271
271
|
}
|
|
272
272
|
return "invalid worker definition";
|
|
273
273
|
}
|
package/extension/contract.ts
CHANGED
|
@@ -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.
|
|
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
|
-
- **
|
|
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. \`
|
|
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 \`
|
|
47
|
-
- The public tools are \`orchestrate\`, \`orchestration_status\`, \`
|
|
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
|
|
package/extension/delivery.ts
CHANGED
|
@@ -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":
|
package/extension/domain.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface WorkerCompaction {
|
|
|
36
36
|
readonly keepRecentTokens?: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export type WorkerLifecycle = "one-shot" | "
|
|
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 === "
|
|
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 !== "
|
|
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";
|
package/extension/index.ts
CHANGED
|
@@ -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}
|
|
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
|
|
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
|
|
package/extension/runtime.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface OrchestratorRuntime {
|
|
|
140
140
|
signal?: AbortSignal,
|
|
141
141
|
onSettlement?: SettlementListener,
|
|
142
142
|
): Promise<AcceptedRun | CompletedRun>;
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 !== "
|
|
357
|
-
throw new Error("
|
|
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
|
|
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
|
|
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 !== "
|
|
411
|
-
throw new Error("
|
|
410
|
+
if (current.lifecycle !== "interactive" || current.status !== "ready") {
|
|
411
|
+
throw new Error("interactive_close requires an owned ready interactive worker");
|
|
412
412
|
}
|
|
413
|
-
this.
|
|
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.
|
|
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 === "
|
|
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
|
|
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
|
|
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
|
|
1087
|
-
const
|
|
1088
|
-
|
|
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(
|
package/extension/tools.ts
CHANGED
|
@@ -59,7 +59,7 @@ const orchestrateSchema = taskSchema;
|
|
|
59
59
|
|
|
60
60
|
const statusSchema = Type.Object({}, STRICT_OBJECT);
|
|
61
61
|
|
|
62
|
-
const
|
|
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
|
|
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
|
-
"
|
|
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: "
|
|
216
|
-
label: "
|
|
216
|
+
name: "interactive_send",
|
|
217
|
+
label: "Interactive Send",
|
|
217
218
|
description:
|
|
218
|
-
"Send follow-up instructions to an owned
|
|
219
|
-
promptSnippet: "
|
|
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
|
|
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:
|
|
224
|
+
parameters: interactiveSendSchema,
|
|
224
225
|
renderCall(args, theme, { expanded }) {
|
|
225
|
-
return
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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: "
|
|
320
|
-
label: "
|
|
321
|
-
description: "Close an owned
|
|
322
|
-
promptSnippet: "
|
|
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
|
|
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:
|
|
327
|
+
parameters: interactiveCloseSchema,
|
|
327
328
|
renderCall(args, theme) {
|
|
328
|
-
return renderCompactCall(theme, "
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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 === "
|
|
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", "
|
|
56
|
+
lifecycle: Schema.Literals(["one-shot", "interactive"]),
|
|
57
57
|
status: Schema.Literals(["completed", "ready", "failed", "aborted"]),
|
|
58
58
|
outcome: WorkerOutcome,
|
|
59
59
|
usage: WorkerUsage,
|