@zachwill/pi-orchestrate 0.9.0 → 0.10.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.
@@ -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 "./domain.js";
14
+ } from "./definition.js";
16
15
  import {
17
16
  createWorkerCatalog,
18
17
  isSupportedToolName,
19
18
  SUPPORTED_TOOL_NAMES,
20
- } from "./domain.js";
19
+ } from "./definition.js";
20
+ import { PACKAGE_ROOT } from "../package-root.js";
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 ?? fileURLToPath(new URL("..", import.meta.url));
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") },
@@ -6,9 +6,9 @@ import type {
6
6
  import {
7
7
  discoverWorkerCatalog,
8
8
  type DiscoverWorkerCatalogOptions,
9
- } from "./catalog.js";
10
- import { appendOrchestratorContract } from "./contract.js";
11
- import type { WorkerCatalog } from "./domain.js";
9
+ } from "./catalog/discovery.js";
10
+ import type { WorkerCatalog } from "./catalog/definition.js";
11
+ import { applyOrchestratorContract } from "./parent/contract.js";
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.js";
19
+ } from "./parent/process-host.js";
20
20
  import {
21
21
  createStatusController,
22
22
  registerOrchestrationPresentation,
23
23
  type StatusController,
24
- } from "./presentation.js";
24
+ type WorkerStateSource,
25
+ } from "./pi/presentation.js";
25
26
  import {
26
- registerOrchestrationTools,
27
+ classifyParentDispatches,
27
28
  type DispatchDecision,
28
- } from "./tools.js";
29
-
30
- const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
31
- "orchestrate",
32
- "interactive_send",
33
- ]);
29
+ } from "./parent/dispatch-policy.js";
30
+ import { registerOrchestrationTools } from "./pi/tools.js";
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?(runtime: ProcessHost["runtime"]): StatusController;
45
+ createStatusController?(workerState: WorkerStateSource): StatusController;
49
46
  }
50
47
 
51
48
  export function createOrchestrationExtension(
@@ -57,7 +54,6 @@ export function createOrchestrationExtension(
57
54
  let host: ProcessHost | undefined;
58
55
  let hostAttachment: ProcessHostAttachment | undefined;
59
56
  let statusController: StatusController | undefined;
60
- let toolsRegistered = false;
61
57
  let activeBinding: OwnerBinding | undefined;
62
58
  let cachedCatalog: WorkerCatalog | undefined;
63
59
 
@@ -76,27 +72,16 @@ export function createOrchestrationExtension(
76
72
  registerOrchestrationPresentation(pi);
77
73
 
78
74
  pi.on("session_start", (_event, ctx) => {
79
- if (activeBinding && host && statusController) {
80
- host.delivery.unbind(
81
- activeBinding.ownerSessionId,
82
- activeBinding.generation,
83
- );
84
- statusController.unbind(activeBinding.ownerSessionId);
85
- }
86
-
87
75
  host ??= dependencies.getHost?.() ?? createProcessHost();
88
76
  statusController ??=
89
- dependencies.createStatusController?.(host.runtime) ??
90
- createStatusController(host.runtime);
91
- if (!toolsRegistered) {
92
- registerOrchestrationTools(pi, {
93
- runtime: host.runtime,
94
- getCatalog: catalogFor,
95
- getDispatchDecision: (toolCallId) =>
96
- dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
97
- });
98
- toolsRegistered = true;
99
- }
77
+ dependencies.createStatusController?.(host.orchestration) ??
78
+ createStatusController(host.orchestration);
79
+ registerOrchestrationTools(pi, {
80
+ orchestration: host.orchestration,
81
+ getCatalog: catalogFor,
82
+ getDispatchDecision: (toolCallId) =>
83
+ dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
84
+ });
100
85
  hostAttachment ??= attachProcessHost(host);
101
86
 
102
87
  dispatchDecisions.clear();
@@ -118,7 +103,7 @@ export function createOrchestrationExtension(
118
103
  pi.on("before_agent_start", (event, ctx) => {
119
104
  cachedCatalog = discoverCatalogFor(ctx);
120
105
  return {
121
- systemPrompt: appendOrchestratorContract(
106
+ systemPrompt: applyOrchestratorContract(
122
107
  event.systemPrompt,
123
108
  cachedCatalog,
124
109
  ),
@@ -132,24 +117,10 @@ export function createOrchestrationExtension(
132
117
  );
133
118
  const ownerSessionId = activeBinding?.ownerSessionId;
134
119
  if (!ownerSessionId) return;
135
- const isOrchestrateGroup =
136
- toolCalls.length > 1 &&
137
- toolCalls.every((toolCall) => toolCall.name === "orchestrate");
138
- const synthesisGroup = isOrchestrateGroup
139
- ? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
140
- : undefined;
141
-
142
- for (const toolCall of toolCalls) {
143
- if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
144
- const mode = isOrchestrateGroup || toolCalls.length === 1
145
- ? "async"
146
- : "inline";
147
- dispatchDecisions.set(toolCall.id, {
148
- mode,
120
+ for (const dispatch of classifyParentDispatches(toolCalls)) {
121
+ dispatchDecisions.set(dispatch.toolCallId, {
122
+ ...dispatch.decision,
149
123
  ownerSessionId,
150
- ...(toolCall.name === "orchestrate" && synthesisGroup
151
- ? { synthesisGroup }
152
- : {}),
153
124
  });
154
125
  }
155
126
  });
@@ -0,0 +1,297 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
3
+ import { Effect, Schema } from "effect";
4
+ import {
5
+ findWorkerByName,
6
+ type WorkerCatalog,
7
+ type WorkerDefinition,
8
+ } from "../catalog/definition.js";
9
+ import {
10
+ MAX_WORKER_INSTRUCTIONS_LENGTH,
11
+ MAX_WORKER_TITLE_LENGTH,
12
+ OrchestrateTaskInput,
13
+ WorkerId,
14
+ type RunMode,
15
+ type SynthesisGroup,
16
+ } from "./model.js";
17
+
18
+ /** Everything one orchestration request needs from its owning parent session. */
19
+ export interface OrchestrationContext {
20
+ readonly ownerSessionId: string;
21
+ readonly cwd: string;
22
+ readonly agentDir: string;
23
+ readonly parentSessionFile: string | undefined;
24
+ readonly projectTrusted: boolean;
25
+ readonly catalog: WorkerCatalog;
26
+ readonly parentModel?: Model<Api>;
27
+ readonly modelRegistry: ModelRegistry;
28
+ readonly synthesisGroup?: SynthesisGroup;
29
+ }
30
+
31
+ export interface AbortTarget {
32
+ readonly workerIds?: readonly string[];
33
+ readonly all?: boolean;
34
+ }
35
+
36
+ const OrchestrationOperation = Schema.Literals([
37
+ "orchestrate",
38
+ "sendInteractive",
39
+ "abort",
40
+ "closeInteractive",
41
+ "snapshot",
42
+ ]);
43
+ export type OrchestrationOperation = typeof OrchestrationOperation.Type;
44
+
45
+ const OrchestrationRejectionReason = Schema.Literals([
46
+ "shutdown",
47
+ "validation",
48
+ "ownership",
49
+ "target",
50
+ "worker-state",
51
+ "unknown-worker",
52
+ "model-unavailable",
53
+ ]);
54
+ export type OrchestrationRejectionReason = typeof OrchestrationRejectionReason.Type;
55
+
56
+ export class OrchestrationActionRejected extends Schema.TaggedError<OrchestrationActionRejected>()(
57
+ "Orchestration.ActionRejected",
58
+ {
59
+ operation: OrchestrationOperation,
60
+ reason: OrchestrationRejectionReason,
61
+ message: Schema.String,
62
+ },
63
+ ) {}
64
+
65
+ /** Admission verdict shared by pure ingress checks and stateful orchestration decisions. */
66
+ export type Decision<A> =
67
+ | {
68
+ readonly _tag: "accepted";
69
+ readonly value: A;
70
+ }
71
+ | {
72
+ readonly _tag: "rejected";
73
+ readonly error: OrchestrationActionRejected;
74
+ };
75
+
76
+ export function accepted<A>(value: A): Decision<A> {
77
+ return { _tag: "accepted", value };
78
+ }
79
+
80
+ export function rejected(
81
+ operation: OrchestrationOperation,
82
+ reason: OrchestrationRejectionReason,
83
+ message: string,
84
+ ): Decision<never> {
85
+ return {
86
+ _tag: "rejected",
87
+ error: actionRejection(operation, reason, message),
88
+ };
89
+ }
90
+
91
+ export function actionRejection(
92
+ operation: OrchestrationOperation,
93
+ reason: OrchestrationRejectionReason,
94
+ message: string,
95
+ ): OrchestrationActionRejected {
96
+ return new OrchestrationActionRejected({ operation, reason, message });
97
+ }
98
+
99
+ export function rejectAction(
100
+ operation: OrchestrationOperation,
101
+ reason: OrchestrationRejectionReason,
102
+ message: string,
103
+ ): Effect.Effect<never, OrchestrationActionRejected> {
104
+ return Effect.fail(actionRejection(operation, reason, message));
105
+ }
106
+
107
+ export function validateContextOwner(
108
+ operation: OrchestrationOperation,
109
+ ownerSessionId: string,
110
+ ): Effect.Effect<void, OrchestrationActionRejected> {
111
+ return typeof ownerSessionId !== "string" || ownerSessionId.trim() === ""
112
+ ? rejectAction(
113
+ operation,
114
+ "validation",
115
+ "ownerSessionId must not be blank",
116
+ )
117
+ : Effect.void;
118
+ }
119
+
120
+ export function validateWorkerId(
121
+ operation: OrchestrationOperation,
122
+ workerId: string,
123
+ ): Effect.Effect<WorkerId, OrchestrationActionRejected> {
124
+ if (typeof workerId !== "string" || workerId.trim() === "") {
125
+ return rejectAction(
126
+ operation,
127
+ "validation",
128
+ "worker_id must not be blank",
129
+ );
130
+ }
131
+ return Schema.decodeUnknownEffect(WorkerId)(workerId).pipe(
132
+ Effect.mapError(() => actionRejection(
133
+ operation,
134
+ "validation",
135
+ "worker_id must use the canonical worker- prefix",
136
+ )),
137
+ );
138
+ }
139
+
140
+ export function validateMode(
141
+ operation: OrchestrationOperation,
142
+ mode: RunMode,
143
+ ): Effect.Effect<void, OrchestrationActionRejected> {
144
+ return mode !== "async" && mode !== "inline"
145
+ ? rejectAction(operation, "validation", "Invalid orchestration mode")
146
+ : Effect.void;
147
+ }
148
+
149
+ export function validateText(
150
+ operation: OrchestrationOperation,
151
+ name: string,
152
+ value: string,
153
+ maximumLength: number,
154
+ ): Effect.Effect<void, OrchestrationActionRejected> {
155
+ if (typeof value !== "string" || value.trim() === "") {
156
+ return rejectAction(operation, "validation", `${name} must not be blank`);
157
+ }
158
+ return value.length > maximumLength
159
+ ? rejectAction(
160
+ operation,
161
+ "validation",
162
+ `${name} must be at most ${maximumLength} characters`,
163
+ )
164
+ : Effect.void;
165
+ }
166
+
167
+ /** Validates one orchestrate request against its context; no orchestration state is read. */
168
+ export function validateOrchestrateRequest(
169
+ context: OrchestrationContext,
170
+ task: OrchestrateTaskInput,
171
+ mode: RunMode,
172
+ ): Effect.Effect<
173
+ { definition: WorkerDefinition; task: OrchestrateTaskInput },
174
+ OrchestrationActionRejected
175
+ > {
176
+ return Effect.gen(function* () {
177
+ yield* validateContextOwner("orchestrate", context.ownerSessionId);
178
+ yield* validateMode("orchestrate", mode);
179
+ if (!task || typeof task !== "object" || Array.isArray(task)) {
180
+ return yield* rejectAction(
181
+ "orchestrate",
182
+ "validation",
183
+ "orchestrate requires one task object",
184
+ );
185
+ }
186
+ if (context.synthesisGroup) {
187
+ yield* validateText(
188
+ "orchestrate",
189
+ "synthesis group ID",
190
+ context.synthesisGroup.id,
191
+ MAX_WORKER_TITLE_LENGTH,
192
+ );
193
+ if (mode !== "async") {
194
+ return yield* rejectAction(
195
+ "orchestrate",
196
+ "validation",
197
+ "Sibling synthesis requires an async task",
198
+ );
199
+ }
200
+ if (
201
+ !Number.isSafeInteger(context.synthesisGroup.size) ||
202
+ context.synthesisGroup.size < 2
203
+ ) {
204
+ return yield* rejectAction(
205
+ "orchestrate",
206
+ "validation",
207
+ "Synthesis group size must be an integer of at least 2",
208
+ );
209
+ }
210
+ }
211
+ yield* validateText(
212
+ "orchestrate",
213
+ "worker",
214
+ task.worker,
215
+ MAX_WORKER_TITLE_LENGTH,
216
+ );
217
+ yield* validateText(
218
+ "orchestrate",
219
+ "title",
220
+ task.title,
221
+ MAX_WORKER_TITLE_LENGTH,
222
+ );
223
+ yield* validateText(
224
+ "orchestrate",
225
+ "instructions",
226
+ task.instructions,
227
+ MAX_WORKER_INSTRUCTIONS_LENGTH,
228
+ );
229
+ const definition = findWorkerByName(context.catalog, task.worker);
230
+ if (!definition) {
231
+ return yield* rejectAction(
232
+ "orchestrate",
233
+ "unknown-worker",
234
+ `Unknown worker: ${task.worker}`,
235
+ );
236
+ }
237
+ const configured = definition.model;
238
+ if (!configured && !context.parentModel) {
239
+ return yield* rejectAction(
240
+ "orchestrate",
241
+ "model-unavailable",
242
+ `Worker "${definition.name}" has no configured model and no parent model is available`,
243
+ );
244
+ }
245
+ if (
246
+ configured &&
247
+ !context.modelRegistry.find(configured.provider, configured.modelId)
248
+ ) {
249
+ return yield* rejectAction(
250
+ "orchestrate",
251
+ "model-unavailable",
252
+ `Worker "${definition.name}" configured model "${configured.provider}/${configured.modelId}" was not found`,
253
+ );
254
+ }
255
+ return { definition, task };
256
+ });
257
+ }
258
+
259
+ export type ValidatedAbortTarget =
260
+ | {
261
+ readonly _tag: "ids";
262
+ readonly workerIds: readonly WorkerId[];
263
+ }
264
+ | {
265
+ readonly _tag: "all";
266
+ };
267
+
268
+ export function validateAbortTarget(
269
+ target: AbortTarget,
270
+ ): Effect.Effect<ValidatedAbortTarget, OrchestrationActionRejected> {
271
+ return Effect.gen(function* () {
272
+ if (!target || typeof target !== "object") {
273
+ return yield* rejectAction("abort", "target", "Invalid abort target");
274
+ }
275
+ const selected = [target.workerIds !== undefined, target.all !== undefined]
276
+ .filter(Boolean).length;
277
+ if (selected !== 1 || (target.all !== undefined && target.all !== true)) {
278
+ return yield* rejectAction(
279
+ "abort",
280
+ "target",
281
+ "Abort target must specify exactly one of workerIds or all: true",
282
+ );
283
+ }
284
+ if (target.workerIds === undefined) return { _tag: "all" };
285
+ if (!Array.isArray(target.workerIds) || target.workerIds.length === 0) {
286
+ return yield* rejectAction(
287
+ "abort",
288
+ "target",
289
+ "workerIds must contain at least one worker ID",
290
+ );
291
+ }
292
+ const workerIds = yield* Effect.all(
293
+ [...new Set(target.workerIds)].map((id) => validateWorkerId("abort", id)),
294
+ );
295
+ return { _tag: "ids", workerIds };
296
+ });
297
+ }
@@ -1,93 +1,5 @@
1
- import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
1
  import { Schema } from "effect";
3
-
4
- export const SUPPORTED_TOOL_NAMES = [
5
- "read",
6
- "bash",
7
- "edit",
8
- "write",
9
- "grep",
10
- "find",
11
- "ls",
12
- ] as const;
13
-
14
- export type SupportedToolName = (typeof SUPPORTED_TOOL_NAMES)[number];
15
-
16
- const supportedToolNames: ReadonlySet<string> = new Set(SUPPORTED_TOOL_NAMES);
17
-
18
- export function isSupportedToolName(value: unknown): value is SupportedToolName {
19
- return typeof value === "string" && supportedToolNames.has(value);
20
- }
21
-
22
- export type WorkerSourceKind = "package" | "user" | "project";
23
-
24
- export interface WorkerSource {
25
- readonly kind: WorkerSourceKind;
26
- readonly filePath: string;
27
- }
28
-
29
- export interface WorkerModel {
30
- readonly provider: string;
31
- readonly modelId: string;
32
- }
33
-
34
- export interface WorkerCompaction {
35
- readonly enabled?: boolean;
36
- readonly reserveTokens?: number;
37
- readonly keepRecentTokens?: number;
38
- }
39
-
40
- export type WorkerLifecycle = "one-shot" | "interactive";
41
-
42
- export interface WorkerDefinition {
43
- readonly name: string;
44
- readonly source: WorkerSource;
45
- readonly description: string;
46
- readonly systemPrompt: string;
47
- readonly lifecycle: WorkerLifecycle;
48
- readonly tools: readonly SupportedToolName[];
49
- readonly skills?: readonly string[];
50
- readonly model?: WorkerModel;
51
- readonly thinking?: ThinkingLevel;
52
- readonly compaction?: WorkerCompaction;
53
- }
54
-
55
- export type CatalogDiagnosticSeverity = "warning" | "error";
56
-
57
- export interface CatalogDiagnostic {
58
- readonly severity: CatalogDiagnosticSeverity;
59
- readonly source: WorkerSourceKind;
60
- readonly message: string;
61
- readonly filePath?: string;
62
- }
63
-
64
- export interface WorkerCatalog {
65
- readonly workers: readonly WorkerDefinition[];
66
- readonly diagnostics: readonly CatalogDiagnostic[];
67
- }
68
-
69
- export function createWorkerCatalog(
70
- workers: readonly WorkerDefinition[],
71
- diagnostics: readonly CatalogDiagnostic[] = [],
72
- ): WorkerCatalog {
73
- return {
74
- workers: [...workers].sort(compareWorkersByName),
75
- diagnostics: [...diagnostics],
76
- };
77
- }
78
-
79
- export function findWorkerByName(
80
- catalog: WorkerCatalog,
81
- name: string,
82
- ): WorkerDefinition | undefined {
83
- return catalog.workers.find((worker) => worker.name === name);
84
- }
85
-
86
- function compareWorkersByName(left: WorkerDefinition, right: WorkerDefinition): number {
87
- if (left.name < right.name) return -1;
88
- if (left.name > right.name) return 1;
89
- return 0;
90
- }
2
+ import type { WorkerLifecycle } from "../catalog/definition.js";
91
3
 
92
4
  export const MAX_WORKER_TITLE_LENGTH = 200;
93
5
  export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
@@ -234,6 +146,13 @@ export const WorkerOutcome = Schema.Union([
234
146
  Schema.Struct({ status: Schema.Literal("closed") }),
235
147
  ]);
236
148
  export type WorkerOutcome = typeof WorkerOutcome.Type;
149
+
150
+ /** Outcomes that can settle a generation; closure is not one of them. */
151
+ export type SettledWorkerOutcome = Exclude<
152
+ WorkerOutcome,
153
+ { readonly status: "closed" }
154
+ >;
155
+
237
156
  export type WorkerStatus =
238
157
  | "starting"
239
158
  | "running"
@@ -266,9 +185,28 @@ export interface WorkerRecord {
266
185
  readonly sessionFile?: string;
267
186
  }
268
187
 
188
+ /** Status of a worker whose generation produced a response outcome. */
189
+ export type SettledWorkerStatus = Extract<
190
+ WorkerStatus,
191
+ "completed" | "ready" | "failed" | "aborted"
192
+ >;
193
+
194
+ /** A worker record whose current generation has settled with a response outcome. */
195
+ export interface SettledWorkerRecord extends WorkerRecord {
196
+ readonly status: SettledWorkerStatus;
197
+ readonly outcome: SettledWorkerOutcome;
198
+ readonly settledAt: number;
199
+ }
200
+
269
201
  export type RunMode = "async" | "inline";
270
202
  export type RunState = "running" | "complete";
271
203
 
204
+ /** One sibling wave whose settlements share a synthesis boundary. */
205
+ export interface SynthesisGroup {
206
+ readonly id: string;
207
+ readonly size: number;
208
+ }
209
+
272
210
  export interface RunRecord {
273
211
  readonly id: RunId;
274
212
  readonly ownerSessionId: string;