@ian-pascoe/pi-minimal-subagents 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -5
- package/package.json +1 -1
- package/src/minimal-subagents-capabilities.ts +8 -12
- package/src/minimal-subagents-config.ts +169 -63
- package/src/minimal-subagents-context.ts +7 -1
- package/src/minimal-subagents-coordinator.ts +932 -130
- package/src/minimal-subagents-delivery-ledger.ts +521 -0
- package/src/minimal-subagents-extension.ts +382 -64
- package/src/minimal-subagents-fork-lifecycle.ts +22 -12
- package/src/minimal-subagents-message-envelope.ts +20 -0
- package/src/minimal-subagents-registry-wire.ts +269 -0
- package/src/minimal-subagents-registry.ts +1487 -137
- package/src/minimal-subagents-render-contract.ts +279 -0
- package/src/minimal-subagents-rendering.ts +445 -399
- package/src/minimal-subagents-session-wire.ts +42 -0
- package/src/minimal-subagents-sessions.ts +428 -120
- package/src/minimal-subagents-shutdown.ts +1 -1
- package/src/minimal-subagents-tool-schemas.ts +21 -7
- package/src/minimal-subagents-tools.ts +46 -18
- package/src/minimal-subagents-types.ts +67 -27
- package/src/minimal-subagents-ui.ts +8 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
2
|
import { assembleImportedContext, contextContainsImages } from "./minimal-subagents-context.js";
|
|
4
3
|
import {
|
|
5
4
|
canAgentContractSpawn,
|
|
@@ -8,9 +7,32 @@ import {
|
|
|
8
7
|
getSubagentDepth,
|
|
9
8
|
resolveOrdinaryToolSelection,
|
|
10
9
|
} from "./minimal-subagents-capabilities.js";
|
|
10
|
+
import {
|
|
11
|
+
addCoordinationDelivery,
|
|
12
|
+
addTerminalDelivery,
|
|
13
|
+
claimDeliveryLedgerTurn,
|
|
14
|
+
createDeliveryLedger,
|
|
15
|
+
deliveryLedgerSnapshot,
|
|
16
|
+
findCoordinationDelivery,
|
|
17
|
+
findTerminalDelivery,
|
|
18
|
+
isDeliveryLedgerTurnClaimed,
|
|
19
|
+
pruneDeliveryLedgerAgents,
|
|
20
|
+
releaseEmptyDeliveryLedgerTurn,
|
|
21
|
+
selectObservableDeliveryTurn,
|
|
22
|
+
setCoordinationDeliveryError,
|
|
23
|
+
setCoordinationDeliveryPath,
|
|
24
|
+
setTerminalDeliveryError,
|
|
25
|
+
setTerminalDeliveryPath,
|
|
26
|
+
settleCoordinationDelivery,
|
|
27
|
+
settleTerminalDelivery,
|
|
28
|
+
type DeliveryLedger,
|
|
29
|
+
type DeliveryLedgerTransition,
|
|
30
|
+
} from "./minimal-subagents-delivery-ledger.js";
|
|
31
|
+
import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
11
32
|
import { createRegistryEvent } from "./minimal-subagents-registry.js";
|
|
12
33
|
import type {
|
|
13
34
|
AgentDetail,
|
|
35
|
+
AgentMessageDisposition,
|
|
14
36
|
AgentMessageResult,
|
|
15
37
|
AgentSessionFactory,
|
|
16
38
|
AgentSummary,
|
|
@@ -23,6 +45,7 @@ import type {
|
|
|
23
45
|
ForkSnapshot,
|
|
24
46
|
HierarchyStatusResult,
|
|
25
47
|
PersistedAgent,
|
|
48
|
+
PersistedCoordinationDelivery,
|
|
26
49
|
PersistedDelivery,
|
|
27
50
|
RegistrySnapshot,
|
|
28
51
|
SpawnParameters,
|
|
@@ -30,6 +53,7 @@ import type {
|
|
|
30
53
|
StatusResult,
|
|
31
54
|
TurnId,
|
|
32
55
|
TurnResult,
|
|
56
|
+
WaitResult,
|
|
33
57
|
} from "./minimal-subagents-types.js";
|
|
34
58
|
|
|
35
59
|
const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
@@ -44,13 +68,28 @@ interface MessageParameters {
|
|
|
44
68
|
|
|
45
69
|
interface TurnWaiter {
|
|
46
70
|
callerId: string;
|
|
47
|
-
resolve: (result:
|
|
71
|
+
resolve: (result: WaitResult) => void;
|
|
48
72
|
reject: (error: Error) => void;
|
|
49
73
|
timeout?: ReturnType<typeof setTimeout>;
|
|
50
74
|
abortSignal?: AbortSignal;
|
|
51
75
|
abortListener?: () => void;
|
|
52
76
|
}
|
|
53
77
|
|
|
78
|
+
interface PendingParentMessage {
|
|
79
|
+
deliveryId: string;
|
|
80
|
+
message: CoordinatorMessage;
|
|
81
|
+
destinationAgentId: string;
|
|
82
|
+
claimed: boolean;
|
|
83
|
+
claimPromise: Promise<void>;
|
|
84
|
+
releaseClaim: () => void;
|
|
85
|
+
cancelGrace?: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface CancelableWait {
|
|
89
|
+
promise: Promise<void>;
|
|
90
|
+
cancel: () => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
function agentDeliveryKey(agentId: string, turnId: string): string {
|
|
55
94
|
return `${agentId}\u0000${turnId}`;
|
|
56
95
|
}
|
|
@@ -70,19 +109,29 @@ function terminalTurnResult(
|
|
|
70
109
|
};
|
|
71
110
|
}
|
|
72
111
|
|
|
112
|
+
function terminalWaitResult(result: TurnResult): WaitResult {
|
|
113
|
+
return { event: "turn", ...structuredClone(result) };
|
|
114
|
+
}
|
|
115
|
+
|
|
73
116
|
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
74
117
|
export class MinimalSubagentsCoordinator {
|
|
75
118
|
private readonly agents = new Map<string, PersistedAgent>();
|
|
76
119
|
private readonly runtimes = new Map<string, ChildAgentRuntime>();
|
|
77
120
|
private readonly runtimeInitializations = new Map<string, Promise<ChildAgentRuntime>>();
|
|
78
|
-
private readonly importedMessages = new Map<string, AgentMessage[]>();
|
|
79
121
|
private readonly tombstones = new Set<string>();
|
|
80
122
|
private readonly pendingAgentIds = new Set<string>();
|
|
81
|
-
private
|
|
123
|
+
private deliveryLedger: DeliveryLedger = createDeliveryLedger();
|
|
82
124
|
private readonly waiters = new Map<string, Set<TurnWaiter>>();
|
|
83
|
-
private readonly
|
|
125
|
+
private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
|
|
126
|
+
private readonly recipientQueues = new Map<string, Promise<unknown>>();
|
|
127
|
+
private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
|
|
128
|
+
private readonly automaticDeliveryKeys = new Set<string>();
|
|
129
|
+
private readonly automaticCoordinationDeliveryIds = new Set<string>();
|
|
130
|
+
private readonly waitHandedDeliveryIds = new Set<string>();
|
|
131
|
+
private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
|
|
84
132
|
private readonly backgroundOperations = new Set<Promise<void>>();
|
|
85
133
|
private acceptingOperations = true;
|
|
134
|
+
private lifecycleEpoch = 0;
|
|
86
135
|
private shutdownPromise?: Promise<void>;
|
|
87
136
|
|
|
88
137
|
constructor(private readonly dependencies: CoordinatorDependencies) {}
|
|
@@ -93,10 +142,11 @@ export class MinimalSubagentsCoordinator {
|
|
|
93
142
|
|
|
94
143
|
/** Return a serializable complete hierarchy checkpoint without process-local runtimes. */
|
|
95
144
|
snapshot(): RegistrySnapshot {
|
|
145
|
+
const ledger = deliveryLedgerSnapshot(this.deliveryLedger);
|
|
96
146
|
return {
|
|
97
147
|
agents: [...this.agents.values()].map((agent) => structuredClone(agent)),
|
|
98
148
|
tombstones: [...this.tombstones],
|
|
99
|
-
|
|
149
|
+
...ledger,
|
|
100
150
|
};
|
|
101
151
|
}
|
|
102
152
|
|
|
@@ -191,10 +241,15 @@ export class MinimalSubagentsCoordinator {
|
|
|
191
241
|
} finally {
|
|
192
242
|
this.pendingAgentIds.delete(agentId);
|
|
193
243
|
}
|
|
244
|
+
if (!identity.sessionLeafId) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Minimal subagents persistent identity: no selected session leaf for ${agent.agent_id}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
194
249
|
agent.session_file = identity.sessionFile;
|
|
195
250
|
agent.session_id = identity.sessionId;
|
|
251
|
+
agent.session_leaf_id = identity.sessionLeafId;
|
|
196
252
|
this.agents.set(agentId, agent);
|
|
197
|
-
this.importedMessages.set(agentId, imported.messages);
|
|
198
253
|
this.dependencies.registry.append(
|
|
199
254
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-created", { agent }),
|
|
200
255
|
);
|
|
@@ -233,7 +288,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
233
288
|
};
|
|
234
289
|
}
|
|
235
290
|
|
|
236
|
-
/** Send one
|
|
291
|
+
/** Send one coordination message to an authorized adjacent agent. */
|
|
237
292
|
async sendAgentMessage(
|
|
238
293
|
callerId: string,
|
|
239
294
|
parameters: MessageParameters,
|
|
@@ -242,39 +297,88 @@ export class MinimalSubagentsCoordinator {
|
|
|
242
297
|
this.assertAccepting();
|
|
243
298
|
this.assertCallerExists(callerId);
|
|
244
299
|
const targetId = this.resolveMessageTarget(callerId, parameters.agent_id);
|
|
300
|
+
const messageId = randomUUID();
|
|
301
|
+
const message = this.createExplicitMessage(
|
|
302
|
+
callerId,
|
|
303
|
+
targetId,
|
|
304
|
+
sourceTurnId,
|
|
305
|
+
messageId,
|
|
306
|
+
parameters.message,
|
|
307
|
+
);
|
|
308
|
+
const sourceAgent = callerId === "root" ? undefined : this.requireAgent(callerId);
|
|
309
|
+
const sentToDirectParent = sourceAgent?.parent_id === targetId;
|
|
310
|
+
const delivery = this.persistCoordinationDelivery(message, targetId);
|
|
245
311
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
this.recordRecentMessage(targetId, message);
|
|
313
|
+
if (sentToDirectParent) {
|
|
314
|
+
if (this.resolveWaiterWithMessage(message, delivery)) {
|
|
315
|
+
return { agent_id: targetId, message_id: messageId, disposition: "delivered-via-wait" };
|
|
316
|
+
}
|
|
317
|
+
this.queuePendingParentMessage(targetId, message, delivery);
|
|
318
|
+
return { agent_id: targetId, message_id: messageId, disposition: "queued" };
|
|
319
|
+
}
|
|
320
|
+
const disposition = await this.enqueueRecipientDelivery(targetId, async () =>
|
|
321
|
+
this.deliverExplicitMessage(message, targetId, delivery),
|
|
322
|
+
);
|
|
323
|
+
return { agent_id: targetId, message_id: messageId, disposition };
|
|
250
324
|
} catch (error) {
|
|
325
|
+
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
326
|
+
if (this.isCoordinationDeliveryCurrent(delivery)) {
|
|
327
|
+
this.deliveryLedger = setCoordinationDeliveryError(
|
|
328
|
+
this.deliveryLedger,
|
|
329
|
+
delivery.delivery_id,
|
|
330
|
+
deliveryError,
|
|
331
|
+
);
|
|
332
|
+
const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
|
|
333
|
+
if (current) this.persistCoordinationDeliveryState(current);
|
|
334
|
+
}
|
|
251
335
|
return {
|
|
252
336
|
agent_id: targetId,
|
|
253
|
-
|
|
254
|
-
|
|
337
|
+
message_id: messageId,
|
|
338
|
+
disposition: "failed",
|
|
339
|
+
error: deliveryError,
|
|
255
340
|
};
|
|
256
341
|
}
|
|
257
342
|
}
|
|
258
343
|
|
|
259
|
-
/** Wait for
|
|
344
|
+
/** Wait for one exact turn and claim its message/result delivery from automatic fallback. */
|
|
260
345
|
wait(
|
|
261
346
|
callerId: string,
|
|
262
347
|
agentId: string,
|
|
263
348
|
timeoutMs?: number,
|
|
264
349
|
signal?: AbortSignal,
|
|
265
|
-
|
|
350
|
+
requestedTurnId?: string,
|
|
351
|
+
): Promise<WaitResult> {
|
|
266
352
|
this.assertAccepting();
|
|
267
353
|
this.assertCallerExists(callerId);
|
|
268
354
|
this.assertCallerTargetsDirectChild(callerId, agentId, "wait");
|
|
269
355
|
const agent = this.requireUsableAgent(agentId, "wait");
|
|
270
|
-
|
|
271
|
-
|
|
356
|
+
const turnId = requestedTurnId ?? this.selectObservableTurnId(agentId, callerId);
|
|
357
|
+
if (!turnId) {
|
|
272
358
|
return Promise.reject(new Error(`Minimal subagents wait: ${agentId} has no turn to observe`));
|
|
273
359
|
}
|
|
274
|
-
const turnId = agent.active_turn_id;
|
|
275
360
|
const key = agentDeliveryKey(agentId, turnId);
|
|
361
|
+
if ([...(this.waiters.get(key) ?? [])].some((waiter) => waiter.callerId === callerId)) {
|
|
362
|
+
return Promise.reject(
|
|
363
|
+
new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
|
|
367
|
+
if (pendingMessage) return Promise.resolve(pendingMessage);
|
|
368
|
+
const retainedResult =
|
|
369
|
+
findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
|
|
370
|
+
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
371
|
+
if (retainedResult) {
|
|
372
|
+
this.claimTerminalDelivery(callerId, retainedResult);
|
|
373
|
+
return Promise.resolve(terminalWaitResult(retainedResult));
|
|
374
|
+
}
|
|
375
|
+
if (agent.active_turn_id !== turnId) {
|
|
376
|
+
return Promise.reject(
|
|
377
|
+
new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
276
380
|
|
|
277
|
-
return new Promise<
|
|
381
|
+
return new Promise<WaitResult>((resolve, reject) => {
|
|
278
382
|
const waiter: TurnWaiter = { callerId, resolve, reject, abortSignal: signal };
|
|
279
383
|
let turnWaiters = this.waiters.get(key);
|
|
280
384
|
if (!turnWaiters) {
|
|
@@ -394,11 +498,12 @@ export class MinimalSubagentsCoordinator {
|
|
|
394
498
|
runtime?.dispose();
|
|
395
499
|
this.runtimes.delete(agent.agent_id);
|
|
396
500
|
if (agent.session_file) {
|
|
397
|
-
await this.dependencies.sessions.
|
|
501
|
+
await this.dependencies.sessions.trashSession(agent);
|
|
398
502
|
result.trashed_session_files.push(agent.session_file);
|
|
399
503
|
}
|
|
400
504
|
this.agents.delete(agent.agent_id);
|
|
401
|
-
this.
|
|
505
|
+
this.pruneDeliveryStateForDeletedAgent(agent.agent_id);
|
|
506
|
+
this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
|
|
402
507
|
this.tombstones.add(agent.agent_id);
|
|
403
508
|
result.deleted_agent_ids.push(agent.agent_id);
|
|
404
509
|
result.tombstoned_agent_ids.push(agent.agent_id);
|
|
@@ -411,10 +516,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
411
516
|
failedAncestors.add(agent.agent_id);
|
|
412
517
|
if (runtime && agent.session_file) {
|
|
413
518
|
try {
|
|
414
|
-
this.runtimes.set(
|
|
415
|
-
agent.agent_id,
|
|
416
|
-
await this.dependencies.sessions.restoreRuntime(agent),
|
|
417
|
-
);
|
|
519
|
+
this.runtimes.set(agent.agent_id, await this.dependencies.sessions.openRuntime(agent));
|
|
418
520
|
} catch (restoreError) {
|
|
419
521
|
agent.availability = "unavailable";
|
|
420
522
|
agent.unavailable_reason = `Deletion recovery failed: ${
|
|
@@ -433,26 +535,46 @@ export class MinimalSubagentsCoordinator {
|
|
|
433
535
|
|
|
434
536
|
/** Restore non-deleted descendants, interrupt unfinished work, and reconcile pending successful output. */
|
|
435
537
|
async restore(snapshot: RegistrySnapshot): Promise<void> {
|
|
436
|
-
|
|
538
|
+
const restoreEpoch = ++this.lifecycleEpoch;
|
|
539
|
+
this.rejectAllWaiters(
|
|
540
|
+
new Error("Minimal subagents wait cancelled because the session branch changed"),
|
|
541
|
+
);
|
|
542
|
+
const abandonedRuntimes = [...this.runtimes.values()];
|
|
437
543
|
this.agents.clear();
|
|
544
|
+
this.deliveryLedger = createDeliveryLedger();
|
|
545
|
+
this.pendingParentMessages.clear();
|
|
546
|
+
this.releaseAllRecipientIdleWaiters();
|
|
547
|
+
this.recipientQueues.clear();
|
|
548
|
+
this.backgroundOperations.clear();
|
|
549
|
+
await Promise.allSettled(
|
|
550
|
+
abandonedRuntimes.map((runtime) => (runtime.isRunning ? runtime.abort() : Promise.resolve())),
|
|
551
|
+
);
|
|
552
|
+
for (const runtime of abandonedRuntimes) runtime.dispose();
|
|
438
553
|
this.runtimes.clear();
|
|
439
554
|
this.runtimeInitializations.clear();
|
|
440
|
-
this.importedMessages.clear();
|
|
441
555
|
this.pendingAgentIds.clear();
|
|
442
556
|
this.tombstones.clear();
|
|
443
|
-
this.
|
|
557
|
+
this.deliveryLedger = createDeliveryLedger();
|
|
444
558
|
this.waiters.clear();
|
|
559
|
+
this.pendingParentMessages.clear();
|
|
560
|
+
this.waitHandedDeliveryIds.clear();
|
|
561
|
+
this.releaseAllRecipientIdleWaiters();
|
|
562
|
+
this.recipientQueues.clear();
|
|
563
|
+
this.backgroundOperations.clear();
|
|
564
|
+
this.automaticDeliveryKeys.clear();
|
|
565
|
+
this.automaticCoordinationDeliveryIds.clear();
|
|
566
|
+
this.automaticDeliveryClaimWaiters.clear();
|
|
567
|
+
this.deliveryLedger = createDeliveryLedger({
|
|
568
|
+
deliveries: snapshot.deliveries,
|
|
569
|
+
coordination_deliveries: snapshot.coordination_deliveries,
|
|
570
|
+
wait_claimed_turns: snapshot.wait_claimed_turns,
|
|
571
|
+
next_delivery_sequence: snapshot.next_delivery_sequence,
|
|
572
|
+
});
|
|
445
573
|
this.acceptingOperations = true;
|
|
446
574
|
this.shutdownPromise = undefined;
|
|
447
575
|
|
|
448
576
|
for (const agent of snapshot.agents) this.agents.set(agent.agent_id, structuredClone(agent));
|
|
449
577
|
for (const tombstone of snapshot.tombstones) this.tombstones.add(tombstone);
|
|
450
|
-
for (const delivery of snapshot.deliveries) {
|
|
451
|
-
this.deliveries.set(
|
|
452
|
-
agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
453
|
-
structuredClone(delivery),
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
578
|
|
|
457
579
|
for (const agent of this.agents.values()) {
|
|
458
580
|
if (agent.active_turn_id) {
|
|
@@ -471,28 +593,41 @@ export class MinimalSubagentsCoordinator {
|
|
|
471
593
|
});
|
|
472
594
|
}
|
|
473
595
|
const previousAvailability = agent.availability;
|
|
474
|
-
const missing = agent.clone_error
|
|
475
|
-
? [agent.clone_error]
|
|
476
|
-
: await this.dependencies.sessions.resolveRestorationMissingDependencies(agent);
|
|
477
|
-
if (missing.length > 0 || !agent.session_file) {
|
|
478
|
-
agent.availability = "unavailable";
|
|
479
|
-
if (previousAvailability !== "unavailable")
|
|
480
|
-
agent.latest_activity_at = this.now().toISOString();
|
|
481
|
-
agent.missing_dependencies = missing.length > 0 ? missing : agent.missing_dependencies;
|
|
482
|
-
agent.unavailable_reason =
|
|
483
|
-
agent.clone_error ??
|
|
484
|
-
(missing.length > 0
|
|
485
|
-
? `Missing dependencies: ${missing.join(", ")}`
|
|
486
|
-
: (agent.unavailable_reason ?? `No persistent session exists for ${agent.agent_id}`));
|
|
487
|
-
this.dependencies.notify?.({
|
|
488
|
-
type: "unavailable",
|
|
489
|
-
agentId: agent.agent_id,
|
|
490
|
-
message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
|
|
491
|
-
});
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
596
|
try {
|
|
495
|
-
|
|
597
|
+
const missing = agent.clone_error
|
|
598
|
+
? [agent.clone_error]
|
|
599
|
+
: await this.dependencies.sessions.resolveRestorationMissingDependencies(agent);
|
|
600
|
+
if (restoreEpoch !== this.lifecycleEpoch) return;
|
|
601
|
+
if (missing.length > 0 || !agent.session_file) {
|
|
602
|
+
agent.availability = "unavailable";
|
|
603
|
+
if (previousAvailability !== "unavailable")
|
|
604
|
+
agent.latest_activity_at = this.now().toISOString();
|
|
605
|
+
agent.missing_dependencies = missing.length > 0 ? missing : agent.missing_dependencies;
|
|
606
|
+
agent.unavailable_reason =
|
|
607
|
+
agent.clone_error ??
|
|
608
|
+
(missing.length > 0
|
|
609
|
+
? `Missing dependencies: ${missing.join(", ")}`
|
|
610
|
+
: (agent.unavailable_reason ?? `No persistent session exists for ${agent.agent_id}`));
|
|
611
|
+
this.dependencies.notify?.({
|
|
612
|
+
type: "unavailable",
|
|
613
|
+
agentId: agent.agent_id,
|
|
614
|
+
message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const runtime = await this.dependencies.sessions.openRuntime(agent);
|
|
619
|
+
if (restoreEpoch !== this.lifecycleEpoch) {
|
|
620
|
+
runtime.dispose();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!runtime.sessionLeafId) {
|
|
624
|
+
runtime.dispose();
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
this.runtimes.set(agent.agent_id, runtime);
|
|
630
|
+
agent.session_leaf_id = runtime.sessionLeafId;
|
|
496
631
|
agent.availability = "available";
|
|
497
632
|
if (previousAvailability !== "available")
|
|
498
633
|
agent.latest_activity_at = this.now().toISOString();
|
|
@@ -504,6 +639,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
504
639
|
message: `Restored ${agent.agent_id}`,
|
|
505
640
|
});
|
|
506
641
|
} catch (error) {
|
|
642
|
+
if (restoreEpoch !== this.lifecycleEpoch) return;
|
|
507
643
|
agent.availability = "unavailable";
|
|
508
644
|
if (previousAvailability !== "unavailable")
|
|
509
645
|
agent.latest_activity_at = this.now().toISOString();
|
|
@@ -515,7 +651,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
515
651
|
});
|
|
516
652
|
}
|
|
517
653
|
}
|
|
518
|
-
await this.reconcileDeliveries(true);
|
|
654
|
+
if (restoreEpoch === this.lifecycleEpoch) await this.reconcileDeliveries(true);
|
|
519
655
|
}
|
|
520
656
|
|
|
521
657
|
/** Schedule delivery reconciliation as coordinator-owned work drained during shutdown. */
|
|
@@ -525,8 +661,51 @@ export class MinimalSubagentsCoordinator {
|
|
|
525
661
|
|
|
526
662
|
/** Reconcile durable destination evidence and replay only successful undelivered output. */
|
|
527
663
|
async reconcileDeliveries(replayMissing = false): Promise<void> {
|
|
528
|
-
|
|
529
|
-
|
|
664
|
+
const pendingItems = [
|
|
665
|
+
...this.deliveryLedger.coordinationDeliveries.map((delivery) => ({
|
|
666
|
+
kind: "coordination" as const,
|
|
667
|
+
sequence: delivery.sequence,
|
|
668
|
+
delivery,
|
|
669
|
+
})),
|
|
670
|
+
...this.deliveryLedger.terminalDeliveries.map((delivery) => ({
|
|
671
|
+
kind: "terminal" as const,
|
|
672
|
+
sequence: delivery.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
673
|
+
delivery,
|
|
674
|
+
})),
|
|
675
|
+
].sort((left, right) => left.sequence - right.sequence);
|
|
676
|
+
const scheduled: Promise<void>[] = [];
|
|
677
|
+
|
|
678
|
+
for (const item of pendingItems) {
|
|
679
|
+
if (item.kind === "coordination") {
|
|
680
|
+
const delivery = item.delivery;
|
|
681
|
+
if (this.hasCoordinationDeliveryEvidence(delivery)) {
|
|
682
|
+
this.settleCoordinationDelivery(delivery);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (!replayMissing || delivery.path === "wait") continue;
|
|
686
|
+
const source = this.agents.get(delivery.message.details.source_agent_id);
|
|
687
|
+
if (source?.parent_id === delivery.destination_agent_id) {
|
|
688
|
+
const key = agentDeliveryKey(
|
|
689
|
+
delivery.message.details.source_agent_id,
|
|
690
|
+
delivery.message.details.source_turn_id,
|
|
691
|
+
);
|
|
692
|
+
const alreadyQueued = this.pendingParentMessages
|
|
693
|
+
.get(key)
|
|
694
|
+
?.some((pending) => pending.deliveryId === delivery.delivery_id);
|
|
695
|
+
if (!alreadyQueued && !this.waitHandedDeliveryIds.has(delivery.delivery_id)) {
|
|
696
|
+
this.queuePendingParentMessage(
|
|
697
|
+
delivery.destination_agent_id,
|
|
698
|
+
delivery.message,
|
|
699
|
+
delivery,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
scheduled.push(this.replayCoordinationDelivery(delivery));
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const delivery = item.delivery;
|
|
530
709
|
const agent = this.agents.get(delivery.source_agent_id);
|
|
531
710
|
const result = delivery.result ?? agent?.latest_result;
|
|
532
711
|
if (!result || result.status !== "completed" || result.turn_id !== delivery.source_turn_id)
|
|
@@ -535,25 +714,24 @@ export class MinimalSubagentsCoordinator {
|
|
|
535
714
|
this.settleDelivery(delivery);
|
|
536
715
|
continue;
|
|
537
716
|
}
|
|
538
|
-
if (!replayMissing) continue;
|
|
539
|
-
|
|
540
|
-
this.dependencies.registry.append(
|
|
541
|
-
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
542
|
-
delivery,
|
|
543
|
-
}),
|
|
544
|
-
);
|
|
545
|
-
await this.deliverAutomaticResult(result, delivery);
|
|
717
|
+
if (!replayMissing || delivery.path === "wait") continue;
|
|
718
|
+
scheduled.push(this.deliverAutomaticResult(result, delivery));
|
|
546
719
|
}
|
|
720
|
+
await Promise.allSettled(scheduled);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Release ordered automatic deliveries after one recipient conversation becomes idle. */
|
|
724
|
+
markRecipientIdle(agentId: string): void {
|
|
725
|
+
const waiters = this.recipientIdleWaiters.get(agentId);
|
|
726
|
+
this.recipientIdleWaiters.delete(agentId);
|
|
727
|
+
for (const resolve of waiters ?? []) resolve();
|
|
547
728
|
}
|
|
548
729
|
|
|
549
730
|
/** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
|
|
550
731
|
async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
|
|
551
|
-
this.acceptingOperations = false;
|
|
552
732
|
const activeRootChildren = this.childrenOf("root");
|
|
553
733
|
for (const child of activeRootChildren) await this.cancelDuringShutdown(child.agent_id);
|
|
554
|
-
await
|
|
555
|
-
await Promise.allSettled(this.backgroundOperations);
|
|
556
|
-
await Promise.allSettled(this.recipientQueues.values());
|
|
734
|
+
await this.waitForSettledOperations();
|
|
557
735
|
const forkAgents: PersistedAgent[] = [];
|
|
558
736
|
const failedSubtrees = new Set<string>();
|
|
559
737
|
|
|
@@ -569,10 +747,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
569
747
|
}
|
|
570
748
|
try {
|
|
571
749
|
const clone = await this.dependencies.sessions.cloneSession(agent);
|
|
750
|
+
if (!clone.sessionLeafId) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Minimal subagents fork clone: no selected session leaf for ${agent.agent_id}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
572
755
|
forkAgents.push({
|
|
573
756
|
...structuredClone(agent),
|
|
574
757
|
session_file: clone.sessionFile,
|
|
575
758
|
session_id: clone.sessionId,
|
|
759
|
+
session_leaf_id: clone.sessionLeafId,
|
|
576
760
|
active_turn_id: undefined,
|
|
577
761
|
active_turn_started_at: undefined,
|
|
578
762
|
});
|
|
@@ -588,11 +772,12 @@ export class MinimalSubagentsCoordinator {
|
|
|
588
772
|
}
|
|
589
773
|
}
|
|
590
774
|
|
|
775
|
+
const snapshot = this.snapshot();
|
|
591
776
|
return {
|
|
777
|
+
...snapshot,
|
|
592
778
|
source_root_session_file: sourceRootSessionFile,
|
|
779
|
+
source_root_session_id: this.dependencies.registry.rootSessionId,
|
|
593
780
|
agents: forkAgents,
|
|
594
|
-
tombstones: [...this.tombstones],
|
|
595
|
-
deliveries: [...this.deliveries.values()].map((delivery) => structuredClone(delivery)),
|
|
596
781
|
};
|
|
597
782
|
}
|
|
598
783
|
|
|
@@ -600,6 +785,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
600
785
|
shutdown(): Promise<void> {
|
|
601
786
|
if (this.shutdownPromise) return this.shutdownPromise;
|
|
602
787
|
this.acceptingOperations = false;
|
|
788
|
+
this.releaseAllRecipientIdleWaiters();
|
|
603
789
|
this.shutdownPromise = this.finishShutdown();
|
|
604
790
|
return this.shutdownPromise;
|
|
605
791
|
}
|
|
@@ -684,7 +870,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
684
870
|
return;
|
|
685
871
|
}
|
|
686
872
|
const outcome = await runtime.runPrompt(task, compact, callerModel, callerThinkingLevel);
|
|
687
|
-
if (agent.active_turn_id !== turnId) return;
|
|
873
|
+
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) return;
|
|
688
874
|
this.settleTurn(agent, turnId, terminalTurnResult(agentId, turnId, outcome));
|
|
689
875
|
} catch (error) {
|
|
690
876
|
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) return;
|
|
@@ -708,19 +894,14 @@ export class MinimalSubagentsCoordinator {
|
|
|
708
894
|
new Error(agent.clone_error ?? `No persistent session exists for ${agent.agent_id}`),
|
|
709
895
|
);
|
|
710
896
|
}
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
importedMessages
|
|
714
|
-
? this.dependencies.sessions.createRuntime({ agent, importedMessages })
|
|
715
|
-
: this.dependencies.sessions.restoreRuntime(agent)
|
|
716
|
-
)
|
|
897
|
+
const initialization = this.dependencies.sessions
|
|
898
|
+
.openRuntime(agent)
|
|
717
899
|
.then((runtime) => {
|
|
718
900
|
if (this.agents.get(agent.agent_id) !== agent) {
|
|
719
901
|
runtime.dispose();
|
|
720
902
|
throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
|
|
721
903
|
}
|
|
722
904
|
this.runtimes.set(agent.agent_id, runtime);
|
|
723
|
-
this.importedMessages.delete(agent.agent_id);
|
|
724
905
|
return runtime;
|
|
725
906
|
})
|
|
726
907
|
.finally(() => {
|
|
@@ -733,6 +914,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
733
914
|
}
|
|
734
915
|
|
|
735
916
|
private beginTurn(agent: PersistedAgent): TurnId {
|
|
917
|
+
// SAFETY: The generated value embeds the canonical agent ID and a fresh turn UUID before branding.
|
|
736
918
|
const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
|
|
737
919
|
const startedAt = this.now().toISOString();
|
|
738
920
|
agent.active_turn_id = turnId;
|
|
@@ -764,34 +946,44 @@ export class MinimalSubagentsCoordinator {
|
|
|
764
946
|
agent.active_turn_started_at = undefined;
|
|
765
947
|
agent.latest_activity_at = settledAt.toISOString();
|
|
766
948
|
agent.latest_result = structuredClone(result);
|
|
949
|
+
const runtimeLeafId = this.runtimes.get(agent.agent_id)?.sessionLeafId;
|
|
950
|
+
if (runtimeLeafId) agent.session_leaf_id = runtimeLeafId;
|
|
767
951
|
this.dependencies.registry.append(
|
|
768
|
-
createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-settled", {
|
|
952
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-settled", {
|
|
953
|
+
result,
|
|
954
|
+
settled_at: settledAt.toISOString(),
|
|
955
|
+
session_leaf_id: runtimeLeafId,
|
|
956
|
+
}),
|
|
769
957
|
);
|
|
770
958
|
const waiterKey = agentDeliveryKey(agent.agent_id, turnId);
|
|
771
959
|
const turnWaiters = this.waiters.get(waiterKey);
|
|
772
960
|
const directParentWaited = [...(turnWaiters ?? [])].some(
|
|
773
961
|
(waiter) => waiter.callerId === agent.parent_id,
|
|
774
962
|
);
|
|
963
|
+
if (directParentWaited && result.status === "completed") {
|
|
964
|
+
this.claimDeliveryTurn(agent.agent_id, turnId);
|
|
965
|
+
}
|
|
966
|
+
const parentClaimedTurn =
|
|
967
|
+
directParentWaited ||
|
|
968
|
+
isDeliveryLedgerTurnClaimed(this.deliveryLedger, agent.agent_id, turnId);
|
|
775
969
|
for (const waiter of turnWaiters ?? []) {
|
|
776
970
|
this.removeWaiter(waiterKey, waiter);
|
|
777
|
-
waiter.resolve(
|
|
971
|
+
waiter.resolve(terminalWaitResult(result));
|
|
778
972
|
}
|
|
779
973
|
if (result.status === "completed") {
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
};
|
|
788
|
-
this.deliveries.set(waiterKey, delivery);
|
|
974
|
+
const added = addTerminalDelivery(this.deliveryLedger, {
|
|
975
|
+
destinationAgentId: agent.parent_id,
|
|
976
|
+
path: parentClaimedTurn ? "wait" : "message",
|
|
977
|
+
result,
|
|
978
|
+
});
|
|
979
|
+
this.applyDeliveryLedgerTransition(added);
|
|
980
|
+
const delivery = added.delivery;
|
|
789
981
|
this.dependencies.registry.append(
|
|
790
982
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
791
983
|
delivery,
|
|
792
984
|
}),
|
|
793
985
|
);
|
|
794
|
-
if (!
|
|
986
|
+
if (!parentClaimedTurn) {
|
|
795
987
|
this.trackBackgroundOperation(this.deliverAutomaticResult(result, delivery));
|
|
796
988
|
}
|
|
797
989
|
this.dependencies.notify?.({
|
|
@@ -806,22 +998,46 @@ export class MinimalSubagentsCoordinator {
|
|
|
806
998
|
message: `${agent.agent_id} failed: ${result.error ?? "unknown error"}`,
|
|
807
999
|
});
|
|
808
1000
|
}
|
|
1001
|
+
if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
|
|
1002
|
+
this.markRecipientIdle(agent.agent_id);
|
|
809
1003
|
}
|
|
810
1004
|
|
|
811
1005
|
private async deliverAutomaticResult(
|
|
812
1006
|
result: TurnResult,
|
|
813
1007
|
delivery: PersistedDelivery,
|
|
814
1008
|
): Promise<void> {
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (this.hasDeliveryEvidence(delivery)) {
|
|
820
|
-
this.settleDelivery(delivery);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
1009
|
+
const deliveryKey = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1010
|
+
if (this.automaticDeliveryKeys.has(deliveryKey)) return;
|
|
1011
|
+
this.automaticDeliveryKeys.add(deliveryKey);
|
|
1012
|
+
const graceMs = this.deliveryGraceMs();
|
|
823
1013
|
try {
|
|
824
1014
|
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
1015
|
+
if (delivery.destination_agent_id !== "root") {
|
|
1016
|
+
await this.ensureRuntime(
|
|
1017
|
+
this.requireUsableAgent(delivery.destination_agent_id, "message"),
|
|
1018
|
+
);
|
|
1019
|
+
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1020
|
+
}
|
|
1021
|
+
if (graceMs > 0) await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
1022
|
+
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1023
|
+
if (this.hasDeliveryEvidence(delivery)) {
|
|
1024
|
+
this.settleDelivery(delivery);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
while (!this.isRecipientIdle(delivery.destination_agent_id)) {
|
|
1028
|
+
const idleWait = this.createRecipientIdleWait(delivery.destination_agent_id);
|
|
1029
|
+
const claimWait = this.createAutomaticDeliveryClaimWait(deliveryKey);
|
|
1030
|
+
await Promise.race([idleWait.promise, claimWait.promise]);
|
|
1031
|
+
idleWait.cancel();
|
|
1032
|
+
claimWait.cancel();
|
|
1033
|
+
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery))
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1037
|
+
if (this.hasDeliveryEvidence(delivery)) {
|
|
1038
|
+
this.settleDelivery(delivery);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
825
1041
|
const message: CoordinatorMessage = {
|
|
826
1042
|
customType: "minimal-subagents.result",
|
|
827
1043
|
content: result.output,
|
|
@@ -829,82 +1045,265 @@ export class MinimalSubagentsCoordinator {
|
|
|
829
1045
|
source_agent_id: delivery.source_agent_id,
|
|
830
1046
|
destination_agent_id: delivery.destination_agent_id,
|
|
831
1047
|
source_turn_id: result.turn_id,
|
|
1048
|
+
message_id: `result:${delivery.source_agent_id}:${result.turn_id}`,
|
|
832
1049
|
status: result.status,
|
|
833
1050
|
elapsed_ms: result.elapsed_ms,
|
|
834
1051
|
usage: result.usage,
|
|
835
1052
|
},
|
|
836
1053
|
};
|
|
837
|
-
await this.deliverToRecipient(
|
|
1054
|
+
await this.deliverToRecipient(
|
|
1055
|
+
delivery.destination_agent_id,
|
|
1056
|
+
message,
|
|
1057
|
+
() => this.isTerminalDeliveryCurrent(delivery),
|
|
1058
|
+
true,
|
|
1059
|
+
);
|
|
838
1060
|
});
|
|
839
1061
|
} catch (error) {
|
|
840
|
-
|
|
1062
|
+
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1063
|
+
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
1064
|
+
this.deliveryLedger = setTerminalDeliveryError(
|
|
1065
|
+
this.deliveryLedger,
|
|
1066
|
+
delivery.source_agent_id,
|
|
1067
|
+
delivery.source_turn_id,
|
|
1068
|
+
deliveryError,
|
|
1069
|
+
);
|
|
841
1070
|
this.dependencies.registry.append(
|
|
842
1071
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
|
|
843
1072
|
source_agent_id: delivery.source_agent_id,
|
|
844
1073
|
source_turn_id: delivery.source_turn_id,
|
|
845
|
-
error:
|
|
1074
|
+
error: deliveryError,
|
|
846
1075
|
}),
|
|
847
1076
|
);
|
|
1077
|
+
} finally {
|
|
1078
|
+
this.automaticDeliveryKeys.delete(deliveryKey);
|
|
848
1079
|
}
|
|
849
1080
|
}
|
|
850
1081
|
|
|
851
|
-
private
|
|
1082
|
+
private persistCoordinationDelivery(
|
|
1083
|
+
message: CoordinatorMessage,
|
|
1084
|
+
destinationAgentId: string,
|
|
1085
|
+
): PersistedCoordinationDelivery {
|
|
1086
|
+
const added = addCoordinationDelivery(this.deliveryLedger, {
|
|
1087
|
+
destinationAgentId,
|
|
1088
|
+
message,
|
|
1089
|
+
});
|
|
1090
|
+
this.deliveryLedger = added.ledger;
|
|
1091
|
+
message.details.delivery_id = added.delivery.delivery_id;
|
|
1092
|
+
this.persistCoordinationDeliveryState(added.delivery);
|
|
1093
|
+
return added.delivery;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private persistCoordinationDeliveryState(delivery: PersistedCoordinationDelivery): void {
|
|
1097
|
+
this.dependencies.registry.append(
|
|
1098
|
+
createRegistryEvent(
|
|
1099
|
+
this.dependencies.registry.rootSessionId,
|
|
1100
|
+
"coordination-delivery-pending",
|
|
1101
|
+
{ delivery },
|
|
1102
|
+
),
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private settleCoordinationDelivery(delivery: PersistedCoordinationDelivery): void {
|
|
1107
|
+
this.deliveryLedger = settleCoordinationDelivery(
|
|
1108
|
+
this.deliveryLedger,
|
|
1109
|
+
delivery.delivery_id,
|
|
1110
|
+
).ledger;
|
|
1111
|
+
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1112
|
+
this.dependencies.registry.append(
|
|
1113
|
+
createRegistryEvent(
|
|
1114
|
+
this.dependencies.registry.rootSessionId,
|
|
1115
|
+
"coordination-delivery-settled",
|
|
1116
|
+
{ delivery_id: delivery.delivery_id },
|
|
1117
|
+
),
|
|
1118
|
+
);
|
|
1119
|
+
this.removeSettledEmptyTurnClaim(
|
|
1120
|
+
delivery.message.details.source_agent_id,
|
|
1121
|
+
delivery.message.details.source_turn_id,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private removeSettledEmptyTurnClaim(sourceAgentId: string, sourceTurnId: string): void {
|
|
1126
|
+
const transition = releaseEmptyDeliveryLedgerTurn(
|
|
1127
|
+
this.deliveryLedger,
|
|
1128
|
+
sourceAgentId,
|
|
1129
|
+
sourceTurnId,
|
|
1130
|
+
this.agents.get(sourceAgentId)?.active_turn_id === sourceTurnId,
|
|
1131
|
+
);
|
|
1132
|
+
this.deliveryLedger = transition.ledger;
|
|
1133
|
+
if (transition.changed) {
|
|
1134
|
+
this.dependencies.registry.append(
|
|
1135
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-turn-released", {
|
|
1136
|
+
source_agent_id: sourceAgentId,
|
|
1137
|
+
source_turn_id: sourceTurnId,
|
|
1138
|
+
}),
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
private claimDeliveryTurn(sourceAgentId: string, sourceTurnId: string): void {
|
|
1144
|
+
const transition = claimDeliveryLedgerTurn(this.deliveryLedger, sourceAgentId, sourceTurnId);
|
|
1145
|
+
this.deliveryLedger = transition.ledger;
|
|
1146
|
+
if (!transition.changed) return;
|
|
1147
|
+
this.dependencies.registry.append(
|
|
1148
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-turn-claimed", {
|
|
1149
|
+
source_agent_id: sourceAgentId,
|
|
1150
|
+
source_turn_id: sourceTurnId,
|
|
1151
|
+
}),
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private selectObservableTurnId(
|
|
1156
|
+
sourceAgentId: string,
|
|
1157
|
+
destinationAgentId: string,
|
|
1158
|
+
): string | undefined {
|
|
1159
|
+
const agent = this.agents.get(sourceAgentId);
|
|
1160
|
+
return selectObservableDeliveryTurn(this.deliveryLedger, {
|
|
1161
|
+
sourceAgentId,
|
|
1162
|
+
destinationAgentId,
|
|
1163
|
+
waitHandedDeliveryIds: this.waitHandedDeliveryIds,
|
|
1164
|
+
activeTurnId: agent?.active_turn_id,
|
|
1165
|
+
latestResultTurnId: agent?.latest_result?.turn_id,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private createExplicitMessage(
|
|
852
1170
|
callerId: string,
|
|
853
1171
|
targetId: string,
|
|
854
1172
|
sourceTurnId: string,
|
|
1173
|
+
messageId: string,
|
|
855
1174
|
content: string,
|
|
856
|
-
):
|
|
857
|
-
|
|
1175
|
+
): CoordinatorMessage {
|
|
1176
|
+
return {
|
|
858
1177
|
customType: "minimal-subagents.message",
|
|
859
1178
|
content,
|
|
860
1179
|
details: {
|
|
861
1180
|
source_agent_id: callerId,
|
|
862
1181
|
destination_agent_id: targetId,
|
|
863
1182
|
source_turn_id: sourceTurnId,
|
|
1183
|
+
message_id: messageId,
|
|
864
1184
|
},
|
|
865
1185
|
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private recordRecentMessage(targetId: string, message: CoordinatorMessage): void {
|
|
866
1189
|
if (targetId !== "root") {
|
|
867
1190
|
const target = this.requireUsableAgent(targetId, "message");
|
|
868
|
-
|
|
869
|
-
source_agent_id:
|
|
870
|
-
turn_id:
|
|
871
|
-
content,
|
|
872
|
-
}
|
|
1191
|
+
const recentMessage = {
|
|
1192
|
+
source_agent_id: message.details.source_agent_id,
|
|
1193
|
+
turn_id: message.details.source_turn_id,
|
|
1194
|
+
content: message.content,
|
|
1195
|
+
};
|
|
1196
|
+
target.recent_messages.push(recentMessage);
|
|
873
1197
|
if (target.recent_messages.length > RECENT_MESSAGE_LIMIT) target.recent_messages.shift();
|
|
1198
|
+
const recordedAt = this.now().toISOString();
|
|
1199
|
+
target.latest_activity_at = recordedAt;
|
|
1200
|
+
this.dependencies.registry.append(
|
|
1201
|
+
createRegistryEvent(
|
|
1202
|
+
this.dependencies.registry.rootSessionId,
|
|
1203
|
+
"agent-message-recorded",
|
|
1204
|
+
{
|
|
1205
|
+
agent_id: target.agent_id,
|
|
1206
|
+
message: recentMessage,
|
|
1207
|
+
recorded_at: recordedAt,
|
|
1208
|
+
},
|
|
1209
|
+
recordedAt,
|
|
1210
|
+
),
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
private async replayCoordinationDelivery(delivery: PersistedCoordinationDelivery): Promise<void> {
|
|
1216
|
+
if (this.automaticCoordinationDeliveryIds.has(delivery.delivery_id)) return;
|
|
1217
|
+
this.automaticCoordinationDeliveryIds.add(delivery.delivery_id);
|
|
1218
|
+
try {
|
|
1219
|
+
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
1220
|
+
if (this.hasCoordinationDeliveryEvidence(delivery)) {
|
|
1221
|
+
this.settleCoordinationDelivery(delivery);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1225
|
+
await this.deliverToRecipient(delivery.destination_agent_id, delivery.message, () =>
|
|
1226
|
+
this.isCoordinationDeliveryCurrent(delivery),
|
|
1227
|
+
);
|
|
1228
|
+
});
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1231
|
+
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1232
|
+
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
1233
|
+
this.deliveryLedger = setCoordinationDeliveryError(
|
|
1234
|
+
this.deliveryLedger,
|
|
1235
|
+
delivery.delivery_id,
|
|
1236
|
+
deliveryError,
|
|
1237
|
+
);
|
|
1238
|
+
const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
|
|
1239
|
+
if (current) this.persistCoordinationDeliveryState(current);
|
|
1240
|
+
} finally {
|
|
1241
|
+
this.automaticCoordinationDeliveryIds.delete(delivery.delivery_id);
|
|
874
1242
|
}
|
|
875
|
-
await this.deliverToRecipient(targetId, message);
|
|
876
1243
|
}
|
|
877
1244
|
|
|
878
|
-
private async
|
|
1245
|
+
private async deliverExplicitMessage(
|
|
1246
|
+
message: CoordinatorMessage,
|
|
1247
|
+
targetId: string,
|
|
1248
|
+
delivery: PersistedCoordinationDelivery,
|
|
1249
|
+
): Promise<AgentMessageDisposition> {
|
|
1250
|
+
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1251
|
+
try {
|
|
1252
|
+
await this.deliverToRecipient(targetId, message, () =>
|
|
1253
|
+
this.isCoordinationDeliveryCurrent(delivery),
|
|
1254
|
+
);
|
|
1255
|
+
return "queued";
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private async deliverToRecipient(
|
|
1263
|
+
targetId: string,
|
|
1264
|
+
message: CoordinatorMessage,
|
|
1265
|
+
isCurrentDelivery: () => boolean = () => true,
|
|
1266
|
+
requireIdleRecipient = false,
|
|
1267
|
+
): Promise<void> {
|
|
1268
|
+
if (!isCurrentDelivery()) {
|
|
1269
|
+
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1270
|
+
}
|
|
879
1271
|
if (!this.acceptingOperations) {
|
|
880
1272
|
throw new Error("Minimal subagents delivery stopped during coordinator shutdown");
|
|
881
1273
|
}
|
|
882
1274
|
if (targetId === "root") {
|
|
883
|
-
await this.dependencies.root.
|
|
1275
|
+
await this.dependencies.root.queueCoordinatorMessage(addCoordinatorMessageEnvelope(message));
|
|
884
1276
|
return;
|
|
885
1277
|
}
|
|
886
1278
|
const target = this.requireUsableAgent(targetId, "message");
|
|
887
|
-
const runtime = await this.ensureRuntime(target);
|
|
1279
|
+
const runtime = this.runtimes.get(targetId) ?? (await this.ensureRuntime(target));
|
|
1280
|
+
if (!isCurrentDelivery()) {
|
|
1281
|
+
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1282
|
+
}
|
|
1283
|
+
const visibleMessage = addCoordinatorMessageEnvelope(message);
|
|
1284
|
+
if (requireIdleRecipient && (target.active_turn_id || runtime.isRunning)) {
|
|
1285
|
+
throw new Error(`Minimal subagents automatic delivery recipient became active: ${targetId}`);
|
|
1286
|
+
}
|
|
888
1287
|
if (target.active_turn_id || runtime.isRunning) {
|
|
889
|
-
await runtime.
|
|
1288
|
+
await runtime.queueCoordinatorMessage(visibleMessage);
|
|
890
1289
|
return;
|
|
891
1290
|
}
|
|
892
1291
|
const turnId = this.beginTurn(target);
|
|
893
1292
|
const runMessage = runtime
|
|
894
|
-
.runMessage(
|
|
1293
|
+
.runMessage(visibleMessage)
|
|
895
1294
|
.then((outcome) => {
|
|
896
|
-
if (target.active_turn_id === turnId) {
|
|
1295
|
+
if (this.agents.get(target.agent_id) === target && target.active_turn_id === turnId) {
|
|
897
1296
|
this.settleTurn(target, turnId, terminalTurnResult(target.agent_id, turnId, outcome));
|
|
898
1297
|
}
|
|
899
1298
|
})
|
|
900
|
-
.catch((
|
|
901
|
-
if (target.active_turn_id === turnId) {
|
|
1299
|
+
.catch((cause) => {
|
|
1300
|
+
if (this.agents.get(target.agent_id) === target && target.active_turn_id === turnId) {
|
|
902
1301
|
this.settleTurn(target, turnId, {
|
|
903
1302
|
agent_id: target.agent_id,
|
|
904
1303
|
turn_id: turnId,
|
|
905
1304
|
status: "failed",
|
|
906
1305
|
output: "",
|
|
907
|
-
error:
|
|
1306
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
908
1307
|
});
|
|
909
1308
|
}
|
|
910
1309
|
});
|
|
@@ -917,12 +1316,17 @@ export class MinimalSubagentsCoordinator {
|
|
|
917
1316
|
void operation.then(cleanup, cleanup);
|
|
918
1317
|
}
|
|
919
1318
|
|
|
920
|
-
private enqueueRecipientDelivery(
|
|
921
|
-
|
|
922
|
-
operation: () => Promise<void>,
|
|
923
|
-
): Promise<void> {
|
|
1319
|
+
private enqueueRecipientDelivery<T>(targetId: string, operation: () => Promise<T>): Promise<T> {
|
|
1320
|
+
const epoch = this.lifecycleEpoch;
|
|
924
1321
|
const previous = this.recipientQueues.get(targetId) ?? Promise.resolve();
|
|
925
|
-
const next = previous
|
|
1322
|
+
const next = previous
|
|
1323
|
+
.catch(() => undefined)
|
|
1324
|
+
.then(() => {
|
|
1325
|
+
if (epoch !== this.lifecycleEpoch) {
|
|
1326
|
+
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1327
|
+
}
|
|
1328
|
+
return operation();
|
|
1329
|
+
});
|
|
926
1330
|
this.recipientQueues.set(targetId, next);
|
|
927
1331
|
const cleanup = () => {
|
|
928
1332
|
if (this.recipientQueues.get(targetId) === next) this.recipientQueues.delete(targetId);
|
|
@@ -960,6 +1364,46 @@ export class MinimalSubagentsCoordinator {
|
|
|
960
1364
|
return targetId;
|
|
961
1365
|
}
|
|
962
1366
|
|
|
1367
|
+
private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
|
|
1368
|
+
this.deliveryLedger = transition.ledger;
|
|
1369
|
+
for (const delivery of transition.prunedTerminalDeliveries) {
|
|
1370
|
+
const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1371
|
+
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1372
|
+
this.dependencies.registry.append(
|
|
1373
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
|
|
1374
|
+
source_agent_id: delivery.source_agent_id,
|
|
1375
|
+
source_turn_id: delivery.source_turn_id,
|
|
1376
|
+
reason: "retention-limit",
|
|
1377
|
+
}),
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private isTerminalDeliveryCurrent(delivery: PersistedDelivery): boolean {
|
|
1383
|
+
const current = findTerminalDelivery(
|
|
1384
|
+
this.deliveryLedger,
|
|
1385
|
+
delivery.source_agent_id,
|
|
1386
|
+
delivery.source_turn_id,
|
|
1387
|
+
);
|
|
1388
|
+
return current?.sequence === delivery.sequence;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private shouldStopAutomaticTerminalDelivery(delivery: PersistedDelivery): boolean {
|
|
1392
|
+
const current = findTerminalDelivery(
|
|
1393
|
+
this.deliveryLedger,
|
|
1394
|
+
delivery.source_agent_id,
|
|
1395
|
+
delivery.source_turn_id,
|
|
1396
|
+
);
|
|
1397
|
+
return current === undefined || current.path === "wait";
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
private isCoordinationDeliveryCurrent(delivery: PersistedCoordinationDelivery): boolean {
|
|
1401
|
+
return (
|
|
1402
|
+
findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id)?.sequence ===
|
|
1403
|
+
delivery.sequence
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
963
1407
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
964
1408
|
if (delivery.destination_agent_id === "root") {
|
|
965
1409
|
return this.dependencies.root.hasDeliveryEvidence(
|
|
@@ -974,15 +1418,36 @@ export class MinimalSubagentsCoordinator {
|
|
|
974
1418
|
);
|
|
975
1419
|
}
|
|
976
1420
|
|
|
1421
|
+
private hasCoordinationDeliveryEvidence(delivery: PersistedCoordinationDelivery): boolean {
|
|
1422
|
+
const sourceAgentId = delivery.message.details.source_agent_id;
|
|
1423
|
+
const sourceTurnId = delivery.message.details.source_turn_id;
|
|
1424
|
+
if (delivery.destination_agent_id === "root") {
|
|
1425
|
+
return this.dependencies.root.hasDeliveryEvidence(
|
|
1426
|
+
sourceAgentId,
|
|
1427
|
+
sourceTurnId,
|
|
1428
|
+
delivery.delivery_id,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return (
|
|
1432
|
+
this.runtimes
|
|
1433
|
+
.get(delivery.destination_agent_id)
|
|
1434
|
+
?.hasDeliveryEvidence(sourceAgentId, sourceTurnId, delivery.delivery_id) ?? false
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
977
1438
|
private settleDelivery(delivery: PersistedDelivery): void {
|
|
978
|
-
|
|
979
|
-
|
|
1439
|
+
this.deliveryLedger = settleTerminalDelivery(
|
|
1440
|
+
this.deliveryLedger,
|
|
1441
|
+
delivery.source_agent_id,
|
|
1442
|
+
delivery.source_turn_id,
|
|
1443
|
+
).ledger;
|
|
980
1444
|
this.dependencies.registry.append(
|
|
981
1445
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
|
|
982
1446
|
source_agent_id: delivery.source_agent_id,
|
|
983
1447
|
source_turn_id: delivery.source_turn_id,
|
|
984
1448
|
}),
|
|
985
1449
|
);
|
|
1450
|
+
this.removeSettledEmptyTurnClaim(delivery.source_agent_id, delivery.source_turn_id);
|
|
986
1451
|
}
|
|
987
1452
|
|
|
988
1453
|
private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
|
|
@@ -1027,7 +1492,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1027
1492
|
return {
|
|
1028
1493
|
...summary,
|
|
1029
1494
|
session_file: agent.session_file,
|
|
1030
|
-
launch_contract: structuredClone(agent.launch_contract)
|
|
1495
|
+
launch_contract: structuredClone(agent.launch_contract),
|
|
1031
1496
|
capability_ceiling: [...agent.capability_ceiling],
|
|
1032
1497
|
spawn_entry_id: agent.spawn_entry_id,
|
|
1033
1498
|
recent_messages: structuredClone(agent.recent_messages),
|
|
@@ -1038,6 +1503,47 @@ export class MinimalSubagentsCoordinator {
|
|
|
1038
1503
|
};
|
|
1039
1504
|
}
|
|
1040
1505
|
|
|
1506
|
+
private pruneDeliveryStateForDeletedAgent(agentId: string): void {
|
|
1507
|
+
const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
|
|
1508
|
+
const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
|
|
1509
|
+
this.deliveryLedger = pruneDeliveryLedgerAgents(this.deliveryLedger, [agentId]).ledger;
|
|
1510
|
+
for (const delivery of previousTerminalDeliveries) {
|
|
1511
|
+
if (!this.isTerminalDeliveryCurrent(delivery)) {
|
|
1512
|
+
this.releaseAutomaticDeliveryClaimWaiters(
|
|
1513
|
+
agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
for (const delivery of previousCoordinationDeliveries) {
|
|
1518
|
+
if (!this.isCoordinationDeliveryCurrent(delivery)) {
|
|
1519
|
+
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
for (const [key, messages] of this.pendingParentMessages) {
|
|
1523
|
+
for (const pending of messages) {
|
|
1524
|
+
if (!findCoordinationDelivery(this.deliveryLedger, pending.deliveryId)) {
|
|
1525
|
+
pending.claimed = true;
|
|
1526
|
+
pending.releaseClaim();
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const retained = messages.filter((pending) =>
|
|
1530
|
+
findCoordinationDelivery(this.deliveryLedger, pending.deliveryId),
|
|
1531
|
+
);
|
|
1532
|
+
if (retained.length === 0) this.pendingParentMessages.delete(key);
|
|
1533
|
+
else this.pendingParentMessages.set(key, retained);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
private pruneRecentMessageProjectionsForDeletedAgent(agentId: string): void {
|
|
1538
|
+
const belongsToDeletedSubtree = (sourceAgentId: string) =>
|
|
1539
|
+
sourceAgentId === agentId || sourceAgentId.startsWith(`${agentId}.`);
|
|
1540
|
+
for (const agent of this.agents.values()) {
|
|
1541
|
+
agent.recent_messages = agent.recent_messages.filter(
|
|
1542
|
+
(message) => !belongsToDeletedSubtree(message.source_agent_id),
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1041
1547
|
private descendantsOf(agentId: string): PersistedAgent[] {
|
|
1042
1548
|
const descendants: PersistedAgent[] = [];
|
|
1043
1549
|
const queue = this.childrenOf(agentId);
|
|
@@ -1125,6 +1631,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1125
1631
|
...structuredClone(agent),
|
|
1126
1632
|
session_file: undefined,
|
|
1127
1633
|
session_id: undefined,
|
|
1634
|
+
session_leaf_id: undefined,
|
|
1128
1635
|
clone_error: cloneError,
|
|
1129
1636
|
active_turn_id: undefined,
|
|
1130
1637
|
active_turn_started_at: undefined,
|
|
@@ -1135,6 +1642,15 @@ export class MinimalSubagentsCoordinator {
|
|
|
1135
1642
|
};
|
|
1136
1643
|
}
|
|
1137
1644
|
|
|
1645
|
+
private rejectAllWaiters(error: Error): void {
|
|
1646
|
+
for (const [key, waiters] of this.waiters) {
|
|
1647
|
+
for (const waiter of waiters) {
|
|
1648
|
+
this.removeWaiter(key, waiter);
|
|
1649
|
+
waiter.reject(error);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1138
1654
|
private removeWaiter(key: string, waiter: TurnWaiter): void {
|
|
1139
1655
|
const turnWaiters = this.waiters.get(key);
|
|
1140
1656
|
turnWaiters?.delete(waiter);
|
|
@@ -1145,6 +1661,286 @@ export class MinimalSubagentsCoordinator {
|
|
|
1145
1661
|
}
|
|
1146
1662
|
}
|
|
1147
1663
|
|
|
1664
|
+
private resolveWaiterWithMessage(
|
|
1665
|
+
message: CoordinatorMessage,
|
|
1666
|
+
delivery: PersistedCoordinationDelivery,
|
|
1667
|
+
): boolean {
|
|
1668
|
+
const destinationAgentId = message.details.destination_agent_id;
|
|
1669
|
+
if (!destinationAgentId) return false;
|
|
1670
|
+
const key = agentDeliveryKey(message.details.source_agent_id, message.details.source_turn_id);
|
|
1671
|
+
const turnWaiters = this.waiters.get(key);
|
|
1672
|
+
const waiter = [...(turnWaiters ?? [])].find(
|
|
1673
|
+
(candidate) => candidate.callerId === destinationAgentId,
|
|
1674
|
+
);
|
|
1675
|
+
if (!waiter) return false;
|
|
1676
|
+
this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
|
|
1677
|
+
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1678
|
+
this.deliveryLedger,
|
|
1679
|
+
delivery.delivery_id,
|
|
1680
|
+
"wait",
|
|
1681
|
+
);
|
|
1682
|
+
const currentDelivery = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
|
|
1683
|
+
if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
|
|
1684
|
+
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1685
|
+
this.removeWaiter(key, waiter);
|
|
1686
|
+
waiter.resolve({
|
|
1687
|
+
event: "message",
|
|
1688
|
+
agent_id: message.details.source_agent_id,
|
|
1689
|
+
turn_id: message.details.source_turn_id,
|
|
1690
|
+
message_id: message.details.message_id,
|
|
1691
|
+
delivery_id: delivery.delivery_id,
|
|
1692
|
+
message: message.content,
|
|
1693
|
+
});
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private claimPendingParentMessage(
|
|
1698
|
+
callerId: string,
|
|
1699
|
+
sourceAgentId: string,
|
|
1700
|
+
sourceTurnId: string,
|
|
1701
|
+
): WaitResult | undefined {
|
|
1702
|
+
const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
|
|
1703
|
+
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1704
|
+
const index = pendingMessages?.findIndex(
|
|
1705
|
+
(pending) => pending.destinationAgentId === callerId && !pending.claimed,
|
|
1706
|
+
);
|
|
1707
|
+
if (index === undefined || index < 0 || !pendingMessages) {
|
|
1708
|
+
const retained = this.deliveryLedger.coordinationDeliveries
|
|
1709
|
+
.filter(
|
|
1710
|
+
(delivery) =>
|
|
1711
|
+
delivery.destination_agent_id === callerId &&
|
|
1712
|
+
delivery.message.details.source_agent_id === sourceAgentId &&
|
|
1713
|
+
delivery.message.details.source_turn_id === sourceTurnId &&
|
|
1714
|
+
!this.waitHandedDeliveryIds.has(delivery.delivery_id),
|
|
1715
|
+
)
|
|
1716
|
+
.sort((left, right) => left.sequence - right.sequence)[0];
|
|
1717
|
+
if (!retained) return undefined;
|
|
1718
|
+
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1719
|
+
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1720
|
+
this.deliveryLedger,
|
|
1721
|
+
retained.delivery_id,
|
|
1722
|
+
"wait",
|
|
1723
|
+
);
|
|
1724
|
+
const currentDelivery = findCoordinationDelivery(this.deliveryLedger, retained.delivery_id);
|
|
1725
|
+
if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
|
|
1726
|
+
this.waitHandedDeliveryIds.add(retained.delivery_id);
|
|
1727
|
+
return {
|
|
1728
|
+
event: "message",
|
|
1729
|
+
agent_id: sourceAgentId,
|
|
1730
|
+
turn_id: sourceTurnId,
|
|
1731
|
+
message_id: retained.message.details.message_id,
|
|
1732
|
+
delivery_id: retained.delivery_id,
|
|
1733
|
+
message: retained.message.content,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
const pending = pendingMessages[index];
|
|
1737
|
+
if (!pending) return undefined;
|
|
1738
|
+
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1739
|
+
const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
|
|
1740
|
+
if (delivery) {
|
|
1741
|
+
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1742
|
+
this.deliveryLedger,
|
|
1743
|
+
delivery.delivery_id,
|
|
1744
|
+
"wait",
|
|
1745
|
+
);
|
|
1746
|
+
const currentDelivery = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
|
|
1747
|
+
if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
|
|
1748
|
+
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1749
|
+
}
|
|
1750
|
+
const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
|
|
1751
|
+
if (sourceResult?.turn_id === sourceTurnId) {
|
|
1752
|
+
this.setTerminalDeliveryPathToWait(callerId, sourceResult);
|
|
1753
|
+
}
|
|
1754
|
+
pending.claimed = true;
|
|
1755
|
+
pending.cancelGrace?.();
|
|
1756
|
+
pending.releaseClaim();
|
|
1757
|
+
pendingMessages.splice(index, 1);
|
|
1758
|
+
if (pendingMessages.length === 0) this.pendingParentMessages.delete(key);
|
|
1759
|
+
return {
|
|
1760
|
+
event: "message",
|
|
1761
|
+
agent_id: sourceAgentId,
|
|
1762
|
+
turn_id: sourceTurnId,
|
|
1763
|
+
message_id: pending.message.details.message_id,
|
|
1764
|
+
delivery_id: pending.deliveryId,
|
|
1765
|
+
message: pending.message.content,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
private queuePendingParentMessage(
|
|
1770
|
+
targetId: string,
|
|
1771
|
+
message: CoordinatorMessage,
|
|
1772
|
+
delivery: PersistedCoordinationDelivery,
|
|
1773
|
+
): void {
|
|
1774
|
+
const key = agentDeliveryKey(message.details.source_agent_id, message.details.source_turn_id);
|
|
1775
|
+
const turnClaimed = () =>
|
|
1776
|
+
isDeliveryLedgerTurnClaimed(
|
|
1777
|
+
this.deliveryLedger,
|
|
1778
|
+
message.details.source_agent_id,
|
|
1779
|
+
message.details.source_turn_id,
|
|
1780
|
+
);
|
|
1781
|
+
const { promise: claimPromise, resolve: releaseClaim } = Promise.withResolvers<void>();
|
|
1782
|
+
const pending: PendingParentMessage = {
|
|
1783
|
+
deliveryId: delivery.delivery_id,
|
|
1784
|
+
message,
|
|
1785
|
+
destinationAgentId: targetId,
|
|
1786
|
+
claimed: false,
|
|
1787
|
+
claimPromise,
|
|
1788
|
+
releaseClaim,
|
|
1789
|
+
};
|
|
1790
|
+
const pendingMessages = this.pendingParentMessages.get(key) ?? [];
|
|
1791
|
+
pendingMessages.push(pending);
|
|
1792
|
+
this.pendingParentMessages.set(key, pendingMessages);
|
|
1793
|
+
|
|
1794
|
+
if (
|
|
1795
|
+
isDeliveryLedgerTurnClaimed(
|
|
1796
|
+
this.deliveryLedger,
|
|
1797
|
+
message.details.source_agent_id,
|
|
1798
|
+
message.details.source_turn_id,
|
|
1799
|
+
)
|
|
1800
|
+
)
|
|
1801
|
+
return;
|
|
1802
|
+
|
|
1803
|
+
const operation = this.enqueueRecipientDelivery(targetId, async () => {
|
|
1804
|
+
if (targetId !== "root") {
|
|
1805
|
+
await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
|
|
1806
|
+
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1807
|
+
}
|
|
1808
|
+
const graceMs = this.deliveryGraceMs();
|
|
1809
|
+
const gracePromise = new Promise<void>((resolve) => {
|
|
1810
|
+
if (graceMs <= 0) {
|
|
1811
|
+
resolve();
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const timer = setTimeout(resolve, graceMs);
|
|
1815
|
+
pending.cancelGrace = () => {
|
|
1816
|
+
clearTimeout(timer);
|
|
1817
|
+
resolve();
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
await Promise.race([pending.claimPromise, gracePromise]);
|
|
1821
|
+
pending.cancelGrace?.();
|
|
1822
|
+
pending.cancelGrace = undefined;
|
|
1823
|
+
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1824
|
+
while (!this.isRecipientIdle(targetId)) {
|
|
1825
|
+
const idleWait = this.createRecipientIdleWait(targetId);
|
|
1826
|
+
await Promise.race([pending.claimPromise, idleWait.promise]);
|
|
1827
|
+
idleWait.cancel();
|
|
1828
|
+
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1829
|
+
}
|
|
1830
|
+
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1831
|
+
this.removePendingParentMessage(key, pending);
|
|
1832
|
+
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1833
|
+
await this.deliverToRecipient(
|
|
1834
|
+
targetId,
|
|
1835
|
+
message,
|
|
1836
|
+
() => this.isCoordinationDeliveryCurrent(delivery),
|
|
1837
|
+
true,
|
|
1838
|
+
);
|
|
1839
|
+
});
|
|
1840
|
+
void operation.catch((cause) => {
|
|
1841
|
+
this.removePendingParentMessage(key, pending);
|
|
1842
|
+
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1843
|
+
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1844
|
+
const deliveryError = cause instanceof Error ? cause.message : String(cause);
|
|
1845
|
+
this.deliveryLedger = setCoordinationDeliveryError(
|
|
1846
|
+
this.deliveryLedger,
|
|
1847
|
+
delivery.delivery_id,
|
|
1848
|
+
deliveryError,
|
|
1849
|
+
);
|
|
1850
|
+
const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
|
|
1851
|
+
if (current) this.persistCoordinationDeliveryState(current);
|
|
1852
|
+
this.dependencies.notify?.({
|
|
1853
|
+
type: "failure",
|
|
1854
|
+
agentId: message.details.source_agent_id,
|
|
1855
|
+
message: `Could not queue message ${message.details.message_id}: ${
|
|
1856
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
1857
|
+
}`,
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
|
|
1863
|
+
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1864
|
+
if (!pendingMessages) return;
|
|
1865
|
+
const index = pendingMessages.indexOf(pending);
|
|
1866
|
+
if (index >= 0) pendingMessages.splice(index, 1);
|
|
1867
|
+
if (pendingMessages.length === 0) this.pendingParentMessages.delete(key);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
private claimTerminalDelivery(callerId: string, result: TurnResult): void {
|
|
1871
|
+
if (!findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id)) return;
|
|
1872
|
+
this.claimDeliveryTurn(result.agent_id, result.turn_id);
|
|
1873
|
+
this.setTerminalDeliveryPathToWait(callerId, result);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
|
|
1877
|
+
const key = agentDeliveryKey(result.agent_id, result.turn_id);
|
|
1878
|
+
const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1879
|
+
if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
|
|
1880
|
+
this.applyDeliveryLedgerTransition(
|
|
1881
|
+
setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
|
|
1882
|
+
);
|
|
1883
|
+
const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1884
|
+
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1885
|
+
if (!retained) return;
|
|
1886
|
+
this.dependencies.registry.append(
|
|
1887
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
1888
|
+
delivery: retained,
|
|
1889
|
+
}),
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
private isRecipientIdle(agentId: string): boolean {
|
|
1894
|
+
if (agentId === "root") return this.dependencies.root.isIdle();
|
|
1895
|
+
const agent = this.agents.get(agentId);
|
|
1896
|
+
if (!agent || agent.active_turn_id) return false;
|
|
1897
|
+
return !this.runtimes.get(agentId)?.isRunning;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
private createRecipientIdleWait(agentId: string): CancelableWait {
|
|
1901
|
+
const { promise, resolve: release } = Promise.withResolvers<void>();
|
|
1902
|
+
const waiters = this.recipientIdleWaiters.get(agentId) ?? new Set();
|
|
1903
|
+
waiters.add(release);
|
|
1904
|
+
this.recipientIdleWaiters.set(agentId, waiters);
|
|
1905
|
+
return {
|
|
1906
|
+
promise,
|
|
1907
|
+
cancel: () => {
|
|
1908
|
+
const currentWaiters = this.recipientIdleWaiters.get(agentId);
|
|
1909
|
+
currentWaiters?.delete(release);
|
|
1910
|
+
if (currentWaiters?.size === 0) this.recipientIdleWaiters.delete(agentId);
|
|
1911
|
+
},
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
private createAutomaticDeliveryClaimWait(deliveryKey: string): CancelableWait {
|
|
1916
|
+
const { promise, resolve: release } = Promise.withResolvers<void>();
|
|
1917
|
+
const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey) ?? new Set();
|
|
1918
|
+
waiters.add(release);
|
|
1919
|
+
this.automaticDeliveryClaimWaiters.set(deliveryKey, waiters);
|
|
1920
|
+
return {
|
|
1921
|
+
promise,
|
|
1922
|
+
cancel: () => {
|
|
1923
|
+
const currentWaiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
|
|
1924
|
+
currentWaiters?.delete(release);
|
|
1925
|
+
if (currentWaiters?.size === 0) this.automaticDeliveryClaimWaiters.delete(deliveryKey);
|
|
1926
|
+
},
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
private releaseAutomaticDeliveryClaimWaiters(deliveryKey: string): void {
|
|
1931
|
+
const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
|
|
1932
|
+
this.automaticDeliveryClaimWaiters.delete(deliveryKey);
|
|
1933
|
+
for (const resolve of waiters ?? []) resolve();
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private releaseAllRecipientIdleWaiters(): void {
|
|
1937
|
+
const allWaiters = [...this.recipientIdleWaiters.values()];
|
|
1938
|
+
this.recipientIdleWaiters.clear();
|
|
1939
|
+
for (const waiters of allWaiters) {
|
|
1940
|
+
for (const resolve of waiters) resolve();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1148
1944
|
private async cancelDuringShutdown(agentId: string): Promise<void> {
|
|
1149
1945
|
const target = this.agents.get(agentId);
|
|
1150
1946
|
if (!target) return;
|
|
@@ -1154,6 +1950,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1154
1950
|
const turnId = agent.active_turn_id;
|
|
1155
1951
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
1156
1952
|
if (runtime) await runtime.abort();
|
|
1953
|
+
if (this.agents.get(agent.agent_id) !== agent) continue;
|
|
1157
1954
|
this.settleTurn(agent, turnId, {
|
|
1158
1955
|
agent_id: agent.agent_id,
|
|
1159
1956
|
turn_id: turnId,
|
|
@@ -1168,6 +1965,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1168
1965
|
const turnId = agent.active_turn_id;
|
|
1169
1966
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
1170
1967
|
if (runtime) await runtime.abort();
|
|
1968
|
+
if (this.agents.get(agent.agent_id) !== agent) return undefined;
|
|
1171
1969
|
this.settleTurn(agent, turnId, {
|
|
1172
1970
|
agent_id: agent.agent_id,
|
|
1173
1971
|
turn_id: turnId,
|
|
@@ -1230,4 +2028,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1230
2028
|
private now(): Date {
|
|
1231
2029
|
return this.dependencies.now?.() ?? new Date();
|
|
1232
2030
|
}
|
|
2031
|
+
|
|
2032
|
+
private deliveryGraceMs(): number {
|
|
2033
|
+
return this.dependencies.automaticDeliveryGraceMs ?? DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS;
|
|
2034
|
+
}
|
|
1233
2035
|
}
|