@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.
- package/extension/catalog.ts +70 -75
- package/extension/contract.ts +73 -7
- package/extension/delivery.ts +65 -17
- package/extension/domain.ts +83 -58
- package/extension/host.ts +316 -30
- package/extension/index.ts +28 -31
- package/extension/presentation.ts +48 -98
- package/extension/runtime.ts +1690 -884
- package/extension/tools.ts +236 -270
- package/extension/tui.ts +50 -0
- package/extension/worker-session.ts +514 -188
- package/extension/worker-settlement.ts +105 -44
- package/package.json +1 -1
- package/extension/scheduler.ts +0 -85
package/extension/catalog.ts
CHANGED
|
@@ -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
|
|
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
|
|
225
|
+
return unknownFieldsDiagnostic("frontmatter", frontmatterFields);
|
|
190
226
|
}
|
|
227
|
+
if (!isUnknownRecord(frontmatter)) return "frontmatter must be a mapping";
|
|
191
228
|
|
|
192
|
-
const
|
|
193
|
-
|
|
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
|
-
|
|
232
|
+
if (field === undefined) return "invalid worker definition";
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
239
|
-
(item) => typeof item === "string" && !isSupportedToolName(item)
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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(
|
package/extension/contract.ts
CHANGED
|
@@ -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, "<!-- pi-orchestrate:contract:start -->")
|
|
17
|
+
.replaceAll(CONTRACT_END, "<!-- pi-orchestrate:contract:end -->");
|
|
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
|
|
64
|
-
if (
|
|
65
|
-
|
|
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 =
|
package/extension/delivery.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
151
|
-
|
|
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 (
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/extension/domain.ts
CHANGED
|
@@ -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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
export
|
|
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()}`
|
|
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()}`
|
|
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++}`
|
|
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++}`
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|