@zachwill/pi-orchestrate 0.8.0 → 0.9.2

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.
@@ -173,105 +173,100 @@ function fieldValue(frontmatter: unknown, field: string): unknown {
173
173
  return field in frontmatter ? frontmatter[field] : undefined;
174
174
  }
175
175
 
176
+ function hasIssuePath(issuePaths: readonly IssuePath[], ...path: readonly PropertyKey[]): boolean {
177
+ return issuePaths.some((entry) => path.every((key, index) => entry.path[index] === key));
178
+ }
179
+
180
+ function unexpectedFieldsAt(
181
+ issuePaths: readonly IssuePath[],
182
+ parentPath: readonly PropertyKey[],
183
+ ): string[] {
184
+ return issuePaths
185
+ .filter(({ path, issue }) =>
186
+ issue._tag === "UnexpectedKey" &&
187
+ path.length === parentPath.length + 1 &&
188
+ parentPath.every((key, index) => path[index] === key) &&
189
+ typeof path[parentPath.length] === "string"
190
+ )
191
+ .map(({ path }) => String(path[parentPath.length]))
192
+ .sort(compareText);
193
+ }
194
+
195
+ function unknownFieldsDiagnostic(scope: string, fields: readonly string[]): string {
196
+ return `unknown ${scope} field${fields.length === 1 ? "" : "s"}: ${fields.join(", ")}`;
197
+ }
198
+
176
199
  function listItems(value: unknown): readonly unknown[] {
177
200
  if (typeof value === "string") return value.split(",").map((item) => item.trim());
178
201
  return Array.isArray(value) ? value : [];
179
202
  }
180
203
 
204
+ const ORDERED_FRONTMATTER_FIELDS = [
205
+ "name", "description", "model", "thinking", "tools", "skills", "compaction", "lifecycle",
206
+ ] as const;
207
+
208
+ const FIXED_FIELD_DIAGNOSTICS: Partial<Record<(typeof ORDERED_FRONTMATTER_FIELDS)[number], string>> = {
209
+ name: "frontmatter field 'name' must be a non-empty string",
210
+ description: "frontmatter field 'description' must be a non-empty string",
211
+ skills: "frontmatter field 'skills' must be a comma string or string array",
212
+ lifecycle: "frontmatter field 'lifecycle' must be 'one-shot' or 'interactive'",
213
+ };
214
+
215
+ const COMPACTION_FIELD_DIAGNOSTICS = [
216
+ ["enabled", "frontmatter field 'compaction.enabled' must be a boolean"],
217
+ ["reserveTokens", "frontmatter field 'compaction.reserveTokens' must be a non-negative integer"],
218
+ ["keepRecentTokens", "frontmatter field 'compaction.keepRecentTokens' must be a non-negative integer"],
219
+ ] as const;
220
+
181
221
  function schemaDiagnostic(issue: SchemaIssue.Issue, frontmatter: unknown): string {
182
222
  const issuePaths = collectIssuePaths(issue);
183
- const unexpected = issuePaths.filter(({ issue }) => issue._tag === "UnexpectedKey");
184
- const frontmatterFields = unexpected
185
- .filter(({ path }) => path.length === 1 && typeof path[0] === "string")
186
- .map(({ path }) => String(path[0]))
187
- .sort(compareText);
223
+ const frontmatterFields = unexpectedFieldsAt(issuePaths, []);
188
224
  if (frontmatterFields.length > 0) {
189
- return `unknown frontmatter field${frontmatterFields.length === 1 ? "" : "s"}: ${frontmatterFields.join(", ")}`;
225
+ return unknownFieldsDiagnostic("frontmatter", frontmatterFields);
190
226
  }
227
+ if (!isUnknownRecord(frontmatter)) return "frontmatter must be a mapping";
191
228
 
192
- const compactionFields = unexpected
193
- .filter(
194
- ({ path }) => path.length === 2 && path[0] === "compaction" && typeof path[1] === "string",
195
- )
196
- .map(({ path }) => String(path[1]))
197
- .sort(compareText);
198
-
199
- if (typeof frontmatter !== "object" || frontmatter === null || Array.isArray(frontmatter)) {
200
- return "frontmatter must be a mapping";
201
- }
202
-
203
- const orderedFields = [
204
- "name",
205
- "description",
206
- "model",
207
- "thinking",
208
- "tools",
209
- "skills",
210
- "compaction",
211
- "lifecycle",
212
- ];
213
- const field = orderedFields.find((candidate) =>
214
- issuePaths.some(({ path }) => path[0] === candidate)
229
+ const field = ORDERED_FRONTMATTER_FIELDS.find((candidate) =>
230
+ hasIssuePath(issuePaths, candidate)
215
231
  );
216
- const value = field === undefined ? undefined : fieldValue(frontmatter, field);
232
+ if (field === undefined) return "invalid worker definition";
217
233
 
218
- if (field === "name" || field === "description") {
219
- return `frontmatter field '${field}' must be a non-empty string`;
220
- }
234
+ const fixedDiagnostic = FIXED_FIELD_DIAGNOSTICS[field];
235
+ if (fixedDiagnostic !== undefined) return fixedDiagnostic;
236
+
237
+ const value = fieldValue(frontmatter, field);
221
238
  if (field === "model") {
222
- if (typeof value !== "string" || value.trim() === "") {
223
- return "frontmatter field 'model' must be a non-empty string";
224
- }
225
- return "frontmatter field 'model' must use provider/model format";
239
+ return typeof value !== "string" || value.trim() === ""
240
+ ? "frontmatter field 'model' must be a non-empty string"
241
+ : "frontmatter field 'model' must use provider/model format";
226
242
  }
227
243
  if (field === "thinking") {
228
- if (typeof value !== "string" || value.trim() === "") {
229
- return "frontmatter field 'thinking' must be a non-empty string";
230
- }
231
- return `unsupported thinking level '${value.trim()}'`;
244
+ return typeof value !== "string" || value.trim() === ""
245
+ ? "frontmatter field 'thinking' must be a non-empty string"
246
+ : `unsupported thinking level '${value.trim()}'`;
232
247
  }
233
248
  if (field === "tools") {
234
249
  const items = listItems(value);
235
250
  const validList = items.length > 0 && items.every(
236
251
  (item) => typeof item === "string" && item !== "",
237
252
  );
238
- const unsupported = validList ? items.find(
239
- (item) => typeof item === "string" && !isSupportedToolName(item),
240
- ) : undefined;
253
+ const unsupported = validList
254
+ ? items.find((item) => typeof item === "string" && !isSupportedToolName(item))
255
+ : undefined;
241
256
  if (typeof unsupported === "string") return `unsupported tool '${unsupported}'`;
242
- if (value === undefined) return "frontmatter field 'tools' is required";
243
- return "frontmatter field 'tools' must be a non-empty comma string or string array";
244
- }
245
- if (field === "skills") {
246
- return "frontmatter field 'skills' must be a comma string or string array";
257
+ return value === undefined
258
+ ? "frontmatter field 'tools' is required"
259
+ : "frontmatter field 'tools' must be a non-empty comma string or string array";
247
260
  }
248
- if (field === "compaction") {
249
- if (compactionFields.length > 0) {
250
- return `unknown compaction field${compactionFields.length === 1 ? "" : "s"}: ${compactionFields.join(", ")}`;
251
- }
252
- if (issuePaths.some(({ path }) => path[0] === "compaction" && path[1] === "enabled")) {
253
- return "frontmatter field 'compaction.enabled' must be a boolean";
254
- }
255
- if (
256
- issuePaths.some(({ path }) =>
257
- path[0] === "compaction" && path[1] === "reserveTokens"
258
- )
259
- ) {
260
- return "frontmatter field 'compaction.reserveTokens' must be a non-negative integer";
261
- }
262
- if (
263
- issuePaths.some(({ path }) =>
264
- path[0] === "compaction" && path[1] === "keepRecentTokens"
265
- )
266
- ) {
267
- return "frontmatter field 'compaction.keepRecentTokens' must be a non-negative integer";
268
- }
269
- return "frontmatter field 'compaction' must be a mapping";
270
- }
271
- if (field === "lifecycle") {
272
- return "frontmatter field 'lifecycle' must be 'one-shot' or 'interactive'";
261
+
262
+ const compactionFields = unexpectedFieldsAt(issuePaths, ["compaction"]);
263
+ if (compactionFields.length > 0) {
264
+ return unknownFieldsDiagnostic("compaction", compactionFields);
273
265
  }
274
- return "invalid worker definition";
266
+ const nestedDiagnostic = COMPACTION_FIELD_DIAGNOSTICS.find(([nestedField]) =>
267
+ hasIssuePath(issuePaths, "compaction", nestedField)
268
+ );
269
+ return nestedDiagnostic?.[1] ?? "frontmatter field 'compaction' must be a mapping";
275
270
  }
276
271
 
277
272
  function parseWorker(
@@ -11,6 +11,12 @@ function sortedWorkers(catalog: WorkerCatalog) {
11
11
  });
12
12
  }
13
13
 
14
+ function escapeContractMarkers(value: string): string {
15
+ return value
16
+ .replaceAll(CONTRACT_START, "&lt;!-- pi-orchestrate:contract:start --&gt;")
17
+ .replaceAll(CONTRACT_END, "&lt;!-- pi-orchestrate:contract:end --&gt;");
18
+ }
19
+
14
20
  function formatCatalog(catalog: WorkerCatalog): string {
15
21
  const workers = sortedWorkers(catalog);
16
22
  if (workers.length === 0) return "- No trusted workers are available for this session.";
@@ -18,11 +24,74 @@ function formatCatalog(catalog: WorkerCatalog): string {
18
24
  return workers
19
25
  .map(
20
26
  (worker) =>
21
- `- \`${worker.name}\` [${worker.source.kind}] (${worker.lifecycle}): ${worker.description}`,
27
+ `- \`${escapeContractMarkers(worker.name)}\` [${worker.source.kind}] (${worker.lifecycle}): ${escapeContractMarkers(worker.description)}`,
22
28
  )
23
29
  .join("\n");
24
30
  }
25
31
 
32
+ interface ContractMarker {
33
+ readonly start: number;
34
+ readonly end: number;
35
+ readonly kind: "start" | "end";
36
+ }
37
+
38
+ function contractMarkers(prompt: string): ContractMarker[] {
39
+ const markers: ContractMarker[] = [];
40
+ for (const [value, kind] of [
41
+ [CONTRACT_START, "start"],
42
+ [CONTRACT_END, "end"],
43
+ ] as const) {
44
+ let offset = 0;
45
+ while (offset < prompt.length) {
46
+ const start = prompt.indexOf(value, offset);
47
+ if (start < 0) break;
48
+ markers.push({ start, end: start + value.length, kind });
49
+ offset = start + value.length;
50
+ }
51
+ }
52
+ return markers.sort((left, right) => left.start - right.start);
53
+ }
54
+
55
+ function removeContractMarkers(prompt: string): {
56
+ readonly prompt: string;
57
+ readonly insertionOffset?: number;
58
+ } {
59
+ const markers = contractMarkers(prompt);
60
+ if (markers.length === 0) return { prompt };
61
+
62
+ const removed: Array<{ start: number; end: number }> = [];
63
+ const stack: ContractMarker[] = [];
64
+ for (const marker of markers) {
65
+ if (marker.kind === "start") {
66
+ stack.push(marker);
67
+ continue;
68
+ }
69
+ const start = stack.pop();
70
+ if (start && stack.length === 0) removed.push({ start: start.start, end: marker.end });
71
+ }
72
+
73
+ for (const marker of markers) {
74
+ if (!removed.some((range) => marker.start >= range.start && marker.end <= range.end)) {
75
+ removed.push({ start: marker.start, end: marker.end });
76
+ }
77
+ }
78
+ removed.sort((left, right) => left.start - right.start);
79
+
80
+ const insertionPoint = markers[0]!.start;
81
+ let insertionOffset = 0;
82
+ let cursor = 0;
83
+ let cleaned = "";
84
+ for (const range of removed) {
85
+ if (range.start < cursor) continue;
86
+ const retained = prompt.slice(cursor, range.start);
87
+ cleaned += retained;
88
+ if (range.start <= insertionPoint) insertionOffset = cleaned.length;
89
+ cursor = range.end;
90
+ }
91
+ cleaned += prompt.slice(cursor);
92
+ return { prompt: cleaned, insertionOffset };
93
+ }
94
+
26
95
  function buildContract(catalog: WorkerCatalog): string {
27
96
  return `${CONTRACT_START}
28
97
  ## Pi Orchestrate Contract
@@ -60,12 +129,9 @@ export function appendOrchestratorContract(
60
129
  catalog: WorkerCatalog,
61
130
  ): string {
62
131
  const section = buildContract(catalog);
63
- const start = systemPrompt.indexOf(CONTRACT_START);
64
- if (start >= 0) {
65
- const end = systemPrompt.indexOf(CONTRACT_END, start);
66
- if (end >= 0) {
67
- return `${systemPrompt.slice(0, start)}${section}${systemPrompt.slice(end + CONTRACT_END.length)}`;
68
- }
132
+ const cleaned = removeContractMarkers(systemPrompt);
133
+ if (cleaned.insertionOffset !== undefined) {
134
+ return `${cleaned.prompt.slice(0, cleaned.insertionOffset)}${section}${cleaned.prompt.slice(cleaned.insertionOffset)}`;
69
135
  }
70
136
 
71
137
  const separator =
@@ -1,4 +1,6 @@
1
- import type { WorkerSettlement } from "./runtime.js";
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { Orchestration } from "./runtime.js";
3
+ import type { WorkerSettlement } from "./worker-settlement.js";
2
4
 
3
5
  export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
4
6
  export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
@@ -38,14 +40,35 @@ interface BoundParent {
38
40
  interface SynthesisGroupState {
39
41
  expected: number;
40
42
  readonly acceptedEventIds: string[];
43
+ finalEventId?: string;
41
44
  }
42
45
 
43
- export class DeliveryCoordinator {
46
+ export interface DeliveryService {
47
+ bind(binding: ParentBinding): void;
48
+ unbind(ownerSessionId: string, generation: ParentBindingGeneration): void;
49
+ markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void;
50
+ markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void;
51
+ accept(settlement: WorkerSettlement): boolean;
52
+ skipSynthesisGroupMember(
53
+ ownerSessionId: string,
54
+ synthesisGroupId: string,
55
+ synthesisGroupSize: number,
56
+ ): void;
57
+ pendingCount(ownerSessionId: string): number;
58
+ clear(): void;
59
+ }
60
+
61
+ export class Delivery extends Context.Service<Delivery, DeliveryService>()(
62
+ "@zachwill/pi-orchestrate/Delivery",
63
+ ) {}
64
+
65
+ export class DeliveryCoordinator implements DeliveryService {
44
66
  private readonly boundParents = new Map<string, BoundParent>();
45
67
  private readonly pendingSettlements: WorkerSettlement[] = [];
46
68
  private readonly flushingOwners = new Set<string>();
47
69
  private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
48
- private readonly finalSynthesisGroupEventIds = new Set<string>();
70
+ // Runtime settlement sequences are process-scoped and monotonic across owners,
71
+ // so one watermark is valid.
49
72
  private highestAcceptedSequence = 0;
50
73
 
51
74
  bind(binding: ParentBinding): void {
@@ -62,15 +85,14 @@ export class DeliveryCoordinator {
62
85
  }
63
86
 
64
87
  markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void {
65
- if (!this.matchesBinding(ownerSessionId, generation)) return;
66
88
  const parent = this.boundParents.get(ownerSessionId);
67
- if (parent) parent.agentRunning = true;
89
+ if (parent?.binding.generation !== generation) return;
90
+ parent.agentRunning = true;
68
91
  }
69
92
 
70
93
  markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
71
- if (!this.matchesBinding(ownerSessionId, generation)) return;
72
94
  const parent = this.boundParents.get(ownerSessionId);
73
- if (!parent) return;
95
+ if (parent?.binding.generation !== generation) return;
74
96
  parent.agentRunning = false;
75
97
  this.flush(ownerSessionId, generation);
76
98
  }
@@ -115,7 +137,6 @@ export class DeliveryCoordinator {
115
137
  this.pendingSettlements.length = 0;
116
138
  this.flushingOwners.clear();
117
139
  this.synthesisGroups.clear();
118
- this.finalSynthesisGroupEventIds.clear();
119
140
  this.highestAcceptedSequence = 0;
120
141
  }
121
142
 
@@ -143,21 +164,23 @@ export class DeliveryCoordinator {
143
164
 
144
165
  this.flushingOwners.add(ownerSessionId);
145
166
  try {
167
+ // Deliver a stable owner-ordered prefix, stopping at the latest complete synthesis boundary.
146
168
  const queued = this.pendingSettlements.filter(
147
169
  (settlement) => settlement.ownerSessionId === ownerSessionId,
148
170
  );
149
171
  let latestFinalIndex = -1;
150
- for (let index = 0; index < queued.length; index += 1) {
151
- const settlement = queued[index];
152
- if (settlement && this.isFinalBoundary(settlement)) latestFinalIndex = index;
172
+ for (const [index, settlement] of queued.entries()) {
173
+ if (this.isFinalBoundary(settlement)) latestFinalIndex = index;
153
174
  }
154
175
  const flushThrough = latestFinalIndex >= 0 ? latestFinalIndex : queued.length - 1;
155
176
  let flushBytesRemaining = MAX_DELIVERY_MARKDOWN_BYTES;
156
177
 
157
- for (let index = 0; index <= flushThrough; index += 1) {
178
+ for (const [index, settlement] of queued.entries()) {
179
+ if (index > flushThrough) break;
180
+ // Synchronous delivery callbacks can change owner, generation, or idle
181
+ // state before the next send.
158
182
  if (!this.canDeliver(ownerSessionId, generation)) return;
159
- const settlement = queued[index];
160
- if (!settlement || !this.pendingSettlements.includes(settlement)) continue;
183
+ if (!this.pendingSettlements.includes(settlement)) continue;
161
184
 
162
185
  const messagesRemaining = flushThrough - index + 1;
163
186
  const fairFlushBytes = Math.floor(flushBytesRemaining / messagesRemaining);
@@ -166,6 +189,8 @@ export class DeliveryCoordinator {
166
189
  fairFlushBytes,
167
190
  flushBytesRemaining,
168
191
  ));
192
+ // Intermediate results add context; only the completed boundary
193
+ // transfers work to a parent turn.
169
194
  const triggerTurn = latestFinalIndex >= 0 && index === flushThrough;
170
195
  const message = this.renderWorkerMessage(settlement, byteLimit);
171
196
  const parent = this.boundParents.get(ownerSessionId);
@@ -174,6 +199,7 @@ export class DeliveryCoordinator {
174
199
  try {
175
200
  parent.binding.sendMessage(message, { triggerTurn });
176
201
  } catch {
202
+ // Keep this settlement and the remaining prefix queued for a later retry.
177
203
  return;
178
204
  }
179
205
 
@@ -240,12 +266,15 @@ export class DeliveryCoordinator {
240
266
  }
241
267
  if (state.acceptedEventIds.length !== state.expected) return;
242
268
  const finalEventId = state.acceptedEventIds.at(-1);
243
- if (finalEventId) this.finalSynthesisGroupEventIds.add(finalEventId);
269
+ if (finalEventId) state.finalEventId = finalEventId;
244
270
  }
245
271
 
246
272
  private isFinalBoundary(settlement: WorkerSettlement): boolean {
247
273
  if (!settlement.synthesisGroupId) return true;
248
- return this.finalSynthesisGroupEventIds.has(settlement.eventId);
274
+ const state = this.synthesisGroups.get(
275
+ synthesisGroupKey(settlement.ownerSessionId, settlement.synthesisGroupId),
276
+ );
277
+ return state?.finalEventId === settlement.eventId;
249
278
  }
250
279
 
251
280
  private finishSynthesisGroup(settlement: WorkerSettlement): void {
@@ -253,10 +282,29 @@ export class DeliveryCoordinator {
253
282
  this.synthesisGroups.delete(
254
283
  synthesisGroupKey(settlement.ownerSessionId, settlement.synthesisGroupId),
255
284
  );
256
- this.finalSynthesisGroupEventIds.delete(settlement.eventId);
257
285
  }
258
286
  }
259
287
 
288
+ /** Process-scoped direct delivery state and its Orchestration settlement subscription. */
289
+ export const deliveryLayer: Layer.Layer<Delivery, never, Orchestration> = Layer.effect(
290
+ Delivery,
291
+ Effect.gen(function* () {
292
+ const orchestration = yield* Orchestration;
293
+ const coordinator = new DeliveryCoordinator();
294
+
295
+ // Registered first so subscription release runs before delivery state is cleared.
296
+ yield* Effect.addFinalizer(() => Effect.sync(() => coordinator.clear()));
297
+ yield* Effect.acquireRelease(
298
+ Effect.sync(() => orchestration.subscribeSettlement((settlement) => {
299
+ coordinator.accept(settlement);
300
+ })),
301
+ (unsubscribe) => Effect.sync(unsubscribe),
302
+ );
303
+
304
+ return Delivery.of(coordinator);
305
+ }),
306
+ );
307
+
260
308
  function synthesisGroupKey(ownerSessionId: string, synthesisGroupId: string): string {
261
309
  return `${ownerSessionId}\u0000${synthesisGroupId}`;
262
310
  }
@@ -1,4 +1,5 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import { Schema } from "effect";
2
3
 
3
4
  export const SUPPORTED_TOOL_NAMES = [
4
5
  "read",
@@ -88,17 +89,36 @@ function compareWorkersByName(left: WorkerDefinition, right: WorkerDefinition):
88
89
  return 0;
89
90
  }
90
91
 
91
- export interface OrchestrateTaskInput {
92
- readonly worker: string;
93
- readonly title: string;
94
- readonly instructions: string;
95
- }
92
+ export const MAX_WORKER_TITLE_LENGTH = 200;
93
+ export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
94
+ export const CANCELLATION_GRACE_MS = 5_000;
96
95
 
97
- declare const workerIdBrand: unique symbol;
98
- declare const runIdBrand: unique symbol;
96
+ const NonBlankString = Schema.String.check(Schema.isPattern(/\S/));
97
+ const WorkerLabel = NonBlankString.check(
98
+ Schema.isMaxLength(MAX_WORKER_TITLE_LENGTH),
99
+ );
100
+ const WorkerInstructions = NonBlankString.check(
101
+ Schema.isMaxLength(MAX_WORKER_INSTRUCTIONS_LENGTH),
102
+ );
103
+
104
+ export const OrchestrateTaskInput = Schema.Struct({
105
+ worker: WorkerLabel,
106
+ title: WorkerLabel,
107
+ instructions: WorkerInstructions,
108
+ });
109
+ export interface OrchestrateTaskInput extends Schema.Schema.Type<typeof OrchestrateTaskInput> {}
99
110
 
100
- export type WorkerId = string & { readonly [workerIdBrand]: "WorkerId" };
101
- export type RunId = string & { readonly [runIdBrand]: "RunId" };
111
+ /** A validated worker identity. Worker IDs are stable across interactive generations. */
112
+ export const WorkerId = Schema.String.check(
113
+ Schema.isPattern(/^worker-\S+$/),
114
+ ).pipe(Schema.brand("WorkerId"));
115
+ export type WorkerId = typeof WorkerId.Type;
116
+
117
+ /** A validated identity for exactly one worker generation. */
118
+ export const RunId = Schema.String.check(
119
+ Schema.isPattern(/^run-\S+$/),
120
+ ).pipe(Schema.brand("RunId"));
121
+ export type RunId = typeof RunId.Type;
102
122
 
103
123
  export type WorkerIdFactory = () => WorkerId;
104
124
  export type RunIdFactory = () => RunId;
@@ -111,13 +131,13 @@ export interface OrchestrateIdFactories {
111
131
  export function createRandomWorkerIdFactory(
112
132
  randomId: () => string = defaultRandomId,
113
133
  ): WorkerIdFactory {
114
- return () => `worker-${randomId()}` as WorkerId;
134
+ return () => WorkerId.make(`worker-${randomId()}`);
115
135
  }
116
136
 
117
137
  export function createRandomRunIdFactory(
118
138
  randomId: () => string = defaultRandomId,
119
139
  ): RunIdFactory {
120
- return () => `run-${randomId()}` as RunId;
140
+ return () => RunId.make(`run-${randomId()}`);
121
141
  }
122
142
 
123
143
  export function createRandomIdFactories(
@@ -131,12 +151,12 @@ export function createRandomIdFactories(
131
151
 
132
152
  export function createSequentialWorkerIdFactory(startAt = 1): WorkerIdFactory {
133
153
  let next = startAt;
134
- return () => `worker-${next++}` as WorkerId;
154
+ return () => WorkerId.make(`worker-${next++}`);
135
155
  }
136
156
 
137
157
  export function createSequentialRunIdFactory(startAt = 1): RunIdFactory {
138
158
  let next = startAt;
139
- return () => `run-${next++}` as RunId;
159
+ return () => RunId.make(`run-${next++}`);
140
160
  }
141
161
 
142
162
  export function createSequentialIdFactories(startAt = 1): OrchestrateIdFactories {
@@ -150,15 +170,23 @@ function defaultRandomId(): string {
150
170
  return globalThis.crypto.randomUUID();
151
171
  }
152
172
 
153
- export interface WorkerUsage {
154
- readonly input: number;
155
- readonly output: number;
156
- readonly cacheRead: number;
157
- readonly cacheWrite: number;
158
- readonly cost: number;
159
- readonly contextTokens: number;
160
- readonly turns: number;
161
- }
173
+ const NonnegativeFinite = Schema.Finite.check(
174
+ Schema.isGreaterThanOrEqualTo(0),
175
+ );
176
+ const NonnegativeInteger = Schema.Int.check(
177
+ Schema.isGreaterThanOrEqualTo(0),
178
+ );
179
+
180
+ export const WorkerUsage = Schema.Struct({
181
+ input: NonnegativeFinite,
182
+ output: NonnegativeFinite,
183
+ cacheRead: NonnegativeFinite,
184
+ cacheWrite: NonnegativeFinite,
185
+ cost: NonnegativeFinite,
186
+ contextTokens: NonnegativeFinite,
187
+ turns: NonnegativeInteger,
188
+ });
189
+ export interface WorkerUsage extends Schema.Schema.Type<typeof WorkerUsage> {}
162
190
 
163
191
  /** Direction of the most recent message across the worker/model boundary. */
164
192
  export type WorkerMessageDirection = "to-model" | "from-model";
@@ -173,38 +201,39 @@ export const EMPTY_WORKER_USAGE: WorkerUsage = Object.freeze({
173
201
  turns: 0,
174
202
  });
175
203
 
176
- export interface WorkerCompletedOutcome {
177
- readonly status: "completed";
178
- readonly assistantText: string;
179
- }
180
-
181
- export interface WorkerReadyOutcome {
182
- readonly status: "ready";
183
- readonly assistantText: string;
184
- }
185
-
186
- export interface WorkerFailedOutcome {
187
- readonly status: "failed";
188
- readonly message: string;
189
- readonly assistantText?: string;
190
- }
191
-
192
- export interface WorkerAbortedOutcome {
193
- readonly status: "aborted";
194
- readonly message?: string;
195
- readonly assistantText?: string;
196
- }
197
-
198
- export interface WorkerClosedOutcome {
199
- readonly status: "closed";
200
- }
201
-
202
- export type WorkerOutcome =
203
- | WorkerCompletedOutcome
204
- | WorkerReadyOutcome
205
- | WorkerFailedOutcome
206
- | WorkerAbortedOutcome
207
- | WorkerClosedOutcome;
204
+ export const WorkerCompletedOutcome = Schema.Struct({
205
+ status: Schema.Literal("completed"),
206
+ assistantText: Schema.String,
207
+ });
208
+ export const WorkerReadyOutcome = Schema.Struct({
209
+ status: Schema.Literal("ready"),
210
+ assistantText: Schema.String,
211
+ });
212
+ export const WorkerFailedOutcome = Schema.Struct({
213
+ status: Schema.Literal("failed"),
214
+ message: Schema.String,
215
+ assistantText: Schema.optionalKey(Schema.String),
216
+ });
217
+ export const WorkerAbortedOutcome = Schema.Struct({
218
+ status: Schema.Literal("aborted"),
219
+ message: Schema.optionalKey(Schema.String),
220
+ assistantText: Schema.optionalKey(Schema.String),
221
+ });
222
+ /** Outcomes emitted in response to a worker generation. */
223
+ export const WorkerResponseOutcome = Schema.Union([
224
+ WorkerCompletedOutcome,
225
+ WorkerReadyOutcome,
226
+ WorkerFailedOutcome,
227
+ WorkerAbortedOutcome,
228
+ ]);
229
+ export type WorkerResponseOutcome = typeof WorkerResponseOutcome.Type;
230
+
231
+ /** Domain outcomes include closure, which is not a generation response. */
232
+ export const WorkerOutcome = Schema.Union([
233
+ WorkerResponseOutcome,
234
+ Schema.Struct({ status: Schema.Literal("closed") }),
235
+ ]);
236
+ export type WorkerOutcome = typeof WorkerOutcome.Type;
208
237
  export type WorkerStatus =
209
238
  | "starting"
210
239
  | "running"
@@ -301,7 +330,3 @@ export function transitionWorkerStatus(
301
330
 
302
331
  return { ...worker, status, outcome: undefined };
303
332
  }
304
-
305
- export const MAX_WORKER_TITLE_LENGTH = 200;
306
- export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
307
- export const CANCELLATION_GRACE_MS = 5_000;