@ian-pascoe/pi-minimal-subagents 0.18.0 → 0.19.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/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.
|
|
3
|
+
"version": "0.19.0",
|
|
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.
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.7.0",
|
|
35
35
|
"minimatch": "^10.2.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
@@ -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(
|