@gotgenes/pi-subagents 17.4.0 → 18.0.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/CHANGELOG.md +22 -0
- package/docs/architecture/architecture.md +34 -22
- package/docs/decisions/0004-reconsider-ui-direction.md +25 -0
- package/docs/plans/0441-remove-agent-definition-management-subtree.md +180 -0
- package/docs/plans/0442-dissolve-agents-conversation-viewer.md +195 -0
- package/docs/plans/0463-file-snapshot-source-evicted-agents.md +306 -0
- package/docs/retro/0441-remove-agent-definition-management-subtree.md +45 -0
- package/docs/retro/0442-dissolve-agents-conversation-viewer.md +108 -0
- package/docs/retro/0462-navigation-renderer-tui-components.md +56 -0
- package/docs/retro/0463-file-snapshot-source-evicted-agents.md +103 -0
- package/package.json +1 -1
- package/src/index.ts +13 -32
- package/src/lifecycle/subagent-manager.ts +47 -1
- package/src/ui/session-navigation.ts +67 -13
- package/src/ui/session-navigator.ts +18 -7
- package/src/ui/subagents-settings.ts +2 -2
- package/src/ui/agent-config-editor.ts +0 -199
- package/src/ui/agent-creation-wizard.ts +0 -233
- package/src/ui/agent-file-ops.ts +0 -59
- package/src/ui/agent-file-writer.ts +0 -55
- package/src/ui/agent-menu.ts +0 -331
- package/src/ui/conversation-viewer.ts +0 -241
- package/src/ui/message-formatters.ts +0 -195
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 463
|
|
3
|
+
issue_title: "pi-subagents: add file-snapshot source to /subagent-sessions for evicted agents"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #463 — pi-subagents: add file-snapshot source to /subagent-sessions for evicted agents
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-23T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 4-step plan for the Phase 19 Step 4b file-snapshot source: implement `fileSnapshotSource(outputFile, readFile)` in the pure `session-navigation.ts`, broaden the `/subagent-sessions` candidate set to evicted agents, and dual-source the handler by `NavigationEntry.kind`.
|
|
13
|
+
The central design fork — how evicted agents enter the picker — was resolved with the operator via `ask_user`.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- **Eviction is memory management.**
|
|
18
|
+
The cleanup sweep's `disposeSession()` frees the in-memory message history; the transcript survives only on disk.
|
|
19
|
+
So rendering an evicted agent *always* reads the file (`fileSnapshotSource`) regardless of candidate-set strategy — the strategy only affects the picker *label*.
|
|
20
|
+
- **Persisted child sessions carry no `type`/`description`.**
|
|
21
|
+
The JSONL has only the conversation plus a header (`id`, `timestamp`, `cwd`, `parentSession`).
|
|
22
|
+
A directory scan (the issue's literal wording) would therefore produce degraded labels and parse every file per open.
|
|
23
|
+
- **Decision: manager-retained descriptors over directory scan.**
|
|
24
|
+
The manager stashes a tiny no-messages `EvictedSubagent` descriptor in `cleanup()` before `removeRecord`, cleared in `clearCompleted()`/`dispose()`.
|
|
25
|
+
Rich labels identical to live entries, bounded memory, no per-open parse.
|
|
26
|
+
Coverage is limited to in-session evictions — which are the sweep's only targets, since a fresh manager per session never reloads prior-process subagents.
|
|
27
|
+
Operator confirmed; an `(evicted)` snapshot marker was also chosen for the label.
|
|
28
|
+
- **`NavigationEntry` becomes a discriminated union** (`live` | `evicted`); this breaks the handler, the `index.ts` call site, and both UI test files, so step 3 folds all of them into one commit.
|
|
29
|
+
- **SDK-runtime call kept direct.** `fileSnapshotSource` calls `parseSessionEntries` / `buildSessionContext` directly rather than injecting them — the injected `readFile` already provides the unit-test seam, and there is no `no-restricted-imports` rule.
|
|
30
|
+
- **Transient dead-code risk noted:** `fileSnapshotSource` and `listEvicted()` have no caller until step 3; flagged not to ship before step 3 lands (CI/`fallow` gate the pushed tip).
|
|
31
|
+
- Release: independent (Phase 19 Step 4b roadmap tag).
|
|
32
|
+
|
|
33
|
+
## Stage: Implementation — TDD (2026-06-23T13:00:00Z)
|
|
34
|
+
|
|
35
|
+
### Session summary
|
|
36
|
+
|
|
37
|
+
Executed all 4 plan steps in order: (1) `fileSnapshotSource` in the pure `session-navigation.ts`, (2) manager-retained `EvictedSubagent` descriptors (`cleanup` capture, `listEvicted`, `clearCompleted`/`dispose` clear), (3) the breaking `NavigationEntry` discriminated union + handler dual-source + `index.ts` wiring + all test updates in one commit, (4) architecture/ADR doc updates.
|
|
38
|
+
Test count went from 1088 to 1099 (+11); full suite, `check`, root `lint`, and `fallow dead-code` all green.
|
|
39
|
+
|
|
40
|
+
### Observations
|
|
41
|
+
|
|
42
|
+
- **No deviations from the plan.**
|
|
43
|
+
All steps landed as written; Module-Level Changes matched the touched files exactly.
|
|
44
|
+
- **Exploratory probe paid off.**
|
|
45
|
+
A disposable script confirmed `buildSessionContext` auto-detects the leaf with no `leafId`, the `type !== "session"` filter drops the header, and empty entries yield `[]` — validating the `fileSnapshotSource` shape before writing the test.
|
|
46
|
+
- **Two ESLint auto-fixes during commit hooks:** a stray `!` non-null assertion in the manager test (step 2) and four `entry?.kind` optional chains on a non-nullish destructured `entry` (`@typescript-eslint/no-unnecessary-condition`, step 3).
|
|
47
|
+
Both fixed and re-committed; `check` + tests confirmed green after.
|
|
48
|
+
- **Transient dead code** between steps 1–2 and 3 (predicted in the plan) cleared at the step-3 tip; final `fallow dead-code` is clean.
|
|
49
|
+
- **Pre-completion reviewer: PASS** — deterministic checks, code design, test artifacts, Mermaid render, and all three cross-step invariants (no inbound core call, read-only overlay, renderer parity) verified; no follow-ups deferred.
|
|
50
|
+
|
|
51
|
+
## Stage: Final Retrospective (2026-06-23T17:43:05Z)
|
|
52
|
+
|
|
53
|
+
### Session summary
|
|
54
|
+
|
|
55
|
+
Single continuous session carried #463 from planning through ship: a 4-step plan, TDD implementation (+11 tests, 1088→1099), pre-completion PASS, and release of `pi-subagents` v17.5.0.
|
|
56
|
+
The defining moment was planning: an `ask_user` design gate plus operator Socratic pushback diverged the candidate-set design from the issue's literal "directory scan" wording to manager-retained descriptors.
|
|
57
|
+
Execution was notably clean — no rework, no plan deviations, two auto-fixed lint nits.
|
|
58
|
+
|
|
59
|
+
### Observations
|
|
60
|
+
|
|
61
|
+
#### What went well
|
|
62
|
+
|
|
63
|
+
- **The `ask_user` gate caught a real design fork the issue body got wrong.**
|
|
64
|
+
The issue's "Proposed change" said "enumerate persisted child-session JSONL files" (a directory scan).
|
|
65
|
+
Exploration revealed the persisted session carries no subagent `type`/`description`, so a scan yields degraded labels.
|
|
66
|
+
The gate surfaced descriptors-vs-scan-vs-hybrid; the operator's "why are we evicting at all?"
|
|
67
|
+
and "tell me more about how the labels degrade" drove a better-grounded decision (descriptors).
|
|
68
|
+
This is the `/plan-issue` "treat Proposed change as a hypothesis" contract working as intended — novel because the divergence was the *enumeration mechanism*, not a surface ambiguity.
|
|
69
|
+
- **Exploratory probe before the first TDD test.**
|
|
70
|
+
A disposable script (`explore-session.mjs`, deleted after) confirmed `buildSessionContext` auto-detects the leaf with no `leafId` and the `type !== "session"` filter drops the header — validating the `fileSnapshotSource` shape before any test was written (the `testing` skill's "inspect the actual runtime shape first" rule).
|
|
71
|
+
- **The plan predicted transient dead code and sequenced to clear it.**
|
|
72
|
+
`fileSnapshotSource` and `listEvicted()` had no caller until step 3; the plan flagged this and ordered the integration step to close it, so the final `fallow dead-code` gate passed with no surprise suppressions.
|
|
73
|
+
- **Pre-completion reviewer ran on `claude-sonnet-4-6`** — model appropriate for the judgment-heavy invariant/design checklist; returned PASS.
|
|
74
|
+
|
|
75
|
+
#### What caused friction (agent side)
|
|
76
|
+
|
|
77
|
+
- `other` — the first `ask_user` led with the enumeration mechanism (descriptors/scan/hybrid) before establishing *why eviction creates the problem*.
|
|
78
|
+
The operator's first reply was "Why are we 'evicting' subagents in the first place?", i.e. asking for the framing I already held but had not front-loaded.
|
|
79
|
+
Impact: two extra `ask_user` round-trips before the decision — but they produced a better-understood outcome, so net-positive; no rework.
|
|
80
|
+
- `other` (lint nit) — four `entry?.kind` optional chains on a destructured array element tripped `@typescript-eslint/no-unnecessary-condition`, and a stray `!` tripped `no-unnecessary-type-assertion`; both surfaced via the pre-commit hook.
|
|
81
|
+
Impact: two re-commits during steps 2–3, auto-fixed, zero rework.
|
|
82
|
+
|
|
83
|
+
#### What caused friction (user side)
|
|
84
|
+
|
|
85
|
+
- None.
|
|
86
|
+
The operator's Socratic questions mid-`ask_user` were the session's highest-value input — they converted a plausible-but-degraded design (scan) into the right one (descriptors).
|
|
87
|
+
If anything, this is a model for how the gate should be used.
|
|
88
|
+
|
|
89
|
+
### Diagnostic details
|
|
90
|
+
|
|
91
|
+
- **Model-performance correlation** — the `pre-completion-reviewer` subagent ran on `anthropic/claude-sonnet-4-6` (judgment-heavy review): appropriate, no mismatch.
|
|
92
|
+
Main-thread `model_change` events included a `deepseek-v4-flash` variant; no quality degradation was observed in any artifact (plan, code, tests all clean), so no actionable correlation.
|
|
93
|
+
- **Escalation-delay tracking** — no `rabbit-hole` points; longest same-error streak was the two lint auto-fixes, each resolved in one re-commit.
|
|
94
|
+
- **Unused-tool detection** — exploration used `colgrep`, `grep`, and a disposable runtime probe; no missed-tool opportunities.
|
|
95
|
+
- **Feedback-loop gap analysis** — verification ran incrementally: `pnpm run check` after the shared-interface steps (2 and 3), per-file `vitest` on each red/green cycle, and the full suite + lint + `fallow` before the docs commit.
|
|
96
|
+
No end-only verification.
|
|
97
|
+
|
|
98
|
+
### Changes made
|
|
99
|
+
|
|
100
|
+
1. Appended this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0463-file-snapshot-source-evicted-agents.md`.
|
|
101
|
+
|
|
102
|
+
No `AGENTS.md` or prompt changes — the session's friction was net-positive (the `ask_user` round-trips) or auto-fixed with zero rework (the lint nits).
|
|
103
|
+
Two candidate changes were considered and rejected as noise (a `code-design` note on `entry?.kind` optional chaining; an `ask-user` "why-before-mechanism" guidance already covered by the skill).
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* steer_subagent — LLM-callable: send a steering message to a running agent
|
|
9
9
|
*
|
|
10
10
|
* Commands:
|
|
11
|
-
* /agents — Interactive agent management menu
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
15
14
|
import {
|
|
16
15
|
createAgentSession,
|
|
17
16
|
DefaultResourceLoader,
|
|
@@ -26,7 +25,6 @@ import { InterruptHandler, SessionLifecycleHandler, ToolStartHandler } from "#sr
|
|
|
26
25
|
import { createChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
27
26
|
import { ConcurrencyLimiter } from "#src/lifecycle/concurrency-limiter";
|
|
28
27
|
import { createSubagentSession, type SubagentSessionDeps } from "#src/lifecycle/create-subagent-session";
|
|
29
|
-
import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
30
28
|
import { SubagentManager } from "#src/lifecycle/subagent-manager";
|
|
31
29
|
import { CompositeSubagentObserver } from "#src/observation/composite-subagent-observer";
|
|
32
30
|
import { type NotificationDetails, NotificationManager } from "#src/observation/notification";
|
|
@@ -44,8 +42,6 @@ import { SettingsManager } from "#src/settings";
|
|
|
44
42
|
import { AgentTool } from "#src/tools/agent-tool";
|
|
45
43
|
import { GetResultTool } from "#src/tools/get-result-tool";
|
|
46
44
|
import { SteerTool } from "#src/tools/steer-tool";
|
|
47
|
-
import { FsAgentFileOps } from "#src/ui/agent-file-ops";
|
|
48
|
-
import { AgentsMenuHandler } from "#src/ui/agent-menu";
|
|
49
45
|
import { AgentWidget } from "#src/ui/agent-widget";
|
|
50
46
|
import { SessionNavigatorHandler } from "#src/ui/session-navigator";
|
|
51
47
|
import { SubagentsSettingsHandler } from "#src/ui/subagents-settings";
|
|
@@ -161,47 +157,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
161
157
|
|
|
162
158
|
pi.registerTool(new SteerTool(manager, pi.events).toToolDefinition());
|
|
163
159
|
|
|
164
|
-
// ---- /
|
|
165
|
-
|
|
166
|
-
const agentsMenu = new AgentsMenuHandler(
|
|
167
|
-
manager,
|
|
168
|
-
registry,
|
|
169
|
-
settings,
|
|
170
|
-
new FsAgentFileOps(),
|
|
171
|
-
join(getAgentDir(), "agents"),
|
|
172
|
-
join(process.cwd(), ".pi", "agents"),
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
pi.registerCommand("agents", {
|
|
176
|
-
description: "Manage agents",
|
|
177
|
-
handler: async (_args, ctx) => {
|
|
178
|
-
await agentsMenu.handle({
|
|
179
|
-
ui: ctx.ui,
|
|
180
|
-
modelRegistry: ctx.modelRegistry,
|
|
181
|
-
parentSnapshot: buildParentSnapshot(ctx),
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ---- /subagents-settings command ----
|
|
160
|
+
// ---- /subagents:settings command ----
|
|
187
161
|
|
|
188
162
|
const subagentsSettings = new SubagentsSettingsHandler(settings);
|
|
189
163
|
|
|
190
|
-
pi.registerCommand("subagents
|
|
164
|
+
pi.registerCommand("subagents:settings", {
|
|
191
165
|
description: "Configure subagent settings (concurrency, turn limits)",
|
|
192
166
|
handler: async (_args, ctx) => {
|
|
193
167
|
await subagentsSettings.handle({ ui: ctx.ui });
|
|
194
168
|
},
|
|
195
169
|
});
|
|
196
170
|
|
|
197
|
-
// ---- /
|
|
171
|
+
// ---- /subagents:sessions command ----
|
|
198
172
|
|
|
199
173
|
const sessionNavigator = new SessionNavigatorHandler();
|
|
200
174
|
|
|
201
|
-
pi.registerCommand("
|
|
175
|
+
pi.registerCommand("subagents:sessions", {
|
|
202
176
|
description: "View a subagent's session transcript (read-only)",
|
|
203
177
|
handler: async (_args, ctx) => {
|
|
204
|
-
await sessionNavigator.handle({
|
|
178
|
+
await sessionNavigator.handle({
|
|
179
|
+
ui: ctx.ui,
|
|
180
|
+
agents: manager.listAgents(),
|
|
181
|
+
evicted: manager.listEvicted(),
|
|
182
|
+
registry,
|
|
183
|
+
cwd: ctx.cwd,
|
|
184
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
185
|
+
});
|
|
205
186
|
},
|
|
206
187
|
});
|
|
207
188
|
}
|
|
@@ -14,12 +14,31 @@ import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent
|
|
|
14
14
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
15
15
|
import { Subagent, type SubagentLifecycleObserver } from "#src/lifecycle/subagent";
|
|
16
16
|
import type { SubagentSession } from "#src/lifecycle/subagent-session";
|
|
17
|
-
import { SubagentState } from "#src/lifecycle/subagent-state";
|
|
17
|
+
import { SubagentState, type SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
18
18
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
19
19
|
|
|
20
20
|
import type { RunConfig } from "#src/runtime";
|
|
21
21
|
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A lightweight snapshot of a subagent evicted by the 10-minute cleanup sweep.
|
|
25
|
+
*
|
|
26
|
+
* The sweep frees the heavy in-memory session (its message history included);
|
|
27
|
+
* this descriptor retains only the fields the session navigator needs to label
|
|
28
|
+
* the agent in the picker, plus the persisted `outputFile` to source its
|
|
29
|
+
* transcript from disk. Carries no messages, so memory stays bounded.
|
|
30
|
+
*/
|
|
31
|
+
export interface EvictedSubagent {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly type: SubagentType;
|
|
34
|
+
readonly description: string;
|
|
35
|
+
readonly status: SubagentStatus;
|
|
36
|
+
readonly startedAt: number;
|
|
37
|
+
readonly completedAt: number | undefined;
|
|
38
|
+
readonly toolUses: number;
|
|
39
|
+
readonly outputFile: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
/** Observer interface for agent lifecycle notifications. */
|
|
24
43
|
export interface SubagentManagerObserver {
|
|
25
44
|
onSubagentStarted(record: Subagent): void;
|
|
@@ -65,6 +84,8 @@ export interface AgentSpawnConfig {
|
|
|
65
84
|
|
|
66
85
|
export class SubagentManager {
|
|
67
86
|
private agents = new Map<string, Subagent>();
|
|
87
|
+
/** Descriptors of agents removed by the cleanup sweep, keyed by id — navigable from disk. */
|
|
88
|
+
private readonly evicted = new Map<string, EvictedSubagent>();
|
|
68
89
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
69
90
|
private readonly observer?: SubagentManagerObserver;
|
|
70
91
|
private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
@@ -220,6 +241,11 @@ export class SubagentManager {
|
|
|
220
241
|
);
|
|
221
242
|
}
|
|
222
243
|
|
|
244
|
+
/** Descriptors of agents evicted by the cleanup sweep, most recent first. */
|
|
245
|
+
listEvicted(): EvictedSubagent[] {
|
|
246
|
+
return [...this.evicted.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
247
|
+
}
|
|
248
|
+
|
|
223
249
|
abort(id: string): boolean {
|
|
224
250
|
const record = this.agents.get(id);
|
|
225
251
|
if (!record) return false;
|
|
@@ -245,6 +271,9 @@ export class SubagentManager {
|
|
|
245
271
|
for (const [id, record] of this.agents) {
|
|
246
272
|
if (record.status === "running" || record.status === "queued") continue;
|
|
247
273
|
if ((record.completedAt ?? 0) >= cutoff) continue;
|
|
274
|
+
// Retain a navigable descriptor before freeing the heavy session. Only an
|
|
275
|
+
// agent with a persisted file can be sourced from disk after eviction.
|
|
276
|
+
if (record.outputFile) this.evicted.set(id, toEvictedSubagent(record, record.outputFile));
|
|
248
277
|
this.removeRecord(id, record);
|
|
249
278
|
}
|
|
250
279
|
}
|
|
@@ -258,6 +287,8 @@ export class SubagentManager {
|
|
|
258
287
|
if (record.status === "running" || record.status === "queued") continue;
|
|
259
288
|
this.removeRecord(id, record);
|
|
260
289
|
}
|
|
290
|
+
// Evicted descriptors belong to the session that swept them — a new session starts empty.
|
|
291
|
+
this.evicted.clear();
|
|
261
292
|
}
|
|
262
293
|
|
|
263
294
|
/** Whether any agents are still running or queued. */
|
|
@@ -314,5 +345,20 @@ export class SubagentManager {
|
|
|
314
345
|
record.disposeSession();
|
|
315
346
|
}
|
|
316
347
|
this.agents.clear();
|
|
348
|
+
this.evicted.clear();
|
|
317
349
|
}
|
|
318
350
|
}
|
|
351
|
+
|
|
352
|
+
/** Capture an evicted agent's navigable fields from its record. */
|
|
353
|
+
function toEvictedSubagent(record: Subagent, outputFile: string): EvictedSubagent {
|
|
354
|
+
return {
|
|
355
|
+
id: record.id,
|
|
356
|
+
type: record.type,
|
|
357
|
+
description: record.description,
|
|
358
|
+
status: record.status,
|
|
359
|
+
startedAt: record.startedAt,
|
|
360
|
+
completedAt: record.completedAt,
|
|
361
|
+
toolUses: record.toolUses,
|
|
362
|
+
outputFile,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-navigation.ts — Pure selection and transcript-sourcing for native session navigation.
|
|
3
3
|
*
|
|
4
|
-
* Splits the unit-testable core of the `/
|
|
4
|
+
* Splits the unit-testable core of the `/subagents:sessions` command from its TUI
|
|
5
5
|
* wiring (`session-navigator.ts`): which subagents are navigable and how a picked
|
|
6
6
|
* agent's transcript is sourced (live, in this slice).
|
|
7
7
|
*
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* components require a `TUI`, `cwd`, and markdown theme.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type
|
|
15
|
+
import { buildSessionContext, parseSessionEntries, type SessionEntry, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
17
|
+
import type { EvictedSubagent } from "#src/lifecycle/subagent-manager";
|
|
17
18
|
import type { SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
18
19
|
import type { AgentSessionEvent, SessionMessage, SubagentType } from "#src/types";
|
|
19
20
|
import { formatDuration, getDisplayName } from "#src/ui/display";
|
|
@@ -37,10 +38,24 @@ export interface NavigableSubagent {
|
|
|
37
38
|
getToolDefinition(name: string): ToolDefinition | undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* A navigable entry plus the label shown in the picker.
|
|
43
|
+
*
|
|
44
|
+
* A `live` entry sources its transcript from the in-memory record; an `evicted`
|
|
45
|
+
* entry sources it from the persisted session file (the record is gone).
|
|
46
|
+
*/
|
|
47
|
+
export type NavigationEntry =
|
|
48
|
+
| { readonly kind: "live"; readonly label: string; readonly record: NavigableSubagent }
|
|
49
|
+
| { readonly kind: "evicted"; readonly label: string; readonly outputFile: string };
|
|
50
|
+
|
|
51
|
+
/** The fields `buildLabel` reads — shared by a live record and an evicted descriptor. */
|
|
52
|
+
interface LabelFields {
|
|
53
|
+
readonly type: SubagentType;
|
|
54
|
+
readonly description: string;
|
|
55
|
+
readonly status: SubagentStatus;
|
|
56
|
+
readonly startedAt: number;
|
|
57
|
+
readonly completedAt: number | undefined;
|
|
58
|
+
readonly toolUses: number;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
/** Running-agent streaming state, surfaced by a live source. */
|
|
@@ -61,14 +76,52 @@ export interface TranscriptSource {
|
|
|
61
76
|
getToolDefinition(name: string): ToolDefinition | undefined;
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Label every navigable subagent for the picker: live records with a viewable
|
|
81
|
+
* session, then agents evicted by the cleanup sweep (deduped against live ids).
|
|
82
|
+
*/
|
|
65
83
|
export function listNavigableAgents(
|
|
66
84
|
agents: readonly NavigableSubagent[],
|
|
85
|
+
evicted: readonly EvictedSubagent[],
|
|
67
86
|
registry: AgentConfigLookup,
|
|
68
87
|
): NavigationEntry[] {
|
|
69
|
-
|
|
88
|
+
const live = agents
|
|
70
89
|
.filter((record) => record.isSessionReady())
|
|
71
|
-
.map((record) => ({ record, label: buildLabel(record, registry) }));
|
|
90
|
+
.map((record): NavigationEntry => ({ kind: "live", record, label: buildLabel(record, registry) }));
|
|
91
|
+
const liveIds = new Set(agents.map((record) => record.id));
|
|
92
|
+
const evictedEntries = evicted
|
|
93
|
+
.filter((descriptor) => !liveIds.has(descriptor.id))
|
|
94
|
+
.map((descriptor): NavigationEntry => ({
|
|
95
|
+
kind: "evicted",
|
|
96
|
+
outputFile: descriptor.outputFile,
|
|
97
|
+
label: buildLabel(descriptor, registry, true),
|
|
98
|
+
}));
|
|
99
|
+
return [...live, ...evictedEntries];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Source a transcript from a persisted child-session JSONL snapshot.
|
|
104
|
+
*
|
|
105
|
+
* For an agent evicted from the manager's map by the 10-minute cleanup sweep:
|
|
106
|
+
* the in-memory record (and its message history) is gone, but the session file
|
|
107
|
+
* survives on disk. Reads the file, drops the `SessionHeader`, and resolves the
|
|
108
|
+
* message list via Pi's own parser. A static snapshot — no subscription, no
|
|
109
|
+
* streaming, no live tool registry. `readFile` is injected so this module makes
|
|
110
|
+
* no `fs` calls.
|
|
111
|
+
*/
|
|
112
|
+
export function fileSnapshotSource(
|
|
113
|
+
outputFile: string,
|
|
114
|
+
readFile: (path: string) => string,
|
|
115
|
+
): TranscriptSource {
|
|
116
|
+
const entries = parseSessionEntries(readFile(outputFile));
|
|
117
|
+
const sessionEntries = entries.filter((entry): entry is SessionEntry => entry.type !== "session");
|
|
118
|
+
const { messages } = buildSessionContext(sessionEntries);
|
|
119
|
+
return {
|
|
120
|
+
getMessages: () => messages,
|
|
121
|
+
subscribe: () => undefined,
|
|
122
|
+
streaming: () => undefined,
|
|
123
|
+
getToolDefinition: () => undefined,
|
|
124
|
+
};
|
|
72
125
|
}
|
|
73
126
|
|
|
74
127
|
/** Source a transcript live from an in-memory record (this slice's only source). */
|
|
@@ -84,8 +137,9 @@ export function liveSource(record: NavigableSubagent): TranscriptSource {
|
|
|
84
137
|
};
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
function buildLabel(
|
|
88
|
-
const name = getDisplayName(
|
|
89
|
-
const duration = formatDuration(
|
|
90
|
-
|
|
140
|
+
function buildLabel(fields: LabelFields, registry: AgentConfigLookup, evicted = false): string {
|
|
141
|
+
const name = getDisplayName(fields.type, registry);
|
|
142
|
+
const duration = formatDuration(fields.startedAt, fields.completedAt);
|
|
143
|
+
const marker = evicted ? " · evicted (snapshot)" : "";
|
|
144
|
+
return `${name} (${fields.description}) · ${fields.toolUses} tools · ${fields.status} · ${duration}${marker}`;
|
|
91
145
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* session-navigator.ts — The `/
|
|
2
|
+
* session-navigator.ts — The `/subagents:sessions` command: pick a subagent and
|
|
3
3
|
* read its transcript through Pi's own per-entry session components.
|
|
4
4
|
*
|
|
5
5
|
* SDK/TUI consumer half of native session navigation. The unit-testable core
|
|
@@ -38,9 +38,10 @@ import {
|
|
|
38
38
|
visibleWidth,
|
|
39
39
|
} from "@earendil-works/pi-tui";
|
|
40
40
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
41
|
+
import type { EvictedSubagent } from "#src/lifecycle/subagent-manager";
|
|
41
42
|
import type { SessionMessage } from "#src/types";
|
|
42
43
|
import { describeActivity, type Theme } from "#src/ui/display";
|
|
43
|
-
import { listNavigableAgents, liveSource, type NavigableSubagent, type TranscriptSource } from "#src/ui/session-navigation";
|
|
44
|
+
import { fileSnapshotSource, listNavigableAgents, liveSource, type NavigableSubagent, type TranscriptSource } from "#src/ui/session-navigation";
|
|
44
45
|
|
|
45
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
47
|
|
|
@@ -64,13 +65,17 @@ export interface SessionNavigatorUI {
|
|
|
64
65
|
custom<R>(component: OverlayComponentFactory<R>, options?: unknown): Promise<R>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/** Parameters for one `/
|
|
68
|
+
/** Parameters for one `/subagents:sessions` invocation. */
|
|
68
69
|
export interface SessionNavigatorParams {
|
|
69
70
|
ui: SessionNavigatorUI;
|
|
70
71
|
agents: readonly NavigableSubagent[];
|
|
72
|
+
/** Descriptors of agents evicted by the cleanup sweep, sourced from disk when picked. */
|
|
73
|
+
evicted: readonly EvictedSubagent[];
|
|
71
74
|
registry: AgentConfigLookup;
|
|
72
75
|
/** Working directory for tool-call rendering (relative path display). */
|
|
73
76
|
cwd: string;
|
|
77
|
+
/** Reads a persisted session file for the file-snapshot source. */
|
|
78
|
+
readFile: (path: string) => string;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/** Options for the read-only transcript overlay. */
|
|
@@ -84,15 +89,15 @@ export interface TranscriptOverlayOptions {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
|
-
* Handler for the `/
|
|
92
|
+
* Handler for the `/subagents:sessions` slash command.
|
|
88
93
|
*
|
|
89
94
|
* Lists navigable subagents, lets the operator pick one, and opens its transcript
|
|
90
95
|
* read-only. Receives the agent snapshot (`manager.listAgents()`) rather than the
|
|
91
96
|
* manager, so it stays a reactive consumer with no inbound call into the core.
|
|
92
97
|
*/
|
|
93
98
|
export class SessionNavigatorHandler {
|
|
94
|
-
async handle({ ui, agents, registry, cwd }: SessionNavigatorParams): Promise<void> {
|
|
95
|
-
const entries = listNavigableAgents(agents, registry);
|
|
99
|
+
async handle({ ui, agents, evicted, registry, cwd, readFile }: SessionNavigatorParams): Promise<void> {
|
|
100
|
+
const entries = listNavigableAgents(agents, evicted, registry);
|
|
96
101
|
if (entries.length === 0) {
|
|
97
102
|
ui.notify("No subagent sessions to view.", "info");
|
|
98
103
|
return;
|
|
@@ -105,7 +110,13 @@ export class SessionNavigatorHandler {
|
|
|
105
110
|
const entry = entries.find((candidate) => candidate.label === choice);
|
|
106
111
|
if (!entry) return;
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
let source: TranscriptSource;
|
|
114
|
+
try {
|
|
115
|
+
source = entry.kind === "live" ? liveSource(entry.record) : fileSnapshotSource(entry.outputFile, readFile);
|
|
116
|
+
} catch {
|
|
117
|
+
ui.notify("Could not read the session transcript file.", "error");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
109
120
|
const markdownTheme = getMarkdownTheme();
|
|
110
121
|
await ui.custom<undefined>(
|
|
111
122
|
(tui, theme, _keybindings, done) =>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ---- Narrow interfaces ----
|
|
2
2
|
|
|
3
|
-
/** Narrow settings interface required by the subagents
|
|
3
|
+
/** Narrow settings interface required by the subagents:settings command. */
|
|
4
4
|
export interface SubagentsSettingsManager {
|
|
5
5
|
readonly maxConcurrent: number;
|
|
6
6
|
readonly defaultMaxTurns: number | undefined;
|
|
@@ -20,7 +20,7 @@ export interface SubagentsSettingsUI {
|
|
|
20
20
|
// ---- Class ----
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Handler for the `/subagents
|
|
23
|
+
* Handler for the `/subagents:settings` slash command.
|
|
24
24
|
*
|
|
25
25
|
* Call `handle({ ui })` from the Pi command registration to open the interactive
|
|
26
26
|
* settings list. Lifted from `AgentsMenuHandler.showSettings`.
|