@ian-pascoe/pi-minimal-subagents 0.2.1 → 0.2.3
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 +18 -15
- package/package.json +1 -1
- package/src/minimal-subagents-coordinator.ts +128 -131
- package/src/minimal-subagents-extension.ts +1 -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 +7 -1
- package/src/minimal-subagents-tools.ts +1 -1
- package/src/minimal-subagents-types.ts +10 -3
package/README.md
CHANGED
|
@@ -141,26 +141,29 @@ 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
|
-
|
|
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 while batching queued messages from one source turn into one
|
|
159
|
+
Pi steer. Root-bound messages remain batchable while the root turn is active; a
|
|
160
|
+
pending terminal result absorbs them. Child sessions drain all available steers
|
|
161
|
+
before the next model call. Destination-session Delivery Evidence still settles
|
|
162
|
+
each batched ledger item independently, preventing duplicate delivery and
|
|
159
163
|
unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
160
|
-
most 20 pending wait-only terminal results per source agent; Coordination
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
details.
|
|
164
|
+
most 20 pending wait-only terminal results per source agent; Coordination Messages
|
|
165
|
+
are not removed by that terminal-retention limit. Delivered messages include
|
|
166
|
+
stable delivery, source-agent, and source-turn identities in persisted details.
|
|
164
167
|
|
|
165
168
|
Deleting a child first verifies its session header and persistent identity,
|
|
166
169
|
then uses the optional `trash` command when available and falls back to
|
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,27 @@ 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) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
|
|
114
|
+
const latest = messages.at(-1);
|
|
115
|
+
if (!latest) throw new Error("Minimal subagents message batch must not be empty");
|
|
116
|
+
if (messages.length === 1) return latest;
|
|
117
|
+
const references = messages.flatMap(
|
|
118
|
+
(message) =>
|
|
119
|
+
message.details.messages ??
|
|
120
|
+
(message.details.delivery_id
|
|
121
|
+
? [{ delivery_id: message.details.delivery_id, message_id: message.details.message_id }]
|
|
122
|
+
: []),
|
|
123
|
+
);
|
|
124
|
+
return {
|
|
125
|
+
...latest,
|
|
126
|
+
content: messages.map((message) => message.content).join("\n\n"),
|
|
127
|
+
details: references.length > 0 ? { ...latest.details, messages: references } : latest.details,
|
|
128
|
+
};
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
@@ -124,11 +139,9 @@ export class MinimalSubagentsCoordinator {
|
|
|
124
139
|
private readonly waiters = new Map<string, Set<TurnWaiter>>();
|
|
125
140
|
private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
|
|
126
141
|
private readonly recipientQueues = new Map<string, Promise<unknown>>();
|
|
127
|
-
private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
|
|
128
142
|
private readonly automaticDeliveryKeys = new Set<string>();
|
|
129
143
|
private readonly automaticCoordinationDeliveryIds = new Set<string>();
|
|
130
144
|
private readonly waitHandedDeliveryIds = new Set<string>();
|
|
131
|
-
private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
|
|
132
145
|
private readonly backgroundOperations = new Set<Promise<void>>();
|
|
133
146
|
private acceptingOperations = true;
|
|
134
147
|
private lifecycleEpoch = 0;
|
|
@@ -363,15 +376,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
363
376
|
new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
|
|
364
377
|
);
|
|
365
378
|
}
|
|
366
|
-
const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
|
|
367
|
-
if (pendingMessage) return Promise.resolve(pendingMessage);
|
|
368
379
|
const retainedResult =
|
|
369
380
|
findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
|
|
370
381
|
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
371
382
|
if (retainedResult) {
|
|
383
|
+
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
372
384
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
373
|
-
return Promise.resolve(terminalWaitResult(retainedResult));
|
|
385
|
+
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
374
386
|
}
|
|
387
|
+
const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
|
|
388
|
+
if (pendingMessage) return Promise.resolve(pendingMessage);
|
|
375
389
|
if (agent.active_turn_id !== turnId) {
|
|
376
390
|
return Promise.reject(
|
|
377
391
|
new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
|
|
@@ -543,7 +557,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
543
557
|
this.agents.clear();
|
|
544
558
|
this.deliveryLedger = createDeliveryLedger();
|
|
545
559
|
this.pendingParentMessages.clear();
|
|
546
|
-
this.releaseAllRecipientIdleWaiters();
|
|
547
560
|
this.recipientQueues.clear();
|
|
548
561
|
this.backgroundOperations.clear();
|
|
549
562
|
await Promise.allSettled(
|
|
@@ -558,12 +571,10 @@ export class MinimalSubagentsCoordinator {
|
|
|
558
571
|
this.waiters.clear();
|
|
559
572
|
this.pendingParentMessages.clear();
|
|
560
573
|
this.waitHandedDeliveryIds.clear();
|
|
561
|
-
this.releaseAllRecipientIdleWaiters();
|
|
562
574
|
this.recipientQueues.clear();
|
|
563
575
|
this.backgroundOperations.clear();
|
|
564
576
|
this.automaticDeliveryKeys.clear();
|
|
565
577
|
this.automaticCoordinationDeliveryIds.clear();
|
|
566
|
-
this.automaticDeliveryClaimWaiters.clear();
|
|
567
578
|
this.deliveryLedger = createDeliveryLedger({
|
|
568
579
|
deliveries: snapshot.deliveries,
|
|
569
580
|
coordination_deliveries: snapshot.coordination_deliveries,
|
|
@@ -720,13 +731,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
720
731
|
await Promise.allSettled(scheduled);
|
|
721
732
|
}
|
|
722
733
|
|
|
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
734
|
/** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
|
|
731
735
|
async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
|
|
732
736
|
const activeRootChildren = this.childrenOf("root");
|
|
@@ -785,7 +789,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
785
789
|
shutdown(): Promise<void> {
|
|
786
790
|
if (this.shutdownPromise) return this.shutdownPromise;
|
|
787
791
|
this.acceptingOperations = false;
|
|
788
|
-
this.releaseAllRecipientIdleWaiters();
|
|
789
792
|
this.shutdownPromise = this.finishShutdown();
|
|
790
793
|
return this.shutdownPromise;
|
|
791
794
|
}
|
|
@@ -999,7 +1002,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
999
1002
|
});
|
|
1000
1003
|
}
|
|
1001
1004
|
if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
|
|
1002
|
-
this.markRecipientIdle(agent.agent_id);
|
|
1003
1005
|
}
|
|
1004
1006
|
|
|
1005
1007
|
private async deliverAutomaticResult(
|
|
@@ -1010,6 +1012,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1010
1012
|
if (this.automaticDeliveryKeys.has(deliveryKey)) return;
|
|
1011
1013
|
this.automaticDeliveryKeys.add(deliveryKey);
|
|
1012
1014
|
const graceMs = this.deliveryGraceMs();
|
|
1015
|
+
let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
|
|
1013
1016
|
try {
|
|
1014
1017
|
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
1015
1018
|
if (delivery.destination_agent_id !== "root") {
|
|
@@ -1024,15 +1027,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1024
1027
|
this.settleDelivery(delivery);
|
|
1025
1028
|
return;
|
|
1026
1029
|
}
|
|
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
1030
|
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1037
1031
|
if (this.hasDeliveryEvidence(delivery)) {
|
|
1038
1032
|
this.settleDelivery(delivery);
|
|
@@ -1051,14 +1045,28 @@ export class MinimalSubagentsCoordinator {
|
|
|
1051
1045
|
usage: result.usage,
|
|
1052
1046
|
},
|
|
1053
1047
|
};
|
|
1048
|
+
batchedCoordinationDeliveries = this.takePendingParentDeliveryBatch(
|
|
1049
|
+
deliveryKey,
|
|
1050
|
+
delivery.destination_agent_id,
|
|
1051
|
+
);
|
|
1052
|
+
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1053
|
+
this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
|
|
1054
|
+
}
|
|
1054
1055
|
await this.deliverToRecipient(
|
|
1055
1056
|
delivery.destination_agent_id,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1057
|
+
combineCoordinatorMessages([
|
|
1058
|
+
...batchedCoordinationDeliveries.map((item) => item.message),
|
|
1059
|
+
message,
|
|
1060
|
+
]),
|
|
1061
|
+
() =>
|
|
1062
|
+
this.isTerminalDeliveryCurrent(delivery) &&
|
|
1063
|
+
batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1059
1064
|
);
|
|
1060
1065
|
});
|
|
1061
1066
|
} catch (error) {
|
|
1067
|
+
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1068
|
+
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1069
|
+
}
|
|
1062
1070
|
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1063
1071
|
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
1064
1072
|
this.deliveryLedger = setTerminalDeliveryError(
|
|
@@ -1263,7 +1271,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1263
1271
|
targetId: string,
|
|
1264
1272
|
message: CoordinatorMessage,
|
|
1265
1273
|
isCurrentDelivery: () => boolean = () => true,
|
|
1266
|
-
requireIdleRecipient = false,
|
|
1267
1274
|
): Promise<void> {
|
|
1268
1275
|
if (!isCurrentDelivery()) {
|
|
1269
1276
|
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
@@ -1281,9 +1288,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1281
1288
|
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1282
1289
|
}
|
|
1283
1290
|
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
1291
|
if (target.active_turn_id || runtime.isRunning) {
|
|
1288
1292
|
await runtime.queueCoordinatorMessage(visibleMessage);
|
|
1289
1293
|
return;
|
|
@@ -1367,8 +1371,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1367
1371
|
private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
|
|
1368
1372
|
this.deliveryLedger = transition.ledger;
|
|
1369
1373
|
for (const delivery of transition.prunedTerminalDeliveries) {
|
|
1370
|
-
const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1371
|
-
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1372
1374
|
this.dependencies.registry.append(
|
|
1373
1375
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
|
|
1374
1376
|
source_agent_id: delivery.source_agent_id,
|
|
@@ -1504,16 +1506,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1504
1506
|
}
|
|
1505
1507
|
|
|
1506
1508
|
private pruneDeliveryStateForDeletedAgent(agentId: string): void {
|
|
1507
|
-
const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
|
|
1508
1509
|
const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
|
|
1509
1510
|
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
1511
|
for (const delivery of previousCoordinationDeliveries) {
|
|
1518
1512
|
if (!this.isCoordinationDeliveryCurrent(delivery)) {
|
|
1519
1513
|
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
@@ -1673,7 +1667,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1673
1667
|
(candidate) => candidate.callerId === destinationAgentId,
|
|
1674
1668
|
);
|
|
1675
1669
|
if (!waiter) return false;
|
|
1676
|
-
this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
|
|
1677
1670
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1678
1671
|
this.deliveryLedger,
|
|
1679
1672
|
delivery.delivery_id,
|
|
@@ -1698,7 +1691,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1698
1691
|
callerId: string,
|
|
1699
1692
|
sourceAgentId: string,
|
|
1700
1693
|
sourceTurnId: string,
|
|
1701
|
-
):
|
|
1694
|
+
): WaitMessageResult | undefined {
|
|
1702
1695
|
const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
|
|
1703
1696
|
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1704
1697
|
const index = pendingMessages?.findIndex(
|
|
@@ -1715,7 +1708,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1715
1708
|
)
|
|
1716
1709
|
.sort((left, right) => left.sequence - right.sequence)[0];
|
|
1717
1710
|
if (!retained) return undefined;
|
|
1718
|
-
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1719
1711
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
1720
1712
|
this.deliveryLedger,
|
|
1721
1713
|
retained.delivery_id,
|
|
@@ -1735,7 +1727,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1735
1727
|
}
|
|
1736
1728
|
const pending = pendingMessages[index];
|
|
1737
1729
|
if (!pending) return undefined;
|
|
1738
|
-
this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
|
|
1739
1730
|
const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
|
|
1740
1731
|
if (delivery) {
|
|
1741
1732
|
this.deliveryLedger = setCoordinationDeliveryPath(
|
|
@@ -1747,10 +1738,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1747
1738
|
if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
|
|
1748
1739
|
this.waitHandedDeliveryIds.add(delivery.delivery_id);
|
|
1749
1740
|
}
|
|
1750
|
-
const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
|
|
1751
|
-
if (sourceResult?.turn_id === sourceTurnId) {
|
|
1752
|
-
this.setTerminalDeliveryPathToWait(callerId, sourceResult);
|
|
1753
|
-
}
|
|
1754
1741
|
pending.claimed = true;
|
|
1755
1742
|
pending.cancelGrace?.();
|
|
1756
1743
|
pending.releaseClaim();
|
|
@@ -1766,6 +1753,20 @@ export class MinimalSubagentsCoordinator {
|
|
|
1766
1753
|
};
|
|
1767
1754
|
}
|
|
1768
1755
|
|
|
1756
|
+
private drainPendingParentMessages(
|
|
1757
|
+
callerId: string,
|
|
1758
|
+
sourceAgentId: string,
|
|
1759
|
+
sourceTurnId: string,
|
|
1760
|
+
): WaitMessageResult[] {
|
|
1761
|
+
const messages: WaitMessageResult[] = [];
|
|
1762
|
+
let message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
|
|
1763
|
+
while (message) {
|
|
1764
|
+
messages.push(message);
|
|
1765
|
+
message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
|
|
1766
|
+
}
|
|
1767
|
+
return messages;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1769
1770
|
private queuePendingParentMessage(
|
|
1770
1771
|
targetId: string,
|
|
1771
1772
|
message: CoordinatorMessage,
|
|
@@ -1800,6 +1801,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1800
1801
|
)
|
|
1801
1802
|
return;
|
|
1802
1803
|
|
|
1804
|
+
let automaticBatch = [delivery];
|
|
1803
1805
|
const operation = this.enqueueRecipientDelivery(targetId, async () => {
|
|
1804
1806
|
if (targetId !== "root") {
|
|
1805
1807
|
await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
|
|
@@ -1821,34 +1823,63 @@ export class MinimalSubagentsCoordinator {
|
|
|
1821
1823
|
pending.cancelGrace?.();
|
|
1822
1824
|
pending.cancelGrace = undefined;
|
|
1823
1825
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1824
|
-
while (
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1826
|
+
while (
|
|
1827
|
+
targetId === "root" &&
|
|
1828
|
+
!this.dependencies.root.isIdle() &&
|
|
1829
|
+
!findTerminalDelivery(
|
|
1830
|
+
this.deliveryLedger,
|
|
1831
|
+
message.details.source_agent_id,
|
|
1832
|
+
message.details.source_turn_id,
|
|
1833
|
+
) &&
|
|
1834
|
+
this.acceptingOperations &&
|
|
1835
|
+
!pending.claimed &&
|
|
1836
|
+
!turnClaimed()
|
|
1837
|
+
) {
|
|
1838
|
+
await Promise.race([
|
|
1839
|
+
pending.claimPromise,
|
|
1840
|
+
new Promise((resolve) => setTimeout(resolve, 25)),
|
|
1841
|
+
]);
|
|
1829
1842
|
}
|
|
1830
1843
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1831
|
-
|
|
1832
|
-
|
|
1844
|
+
const terminalDelivery = findTerminalDelivery(
|
|
1845
|
+
this.deliveryLedger,
|
|
1846
|
+
message.details.source_agent_id,
|
|
1847
|
+
message.details.source_turn_id,
|
|
1848
|
+
);
|
|
1849
|
+
if (
|
|
1850
|
+
terminalDelivery?.destination_agent_id === targetId &&
|
|
1851
|
+
terminalDelivery.path === "message"
|
|
1852
|
+
) {
|
|
1853
|
+
for (const queued of this.pendingParentMessages.get(key) ?? []) {
|
|
1854
|
+
queued.cancelGrace?.();
|
|
1855
|
+
queued.releaseClaim();
|
|
1856
|
+
}
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
automaticBatch = this.takePendingParentDeliveryBatch(key, targetId);
|
|
1860
|
+
for (const batchedDelivery of automaticBatch) {
|
|
1861
|
+
this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
|
|
1862
|
+
}
|
|
1863
|
+
if (automaticBatch.length === 0) return;
|
|
1833
1864
|
await this.deliverToRecipient(
|
|
1834
1865
|
targetId,
|
|
1835
|
-
message,
|
|
1836
|
-
() => this.isCoordinationDeliveryCurrent(
|
|
1837
|
-
true,
|
|
1866
|
+
combineCoordinatorMessages(automaticBatch.map((item) => item.message)),
|
|
1867
|
+
() => automaticBatch.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1838
1868
|
);
|
|
1839
1869
|
});
|
|
1840
1870
|
void operation.catch((cause) => {
|
|
1841
|
-
this.removePendingParentMessage(key, pending);
|
|
1842
|
-
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1843
|
-
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1844
1871
|
const deliveryError = cause instanceof Error ? cause.message : String(cause);
|
|
1845
|
-
|
|
1846
|
-
this.
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1872
|
+
for (const batchedDelivery of automaticBatch) {
|
|
1873
|
+
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1874
|
+
if (!this.isCoordinationDeliveryCurrent(batchedDelivery)) continue;
|
|
1875
|
+
this.deliveryLedger = setCoordinationDeliveryError(
|
|
1876
|
+
this.deliveryLedger,
|
|
1877
|
+
batchedDelivery.delivery_id,
|
|
1878
|
+
deliveryError,
|
|
1879
|
+
);
|
|
1880
|
+
const current = findCoordinationDelivery(this.deliveryLedger, batchedDelivery.delivery_id);
|
|
1881
|
+
if (current) this.persistCoordinationDeliveryState(current);
|
|
1882
|
+
}
|
|
1852
1883
|
this.dependencies.notify?.({
|
|
1853
1884
|
type: "failure",
|
|
1854
1885
|
agentId: message.details.source_agent_id,
|
|
@@ -1859,6 +1890,25 @@ export class MinimalSubagentsCoordinator {
|
|
|
1859
1890
|
});
|
|
1860
1891
|
}
|
|
1861
1892
|
|
|
1893
|
+
private takePendingParentDeliveryBatch(
|
|
1894
|
+
key: string,
|
|
1895
|
+
targetId: string,
|
|
1896
|
+
): PersistedCoordinationDelivery[] {
|
|
1897
|
+
const batch = (this.pendingParentMessages.get(key) ?? []).filter(
|
|
1898
|
+
(pending) => pending.destinationAgentId === targetId && !pending.claimed,
|
|
1899
|
+
);
|
|
1900
|
+
for (const pending of batch) {
|
|
1901
|
+
pending.claimed = true;
|
|
1902
|
+
pending.cancelGrace?.();
|
|
1903
|
+
pending.releaseClaim();
|
|
1904
|
+
this.removePendingParentMessage(key, pending);
|
|
1905
|
+
}
|
|
1906
|
+
return batch.flatMap((pending) => {
|
|
1907
|
+
const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
|
|
1908
|
+
return delivery ? [delivery] : [];
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1862
1912
|
private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
|
|
1863
1913
|
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1864
1914
|
if (!pendingMessages) return;
|
|
@@ -1874,14 +1924,12 @@ export class MinimalSubagentsCoordinator {
|
|
|
1874
1924
|
}
|
|
1875
1925
|
|
|
1876
1926
|
private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
|
|
1877
|
-
const key = agentDeliveryKey(result.agent_id, result.turn_id);
|
|
1878
1927
|
const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1879
1928
|
if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
|
|
1880
1929
|
this.applyDeliveryLedgerTransition(
|
|
1881
1930
|
setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
|
|
1882
1931
|
);
|
|
1883
1932
|
const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
|
|
1884
|
-
this.releaseAutomaticDeliveryClaimWaiters(key);
|
|
1885
1933
|
if (!retained) return;
|
|
1886
1934
|
this.dependencies.registry.append(
|
|
1887
1935
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
@@ -1890,57 +1938,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1890
1938
|
);
|
|
1891
1939
|
}
|
|
1892
1940
|
|
|
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
1941
|
private async cancelDuringShutdown(agentId: string): Promise<void> {
|
|
1945
1942
|
const target = this.agents.get(agentId);
|
|
1946
1943
|
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,
|
|
@@ -100,6 +99,7 @@ function createRootConversationEndpoint(
|
|
|
100
99
|
},
|
|
101
100
|
);
|
|
102
101
|
},
|
|
102
|
+
isIdle: () => context.isIdle(),
|
|
103
103
|
hasDeliveryEvidence: (sourceAgentId, sourceTurnId, deliveryId) =>
|
|
104
104
|
findDeliveryEvidence(
|
|
105
105
|
context.sessionManager.getBranch(),
|
|
@@ -107,7 +107,6 @@ function createRootConversationEndpoint(
|
|
|
107
107
|
sourceTurnId,
|
|
108
108
|
deliveryId,
|
|
109
109
|
),
|
|
110
|
-
isIdle: () => context.isIdle(),
|
|
111
110
|
};
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -331,7 +330,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
331
330
|
this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
|
|
332
331
|
this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
|
|
333
332
|
this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
|
|
334
|
-
this.pi.on("agent_settled", (event, context) => this.releaseSettledRecipient(event, context));
|
|
335
333
|
this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
|
|
336
334
|
}
|
|
337
335
|
|
|
@@ -554,12 +552,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
554
552
|
}
|
|
555
553
|
}
|
|
556
554
|
|
|
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
555
|
private async shutdownSession(
|
|
564
556
|
event: SessionShutdownEvent,
|
|
565
557
|
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;
|
|
@@ -516,6 +520,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
516
520
|
private readonly modelById: ReadonlyMap<string, Model<any>>,
|
|
517
521
|
onSessionActivity?: () => void,
|
|
518
522
|
) {
|
|
523
|
+
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
524
|
+
session.agent.steeringMode = "all";
|
|
519
525
|
this.unsubscribe = session.subscribe((event) => {
|
|
520
526
|
if (event.type !== "entry_appended") return;
|
|
521
527
|
if (
|
|
@@ -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 {
|
|
@@ -182,6 +188,7 @@ export interface CoordinatorMessage {
|
|
|
182
188
|
status?: TurnStatus;
|
|
183
189
|
elapsed_ms?: number;
|
|
184
190
|
usage?: Usage;
|
|
191
|
+
messages?: Array<{ delivery_id?: string; message_id?: string }>;
|
|
185
192
|
};
|
|
186
193
|
}
|
|
187
194
|
|
|
@@ -242,9 +249,9 @@ export interface AgentSessionFactory {
|
|
|
242
249
|
export interface RootConversationEndpoint {
|
|
243
250
|
/** Queue one typed coordinator message into the root conversation. */
|
|
244
251
|
queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
245
|
-
|
|
246
|
-
/** Report whether automatic delivery can start a new root turn without racing an active wait. */
|
|
252
|
+
/** Whether a coordinator message can start immediately instead of joining Pi's steer queue. */
|
|
247
253
|
isIdle(): boolean;
|
|
254
|
+
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
/** Stores root-owned agent identity, launch contract, availability, and latest activity. */
|