@zachwill/pi-orchestrate 0.9.2 → 0.11.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/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +19 -36
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +179 -473
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/parent/contract.ts +147 -0
- package/extension/{delivery.ts → parent/delivery.ts} +5 -8
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +33 -26
- package/extension/{presentation.ts → pi/presentation.ts} +39 -27
- package/extension/pi/tool-renderer.ts +422 -0
- package/extension/pi/tools.ts +470 -0
- package/extension/worker/child-sessions.ts +291 -0
- package/extension/{worker-session.ts → worker/session.ts} +33 -291
- package/package.json +2 -1
- package/extension/contract.ts +0 -144
- package/extension/tools.ts +0 -837
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
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.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export const SUPPORTED_TOOL_NAMES = [
|
|
4
|
+
"read",
|
|
5
|
+
"bash",
|
|
6
|
+
"edit",
|
|
7
|
+
"write",
|
|
8
|
+
"grep",
|
|
9
|
+
"find",
|
|
10
|
+
"ls",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type SupportedToolName = (typeof SUPPORTED_TOOL_NAMES)[number];
|
|
14
|
+
|
|
15
|
+
const supportedToolNames: ReadonlySet<string> = new Set(SUPPORTED_TOOL_NAMES);
|
|
16
|
+
|
|
17
|
+
export function isSupportedToolName(value: unknown): value is SupportedToolName {
|
|
18
|
+
return typeof value === "string" && supportedToolNames.has(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type WorkerSourceKind = "package" | "user" | "project";
|
|
22
|
+
|
|
23
|
+
export interface WorkerSource {
|
|
24
|
+
readonly kind: WorkerSourceKind;
|
|
25
|
+
readonly filePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkerModel {
|
|
29
|
+
readonly provider: string;
|
|
30
|
+
readonly modelId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkerCompaction {
|
|
34
|
+
readonly enabled?: boolean;
|
|
35
|
+
readonly reserveTokens?: number;
|
|
36
|
+
readonly keepRecentTokens?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type WorkerLifecycle = "one-shot" | "interactive";
|
|
40
|
+
|
|
41
|
+
export interface WorkerDefinition {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly source: WorkerSource;
|
|
44
|
+
readonly description: string;
|
|
45
|
+
readonly systemPrompt: string;
|
|
46
|
+
readonly lifecycle: WorkerLifecycle;
|
|
47
|
+
readonly tools: readonly SupportedToolName[];
|
|
48
|
+
readonly skills?: readonly string[];
|
|
49
|
+
readonly model?: WorkerModel;
|
|
50
|
+
readonly thinking?: ThinkingLevel;
|
|
51
|
+
readonly compaction?: WorkerCompaction;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CatalogDiagnosticSeverity = "warning" | "error";
|
|
55
|
+
|
|
56
|
+
export interface CatalogDiagnostic {
|
|
57
|
+
readonly severity: CatalogDiagnosticSeverity;
|
|
58
|
+
readonly source: WorkerSourceKind;
|
|
59
|
+
readonly message: string;
|
|
60
|
+
readonly filePath?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorkerCatalog {
|
|
64
|
+
readonly workers: readonly WorkerDefinition[];
|
|
65
|
+
readonly diagnostics: readonly CatalogDiagnostic[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createWorkerCatalog(
|
|
69
|
+
workers: readonly WorkerDefinition[],
|
|
70
|
+
diagnostics: readonly CatalogDiagnostic[] = [],
|
|
71
|
+
): WorkerCatalog {
|
|
72
|
+
return {
|
|
73
|
+
workers: [...workers].sort(compareWorkersByName),
|
|
74
|
+
diagnostics: [...diagnostics],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function findWorkerByName(
|
|
79
|
+
catalog: WorkerCatalog,
|
|
80
|
+
name: string,
|
|
81
|
+
): WorkerDefinition | undefined {
|
|
82
|
+
return catalog.workers.find((worker) => worker.name === name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compareWorkersByName(left: WorkerDefinition, right: WorkerDefinition): number {
|
|
86
|
+
if (left.name < right.name) return -1;
|
|
87
|
+
if (left.name > right.name) return 1;
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
@@ -5,19 +5,19 @@ import {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { basename, extname, join } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { Effect, Result, Schema, SchemaGetter, type SchemaIssue } from "effect";
|
|
10
9
|
import type {
|
|
11
10
|
CatalogDiagnostic,
|
|
12
11
|
WorkerCatalog,
|
|
13
12
|
WorkerDefinition,
|
|
14
13
|
WorkerSourceKind,
|
|
15
|
-
} from "./
|
|
14
|
+
} from "./definition.ts";
|
|
16
15
|
import {
|
|
17
16
|
createWorkerCatalog,
|
|
18
17
|
isSupportedToolName,
|
|
19
18
|
SUPPORTED_TOOL_NAMES,
|
|
20
|
-
} from "./
|
|
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;
|
|
@@ -302,7 +302,7 @@ function parseWorker(
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
function sourceDirectories(options: DiscoverWorkerCatalogOptions): CatalogSource[] {
|
|
305
|
-
const packageRoot = options.packageRoot ??
|
|
305
|
+
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
306
306
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
307
307
|
const sources: CatalogSource[] = [
|
|
308
308
|
{ kind: "package", directory: join(packageRoot, "examples", "workers") },
|
package/extension/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type {
|
|
|
6
6
|
import {
|
|
7
7
|
discoverWorkerCatalog,
|
|
8
8
|
type DiscoverWorkerCatalogOptions,
|
|
9
|
-
} from "./catalog.
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
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,21 +16,18 @@ import {
|
|
|
16
16
|
detachProcessHost,
|
|
17
17
|
type ProcessHost,
|
|
18
18
|
type ProcessHostAttachment,
|
|
19
|
-
} from "./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.ts";
|
|
25
26
|
import {
|
|
26
|
-
|
|
27
|
+
classifyParentDispatches,
|
|
27
28
|
type DispatchDecision,
|
|
28
|
-
} from "./
|
|
29
|
-
|
|
30
|
-
const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
31
|
-
"orchestrate",
|
|
32
|
-
"interactive_send",
|
|
33
|
-
]);
|
|
29
|
+
} from "./parent/dispatch-policy.ts";
|
|
30
|
+
import { registerOrchestrationTools } from "./pi/tools.ts";
|
|
34
31
|
|
|
35
32
|
interface StoredDispatchDecision extends DispatchDecision {
|
|
36
33
|
readonly ownerSessionId: string;
|
|
@@ -45,7 +42,7 @@ export interface OrchestrationExtensionDependencies {
|
|
|
45
42
|
getHost?(): ProcessHost;
|
|
46
43
|
destroyHost?(host: ProcessHost): Promise<void>;
|
|
47
44
|
discoverCatalog?(options: DiscoverWorkerCatalogOptions): WorkerCatalog;
|
|
48
|
-
createStatusController?(
|
|
45
|
+
createStatusController?(workerState: WorkerStateSource): StatusController;
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
export function createOrchestrationExtension(
|
|
@@ -77,10 +74,10 @@ export function createOrchestrationExtension(
|
|
|
77
74
|
pi.on("session_start", (_event, ctx) => {
|
|
78
75
|
host ??= dependencies.getHost?.() ?? createProcessHost();
|
|
79
76
|
statusController ??=
|
|
80
|
-
dependencies.createStatusController?.(host.
|
|
81
|
-
createStatusController(host.
|
|
77
|
+
dependencies.createStatusController?.(host.orchestration) ??
|
|
78
|
+
createStatusController(host.orchestration);
|
|
82
79
|
registerOrchestrationTools(pi, {
|
|
83
|
-
|
|
80
|
+
orchestration: host.orchestration,
|
|
84
81
|
getCatalog: catalogFor,
|
|
85
82
|
getDispatchDecision: (toolCallId) =>
|
|
86
83
|
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
@@ -106,7 +103,7 @@ export function createOrchestrationExtension(
|
|
|
106
103
|
pi.on("before_agent_start", (event, ctx) => {
|
|
107
104
|
cachedCatalog = discoverCatalogFor(ctx);
|
|
108
105
|
return {
|
|
109
|
-
systemPrompt:
|
|
106
|
+
systemPrompt: applyOrchestratorContract(
|
|
110
107
|
event.systemPrompt,
|
|
111
108
|
cachedCatalog,
|
|
112
109
|
),
|
|
@@ -120,26 +117,10 @@ export function createOrchestrationExtension(
|
|
|
120
117
|
);
|
|
121
118
|
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
122
119
|
if (!ownerSessionId) return;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
toolCalls.length > 1 &&
|
|
127
|
-
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
128
|
-
const synthesisGroup = isOrchestrateGroup
|
|
129
|
-
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
130
|
-
: undefined;
|
|
131
|
-
|
|
132
|
-
for (const toolCall of toolCalls) {
|
|
133
|
-
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
|
|
134
|
-
const mode = isOrchestrateGroup || toolCalls.length === 1
|
|
135
|
-
? "async"
|
|
136
|
-
: "inline";
|
|
137
|
-
dispatchDecisions.set(toolCall.id, {
|
|
138
|
-
mode,
|
|
120
|
+
for (const dispatch of classifyParentDispatches(toolCalls)) {
|
|
121
|
+
dispatchDecisions.set(dispatch.toolCallId, {
|
|
122
|
+
...dispatch.decision,
|
|
139
123
|
ownerSessionId,
|
|
140
|
-
...(toolCall.name === "orchestrate" && synthesisGroup
|
|
141
|
-
? { synthesisGroup }
|
|
142
|
-
: {}),
|
|
143
124
|
});
|
|
144
125
|
}
|
|
145
126
|
});
|
|
@@ -148,6 +129,8 @@ export function createOrchestrationExtension(
|
|
|
148
129
|
const decision = dispatchDecisions.get(event.toolCallId);
|
|
149
130
|
dispatchDecisions.delete(event.toolCallId);
|
|
150
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.
|
|
151
134
|
host?.delivery.skipSynthesisGroupMember(
|
|
152
135
|
decision.ownerSessionId,
|
|
153
136
|
decision.synthesisGroup.id,
|