@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.2.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,529 @@
1
+ import type {
2
+ CoordinatorMessage,
3
+ DeliveryPath,
4
+ PersistedCoordinationDelivery,
5
+ PersistedDelivery,
6
+ TurnResult,
7
+ } from "./minimal-subagents-types.js";
8
+
9
+ /** Maximum pending wait-only terminal results retained for one source agent. */
10
+ export const WAIT_TERMINAL_RETENTION_LIMIT = 20;
11
+
12
+ /** Immutable, process-local state for sequenced pending deliveries and durable wait claims. */
13
+ export interface DeliveryLedger {
14
+ /** Pending successful terminal results, including at most 20 wait-only items per source. */
15
+ readonly terminalDeliveries: readonly PersistedDelivery[];
16
+ /** Pending Coordination Messages, which are never removed by terminal retention. */
17
+ readonly coordinationDeliveries: readonly PersistedCoordinationDelivery[];
18
+ /** Source-agent and source-turn compound keys durably owned by wait delivery. */
19
+ readonly waitClaimedTurns: readonly string[];
20
+ /** Next positive safe sequence shared by both delivery kinds. */
21
+ readonly nextSequence: number;
22
+ }
23
+
24
+ /** Serializable fields owned by the Delivery Ledger inside a Registry checkpoint. */
25
+ export interface DeliveryLedgerSnapshot {
26
+ deliveries: PersistedDelivery[];
27
+ coordination_deliveries: PersistedCoordinationDelivery[];
28
+ wait_claimed_turns: string[];
29
+ next_delivery_sequence: number;
30
+ }
31
+
32
+ /** Returns one immutable Delivery Ledger transition and any terminal items evicted by retention. */
33
+ export interface DeliveryLedgerTransition {
34
+ ledger: DeliveryLedger;
35
+ prunedTerminalDeliveries: PersistedDelivery[];
36
+ }
37
+
38
+ /** Returns a newly sequenced terminal delivery with its immutable ledger transition. */
39
+ export interface AddedTerminalDelivery extends DeliveryLedgerTransition {
40
+ delivery: PersistedDelivery;
41
+ }
42
+
43
+ /** Returns a newly sequenced Coordination Message with its immutable ledger transition. */
44
+ export interface AddedCoordinationDelivery extends DeliveryLedgerTransition {
45
+ delivery: PersistedCoordinationDelivery;
46
+ }
47
+
48
+ /** Describes caller-visible inputs used to select the oldest observable source turn. */
49
+ export interface SelectObservableDeliveryTurnOptions {
50
+ sourceAgentId: string;
51
+ destinationAgentId: string;
52
+ waitHandedDeliveryIds: ReadonlySet<string>;
53
+ activeTurnId?: string;
54
+ latestResultTurnId?: string;
55
+ }
56
+
57
+ function deliveryTurnKey(sourceAgentId: string, sourceTurnId: string): string {
58
+ return `${sourceAgentId}\u0000${sourceTurnId}`;
59
+ }
60
+
61
+ function cloneTerminalDelivery(delivery: PersistedDelivery): PersistedDelivery {
62
+ return structuredClone(delivery);
63
+ }
64
+
65
+ function cloneCoordinationDelivery(
66
+ delivery: PersistedCoordinationDelivery,
67
+ ): PersistedCoordinationDelivery {
68
+ return structuredClone(delivery);
69
+ }
70
+
71
+ function createTransition(
72
+ ledger: DeliveryLedger,
73
+ prunedTerminalDeliveries: readonly PersistedDelivery[] = [],
74
+ ): DeliveryLedgerTransition {
75
+ return {
76
+ ledger,
77
+ prunedTerminalDeliveries: prunedTerminalDeliveries.map(cloneTerminalDelivery),
78
+ };
79
+ }
80
+
81
+ function normalizeDeliveryLedgerRetention(ledger: DeliveryLedger): DeliveryLedgerTransition {
82
+ const retainedKeys = new Set<string>();
83
+ const prunedTerminalDeliveries: PersistedDelivery[] = [];
84
+ const waitDeliveriesBySource = new Map<string, PersistedDelivery[]>();
85
+ for (const delivery of ledger.terminalDeliveries) {
86
+ if (delivery.path !== "wait") continue;
87
+ const sourceDeliveries = waitDeliveriesBySource.get(delivery.source_agent_id) ?? [];
88
+ sourceDeliveries.push(delivery);
89
+ waitDeliveriesBySource.set(delivery.source_agent_id, sourceDeliveries);
90
+ }
91
+ for (const sourceDeliveries of waitDeliveriesBySource.values()) {
92
+ sourceDeliveries.sort((left, right) => (right.sequence ?? 0) - (left.sequence ?? 0));
93
+ for (const delivery of sourceDeliveries.slice(0, WAIT_TERMINAL_RETENTION_LIMIT)) {
94
+ retainedKeys.add(deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id));
95
+ }
96
+ prunedTerminalDeliveries.push(...sourceDeliveries.slice(WAIT_TERMINAL_RETENTION_LIMIT));
97
+ }
98
+ if (prunedTerminalDeliveries.length === 0) return createTransition(ledger);
99
+
100
+ const terminalDeliveries = ledger.terminalDeliveries.filter(
101
+ (delivery) =>
102
+ delivery.path !== "wait" ||
103
+ retainedKeys.has(deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id)),
104
+ );
105
+ const coordinationTurnKeys = new Set(
106
+ ledger.coordinationDeliveries.map((delivery) =>
107
+ deliveryTurnKey(
108
+ delivery.message.details.source_agent_id,
109
+ delivery.message.details.source_turn_id,
110
+ ),
111
+ ),
112
+ );
113
+ const prunedTerminalKeys = new Set(
114
+ prunedTerminalDeliveries.map((delivery) =>
115
+ deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id),
116
+ ),
117
+ );
118
+ const waitClaimedTurns = ledger.waitClaimedTurns.filter(
119
+ (key) => !prunedTerminalKeys.has(key) || coordinationTurnKeys.has(key),
120
+ );
121
+ return createTransition(
122
+ {
123
+ ...ledger,
124
+ terminalDeliveries,
125
+ waitClaimedTurns,
126
+ },
127
+ prunedTerminalDeliveries,
128
+ );
129
+ }
130
+
131
+ /** Rehydrate a Delivery Ledger, assigning deterministic sequences to legacy V1 terminal items. */
132
+ export function createDeliveryLedger(
133
+ snapshot: Partial<DeliveryLedgerSnapshot> = {},
134
+ ): DeliveryLedger {
135
+ const coordinationDeliveries = (snapshot.coordination_deliveries ?? [])
136
+ .filter((delivery) => !delivery.settled)
137
+ .map(cloneCoordinationDelivery);
138
+ let nextSequence = Math.max(
139
+ 1,
140
+ snapshot.next_delivery_sequence ?? 1,
141
+ ...coordinationDeliveries.map((delivery) => delivery.sequence + 1),
142
+ ...(snapshot.deliveries ?? []).flatMap((delivery) =>
143
+ delivery.settled || delivery.sequence === undefined ? [] : [delivery.sequence + 1],
144
+ ),
145
+ );
146
+ const terminalDeliveries = (snapshot.deliveries ?? [])
147
+ .filter((delivery) => !delivery.settled)
148
+ .map((delivery) => {
149
+ const restored = cloneTerminalDelivery(delivery);
150
+ if (restored.sequence === undefined) restored.sequence = nextSequence++;
151
+ return restored;
152
+ });
153
+ return normalizeDeliveryLedgerRetention({
154
+ terminalDeliveries,
155
+ coordinationDeliveries,
156
+ waitClaimedTurns: [...(snapshot.wait_claimed_turns ?? [])],
157
+ nextSequence,
158
+ }).ledger;
159
+ }
160
+
161
+ /** Serialize pending Delivery Ledger state without exposing mutable internal references. */
162
+ export function deliveryLedgerSnapshot(ledger: DeliveryLedger): DeliveryLedgerSnapshot {
163
+ return {
164
+ deliveries: ledger.terminalDeliveries.map(cloneTerminalDelivery),
165
+ coordination_deliveries: ledger.coordinationDeliveries.map(cloneCoordinationDelivery),
166
+ wait_claimed_turns: [...ledger.waitClaimedTurns],
167
+ next_delivery_sequence: ledger.nextSequence,
168
+ };
169
+ }
170
+
171
+ /** Allocate and retain one pending successful terminal result. */
172
+ export function addTerminalDelivery(
173
+ ledger: DeliveryLedger,
174
+ input: { destinationAgentId: string; path: DeliveryPath; result: TurnResult },
175
+ ): AddedTerminalDelivery {
176
+ const delivery: PersistedDelivery = {
177
+ source_agent_id: input.result.agent_id,
178
+ source_turn_id: input.result.turn_id,
179
+ destination_agent_id: input.destinationAgentId,
180
+ path: input.path,
181
+ settled: false,
182
+ sequence: ledger.nextSequence,
183
+ result: structuredClone(input.result),
184
+ };
185
+ const transition = normalizeDeliveryLedgerRetention({
186
+ ...ledger,
187
+ terminalDeliveries: [
188
+ ...ledger.terminalDeliveries.filter(
189
+ (current) =>
190
+ deliveryTurnKey(current.source_agent_id, current.source_turn_id) !==
191
+ deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id),
192
+ ),
193
+ delivery,
194
+ ],
195
+ nextSequence: ledger.nextSequence + 1,
196
+ });
197
+ return { ...transition, delivery: cloneTerminalDelivery(delivery) };
198
+ }
199
+
200
+ /** Replay or update one already-sequenced terminal delivery. */
201
+ export function upsertTerminalDelivery(
202
+ ledger: DeliveryLedger,
203
+ delivery: PersistedDelivery,
204
+ ): DeliveryLedgerTransition {
205
+ const restored = cloneTerminalDelivery(delivery);
206
+ const existing = ledger.terminalDeliveries.find(
207
+ (current) =>
208
+ deliveryTurnKey(current.source_agent_id, current.source_turn_id) ===
209
+ deliveryTurnKey(restored.source_agent_id, restored.source_turn_id),
210
+ );
211
+ const sequence = restored.sequence ?? existing?.sequence ?? ledger.nextSequence;
212
+ restored.sequence = sequence;
213
+ return normalizeDeliveryLedgerRetention({
214
+ ...ledger,
215
+ terminalDeliveries: [
216
+ ...ledger.terminalDeliveries.filter(
217
+ (current) =>
218
+ deliveryTurnKey(current.source_agent_id, current.source_turn_id) !==
219
+ deliveryTurnKey(restored.source_agent_id, restored.source_turn_id),
220
+ ),
221
+ restored,
222
+ ],
223
+ nextSequence: Math.max(ledger.nextSequence, sequence + 1),
224
+ });
225
+ }
226
+
227
+ /** Allocate and retain one pending Coordination Message delivery. */
228
+ export function addCoordinationDelivery(
229
+ ledger: DeliveryLedger,
230
+ input: { destinationAgentId: string; message: CoordinatorMessage },
231
+ ): AddedCoordinationDelivery {
232
+ const deliveryId = `message:${input.message.details.message_id}`;
233
+ const message = structuredClone(input.message);
234
+ message.details.delivery_id = deliveryId;
235
+ const delivery: PersistedCoordinationDelivery = {
236
+ delivery_id: deliveryId,
237
+ sequence: ledger.nextSequence,
238
+ destination_agent_id: input.destinationAgentId,
239
+ path: "message",
240
+ settled: false,
241
+ message,
242
+ };
243
+ return {
244
+ ledger: {
245
+ ...ledger,
246
+ coordinationDeliveries: [
247
+ ...ledger.coordinationDeliveries.filter(
248
+ (current) => current.delivery_id !== delivery.delivery_id,
249
+ ),
250
+ delivery,
251
+ ],
252
+ nextSequence: ledger.nextSequence + 1,
253
+ },
254
+ delivery: cloneCoordinationDelivery(delivery),
255
+ prunedTerminalDeliveries: [],
256
+ };
257
+ }
258
+
259
+ /** Replay or update one already-sequenced Coordination Message delivery. */
260
+ export function upsertCoordinationDelivery(
261
+ ledger: DeliveryLedger,
262
+ delivery: PersistedCoordinationDelivery,
263
+ ): DeliveryLedgerTransition {
264
+ return createTransition({
265
+ ...ledger,
266
+ coordinationDeliveries: [
267
+ ...ledger.coordinationDeliveries.filter(
268
+ (current) => current.delivery_id !== delivery.delivery_id,
269
+ ),
270
+ cloneCoordinationDelivery(delivery),
271
+ ],
272
+ nextSequence: Math.max(ledger.nextSequence, delivery.sequence + 1),
273
+ });
274
+ }
275
+
276
+ /** Change one terminal delivery path and enforce wait-only terminal retention. */
277
+ export function setTerminalDeliveryPath(
278
+ ledger: DeliveryLedger,
279
+ sourceAgentId: string,
280
+ sourceTurnId: string,
281
+ path: DeliveryPath,
282
+ ): DeliveryLedgerTransition {
283
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
284
+ return normalizeDeliveryLedgerRetention({
285
+ ...ledger,
286
+ terminalDeliveries: ledger.terminalDeliveries.map((delivery) =>
287
+ deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id) === key
288
+ ? { ...delivery, path }
289
+ : delivery,
290
+ ),
291
+ });
292
+ }
293
+
294
+ /** Change one Coordination Message path without changing its sequence identity. */
295
+ export function setCoordinationDeliveryPath(
296
+ ledger: DeliveryLedger,
297
+ deliveryId: string,
298
+ path: DeliveryPath,
299
+ ): DeliveryLedger {
300
+ return {
301
+ ...ledger,
302
+ coordinationDeliveries: ledger.coordinationDeliveries.map((delivery) =>
303
+ delivery.delivery_id === deliveryId ? { ...delivery, path } : delivery,
304
+ ),
305
+ };
306
+ }
307
+
308
+ /** Record or clear one terminal delivery attempt error. */
309
+ export function setTerminalDeliveryError(
310
+ ledger: DeliveryLedger,
311
+ sourceAgentId: string,
312
+ sourceTurnId: string,
313
+ error: string | undefined,
314
+ ): DeliveryLedger {
315
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
316
+ return {
317
+ ...ledger,
318
+ terminalDeliveries: ledger.terminalDeliveries.map((delivery) => {
319
+ if (deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id) !== key)
320
+ return delivery;
321
+ const next = { ...delivery };
322
+ if (error === undefined) delete next.error;
323
+ else next.error = error;
324
+ return next;
325
+ }),
326
+ };
327
+ }
328
+
329
+ /** Record or clear one Coordination Message delivery attempt error. */
330
+ export function setCoordinationDeliveryError(
331
+ ledger: DeliveryLedger,
332
+ deliveryId: string,
333
+ error: string | undefined,
334
+ ): DeliveryLedger {
335
+ return {
336
+ ...ledger,
337
+ coordinationDeliveries: ledger.coordinationDeliveries.map((delivery) => {
338
+ if (delivery.delivery_id !== deliveryId) return delivery;
339
+ const next = { ...delivery };
340
+ if (error === undefined) delete next.error;
341
+ else next.error = error;
342
+ return next;
343
+ }),
344
+ };
345
+ }
346
+
347
+ /** Remove one keyed terminal delivery while preserving its wait claim for explicit release policy. */
348
+ export function settleTerminalDelivery(
349
+ ledger: DeliveryLedger,
350
+ sourceAgentId: string,
351
+ sourceTurnId: string,
352
+ ): DeliveryLedgerTransition {
353
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
354
+ return createTransition({
355
+ ...ledger,
356
+ terminalDeliveries: ledger.terminalDeliveries.filter(
357
+ (delivery) => deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id) !== key,
358
+ ),
359
+ });
360
+ }
361
+
362
+ /** Remove one keyed Coordination Message while preserving its wait claim for explicit release policy. */
363
+ export function settleCoordinationDelivery(
364
+ ledger: DeliveryLedger,
365
+ deliveryId: string,
366
+ ): DeliveryLedgerTransition {
367
+ return createTransition({
368
+ ...ledger,
369
+ coordinationDeliveries: ledger.coordinationDeliveries.filter(
370
+ (delivery) => delivery.delivery_id !== deliveryId,
371
+ ),
372
+ });
373
+ }
374
+
375
+ /** Durably claim all retained items for one source turn for wait delivery. */
376
+ export function claimDeliveryLedgerTurn(
377
+ ledger: DeliveryLedger,
378
+ sourceAgentId: string,
379
+ sourceTurnId: string,
380
+ ): DeliveryLedgerTransition & { changed: boolean } {
381
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
382
+ if (ledger.waitClaimedTurns.includes(key)) return { ...createTransition(ledger), changed: false };
383
+ return {
384
+ ...createTransition({ ...ledger, waitClaimedTurns: [...ledger.waitClaimedTurns, key] }),
385
+ changed: true,
386
+ };
387
+ }
388
+
389
+ /** Release one source-turn wait claim regardless of retained delivery state. */
390
+ export function releaseDeliveryLedgerTurn(
391
+ ledger: DeliveryLedger,
392
+ sourceAgentId: string,
393
+ sourceTurnId: string,
394
+ ): DeliveryLedgerTransition & { changed: boolean } {
395
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
396
+ if (!ledger.waitClaimedTurns.includes(key))
397
+ return { ...createTransition(ledger), changed: false };
398
+ return {
399
+ ...createTransition({
400
+ ...ledger,
401
+ waitClaimedTurns: ledger.waitClaimedTurns.filter((current) => current !== key),
402
+ }),
403
+ changed: true,
404
+ };
405
+ }
406
+
407
+ /** Release a source-turn wait claim only after the turn and all its retained items are absent. */
408
+ export function releaseEmptyDeliveryLedgerTurn(
409
+ ledger: DeliveryLedger,
410
+ sourceAgentId: string,
411
+ sourceTurnId: string,
412
+ sourceTurnActive: boolean,
413
+ ): DeliveryLedgerTransition & { changed: boolean } {
414
+ if (sourceTurnActive) return { ...createTransition(ledger), changed: false };
415
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
416
+ const hasTerminal = ledger.terminalDeliveries.some(
417
+ (delivery) => deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id) === key,
418
+ );
419
+ const hasCoordination = ledger.coordinationDeliveries.some(
420
+ (delivery) =>
421
+ deliveryTurnKey(
422
+ delivery.message.details.source_agent_id,
423
+ delivery.message.details.source_turn_id,
424
+ ) === key,
425
+ );
426
+ return hasTerminal || hasCoordination
427
+ ? { ...createTransition(ledger), changed: false }
428
+ : releaseDeliveryLedgerTurn(ledger, sourceAgentId, sourceTurnId);
429
+ }
430
+
431
+ /** Select the oldest claimed, then oldest sequenced, observable source turn. */
432
+ export function selectObservableDeliveryTurn(
433
+ ledger: DeliveryLedger,
434
+ options: SelectObservableDeliveryTurnOptions,
435
+ ): string | undefined {
436
+ const candidates = new Map<string, { claimed: boolean; sequence: number }>();
437
+ const addCandidate = (turnId: string, sequence: number) => {
438
+ const candidate = {
439
+ claimed: ledger.waitClaimedTurns.includes(deliveryTurnKey(options.sourceAgentId, turnId)),
440
+ sequence,
441
+ };
442
+ const existing = candidates.get(turnId);
443
+ if (!existing || sequence < existing.sequence) candidates.set(turnId, candidate);
444
+ };
445
+ for (const delivery of ledger.coordinationDeliveries) {
446
+ if (
447
+ delivery.destination_agent_id === options.destinationAgentId &&
448
+ delivery.message.details.source_agent_id === options.sourceAgentId &&
449
+ !options.waitHandedDeliveryIds.has(delivery.delivery_id)
450
+ ) {
451
+ addCandidate(delivery.message.details.source_turn_id, delivery.sequence);
452
+ }
453
+ }
454
+ for (const delivery of ledger.terminalDeliveries) {
455
+ if (
456
+ delivery.destination_agent_id === options.destinationAgentId &&
457
+ delivery.source_agent_id === options.sourceAgentId
458
+ ) {
459
+ addCandidate(delivery.source_turn_id, delivery.sequence ?? Number.MAX_SAFE_INTEGER);
460
+ }
461
+ }
462
+ const retained = [...candidates.entries()].sort((left, right) => {
463
+ if (left[1].claimed !== right[1].claimed) return left[1].claimed ? -1 : 1;
464
+ return left[1].sequence - right[1].sequence;
465
+ })[0]?.[0];
466
+ return retained ?? options.activeTurnId ?? options.latestResultTurnId;
467
+ }
468
+
469
+ /** Remove delivery items and wait claims owned by any deleted agent subtree. */
470
+ export function pruneDeliveryLedgerAgents(
471
+ ledger: DeliveryLedger,
472
+ deletedAgentIds: readonly string[],
473
+ ): DeliveryLedgerTransition {
474
+ const belongsToDeletedSubtree = (agentId: string) =>
475
+ deletedAgentIds.some(
476
+ (deletedId) => agentId === deletedId || agentId.startsWith(`${deletedId}.`),
477
+ );
478
+ return createTransition({
479
+ ...ledger,
480
+ terminalDeliveries: ledger.terminalDeliveries.filter(
481
+ (delivery) =>
482
+ !belongsToDeletedSubtree(delivery.source_agent_id) &&
483
+ !belongsToDeletedSubtree(delivery.destination_agent_id),
484
+ ),
485
+ coordinationDeliveries: ledger.coordinationDeliveries.filter(
486
+ (delivery) =>
487
+ !belongsToDeletedSubtree(delivery.message.details.source_agent_id) &&
488
+ !belongsToDeletedSubtree(delivery.destination_agent_id),
489
+ ),
490
+ waitClaimedTurns: ledger.waitClaimedTurns.filter((key) => {
491
+ const separatorIndex = key.indexOf("\u0000");
492
+ const sourceAgentId = separatorIndex >= 0 ? key.slice(0, separatorIndex) : key;
493
+ return !belongsToDeletedSubtree(sourceAgentId);
494
+ }),
495
+ });
496
+ }
497
+
498
+ /** Return one pending terminal delivery by source-turn identity. */
499
+ export function findTerminalDelivery(
500
+ ledger: DeliveryLedger,
501
+ sourceAgentId: string,
502
+ sourceTurnId: string,
503
+ ): PersistedDelivery | undefined {
504
+ const key = deliveryTurnKey(sourceAgentId, sourceTurnId);
505
+ const delivery = ledger.terminalDeliveries.find(
506
+ (candidate) => deliveryTurnKey(candidate.source_agent_id, candidate.source_turn_id) === key,
507
+ );
508
+ return delivery ? cloneTerminalDelivery(delivery) : undefined;
509
+ }
510
+
511
+ /** Return one pending Coordination Message by stable delivery identity. */
512
+ export function findCoordinationDelivery(
513
+ ledger: DeliveryLedger,
514
+ deliveryId: string,
515
+ ): PersistedCoordinationDelivery | undefined {
516
+ const delivery = ledger.coordinationDeliveries.find(
517
+ (candidate) => candidate.delivery_id === deliveryId,
518
+ );
519
+ return delivery ? cloneCoordinationDelivery(delivery) : undefined;
520
+ }
521
+
522
+ /** Report whether one source turn currently has a durable wait claim. */
523
+ export function isDeliveryLedgerTurnClaimed(
524
+ ledger: DeliveryLedger,
525
+ sourceAgentId: string,
526
+ sourceTurnId: string,
527
+ ): boolean {
528
+ return ledger.waitClaimedTurns.includes(deliveryTurnKey(sourceAgentId, sourceTurnId));
529
+ }