@ian-pascoe/pi-minimal-subagents 0.2.2 → 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
CHANGED
|
@@ -155,14 +155,15 @@ items retain their sequence; gaps from skipped malformed records are valid.
|
|
|
155
155
|
Claims can name only active, latest, or retained turns. Wait-returned messages
|
|
156
156
|
retain individual delivery evidence; terminal wait ownership remains durable
|
|
157
157
|
across reloads, forks, and newer turns. Automatic fallback retains its ordered
|
|
158
|
-
queue reservation
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
163
|
+
unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
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.
|
|
166
167
|
|
|
167
168
|
Deleting a child first verifies its session header and persistent identity,
|
|
168
169
|
then uses the optional `trash` command when available and falls back to
|
package/package.json
CHANGED
|
@@ -110,6 +110,24 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
110
110
|
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
111
111
|
}
|
|
112
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
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
114
132
|
export class MinimalSubagentsCoordinator {
|
|
115
133
|
private readonly agents = new Map<string, PersistedAgent>();
|
|
@@ -994,6 +1012,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
994
1012
|
if (this.automaticDeliveryKeys.has(deliveryKey)) return;
|
|
995
1013
|
this.automaticDeliveryKeys.add(deliveryKey);
|
|
996
1014
|
const graceMs = this.deliveryGraceMs();
|
|
1015
|
+
let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
|
|
997
1016
|
try {
|
|
998
1017
|
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
999
1018
|
if (delivery.destination_agent_id !== "root") {
|
|
@@ -1026,11 +1045,28 @@ export class MinimalSubagentsCoordinator {
|
|
|
1026
1045
|
usage: result.usage,
|
|
1027
1046
|
},
|
|
1028
1047
|
};
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
+
}
|
|
1055
|
+
await this.deliverToRecipient(
|
|
1056
|
+
delivery.destination_agent_id,
|
|
1057
|
+
combineCoordinatorMessages([
|
|
1058
|
+
...batchedCoordinationDeliveries.map((item) => item.message),
|
|
1059
|
+
message,
|
|
1060
|
+
]),
|
|
1061
|
+
() =>
|
|
1062
|
+
this.isTerminalDeliveryCurrent(delivery) &&
|
|
1063
|
+
batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1031
1064
|
);
|
|
1032
1065
|
});
|
|
1033
1066
|
} catch (error) {
|
|
1067
|
+
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1068
|
+
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1069
|
+
}
|
|
1034
1070
|
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1035
1071
|
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
1036
1072
|
this.deliveryLedger = setTerminalDeliveryError(
|
|
@@ -1765,6 +1801,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1765
1801
|
)
|
|
1766
1802
|
return;
|
|
1767
1803
|
|
|
1804
|
+
let automaticBatch = [delivery];
|
|
1768
1805
|
const operation = this.enqueueRecipientDelivery(targetId, async () => {
|
|
1769
1806
|
if (targetId !== "root") {
|
|
1770
1807
|
await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
|
|
@@ -1786,25 +1823,63 @@ export class MinimalSubagentsCoordinator {
|
|
|
1786
1823
|
pending.cancelGrace?.();
|
|
1787
1824
|
pending.cancelGrace = undefined;
|
|
1788
1825
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
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
|
+
]);
|
|
1842
|
+
}
|
|
1789
1843
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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;
|
|
1864
|
+
await this.deliverToRecipient(
|
|
1865
|
+
targetId,
|
|
1866
|
+
combineCoordinatorMessages(automaticBatch.map((item) => item.message)),
|
|
1867
|
+
() => automaticBatch.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1794
1868
|
);
|
|
1795
1869
|
});
|
|
1796
1870
|
void operation.catch((cause) => {
|
|
1797
|
-
this.removePendingParentMessage(key, pending);
|
|
1798
|
-
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1799
|
-
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1800
1871
|
const deliveryError = cause instanceof Error ? cause.message : String(cause);
|
|
1801
|
-
|
|
1802
|
-
this.
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
+
}
|
|
1808
1883
|
this.dependencies.notify?.({
|
|
1809
1884
|
type: "failure",
|
|
1810
1885
|
agentId: message.details.source_agent_id,
|
|
@@ -1815,6 +1890,25 @@ export class MinimalSubagentsCoordinator {
|
|
|
1815
1890
|
});
|
|
1816
1891
|
}
|
|
1817
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
|
+
|
|
1818
1912
|
private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
|
|
1819
1913
|
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1820
1914
|
if (!pendingMessages) return;
|
|
@@ -520,6 +520,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
520
520
|
private readonly modelById: ReadonlyMap<string, Model<any>>,
|
|
521
521
|
onSessionActivity?: () => void,
|
|
522
522
|
) {
|
|
523
|
+
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
524
|
+
session.agent.steeringMode = "all";
|
|
523
525
|
this.unsubscribe = session.subscribe((event) => {
|
|
524
526
|
if (event.type !== "entry_appended") return;
|
|
525
527
|
if (
|
|
@@ -188,6 +188,7 @@ export interface CoordinatorMessage {
|
|
|
188
188
|
status?: TurnStatus;
|
|
189
189
|
elapsed_ms?: number;
|
|
190
190
|
usage?: Usage;
|
|
191
|
+
messages?: Array<{ delivery_id?: string; message_id?: string }>;
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -248,6 +249,8 @@ export interface AgentSessionFactory {
|
|
|
248
249
|
export interface RootConversationEndpoint {
|
|
249
250
|
/** Queue one typed coordinator message into the root conversation. */
|
|
250
251
|
queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
252
|
+
/** Whether a coordinator message can start immediately instead of joining Pi's steer queue. */
|
|
253
|
+
isIdle(): boolean;
|
|
251
254
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
|
|
252
255
|
}
|
|
253
256
|
|