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