@zachwill/pi-orchestrate 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -108
- package/extension/catalog/discovery.ts +3 -3
- package/extension/index.ts +9 -7
- package/extension/orchestration/admission.ts +2 -2
- package/extension/orchestration/model.ts +1 -1
- package/extension/orchestration/service.ts +9 -14
- package/extension/orchestration/settlement.ts +1 -1
- package/extension/parent/contract.ts +24 -20
- package/extension/parent/delivery.ts +2 -2
- package/extension/parent/dispatch-policy.ts +1 -1
- package/extension/parent/process-host.ts +9 -5
- package/extension/pi/presentation.ts +13 -7
- package/extension/pi/tool-renderer.ts +2 -2
- package/extension/pi/tools.ts +8 -8
- package/extension/worker/child-sessions.ts +1 -1
- package/extension/worker/session.ts +13 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,125 +1,53 @@
|
|
|
1
1
|
# Pi Orchestrate
|
|
2
2
|
|
|
3
|
-
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi
|
|
3
|
+
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi session delegate work to direct child sessions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Each worker gets a focused brief and a separate conversation.
|
|
6
|
+
- Workers run independently and return their results to the parent.
|
|
7
|
+
- Only the parent can delegate; workers cannot create more workers.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
pi install npm:@zachwill/pi-orchestrate
|
|
9
|
-
```
|
|
9
|
+
## The model
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Each `orchestrate` dispatch creates a fresh worker session with its own transcript. The worker receives a complete brief from the parent but not the parent's conversation.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A worker definition is reusable configuration: it selects the worker's prompt, tools, lifecycle, and optional model settings. It is not a running or retained session.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Independent workers dispatched together run concurrently. Their results return only to the parent session that started them, and the parent synthesizes the group after every worker finishes. A rejected or failed worker does not cancel its peers. Orchestration dispatched alongside unrelated tool calls runs inline instead of in the background.
|
|
16
16
|
|
|
17
17
|
Workers have one of two lifecycles:
|
|
18
18
|
|
|
19
|
-
- `one-shot`
|
|
20
|
-
- `interactive`
|
|
21
|
-
|
|
22
|
-
A **worker ID** identifies a worker session. A **run ID** identifies one generation within that session. Each interactive follow-up creates a new run while keeping the same worker ID.
|
|
23
|
-
|
|
24
|
-
## Tools
|
|
19
|
+
- A `one-shot` worker returns one result and stops.
|
|
20
|
+
- An `interactive` worker returns a result and remains available for follow-up work.
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
A **worker ID** identifies a worker session. A **run ID** identifies one generation within it. An interactive follow-up creates a new run while preserving the worker ID and prior session context.
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```text
|
|
31
|
-
orchestrate({ worker, title, instructions })
|
|
32
|
-
```
|
|
24
|
+
Interactive workers remain available across session switches and extension reloads within the same Pi process. Closing one releases its retained session; process shutdown releases any that remain.
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
A sole call runs asynchronously. To run independent work concurrently, send the complete wave as sibling calls in one assistant response:
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
orchestrate({
|
|
40
|
-
worker: "investigator",
|
|
41
|
-
title: "Trace configuration loading",
|
|
42
|
-
instructions: "Trace configuration loading from entry point to runtime. Read only. Return the relevant symbols, file paths, and a concise data-flow summary."
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
orchestrate({
|
|
46
|
-
worker: "investigator",
|
|
47
|
-
title: "Trace shutdown cleanup",
|
|
48
|
-
instructions: "Trace shutdown and cleanup behavior. Read only. Return the relevant symbols, lifecycle invariants, and uncovered edge cases."
|
|
49
|
-
})
|
|
50
|
-
```
|
|
26
|
+
## Agent interface
|
|
51
27
|
|
|
52
|
-
|
|
28
|
+
Pi Orchestrate gives the parent five model-facing tools:
|
|
53
29
|
|
|
54
|
-
|
|
30
|
+
| Tool | Purpose |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `orchestrate` | Start a fresh worker with a title and complete brief |
|
|
33
|
+
| `interactive_send` | Continue an owned interactive worker that is ready |
|
|
34
|
+
| `interactive_close` | Release an owned interactive worker that is ready |
|
|
35
|
+
| `worker_abort` | Stop active workers owned by the parent |
|
|
36
|
+
| `worker_status` | Inspect the trusted catalog and diagnose the parent's worker state |
|
|
55
37
|
|
|
56
|
-
|
|
38
|
+
The extension supplies the parent with the exact dispatch and lifecycle rules for these tools. The README describes their behavior rather than duplicating those model instructions.
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### `worker_abort`
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
worker_abort({ worker_ids: ["worker-…"] })
|
|
64
|
-
worker_abort({ all: true })
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Stops active workers owned by the current parent session. Explicit IDs are validated together. `{ all: true }` is a no-op when no owned workers are active and does not close interactive workers that are already `ready`.
|
|
68
|
-
|
|
69
|
-
Completed one-shot workers need no cleanup. Abort active interactive work before closing its retained session.
|
|
70
|
-
|
|
71
|
-
### `worker_status`
|
|
72
|
-
|
|
73
|
-
```text
|
|
74
|
-
worker_status({})
|
|
75
|
-
```
|
|
40
|
+
## Worker definitions
|
|
76
41
|
|
|
77
|
-
|
|
42
|
+
The package includes four fallback definitions in [`examples/workers/`](examples/workers/): `scout` for small factual probes, `investigator` for read-only cross-file research, `worker` for bounded implementation, and `web` for public web research. The first three inherit the parent's active model. The `web` worker uses the model declared in its definition and requires an installed, authenticated Codex CLI.
|
|
78
43
|
|
|
79
|
-
|
|
44
|
+
Definitions are loaded by name in this precedence order:
|
|
80
45
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```text
|
|
84
|
-
interactive_close({ worker_id: "worker-…" })
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Closes an owned interactive worker whose status is `ready`. It releases the retained session; it cannot close active work or a one-shot worker.
|
|
88
|
-
|
|
89
|
-
### `interactive_send`
|
|
90
|
-
|
|
91
|
-
```text
|
|
92
|
-
interactive_send({
|
|
93
|
-
worker_id: "worker-…",
|
|
94
|
-
instructions: "Verify the first risk against the tests and cite the relevant cases."
|
|
95
|
-
})
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Starts a follow-up run on an owned interactive worker whose status is `ready`. The worker keeps its ID and prior session context. Send `interactive_send` as the only tool call in the assistant message to run it asynchronously; sibling tool calls make it inline and blocking.
|
|
99
|
-
|
|
100
|
-
Interactive workers remain available across extension reloads and session switches within the same Pi process. Close them when their continuity is no longer useful. Runtime shutdown releases retained sessions automatically.
|
|
101
|
-
|
|
102
|
-
## Parent responsibilities
|
|
103
|
-
|
|
104
|
-
The parent agent still owns the result:
|
|
105
|
-
|
|
106
|
-
- Delegate bounded, independent scopes and materially distinct validation perspectives. Keep trivial or tightly coupled work in the parent.
|
|
107
|
-
- Use as many workers as the task supports instead of a small fixed count. User-named roles and counts are a floor unless the user sets an exact cap.
|
|
108
|
-
- Give each worker a complete brief and non-overlapping write scope. Intentional overlap should serve a distinct review perspective.
|
|
109
|
-
- Dispatch each full wave together. As findings expose new independent work, dispatch another full wave.
|
|
110
|
-
- Review the evidence and changes, resolve conflicts, verify the integrated result, and answer from the parent session.
|
|
111
|
-
|
|
112
|
-
## Configure workers
|
|
113
|
-
|
|
114
|
-
Worker definitions are loaded by name in this precedence order:
|
|
115
|
-
|
|
116
|
-
1. Package fallbacks in [`examples/workers/`](examples/workers/)
|
|
46
|
+
1. Package fallbacks in `examples/workers/`
|
|
117
47
|
2. User definitions in `~/.pi/agent/pi-orchestrate/workers/*.md`
|
|
118
48
|
3. Project definitions in `<project>/.pi/pi-orchestrate/workers/*.md`, when Pi trusts the project
|
|
119
49
|
|
|
120
|
-
A later definition replaces an earlier definition with the same name. Untrusted projects contribute no project definitions.
|
|
121
|
-
|
|
122
|
-
The bundled `scout`, `investigator`, and `worker` inherit the parent's active model. The bundled `web` worker requires an installed, authenticated Codex CLI and uses `openai-codex/gpt-5.6-sol`. Copy a fallback into a user or project directory to customize it.
|
|
50
|
+
A later definition replaces an earlier definition with the same name. Untrusted projects contribute no project definitions. Copy a fallback into a user or project directory to replace it.
|
|
123
51
|
|
|
124
52
|
A definition is a Markdown file whose basename matches its `name`:
|
|
125
53
|
|
|
@@ -134,20 +62,31 @@ lifecycle: interactive
|
|
|
134
62
|
Inspect only the assigned scope. Do not modify files. Return concise findings with file paths.
|
|
135
63
|
```
|
|
136
64
|
|
|
137
|
-
|
|
65
|
+
The frontmatter is strict:
|
|
66
|
+
|
|
67
|
+
| Field | Meaning |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `name` | Required definition name; must match the filename |
|
|
70
|
+
| `description` | Required catalog description used when choosing a worker |
|
|
71
|
+
| `tools` | Required nonempty tool list |
|
|
72
|
+
| `lifecycle` | `one-shot` or `interactive`; defaults to `one-shot` |
|
|
73
|
+
| `model` | Optional `provider/model` coordinate; omission inherits the parent's model |
|
|
74
|
+
| `thinking` | Optional thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
|
75
|
+
| `skills` | Optional exact skill allowlist; `[]` disables skills and omission uses normal discovery |
|
|
76
|
+
| `compaction` | Optional Pi compaction settings: `enabled`, `reserveTokens`, and `keepRecentTokens` |
|
|
138
77
|
|
|
139
|
-
|
|
78
|
+
The Markdown body is the worker's nonempty system prompt. `tools` and `skills` accept either YAML arrays or comma-separated strings. Supported Pi tools are `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls`. Unknown fields and malformed definitions are rejected and appear in catalog diagnostics.
|
|
140
79
|
|
|
141
|
-
|
|
80
|
+
Each `orchestrate` call starts a new worker session from the selected definition. Only `interactive_send` continues an existing session.
|
|
142
81
|
|
|
143
|
-
|
|
82
|
+
## Trust boundary
|
|
144
83
|
|
|
145
|
-
|
|
84
|
+
Workers run in the parent process and are not security sandboxes. They share its filesystem and environment permissions.
|
|
146
85
|
|
|
147
|
-
Workers
|
|
86
|
+
Workers can use global Pi settings, authentication, packages, extensions, skills, and context. Trusted projects may add project-scoped resources. Untrusted projects do not contribute project workers, settings, extensions, skills, or context; global resources remain available.
|
|
148
87
|
|
|
149
|
-
|
|
88
|
+
A definition's `tools` field controls Pi's tool allowlist, not operating-system authority. A worker with `bash` can start external processes, including other agent CLIs. A read-only prompt also does not prevent writes when the worker has a write-capable tool.
|
|
150
89
|
|
|
151
|
-
|
|
90
|
+
Concurrent workers share the same working tree, so overlapping write scopes can collide. The parent owns reviewing their output and integrating the result.
|
|
152
91
|
|
|
153
|
-
Pi Orchestrate excludes itself from child sessions and
|
|
92
|
+
Pi Orchestrate excludes itself from child sessions and keeps workers as direct Pi children.
|
|
@@ -11,13 +11,13 @@ import type {
|
|
|
11
11
|
WorkerCatalog,
|
|
12
12
|
WorkerDefinition,
|
|
13
13
|
WorkerSourceKind,
|
|
14
|
-
} from "./definition.
|
|
14
|
+
} from "./definition.ts";
|
|
15
15
|
import {
|
|
16
16
|
createWorkerCatalog,
|
|
17
17
|
isSupportedToolName,
|
|
18
18
|
SUPPORTED_TOOL_NAMES,
|
|
19
|
-
} from "./definition.
|
|
20
|
-
import { PACKAGE_ROOT } from "../package-root.
|
|
19
|
+
} from "./definition.ts";
|
|
20
|
+
import { PACKAGE_ROOT } from "../package-root.ts";
|
|
21
21
|
|
|
22
22
|
const MAX_WORKER_BYTES = 64 * 1024;
|
|
23
23
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
package/extension/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type {
|
|
|
6
6
|
import {
|
|
7
7
|
discoverWorkerCatalog,
|
|
8
8
|
type DiscoverWorkerCatalogOptions,
|
|
9
|
-
} from "./catalog/discovery.
|
|
10
|
-
import type { WorkerCatalog } from "./catalog/definition.
|
|
11
|
-
import { applyOrchestratorContract } from "./parent/contract.
|
|
9
|
+
} from "./catalog/discovery.ts";
|
|
10
|
+
import type { WorkerCatalog } from "./catalog/definition.ts";
|
|
11
|
+
import { applyOrchestratorContract } from "./parent/contract.ts";
|
|
12
12
|
import {
|
|
13
13
|
attachProcessHost,
|
|
14
14
|
createProcessHost,
|
|
@@ -16,18 +16,18 @@ import {
|
|
|
16
16
|
detachProcessHost,
|
|
17
17
|
type ProcessHost,
|
|
18
18
|
type ProcessHostAttachment,
|
|
19
|
-
} from "./parent/process-host.
|
|
19
|
+
} from "./parent/process-host.ts";
|
|
20
20
|
import {
|
|
21
21
|
createStatusController,
|
|
22
22
|
registerOrchestrationPresentation,
|
|
23
23
|
type StatusController,
|
|
24
24
|
type WorkerStateSource,
|
|
25
|
-
} from "./pi/presentation.
|
|
25
|
+
} from "./pi/presentation.ts";
|
|
26
26
|
import {
|
|
27
27
|
classifyParentDispatches,
|
|
28
28
|
type DispatchDecision,
|
|
29
|
-
} from "./parent/dispatch-policy.
|
|
30
|
-
import { registerOrchestrationTools } from "./pi/tools.
|
|
29
|
+
} from "./parent/dispatch-policy.ts";
|
|
30
|
+
import { registerOrchestrationTools } from "./pi/tools.ts";
|
|
31
31
|
|
|
32
32
|
interface StoredDispatchDecision extends DispatchDecision {
|
|
33
33
|
readonly ownerSessionId: string;
|
|
@@ -129,6 +129,8 @@ export function createOrchestrationExtension(
|
|
|
129
129
|
const decision = dispatchDecisions.get(event.toolCallId);
|
|
130
130
|
dispatchDecisions.delete(event.toolCallId);
|
|
131
131
|
if (!event.isError || !decision?.synthesisGroup) return;
|
|
132
|
+
// Group size is counted before admission, so a failed member must still be
|
|
133
|
+
// accounted for as skipped or the final synthesis turn will never trigger.
|
|
132
134
|
host?.delivery.skipSynthesisGroupMember(
|
|
133
135
|
decision.ownerSessionId,
|
|
134
136
|
decision.synthesisGroup.id,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
findWorkerByName,
|
|
6
6
|
type WorkerCatalog,
|
|
7
7
|
type WorkerDefinition,
|
|
8
|
-
} from "../catalog/definition.
|
|
8
|
+
} from "../catalog/definition.ts";
|
|
9
9
|
import {
|
|
10
10
|
MAX_WORKER_INSTRUCTIONS_LENGTH,
|
|
11
11
|
MAX_WORKER_TITLE_LENGTH,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
WorkerId,
|
|
14
14
|
type RunMode,
|
|
15
15
|
type SynthesisGroup,
|
|
16
|
-
} from "./model.
|
|
16
|
+
} from "./model.ts";
|
|
17
17
|
|
|
18
18
|
/** Everything one orchestration request needs from its owning parent session. */
|
|
19
19
|
export interface OrchestrationContext {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
import type { WorkerLifecycle } from "../catalog/definition.
|
|
2
|
+
import type { WorkerLifecycle } from "../catalog/definition.ts";
|
|
3
3
|
|
|
4
4
|
export const MAX_WORKER_TITLE_LENGTH = 200;
|
|
5
5
|
export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FiberSet,
|
|
11
11
|
Layer,
|
|
12
12
|
} from "effect";
|
|
13
|
-
import type { WorkerDefinition } from "../catalog/definition.
|
|
13
|
+
import type { WorkerDefinition } from "../catalog/definition.ts";
|
|
14
14
|
import {
|
|
15
15
|
CANCELLATION_GRACE_MS,
|
|
16
16
|
EMPTY_WORKER_USAGE,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type WorkerOutcome,
|
|
31
31
|
type WorkerRecord,
|
|
32
32
|
type WorkerUsage,
|
|
33
|
-
} from "./model.
|
|
33
|
+
} from "./model.ts";
|
|
34
34
|
import {
|
|
35
35
|
OrchestrationActionRejected,
|
|
36
36
|
accepted,
|
|
@@ -46,21 +46,21 @@ import {
|
|
|
46
46
|
type OrchestrationContext,
|
|
47
47
|
type OrchestrationOperation,
|
|
48
48
|
type ValidatedAbortTarget,
|
|
49
|
-
} from "./admission.
|
|
49
|
+
} from "./admission.ts";
|
|
50
50
|
import {
|
|
51
51
|
createWorkerSettlement,
|
|
52
52
|
type SettlementFailureStage,
|
|
53
53
|
type WorkerSettlement,
|
|
54
|
-
} from "./settlement.
|
|
54
|
+
} from "./settlement.ts";
|
|
55
55
|
import {
|
|
56
56
|
ChildSessions,
|
|
57
57
|
type ChildSessionsService,
|
|
58
|
-
} from "../worker/child-sessions.
|
|
58
|
+
} from "../worker/child-sessions.ts";
|
|
59
59
|
import type {
|
|
60
60
|
WorkerSessionAbortError,
|
|
61
61
|
WorkerSessionHandle,
|
|
62
62
|
WorkerSessionObservation,
|
|
63
|
-
} from "../worker/session.
|
|
63
|
+
} from "../worker/session.ts";
|
|
64
64
|
|
|
65
65
|
export const MAX_TERMINAL_WORKER_HISTORY = 100;
|
|
66
66
|
export const MAX_COMPLETED_RUN_HISTORY = 100;
|
|
@@ -1479,8 +1479,8 @@ function makeSettlement(
|
|
|
1479
1479
|
}
|
|
1480
1480
|
const sequence = draft.settlementSequence + 1;
|
|
1481
1481
|
draft.settlementSequence = sequence;
|
|
1482
|
-
// Persisted generation
|
|
1483
|
-
//
|
|
1482
|
+
// Persisted generation stores workerEpoch, distinguishes follow-up settlements
|
|
1483
|
+
// for one stable worker ID, and makes event IDs generation-specific.
|
|
1484
1484
|
const settlement = createWorkerSettlement({
|
|
1485
1485
|
sequence,
|
|
1486
1486
|
generation: worker.workerEpoch,
|
|
@@ -1514,12 +1514,7 @@ function pruneHistory(draft: OrchestrationState): Set<string> {
|
|
|
1514
1514
|
draft.runs.delete(runId);
|
|
1515
1515
|
}
|
|
1516
1516
|
while (draft.terminalWorkerOrder.length > MAX_TERMINAL_WORKER_HISTORY) {
|
|
1517
|
-
const
|
|
1518
|
-
const worker = draft.workers.get(workerId);
|
|
1519
|
-
return !worker || isTerminalWorkerStatus(worker.record.status);
|
|
1520
|
-
});
|
|
1521
|
-
if (index < 0) break;
|
|
1522
|
-
const removed = draft.terminalWorkerOrder.splice(index, 1)[0];
|
|
1517
|
+
const removed = draft.terminalWorkerOrder.shift();
|
|
1523
1518
|
if (!removed) break;
|
|
1524
1519
|
const worker = draft.workers.get(removed);
|
|
1525
1520
|
if (worker) owners.add(worker.record.ownerSessionId);
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
WorkerUsage,
|
|
11
11
|
type RunRecord,
|
|
12
12
|
type SettledWorkerRecord,
|
|
13
|
-
} from "./model.
|
|
13
|
+
} from "./model.ts";
|
|
14
14
|
|
|
15
15
|
const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
16
16
|
const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkerCatalog } from "../catalog/definition.
|
|
1
|
+
import type { WorkerCatalog } from "../catalog/definition.ts";
|
|
2
2
|
|
|
3
3
|
const CONTRACT_START = "<!-- pi-orchestrate:contract:start -->";
|
|
4
4
|
const CONTRACT_END = "<!-- pi-orchestrate:contract:end -->";
|
|
@@ -98,27 +98,31 @@ function buildContract(catalog: WorkerCatalog): string {
|
|
|
98
98
|
|
|
99
99
|
You are the parent orchestrator and own the task end to end.
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
- 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.
|
|
103
|
-
- Before dispatching, enumerate the full first parallel wave from the work itself.
|
|
104
|
-
- **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.
|
|
105
|
-
- **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.
|
|
106
|
-
- **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 })\`.
|
|
107
|
-
- 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.
|
|
108
|
-
- 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.
|
|
109
|
-
- Give every worker a thorough, self-contained brief with the objective, paths and scope, context, success criteria, and expected output. State forbidden actions explicitly.
|
|
110
|
-
- 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.
|
|
111
|
-
- 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.
|
|
112
|
-
- Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
|
|
113
|
-
- 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.
|
|
114
|
-
- 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.
|
|
115
|
-
- The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion.
|
|
116
|
-
- 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.
|
|
117
|
-
- The public tools are \`orchestrate\`, \`worker_status\`, \`interactive_send\`, \`worker_abort\`, and \`interactive_close\`.
|
|
101
|
+
### Delegation
|
|
118
102
|
|
|
119
|
-
|
|
103
|
+
- Keep trivial or tightly coupled work in the parent. Delegate work that can proceed independently or benefit from independent judgment.
|
|
104
|
+
- Choose worker scopes and counts from the task. Treat workers or counts named by the user as a floor unless the user sets an exact cap.
|
|
105
|
+
- Each \`orchestrate\` call creates a fresh worker session. Multiple calls may use the same worker definition and identical instructions when independent judgments are useful. Do not vary briefs merely to make them appear different. Interactive follow-up instead continues one worker ID with its existing context.
|
|
106
|
+
- Give each worker a self-contained brief with its objective, context, paths and scope, forbidden actions, success criteria, and expected output. Workers do not receive the parent conversation.
|
|
107
|
+
|
|
108
|
+
### Parallel dispatch
|
|
109
|
+
|
|
110
|
+
- Form the complete wave before emitting any tool call.
|
|
111
|
+
- For one worker, make one fully briefed \`orchestrate\` call.
|
|
112
|
+
- For N workers where N > 1, make exactly one \`multi_tool_use.parallel\` call. Its \`tool_uses\` must contain exactly N \`functions.orchestrate\` entries and no other tools.
|
|
113
|
+
- If \`multi_tool_use.parallel\` is not present, emit all N \`orchestrate\` calls as native siblings in one assistant response.
|
|
114
|
+
- Never dispatch a multi-worker wave as separate assistant responses. An admitted sole asynchronous \`orchestrate\` call ends the parent turn, so omitted workers cannot be added afterward.
|
|
115
|
+
- The expanded tool-call group must contain only the intended \`orchestrate\` calls. Mixing another tool into the group makes orchestration inline and blocking.
|
|
120
116
|
|
|
121
|
-
|
|
117
|
+
### Completion and lifecycle
|
|
118
|
+
|
|
119
|
+
- Calls are admitted independently; a rejected call does not stop its siblings.
|
|
120
|
+
- After dispatching, wait for automatic result delivery instead of polling \`worker_status\`. When results expose more independent work, dispatch another complete wave.
|
|
121
|
+
- Automatic delivery requires no keepalive activity. While awaiting it, do not call \`sleep\`, poll with any tool, inspect files or processes to infer worker progress, or issue no-op tool calls. Perform only genuinely independent work that would be useful even if no worker were active; otherwise end the turn.
|
|
122
|
+
- The parent reviews and synthesizes worker results, resolves conflicts, integrates changes, and runs the relevant verification.
|
|
123
|
+
- Prefer one-shot workers. Use interactive workers only when retained context is useful, and follow the ownership and status requirements in the lifecycle tool descriptions.
|
|
124
|
+
|
|
125
|
+
### Trusted worker catalog
|
|
122
126
|
|
|
123
127
|
${formatCatalog(catalog)}
|
|
124
128
|
${CONTRACT_END}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
import { Orchestration } from "../orchestration/service.
|
|
3
|
-
import type { WorkerSettlement } from "../orchestration/settlement.
|
|
2
|
+
import { Orchestration } from "../orchestration/service.ts";
|
|
3
|
+
import type { WorkerSettlement } from "../orchestration/settlement.ts";
|
|
4
4
|
|
|
5
5
|
export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
|
|
6
6
|
export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Delivery,
|
|
4
4
|
deliveryLayer,
|
|
5
5
|
type DeliveryService,
|
|
6
|
-
} from "./delivery.
|
|
6
|
+
} from "./delivery.ts";
|
|
7
7
|
import {
|
|
8
8
|
Orchestration,
|
|
9
9
|
orchestrationLayer,
|
|
@@ -14,14 +14,16 @@ import {
|
|
|
14
14
|
type OwnerSnapshot,
|
|
15
15
|
type SettlementListener,
|
|
16
16
|
type UnsubscribeSettlement,
|
|
17
|
-
} from "../orchestration/service.
|
|
17
|
+
} from "../orchestration/service.ts";
|
|
18
18
|
import type {
|
|
19
19
|
AbortTarget,
|
|
20
20
|
OrchestrationContext,
|
|
21
|
-
} from "../orchestration/admission.
|
|
22
|
-
import { createChildSessionsLayer } from "../worker/child-sessions.
|
|
23
|
-
import type { OrchestrateTaskInput, RunMode } from "../orchestration/model.
|
|
21
|
+
} from "../orchestration/admission.ts";
|
|
22
|
+
import { createChildSessionsLayer } from "../worker/child-sessions.ts";
|
|
23
|
+
import type { OrchestrateTaskInput, RunMode } from "../orchestration/model.ts";
|
|
24
24
|
|
|
25
|
+
// Bump this key when ProcessHost/OrchestrationClient changes incompatibly.
|
|
26
|
+
// Multiple package copies can coexist in one Pi process and adopt the same host.
|
|
25
27
|
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
26
28
|
|
|
27
29
|
type DispatchResult<M extends RunMode> = M extends "async"
|
|
@@ -223,6 +225,8 @@ export class ManagedOrchestrationClient<R = never> implements OrchestrationClien
|
|
|
223
225
|
if (signal?.aborted) throw abortSignalReason(signal);
|
|
224
226
|
if (!signal) return this.effectRuntime.runPromise(effect);
|
|
225
227
|
|
|
228
|
+
// Race with a private sentinel because Effect may transform failures; after
|
|
229
|
+
// interruption settles, Pi must receive the caller's exact AbortSignal.reason.
|
|
226
230
|
const signalInterruption = {};
|
|
227
231
|
const exit = await this.effectRuntime.runPromiseExit(
|
|
228
232
|
Effect.raceFirst(effect, abortSignalEffect(signal, signalInterruption)),
|
|
@@ -19,18 +19,18 @@ import type {
|
|
|
19
19
|
WorkerOutcome,
|
|
20
20
|
WorkerRecord,
|
|
21
21
|
WorkerStatus,
|
|
22
|
-
} from "../orchestration/model.
|
|
23
|
-
import type { OwnerSnapshot } from "../orchestration/service.
|
|
22
|
+
} from "../orchestration/model.ts";
|
|
23
|
+
import type { OwnerSnapshot } from "../orchestration/service.ts";
|
|
24
24
|
import {
|
|
25
25
|
disposeComponent,
|
|
26
26
|
formatElapsed,
|
|
27
27
|
resultAppearance,
|
|
28
28
|
WidthBoundComponent,
|
|
29
|
-
} from "./tui.
|
|
29
|
+
} from "./tui.ts";
|
|
30
30
|
import {
|
|
31
31
|
decodePersistedWorkerSettlement,
|
|
32
32
|
type WorkerSettlement,
|
|
33
|
-
} from "../orchestration/settlement.
|
|
33
|
+
} from "../orchestration/settlement.ts";
|
|
34
34
|
|
|
35
35
|
export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
36
36
|
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
@@ -52,6 +52,10 @@ const WORKER_ANIMATIONS = {
|
|
|
52
52
|
} as const;
|
|
53
53
|
const ANIMATION_CYCLE_TICKS = 40;
|
|
54
54
|
const SPINNER_INTERVAL_MS = 140;
|
|
55
|
+
const TURN_USAGE_MIN_WIDTH = 12;
|
|
56
|
+
const CONTEXT_USAGE_MIN_WIDTH = 28;
|
|
57
|
+
const WIDE_WORKER_ROW_MIN_WIDTH = 72;
|
|
58
|
+
const WORKER_NAME_SLACK_COLUMNS = 10;
|
|
55
59
|
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
56
60
|
|
|
57
61
|
/** Owner-scoped worker state feed consumed by the parent's status presentation. */
|
|
@@ -211,12 +215,14 @@ export class WorkerStatusComponent implements Component {
|
|
|
211
215
|
);
|
|
212
216
|
const turns = formatTurnMarker(worker);
|
|
213
217
|
const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
214
|
-
const usageFields = width >=
|
|
218
|
+
const usageFields = width >= CONTEXT_USAGE_MIN_WIDTH
|
|
219
|
+
? [turns, context]
|
|
220
|
+
: width >= TURN_USAGE_MIN_WIDTH ? [turns] : [];
|
|
215
221
|
const workerName = this.theme.fg("muted", this.theme.italic(worker.worker));
|
|
216
222
|
const workerNameFits = visibleWidth(
|
|
217
223
|
`⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
|
|
218
|
-
) +
|
|
219
|
-
const suffixFields = width >=
|
|
224
|
+
) + WORKER_NAME_SLACK_COLUMNS <= width;
|
|
225
|
+
const suffixFields = width >= WIDE_WORKER_ROW_MIN_WIDTH && workerNameFits
|
|
220
226
|
? [workerName, ...usageFields]
|
|
221
227
|
: usageFields;
|
|
222
228
|
const prefix = `${glyph} `;
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
formatElapsed,
|
|
20
20
|
resultAppearance,
|
|
21
21
|
WidthBoundComponent,
|
|
22
|
-
} from "./tui.
|
|
22
|
+
} from "./tui.ts";
|
|
23
23
|
import {
|
|
24
24
|
decodeInlineWorkerToolDetails,
|
|
25
25
|
type InlineWorkerSettlementDetails,
|
|
26
26
|
WorkerSettlement,
|
|
27
|
-
} from "../orchestration/settlement.
|
|
27
|
+
} from "../orchestration/settlement.ts";
|
|
28
28
|
|
|
29
29
|
const MAX_INSTRUCTION_PREVIEW_LINES = 2;
|
|
30
30
|
|
package/extension/pi/tools.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
CatalogDiagnostic,
|
|
14
14
|
WorkerCatalog,
|
|
15
15
|
WorkerDefinition,
|
|
16
|
-
} from "../catalog/definition.
|
|
16
|
+
} from "../catalog/definition.ts";
|
|
17
17
|
import {
|
|
18
18
|
MAX_WORKER_INSTRUCTIONS_LENGTH,
|
|
19
19
|
MAX_WORKER_TITLE_LENGTH,
|
|
@@ -21,32 +21,32 @@ import {
|
|
|
21
21
|
type WorkerOutcome,
|
|
22
22
|
type WorkerRecord,
|
|
23
23
|
type WorkerUsage,
|
|
24
|
-
} from "../orchestration/model.
|
|
24
|
+
} from "../orchestration/model.ts";
|
|
25
25
|
import type {
|
|
26
26
|
AbortTarget,
|
|
27
27
|
OrchestrationContext,
|
|
28
|
-
} from "../orchestration/admission.
|
|
28
|
+
} from "../orchestration/admission.ts";
|
|
29
29
|
import type {
|
|
30
30
|
AcceptedRun,
|
|
31
31
|
CompletedRun,
|
|
32
32
|
OwnerSnapshot,
|
|
33
33
|
SettlementListener,
|
|
34
34
|
WorkerRunResult,
|
|
35
|
-
} from "../orchestration/service.
|
|
36
|
-
import type { DispatchDecision } from "../parent/dispatch-policy.
|
|
37
|
-
import type { OrchestrationClient } from "../parent/process-host.
|
|
35
|
+
} from "../orchestration/service.ts";
|
|
36
|
+
import type { DispatchDecision } from "../parent/dispatch-policy.ts";
|
|
37
|
+
import type { OrchestrationClient } from "../parent/process-host.ts";
|
|
38
38
|
import {
|
|
39
39
|
interactiveCloseToolRenderer,
|
|
40
40
|
interactiveSendToolRenderer,
|
|
41
41
|
orchestrateToolRenderer,
|
|
42
42
|
workerAbortToolRenderer,
|
|
43
43
|
workerStatusToolRenderer,
|
|
44
|
-
} from "./tool-renderer.
|
|
44
|
+
} from "./tool-renderer.ts";
|
|
45
45
|
import {
|
|
46
46
|
encodeInlineWorkerToolDetails,
|
|
47
47
|
type InlineWorkerSettlementDetails,
|
|
48
48
|
type WorkerSettlement,
|
|
49
|
-
} from "../orchestration/settlement.
|
|
49
|
+
} from "../orchestration/settlement.ts";
|
|
50
50
|
|
|
51
51
|
const STRICT_OBJECT = { additionalProperties: false } as const;
|
|
52
52
|
const shortTextSchema = Type.String({
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type WorkerSessionCreationError,
|
|
15
15
|
type WorkerSessionDependencies,
|
|
16
16
|
type WorkerSessionHandle,
|
|
17
|
-
} from "./session.
|
|
17
|
+
} from "./session.ts";
|
|
18
18
|
|
|
19
19
|
export class WorkerSessionAcquisitionClosedError extends Schema.TaggedError<WorkerSessionAcquisitionClosedError>()(
|
|
20
20
|
"WorkerSession.AcquisitionClosedError",
|
|
@@ -24,13 +24,13 @@ import {
|
|
|
24
24
|
Schema,
|
|
25
25
|
Scope,
|
|
26
26
|
} from "effect";
|
|
27
|
-
import type { WorkerDefinition } from "../catalog/definition.
|
|
27
|
+
import type { WorkerDefinition } from "../catalog/definition.ts";
|
|
28
28
|
import type {
|
|
29
29
|
WorkerMessageDirection,
|
|
30
30
|
WorkerOutcome,
|
|
31
31
|
WorkerUsage,
|
|
32
|
-
} from "../orchestration/model.
|
|
33
|
-
import { PACKAGE_ROOT } from "../package-root.
|
|
32
|
+
} from "../orchestration/model.ts";
|
|
33
|
+
import { PACKAGE_ROOT } from "../package-root.ts";
|
|
34
34
|
|
|
35
35
|
const DIRECT_CHILD_BOUNDARY =
|
|
36
36
|
"You are a direct child worker session. Do not spawn, delegate to, or orchestrate descendant Pi worker sessions. Complete the assigned task yourself and return the result directly to the parent orchestrator.";
|
|
@@ -385,6 +385,8 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
|
385
385
|
): Effect.Effect<DefaultWorkerSessionHandle> {
|
|
386
386
|
return Effect.gen(function* () {
|
|
387
387
|
const handle = new DefaultWorkerSessionHandle(runtime, interactive, sessionFile);
|
|
388
|
+
// Cache the disposal Effect so repeated or concurrent callers join the same
|
|
389
|
+
// uninterruptible Scope.close rather than skipping an in-progress disposal.
|
|
388
390
|
handle.disposeOperation = yield* Effect.cached(
|
|
389
391
|
Effect.sync(() => handle.beginDispose()).pipe(
|
|
390
392
|
Effect.andThen(disposeWorkerSession(scope, cleanupReporter)),
|
|
@@ -457,6 +459,8 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
|
457
459
|
prompt(instructions: string): Effect.Effect<WorkerOutcome, never> {
|
|
458
460
|
return Effect.fn("WorkerSession.prompt")(function* (this: DefaultWorkerSessionHandle) {
|
|
459
461
|
const completion = yield* Effect.sync(() => this.startPrompt(instructions));
|
|
462
|
+
// Interrupting this waiter does not cancel Pi's prompt. abort() owns physical
|
|
463
|
+
// cancellation, and completePrompt releases Active state when it settles.
|
|
460
464
|
const { failureMessage, message, prompt } = yield* Effect.promise(() => completion);
|
|
461
465
|
const text = assistantText(message);
|
|
462
466
|
const assistantPayload = text === undefined ? {} : { assistantText: text };
|
|
@@ -556,7 +560,7 @@ class DefaultWorkerSessionHandle implements WorkerSessionHandle {
|
|
|
556
560
|
}
|
|
557
561
|
|
|
558
562
|
function safelyNotify(callback: () => void): void {
|
|
559
|
-
try { callback(); } catch { /* One
|
|
563
|
+
try { callback(); } catch { /* One observer cannot block the others. */ }
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
function subscribe<T>(listeners: Set<(value: T) => void>, listener: (value: T) => void): () => void {
|
|
@@ -793,6 +797,8 @@ function agentSessionFinalizer(
|
|
|
793
797
|
() => ownership.session.dispose(),
|
|
794
798
|
);
|
|
795
799
|
const runtime = ownership.runtime;
|
|
800
|
+
// The runtime owns normal raw-session disposal after creation. Before that
|
|
801
|
+
// handoff, or if runtime disposal fails, close the raw session directly.
|
|
796
802
|
return runtime
|
|
797
803
|
? bestEffortCleanup(reporter, "runtime", () => runtime.dispose(), disposeRawSession)
|
|
798
804
|
: disposeRawSession;
|
|
@@ -862,9 +868,9 @@ export const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
|
|
|
862
868
|
const { selected, modelRuntime } = yield* prepareChildModelRuntime(options, dependencies);
|
|
863
869
|
const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
|
|
864
870
|
|
|
865
|
-
// createAgentSessionServices
|
|
866
|
-
// Pi 0.80.10
|
|
867
|
-
// errors and aborts remain typed acquisition failures
|
|
871
|
+
// createAgentSessionServices registers extension providers and refreshes, but
|
|
872
|
+
// discards that result in Pi 0.80.10. Keep this worker-owned probe so provider
|
|
873
|
+
// errors and aborts remain typed acquisition failures; remove it when Pi surfaces them.
|
|
868
874
|
yield* refreshModelRuntime(modelRuntime, definition);
|
|
869
875
|
const model = yield* Effect.try({
|
|
870
876
|
try: () => {
|