@ian-pascoe/pi-minimal-subagents 0.8.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -570,7 +570,7 @@ export class MinimalSubagentsCoordinator {
570
570
  const runtime = this.runtimes.get(agent.agent_id);
571
571
  try {
572
572
  if (agent.active_turn_id) await this.cancelActiveTurn(agent);
573
- runtime?.dispose();
573
+ await runtime?.dispose();
574
574
  this.runtimes.delete(agent.agent_id);
575
575
  if (agent.session_file) {
576
576
  await this.dependencies.sessions.trashSession(agent);
@@ -622,7 +622,7 @@ export class MinimalSubagentsCoordinator {
622
622
  await Promise.allSettled(
623
623
  abandonedRuntimes.map((runtime) => (runtime.isRunning ? runtime.abort() : Promise.resolve())),
624
624
  );
625
- for (const runtime of abandonedRuntimes) runtime.dispose();
625
+ for (const runtime of abandonedRuntimes) await runtime.dispose();
626
626
  this.runtimes.clear();
627
627
  this.runtimeInitializations.clear();
628
628
  this.pendingAgentIds.clear();
@@ -892,7 +892,7 @@ export class MinimalSubagentsCoordinator {
892
892
  await Promise.allSettled(this.runtimeInitializations.values());
893
893
  await Promise.allSettled(this.backgroundOperations);
894
894
  await Promise.allSettled(this.recipientQueues.values());
895
- for (const runtime of this.runtimes.values()) runtime.dispose();
895
+ for (const runtime of this.runtimes.values()) await runtime.dispose();
896
896
  this.runtimes.clear();
897
897
  }
898
898
 
@@ -910,7 +910,7 @@ export class MinimalSubagentsCoordinator {
910
910
  const runtime = await this.ensureRuntime(agent);
911
911
  if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) {
912
912
  if (!this.agents.has(agentId) || !this.acceptingOperations) {
913
- runtime.dispose();
913
+ await runtime.dispose();
914
914
  this.runtimes.delete(agentId);
915
915
  }
916
916
  return;
@@ -943,13 +943,13 @@ export class MinimalSubagentsCoordinator {
943
943
  const openingForTurn = agent.active_turn_id !== undefined;
944
944
  const initialization = this.dependencies.sessions
945
945
  .openRuntime(agent)
946
- .then((runtime) => {
946
+ .then(async (runtime) => {
947
947
  if (this.agents.get(agent.agent_id) !== agent) {
948
- runtime.dispose();
948
+ await runtime.dispose();
949
949
  throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
950
950
  }
951
951
  if (!runtime.sessionLeafId) {
952
- runtime.dispose();
952
+ await runtime.dispose();
953
953
  throw new Error(
954
954
  `Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
955
955
  );
@@ -80,6 +80,7 @@ import { MinimalSubagentsUiController } from "./minimal-subagents-ui.js";
80
80
  import type {
81
81
  AgentSessionFactory,
82
82
  CallerSnapshot,
83
+ ChildSessionObserver,
83
84
  CoordinatorNotification,
84
85
  ForkSnapshot,
85
86
  PersistedAgent,
@@ -585,6 +586,19 @@ export class MinimalSubagentsLifecycleController {
585
586
  projectTrusted: context.isProjectTrusted(),
586
587
  maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
587
588
  onChildSessionActivity: () => activeCoordinator?.scheduleDeliveryReconciliation(),
589
+ observeSession: (session, agentId, resourceInputs) => {
590
+ let observer: ChildSessionObserver | undefined;
591
+ this.pi.events.emit("pi-minimal-subagents:observe-session", {
592
+ rootSessionId,
593
+ agentId,
594
+ session,
595
+ resourceInputs,
596
+ attach: (attached: ChildSessionObserver) => {
597
+ observer = attached;
598
+ },
599
+ });
600
+ return observer;
601
+ },
588
602
  getCoordinatorTools: (callerId) => {
589
603
  const coordinator = requireActiveCoordinator();
590
604
  return createCoordinatorToolDefinitions({
@@ -17,6 +17,7 @@ import {
17
17
  SettingsManager,
18
18
  sessionEntryToContextMessages,
19
19
  type AgentSessionEvent,
20
+ type Extension,
20
21
  type SessionEntry,
21
22
  type ToolDefinition,
22
23
  } from "@earendil-works/pi-coding-agent";
@@ -53,6 +54,7 @@ import { addMinimalSubagentsUsage } from "./minimal-subagents-usage.js";
53
54
  import type {
54
55
  AgentSessionFactory,
55
56
  ChildAgentRuntime,
57
+ ChildSessionObserver,
56
58
  ChildAgentTranscriptSnapshot,
57
59
  CoordinatorMessage,
58
60
  PersistedAgent,
@@ -180,6 +182,15 @@ export interface PiAgentSessionFactoryOptions {
180
182
  sessionFileTrash?: SessionFileTrashCapability;
181
183
  getCoordinatorTools: (callerId: string) => ToolDefinition[];
182
184
  onChildSessionActivity?: () => void;
185
+ observeSession?: (
186
+ session: AgentSession,
187
+ agentId: string,
188
+ resourceInputs: {
189
+ agentDir: string;
190
+ extensions: readonly Pick<Extension, "path" | "resolvedPath" | "sourceInfo" | "hidden">[];
191
+ flagValues: ReadonlyMap<string, boolean | string>;
192
+ },
193
+ ) => ChildSessionObserver | undefined;
183
194
  }
184
195
 
185
196
  /** Build one child prompt using the active delegation depth rather than persisted launch state. */
@@ -494,7 +505,7 @@ function collectChildTurnOutcome(
494
505
  error: "No terminal assistant response",
495
506
  };
496
507
  }
497
- if (finalAssistant.stopReason === "aborted") {
508
+ if (aborted || finalAssistant.stopReason === "aborted") {
498
509
  return {
499
510
  status: "cancelled",
500
511
  output: assistantText(finalAssistant),
@@ -554,6 +565,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
554
565
  private readonly modelRuntime: ModelRuntime,
555
566
  private readonly modelById: ReadonlyMap<string, Model<any>>,
556
567
  onSessionActivity?: () => void,
568
+ private readonly observer?: ChildSessionObserver,
557
569
  ) {
558
570
  // Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
559
571
  session.agent.steeringMode = "all";
@@ -613,12 +625,16 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
613
625
 
614
626
  async abort(): Promise<void> {
615
627
  this.aborted = true;
616
- await this.session.abort();
628
+ await Promise.all([this.observer?.abort(), this.session.abort()]);
617
629
  }
618
630
 
619
- dispose(): void {
631
+ async dispose(): Promise<void> {
620
632
  this.unsubscribe();
621
- this.session.dispose();
633
+ try {
634
+ await this.observer?.dispose();
635
+ } finally {
636
+ this.session.dispose();
637
+ }
622
638
  }
623
639
 
624
640
  getRuntimeProfile(): RuntimeProfile | undefined {
@@ -715,7 +731,20 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
715
731
 
716
732
  private async captureTurn(operation: () => Promise<void>): Promise<RuntimeTurnOutcome> {
717
733
  this.aborted = false;
718
- return captureChildTurnOutcome(this.session, operation, () => this.aborted);
734
+ this.observer?.beginTurn();
735
+ return captureChildTurnOutcome(
736
+ this.session,
737
+ async () => {
738
+ try {
739
+ await operation();
740
+ if (!this.aborted) await this.observer?.finishTurn();
741
+ } catch (error) {
742
+ await this.observer?.abort();
743
+ throw error;
744
+ }
745
+ },
746
+ () => this.aborted,
747
+ );
719
748
  }
720
749
 
721
750
  private async compactImportedContext(
@@ -1187,6 +1216,18 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
1187
1216
  modelRuntime,
1188
1217
  this.modelById,
1189
1218
  this.options.onChildSessionActivity,
1219
+ this.options.observeSession?.(session, agent.agent_id, {
1220
+ agentDir: this.options.agentDir,
1221
+ extensions: resourceLoader
1222
+ .getExtensions()
1223
+ .extensions.map(({ path, resolvedPath, sourceInfo, hidden }) => ({
1224
+ path,
1225
+ resolvedPath,
1226
+ sourceInfo,
1227
+ hidden,
1228
+ })),
1229
+ flagValues: new Map(resourceLoader.getExtensions().runtime.flagValues),
1230
+ }),
1190
1231
  );
1191
1232
  }
1192
1233
 
@@ -224,6 +224,14 @@ export interface CoordinatorMessage {
224
224
  };
225
225
  }
226
226
 
227
+ /** Optional review work attached by the root and awaited by the existing child owner. */
228
+ export interface ChildSessionObserver {
229
+ beginTurn(): void;
230
+ finishTurn(): Promise<void>;
231
+ abort(): Promise<void>;
232
+ dispose(): Promise<void>;
233
+ }
234
+
227
235
  /** Process-local adapter around one SDK-created Pi child session. */
228
236
  export interface ChildAgentRuntime {
229
237
  readonly sessionLeafId: string | undefined;
@@ -238,7 +246,7 @@ export interface ChildAgentRuntime {
238
246
  /** Queue one typed coordinator message into the child session. */
239
247
  queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
240
248
  abort(): Promise<void>;
241
- dispose(): void;
249
+ dispose(): void | Promise<void>;
242
250
  /** Return the live Runtime Profile, or undefined when the SDK session has no model. */
243
251
  getRuntimeProfile(): RuntimeProfile | undefined;
244
252
  /** Return the effective ordinary tools after child extensions apply runtime adapters. */