@ian-pascoe/pi-minimal-subagents 0.18.0 → 0.19.1

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
@@ -433,6 +433,13 @@ process-local fork handoff is lost, recovery reads only the destination's
433
433
  selected branch and proceeds only when its canonical `parentSession` proves the
434
434
  source file; it never substitutes the source session's newer head.
435
435
 
436
+ Every child session also carries an Agent Lineage Record, defined by
437
+ `@ian-pascoe/pi-utils/agent-lineage`, naming its own session, its Root Agent's
438
+ session, its agent ID, and its parent agent. Other extensions read it to find a
439
+ Child Agent's ancestors and descendants without depending on this package; it
440
+ grants them nothing. A fork clone writes its own record when its destination
441
+ adopts it. Children created before the record existed have none.
442
+
436
443
  ## Tool annotations
437
444
 
438
445
  The coordinator tools declare MCP-style `annotations`, which Pi reports through `pi.getAllTools()` for permission extensions and does not send to model providers. `subagent` is destructive and open-world because a Child Agent can use any tool it is granted. `agent_message` is not read-only but is non-destructive and closed-world. `subagent_cancel` is non-destructive and idempotent; `subagent_delete` is destructive and idempotent. `subagent_status` and `subagent_wait` are read-only (`subagent_wait` consumes queued deliveries, so it is not idempotent).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -31,7 +31,7 @@
31
31
  "provenance": true
32
32
  },
33
33
  "dependencies": {
34
- "@ian-pascoe/pi-utils": "^0.6.0",
34
+ "@ian-pascoe/pi-utils": "^0.7.1",
35
35
  "minimatch": "^10.2.6"
36
36
  },
37
37
  "peerDependencies": {
@@ -39,7 +39,7 @@
39
39
  "@earendil-works/pi-ai": ">=1.1.0",
40
40
  "@earendil-works/pi-coding-agent": ">=1.1.0",
41
41
  "@earendil-works/pi-tui": ">=1.1.0",
42
- "typebox": "*"
42
+ "typebox": ">=1"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=22.19.0"
@@ -21,6 +21,12 @@ import {
21
21
  type SessionEntry,
22
22
  type ToolDefinition,
23
23
  } from "@earendil-works/pi-coding-agent";
24
+ import {
25
+ AGENT_LINEAGE_ENTRY_TYPE,
26
+ agentLineageRecord,
27
+ readAgentLineage,
28
+ type AgentLineage,
29
+ } from "@ian-pascoe/pi-utils/agent-lineage";
24
30
  import type { Static, TSchema } from "typebox";
25
31
  import { Value } from "typebox/value";
26
32
  import {
@@ -284,6 +290,20 @@ function appendImportedMessage(sessionManager: SessionManager, message: AgentMes
284
290
  sessionManager.appendMessage(message as Parameters<SessionManager["appendMessage"]>[0]);
285
291
  }
286
292
 
293
+ /** The shared Agent Lineage of a child session, readable by packages that do not import this one. */
294
+ function agentLineage(
295
+ agent: PersistedAgent,
296
+ rootSessionId: string,
297
+ sessionId: string,
298
+ ): AgentLineage {
299
+ return {
300
+ sessionId,
301
+ rootSessionId,
302
+ agentId: agent.agent_id,
303
+ parentAgentId: agent.parent_id === "root" ? null : agent.parent_id,
304
+ };
305
+ }
306
+
287
307
  /** Create and force-flush a writable child JSONL identity before its first model response. */
288
308
  export function createPersistentChildIdentity(
289
309
  options: PersistentIdentityOptions,
@@ -296,6 +316,12 @@ export function createPersistentChildIdentity(
296
316
  direct_parent_id: options.agent.parent_id,
297
317
  created_at: options.agent.created_at,
298
318
  });
319
+ sessionManager.appendCustomEntry(
320
+ AGENT_LINEAGE_ENTRY_TYPE,
321
+ agentLineageRecord(
322
+ agentLineage(options.agent, options.rootSessionId, sessionManager.getSessionId()),
323
+ ),
324
+ );
299
325
  sessionManager.appendSessionInfo(`[subagent] ${options.agent.agent_id}`);
300
326
  sessionManager.appendModelChange(
301
327
  options.agent.launch_contract.model.slice(0, options.agent.launch_contract.model.indexOf("/")),
@@ -1043,6 +1069,15 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
1043
1069
  direct_parent_id: agent.parent_id,
1044
1070
  });
1045
1071
  }
1072
+ // A clone inherits its source's record, which names the source session; record this one's own.
1073
+ const lineage = agentLineage(agent, this.options.rootSessionId, sessionManager.getSessionId());
1074
+ const recorded = readAgentLineage(lineage.sessionId, sessionManager.getBranch());
1075
+ if (
1076
+ recorded?.rootSessionId !== lineage.rootSessionId ||
1077
+ recorded.agentId !== lineage.agentId ||
1078
+ recorded.parentAgentId !== lineage.parentAgentId
1079
+ )
1080
+ sessionManager.appendCustomEntry(AGENT_LINEAGE_ENTRY_TYPE, agentLineageRecord(lineage));
1046
1081
  return {
1047
1082
  sessionFile,
1048
1083
  sessionId: sessionManager.getSessionId(),
@@ -13,6 +13,8 @@ import {
13
13
  type Component,
14
14
  type OverlayHandle,
15
15
  type TUI,
16
+ type TuiMouseEvent,
17
+ type TuiMouseEventResult,
16
18
  } from "@earendil-works/pi-tui";
17
19
  import { hintLine, joinInline, noticeText } from "@ian-pascoe/pi-utils/ui";
18
20
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
@@ -143,6 +145,9 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
143
145
  private transcriptLayout?: TranscriptLayout;
144
146
  private readonly transcriptCache = createTranscriptRenderCache();
145
147
  private bodyHeight = 1;
148
+ /** Panel row of the first body line in the last render; undefined when no body was drawn. */
149
+ private bodyTop?: number;
150
+ private mousePressedAgentId?: string;
146
151
  private ensureSelectionVisible = true;
147
152
  private toolOutputExpanded = false;
148
153
  private disposed = false;
@@ -204,6 +209,50 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
204
209
  this.tui.requestRender();
205
210
  }
206
211
 
212
+ /**
213
+ * Handle the mouse like Pi's SelectList. The wheel mirrors the arrow keys: one row of selection
214
+ * per event in the tree, the wheel's logical lines in the transcript. In the tree, pressing a
215
+ * row selects it and clicking it opens its transcript. Other events stay unhandled so Pi's
216
+ * screen text selection still copies transcript text.
217
+ */
218
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
219
+ if (event.type === "wheel") {
220
+ if (!event.wheelDelta) return undefined;
221
+ this.moveSelection(
222
+ this.view === "transcript" ? event.wheelDelta : Math.sign(event.wheelDelta),
223
+ );
224
+ return { handled: true };
225
+ }
226
+ if (this.view !== "tree" || event.button !== "left") return undefined;
227
+ if (event.type === "press") {
228
+ const agentId = this.agentIdAtRow(event.y);
229
+ this.mousePressedAgentId = agentId;
230
+ if (!agentId) return undefined;
231
+ this.selectedAgentId = agentId;
232
+ this.ensureSelectionVisible = true;
233
+ return { handled: true, focus: true };
234
+ }
235
+ if (event.type === "click") {
236
+ const agentId = this.mousePressedAgentId ?? this.agentIdAtRow(event.y);
237
+ this.mousePressedAgentId = undefined;
238
+ if (!agentId || !this.flattened.some(({ agent }) => agent.agent_id === agentId)) {
239
+ return undefined;
240
+ }
241
+ this.selectedAgentId = agentId;
242
+ this.openSelectedTranscript();
243
+ return { handled: true };
244
+ }
245
+ return undefined;
246
+ }
247
+
248
+ /** The Child Agent drawn on a panel row in the tree, if any. */
249
+ private agentIdAtRow(row: number): string | undefined {
250
+ if (this.bodyTop === undefined) return undefined;
251
+ const bodyRow = row - this.bodyTop;
252
+ if (bodyRow < 0 || bodyRow >= this.bodyHeight) return undefined;
253
+ return this.flattened[this.scrollOffset + bodyRow]?.agent.agent_id;
254
+ }
255
+
207
256
  /** Render the tree or Child Session Transcript in Pi's selector frame, within the terminal. */
208
257
  render(width: number): string[] {
209
258
  if (width <= 0) return [];
@@ -214,6 +263,7 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
214
263
  this.tui.terminal.rows - 2 * STATUS_PANEL_MARGIN,
215
264
  ),
216
265
  );
266
+ this.bodyTop = undefined;
217
267
  if (width < 6 || height < 5) {
218
268
  const cancelKey = this.keybindings.getKeys("tui.select.cancel").join("/");
219
269
  return new Text(this.theme.fg("muted", `${cancelKey} back · Enlarge terminal`), 0, 0)
@@ -235,6 +285,8 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
235
285
  ].slice(0, Math.min(3, height - 4));
236
286
  const visibleHeader = header.slice(0, Math.max(1, height - help.length - 3));
237
287
  this.bodyHeight = Math.max(1, height - 2 - visibleHeader.length - help.length);
288
+ // The top border precedes the header.
289
+ this.bodyTop = 1 + visibleHeader.length;
238
290
  let body: string[];
239
291
  if (transcriptView) {
240
292
  const layout = renderTranscriptSnapshot(