@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 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
162
- most 20 pending wait-only terminal results per source agent; Coordination
163
- Messages are not removed by that terminal-retention limit. Delivered messages
164
- include stable delivery, source-agent, and source-turn identities in persisted
165
- details.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -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
- await this.deliverToRecipient(delivery.destination_agent_id, message, () =>
1030
- this.isTerminalDeliveryCurrent(delivery),
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
- this.removePendingParentMessage(key, pending);
1791
- this.waitHandedDeliveryIds.add(delivery.delivery_id);
1792
- await this.deliverToRecipient(targetId, message, () =>
1793
- this.isCoordinationDeliveryCurrent(delivery),
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
- this.deliveryLedger = setCoordinationDeliveryError(
1802
- this.deliveryLedger,
1803
- delivery.delivery_id,
1804
- deliveryError,
1805
- );
1806
- const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1807
- if (current) this.persistCoordinationDeliveryState(current);
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;
@@ -99,6 +99,7 @@ function createRootConversationEndpoint(
99
99
  },
100
100
  );
101
101
  },
102
+ isIdle: () => context.isIdle(),
102
103
  hasDeliveryEvidence: (sourceAgentId, sourceTurnId, deliveryId) =>
103
104
  findDeliveryEvidence(
104
105
  context.sessionManager.getBranch(),
@@ -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