@ian-pascoe/pi-minimal-subagents 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/package.json +1 -1
- package/src/minimal-subagents-coordinator.ts +27 -124
- package/src/minimal-subagents-extension.ts +0 -9
- package/src/minimal-subagents-render-contract.ts +2 -0
- package/src/minimal-subagents-rendering.ts +14 -3
- package/src/minimal-subagents-session-wire.ts +6 -0
- package/src/minimal-subagents-sessions.ts +5 -1
- package/src/minimal-subagents-tools.ts +1 -1
- package/src/minimal-subagents-types.ts +7 -3
package/README.md
CHANGED
|
@@ -141,22 +141,24 @@ the complete read-only discovery bundle.
|
|
|
141
141
|
`agent_message` reports whether a message was delivered through an active
|
|
142
142
|
parent wait, queued for the recipient, or failed. `subagent_wait` can return an
|
|
143
143
|
intermediate Wait Event containing a Coordination Message before the child turn
|
|
144
|
-
settles
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
settles. That event claims only its message, so later unconsumed messages and the
|
|
145
|
+
terminal result retain automatic fallback. If the turn has already settled, one
|
|
146
|
+
wait returns its terminal result with queued messages in `messages`. Pass
|
|
147
|
+
optional `turn_id` to address an older retained turn exactly. Without it, waits
|
|
148
|
+
select the oldest observable claimed or pending turn before the active/latest
|
|
149
|
+
turn. A caller may have only one outstanding wait for the same source turn; a
|
|
150
|
+
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
149
151
|
|
|
150
152
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
|
151
153
|
globally increasing sequence, and wait ownership before delivery. Existing
|
|
152
154
|
items retain their sequence; gaps from skipped malformed records are valid.
|
|
153
|
-
Claims can name only active, latest, or retained turns.
|
|
154
|
-
|
|
155
|
-
reloads, forks, and newer turns. Automatic fallback retains its ordered
|
|
156
|
-
reservation
|
|
157
|
-
|
|
158
|
-
Evidence settles and compacts ledger items, preventing duplicate delivery
|
|
159
|
-
unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
155
|
+
Claims can name only active, latest, or retained turns. Wait-returned messages
|
|
156
|
+
retain individual delivery evidence; terminal wait ownership remains durable
|
|
157
|
+
across reloads, forks, and newer turns. Automatic fallback retains its ordered
|
|
158
|
+
queue reservation and sends every unclaimed Coordination Message and terminal
|
|
159
|
+
result as a Pi steer, including while the recipient is active. Destination-session
|
|
160
|
+
Delivery Evidence settles and compacts ledger items, preventing duplicate delivery
|
|
161
|
+
and unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
160
162
|
most 20 pending wait-only terminal results per source agent; Coordination
|
|
161
163
|
Messages are not removed by that terminal-retention limit. Delivered messages
|
|
162
164
|
include stable delivery, source-agent, and source-turn identities in persisted
|
package/package.json
CHANGED
|
@@ -53,6 +53,7 @@ import type {
|
|
|
53
53
|
StatusResult,
|
|
54
54
|
TurnId,
|
|
55
55
|
TurnResult,
|
|
56
|
+
WaitMessageResult,
|
|
56
57
|
WaitResult,
|
|
57
58
|
} from "./minimal-subagents-types.js";
|
|
58
59
|
|
|
@@ -85,11 +86,6 @@ interface PendingParentMessage {
|
|
|
85
86
|
cancelGrace?: () => void;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
interface CancelableWait {
|
|
89
|
-
promise: Promise<void>;
|
|
90
|
-
cancel: () => void;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
89
|
function agentDeliveryKey(agentId: string, turnId: string): string {
|
|
94
90
|
return `${agentId}\u0000${turnId}`;
|
|
95
91
|
}
|
|
@@ -109,8 +105,9 @@ function terminalTurnResult(
|
|
|
109
105
|
};
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
function terminalWaitResult(result: TurnResult): WaitResult {
|
|
113
|
-
|
|
108
|
+
function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] = []): WaitResult {
|
|
109
|
+
const terminal = { event: "turn" as const, ...structuredClone(result) };
|
|
110
|
+
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
@@ -124,11 +121,9 @@ export class MinimalSubagentsCoordinator {
|
|
|
124
121
|
private readonly waiters = new Map<string, Set<TurnWaiter>>();
|
|
125
122
|
private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
|
|
126
123
|
private readonly recipientQueues = new Map<string, Promise<unknown>>();
|
|
127
|
-
private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
|
|
128
124
|
private readonly automaticDeliveryKeys = new Set<string>();
|
|
129
125
|
private readonly automaticCoordinationDeliveryIds = new Set<string>();
|
|
130
126
|
private readonly waitHandedDeliveryIds = new Set<string>();
|
|
131
|
-
private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
|
|
132
127
|
private readonly backgroundOperations = new Set<Promise<void>>();
|
|
133
128
|
private acceptingOperations = true;
|
|
134
129
|
private lifecycleEpoch = 0;
|
|
@@ -363,15 +358,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
363
358
|
new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
|
|
364
359
|
);
|
|
365
360
|
}
|
|
366
|
-
const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
|
|
367
|
-
if (pendingMessage) return Promise.resolve(pendingMessage);
|
|
368
361
|
const retainedResult =
|
|
369
362
|
findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
|
|
370
363
|
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
371
364
|
if (retainedResult) {
|
|
365
|
+
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
372
366
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
373
|
-
return Promise.resolve(terminalWaitResult(retainedResult));
|
|
367
|
+
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
374
368
|
}
|
|
369
|
+
const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
|
|
370
|
+
if (pendingMessage) return Promise.resolve(pendingMessage);
|
|
375
371
|
if (agent.active_turn_id !== turnId) {
|
|
376
372
|
return Promise.reject(
|
|
377
373
|
new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
|
|
@@ -543,7 +539,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
543
539
|
this.agents.clear();
|
|
544
540
|
this.deliveryLedger = createDeliveryLedger();
|
|
545
541
|
this.pendingParentMessages.clear();
|
|
546
|
-
this.releaseAllRecipientIdleWaiters();
|
|
547
542
|
this.recipientQueues.clear();
|
|
548
543
|
this.backgroundOperations.clear();
|
|
549
544
|
await Promise.allSettled(
|
|
@@ -558,12 +553,10 @@ export class MinimalSubagentsCoordinator {
|
|
|
558
553
|
this.waiters.clear();
|
|
559
554
|
this.pendingParentMessages.clear();
|
|
560
555
|
this.waitHandedDeliveryIds.clear();
|
|
561
|
-
this.releaseAllRecipientIdleWaiters();
|
|
562
556
|
this.recipientQueues.clear();
|
|
563
557
|
this.backgroundOperations.clear();
|
|
564
558
|
this.automaticDeliveryKeys.clear();
|
|
565
559
|
this.automaticCoordinationDeliveryIds.clear();
|
|
566
|
-
this.automaticDeliveryClaimWaiters.clear();
|
|
567
560
|
this.deliveryLedger = createDeliveryLedger({
|
|
568
561
|
deliveries: snapshot.deliveries,
|
|
569
562
|
coordination_deliveries: snapshot.coordination_deliveries,
|
|
@@ -720,13 +713,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
720
713
|
await Promise.allSettled(scheduled);
|
|
721
714
|
}
|
|
722
715
|
|
|
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();
|
|
728
|
-
}
|
|
729
|
-
|
|
730
716
|
/** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
|
|
731
717
|
async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
|
|
732
718
|
const activeRootChildren = this.childrenOf("root");
|
|
@@ -785,7 +771,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
785
771
|
shutdown(): Promise<void> {
|
|
786
772
|
if (this.shutdownPromise) return this.shutdownPromise;
|
|
787
773
|
this.acceptingOperations = false;
|
|
788
|
-
this.releaseAllRecipientIdleWaiters();
|
|
789
774
|
this.shutdownPromise = this.finishShutdown();
|
|
790
775
|
return this.shutdownPromise;
|
|
791
776
|
}
|
|
@@ -999,7 +984,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
999
984
|
});
|
|
1000
985
|
}
|
|
1001
986
|
if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
|
|
1002
|
-
this.markRecipientIdle(agent.agent_id);
|
|
1003
987
|
}
|
|
1004
988
|
|
|
1005
989
|
private async deliverAutomaticResult(
|
|
@@ -1024,15 +1008,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1024
1008
|
this.settleDelivery(delivery);
|
|
1025
1009
|
return;
|
|
1026
1010
|
}
|
|
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
1011
|
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1037
1012
|
if (this.hasDeliveryEvidence(delivery)) {
|
|
1038
1013
|
this.settleDelivery(delivery);
|
|
@@ -1051,11 +1026,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1051
1026
|
usage: result.usage,
|
|
1052
1027
|
},
|
|
1053
1028
|
};
|
|
1054
|
-
await this.deliverToRecipient(
|
|
1055
|
-
delivery
|
|
1056
|
-
message,
|
|
1057
|
-
() => this.isTerminalDeliveryCurrent(delivery),
|
|
1058
|
-
true,
|
|
1029
|
+
await this.deliverToRecipient(delivery.destination_agent_id, message, () =>
|
|
1030
|
+
this.isTerminalDeliveryCurrent(delivery),
|
|
1059
1031
|
);
|
|
1060
1032
|
});
|
|
1061
1033
|
} catch (error) {
|
|
@@ -1263,7 +1235,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1263
1235
|
targetId: string,
|
|
1264
1236
|
message: CoordinatorMessage,
|
|
1265
1237
|
isCurrentDelivery: () => boolean = () => true,
|
|
1266
|
-
requireIdleRecipient = false,
|
|
1267
1238
|
): Promise<void> {
|
|
1268
1239
|
if (!isCurrentDelivery()) {
|
|
1269
1240
|
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
@@ -1281,9 +1252,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1281
1252
|
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1282
1253
|
}
|
|
1283
1254
|
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
|
-
}
|
|
1287
1255
|
if (target.active_turn_id || runtime.isRunning) {
|
|
1288
1256
|
await runtime.queueCoordinatorMessage(visibleMessage);
|
|
1289
1257
|
return;
|
|
@@ -1367,8 +1335,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1367
1335
|
private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
|
|
1368
1336
|
this.deliveryLedger = transition.ledger;
|
|
1369
1337
|
for (const delivery of transition.prunedTerminalDeliveries) {
|
|
1370
|
-
const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1371
|
-
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1372
1338
|
this.dependencies.registry.append(
|
|
1373
1339
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
|
|
1374
1340
|
source_agent_id: delivery.source_agent_id,
|
|
@@ -1504,16 +1470,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1504
1470
|
}
|
|
1505
1471
|
|
|
1506
1472
|
private pruneDeliveryStateForDeletedAgent(agentId: string): void {
|
|
1507
|
-
const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
|
|
1508
1473
|
const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
|
|
1509
1474
|
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
1475
|
for (const delivery of previousCoordinationDeliveries) {
|
|
1518
1476
|
if (!this.isCoordinationDeliveryCurrent(delivery)) {
|
|
1519
1477
|
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
@@ -1673,7 +1631,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1673
1631
|
(candidate) => candidate.callerId === destinationAgentId,
|
|
1674
1632
|
);
|
|
1675
1633
|
if (!waiter) return false;
|
|
1676
|
-
this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
|
|
1677
1634
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1678
1635
|
this.deliveryLedger,
|
|
1679
1636
|
delivery.delivery_id,
|
|
@@ -1698,7 +1655,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1698
1655
|
callerId: string,
|
|
1699
1656
|
sourceAgentId: string,
|
|
1700
1657
|
sourceTurnId: string,
|
|
1701
|
-
):
|
|
1658
|
+
): WaitMessageResult | undefined {
|
|
1702
1659
|
const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
|
|
1703
1660
|
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1704
1661
|
const index = pendingMessages?.findIndex(
|
|
@@ -1715,7 +1672,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1715
1672
|
)
|
|
1716
1673
|
.sort((left, right) => left.sequence - right.sequence)[0];
|
|
1717
1674
|
if (!retained) return undefined;
|
|
1718
|
-
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1719
1675
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1720
1676
|
this.deliveryLedger,
|
|
1721
1677
|
retained.delivery_id,
|
|
@@ -1735,7 +1691,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1735
1691
|
}
|
|
1736
1692
|
const pending = pendingMessages[index];
|
|
1737
1693
|
if (!pending) return undefined;
|
|
1738
|
-
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1739
1694
|
const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
|
|
1740
1695
|
if (delivery) {
|
|
1741
1696
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
@@ -1747,10 +1702,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1747
1702
|
if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
|
|
1748
1703
|
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1749
1704
|
}
|
|
1750
|
-
const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
|
|
1751
|
-
if (sourceResult?.turn_id === sourceTurnId) {
|
|
1752
|
-
this.setTerminalDeliveryPathToWait(callerId, sourceResult);
|
|
1753
|
-
}
|
|
1754
1705
|
pending.claimed = true;
|
|
1755
1706
|
pending.cancelGrace?.();
|
|
1756
1707
|
pending.releaseClaim();
|
|
@@ -1766,6 +1717,20 @@ export class MinimalSubagentsCoordinator {
|
|
|
1766
1717
|
};
|
|
1767
1718
|
}
|
|
1768
1719
|
|
|
1720
|
+
private drainPendingParentMessages(
|
|
1721
|
+
callerId: string,
|
|
1722
|
+
sourceAgentId: string,
|
|
1723
|
+
sourceTurnId: string,
|
|
1724
|
+
): WaitMessageResult[] {
|
|
1725
|
+
const messages: WaitMessageResult[] = [];
|
|
1726
|
+
let message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
|
|
1727
|
+
while (message) {
|
|
1728
|
+
messages.push(message);
|
|
1729
|
+
message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
|
|
1730
|
+
}
|
|
1731
|
+
return messages;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1769
1734
|
private queuePendingParentMessage(
|
|
1770
1735
|
targetId: string,
|
|
1771
1736
|
message: CoordinatorMessage,
|
|
@@ -1821,20 +1786,11 @@ export class MinimalSubagentsCoordinator {
|
|
|
1821
1786
|
pending.cancelGrace?.();
|
|
1822
1787
|
pending.cancelGrace = undefined;
|
|
1823
1788
|
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
1789
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1831
1790
|
this.removePendingParentMessage(key, pending);
|
|
1832
1791
|
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1833
|
-
await this.deliverToRecipient(
|
|
1834
|
-
|
|
1835
|
-
message,
|
|
1836
|
-
() => this.isCoordinationDeliveryCurrent(delivery),
|
|
1837
|
-
true,
|
|
1792
|
+
await this.deliverToRecipient(targetId, message, () =>
|
|
1793
|
+
this.isCoordinationDeliveryCurrent(delivery),
|
|
1838
1794
|
);
|
|
1839
1795
|
});
|
|
1840
1796
|
void operation.catch((cause) => {
|
|
@@ -1874,14 +1830,12 @@ export class MinimalSubagentsCoordinator {
|
|
|
1874
1830
|
}
|
|
1875
1831
|
|
|
1876
1832
|
private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
|
|
1877
|
-
const key = agentDeliveryKey(result.agent_id, result.turn_id);
|
|
1878
1833
|
const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1879
1834
|
if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
|
|
1880
1835
|
this.applyDeliveryLedgerTransition(
|
|
1881
1836
|
setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
|
|
1882
1837
|
);
|
|
1883
1838
|
const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1884
|
-
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1885
1839
|
if (!retained) return;
|
|
1886
1840
|
this.dependencies.registry.append(
|
|
1887
1841
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
@@ -1890,57 +1844,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1890
1844
|
);
|
|
1891
1845
|
}
|
|
1892
1846
|
|
|
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
|
-
|
|
1944
1847
|
private async cancelDuringShutdown(agentId: string): Promise<void> {
|
|
1945
1848
|
const target = this.agents.get(agentId);
|
|
1946
1849
|
if (!target) return;
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
getAgentDir,
|
|
6
6
|
SessionManager,
|
|
7
7
|
SettingsManager,
|
|
8
|
-
type AgentSettledEvent,
|
|
9
8
|
type ExtensionAPI,
|
|
10
9
|
type ExtensionContext,
|
|
11
10
|
type ExtensionFactory,
|
|
@@ -107,7 +106,6 @@ function createRootConversationEndpoint(
|
|
|
107
106
|
sourceTurnId,
|
|
108
107
|
deliveryId,
|
|
109
108
|
),
|
|
110
|
-
isIdle: () => context.isIdle(),
|
|
111
109
|
};
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -331,7 +329,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
331
329
|
this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
|
|
332
330
|
this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
|
|
333
331
|
this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
|
|
334
|
-
this.pi.on("agent_settled", (event, context) => this.releaseSettledRecipient(event, context));
|
|
335
332
|
this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
|
|
336
333
|
}
|
|
337
334
|
|
|
@@ -554,12 +551,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
554
551
|
}
|
|
555
552
|
}
|
|
556
553
|
|
|
557
|
-
private releaseSettledRecipient(_event: AgentSettledEvent, context: ExtensionContext): void {
|
|
558
|
-
if (!this.coordinator) return;
|
|
559
|
-
if (context.isIdle()) this.coordinator.markRecipientIdle("root");
|
|
560
|
-
this.uiController?.refresh();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
554
|
private async shutdownSession(
|
|
564
555
|
event: SessionShutdownEvent,
|
|
565
556
|
context: ExtensionContext,
|
|
@@ -132,6 +132,7 @@ const WaitMessageDetailsSchema = Type.Object({
|
|
|
132
132
|
agent_id: Type.String(),
|
|
133
133
|
turn_id: Type.String(),
|
|
134
134
|
message_id: Type.String(),
|
|
135
|
+
delivery_id: Type.Optional(Type.String()),
|
|
135
136
|
message: Type.String(),
|
|
136
137
|
elapsed_ms: Type.Optional(Type.Number()),
|
|
137
138
|
usage: Type.Optional(RenderUsageSchema),
|
|
@@ -145,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
|
|
|
145
146
|
error: Type.Optional(Type.String()),
|
|
146
147
|
elapsed_ms: Type.Optional(Type.Number()),
|
|
147
148
|
usage: Type.Optional(RenderUsageSchema),
|
|
149
|
+
messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
|
|
148
150
|
});
|
|
149
151
|
const StatusDetailsSchema = Type.Union([
|
|
150
152
|
Type.Object({
|
|
@@ -341,9 +341,12 @@ function renderWaitResult(
|
|
|
341
341
|
: details.status;
|
|
342
342
|
const duration = formatSubagentDuration(details.elapsed_ms);
|
|
343
343
|
const tokens = formatSubagentTokenCount(details.usage?.totalTokens);
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
const drainedMessageCount = details.event === "message" ? 0 : (details.messages?.length ?? 0);
|
|
345
|
+
const metrics = [
|
|
346
|
+
duration,
|
|
347
|
+
tokens ? `${tokens} tokens` : undefined,
|
|
348
|
+
drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
|
|
349
|
+
].filter((metric): metric is string => metric !== undefined);
|
|
347
350
|
const summary = renderSubagentSummary(theme, status, agentId, metrics);
|
|
348
351
|
if (options.isPartial || !options.expanded) {
|
|
349
352
|
return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
|
|
@@ -356,6 +359,14 @@ function renderWaitResult(
|
|
|
356
359
|
container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
|
|
357
360
|
return container;
|
|
358
361
|
}
|
|
362
|
+
if (details.messages && details.messages.length > 0) {
|
|
363
|
+
appendTextSection(
|
|
364
|
+
container,
|
|
365
|
+
theme,
|
|
366
|
+
"Messages",
|
|
367
|
+
details.messages.map((message) => message.message).join("\n\n"),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
359
370
|
const output = details.output ?? "";
|
|
360
371
|
if (status === "completed") {
|
|
361
372
|
if (output.length > 0) {
|
|
@@ -28,6 +28,11 @@ export const ForkOwnershipRecordSchema = Type.Object({
|
|
|
28
28
|
direct_parent_id: Type.String(),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
const DeliveryEvidenceMessageSchema = Type.Object({
|
|
32
|
+
delivery_id: Type.Optional(Type.String()),
|
|
33
|
+
message_id: Type.Optional(Type.String()),
|
|
34
|
+
});
|
|
35
|
+
|
|
31
36
|
/** Parses durable custom-message and wait-tool Delivery Evidence details. */
|
|
32
37
|
export const DeliveryEvidenceDetailsSchema = Type.Object({
|
|
33
38
|
event: Type.Optional(Type.String()),
|
|
@@ -35,6 +40,7 @@ export const DeliveryEvidenceDetailsSchema = Type.Object({
|
|
|
35
40
|
source_turn_id: Type.String(),
|
|
36
41
|
delivery_id: Type.Optional(Type.String()),
|
|
37
42
|
message_id: Type.Optional(Type.String()),
|
|
43
|
+
messages: Type.Optional(Type.Array(DeliveryEvidenceMessageSchema)),
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
export type ChildSessionIdentityRecord = Static<typeof ChildSessionIdentityRecordSchema>;
|
|
@@ -411,7 +411,11 @@ export function findDeliveryEvidence(
|
|
|
411
411
|
return (
|
|
412
412
|
details.source_agent_id === sourceAgentId &&
|
|
413
413
|
details.source_turn_id === sourceTurnId &&
|
|
414
|
-
(details.delivery_id === deliveryId ||
|
|
414
|
+
(details.delivery_id === deliveryId ||
|
|
415
|
+
details.message_id === deliveryId ||
|
|
416
|
+
details.messages?.some(
|
|
417
|
+
(message) => message.delivery_id === deliveryId || message.message_id === deliveryId,
|
|
418
|
+
))
|
|
415
419
|
);
|
|
416
420
|
}
|
|
417
421
|
if (waitToolResult && details.event === "message") return false;
|
|
@@ -202,7 +202,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
202
202
|
name: "subagent_wait",
|
|
203
203
|
label: "Subagent Wait",
|
|
204
204
|
description:
|
|
205
|
-
"Wait for one direct child's oldest observable turn, or select an exact retained turn_id.
|
|
205
|
+
"Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout never cancels the child.",
|
|
206
206
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
207
207
|
parameters: options.schemas.subagent_wait,
|
|
208
208
|
async execute(_toolCallId, parameters, signal, onUpdate) {
|
|
@@ -75,8 +75,14 @@ export interface WaitMessageResult {
|
|
|
75
75
|
message: string;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Reports one terminal child turn and any earlier messages drained by the same wait. */
|
|
79
|
+
export interface WaitTurnResult extends TurnResult {
|
|
80
|
+
event: "turn";
|
|
81
|
+
messages?: WaitMessageResult[];
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
/** Reports one terminal child turn returned by subagent_wait. */
|
|
79
|
-
export type WaitResult = WaitMessageResult |
|
|
85
|
+
export type WaitResult = WaitMessageResult | WaitTurnResult;
|
|
80
86
|
|
|
81
87
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
82
88
|
export interface AgentSummary extends RuntimeProfile {
|
|
@@ -243,8 +249,6 @@ export interface RootConversationEndpoint {
|
|
|
243
249
|
/** Queue one typed coordinator message into the root conversation. */
|
|
244
250
|
queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
245
251
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
|
|
246
|
-
/** Report whether automatic delivery can start a new root turn without racing an active wait. */
|
|
247
|
-
isIdle(): boolean;
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
/** Stores root-owned agent identity, launch contract, availability, and latest activity. */
|