@gotgenes/pi-subagents 5.8.1 → 6.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 +35 -0
- package/docs/architecture/architecture.md +19 -16
- package/docs/plans/0061-session-format-transcript.md +284 -0
- package/docs/plans/0077-inject-project-agents-dir.md +115 -0
- package/docs/retro/0066-replace-as-any-with-sdk-types.md +44 -0
- package/docs/retro/0077-inject-project-agents-dir.md +33 -0
- package/package.json +6 -3
- package/src/agent-manager.ts +13 -12
- package/src/agent-runner.ts +22 -2
- package/src/index.ts +1 -0
- package/src/session-dir.ts +38 -0
- package/src/tools/agent-tool.ts +5 -17
- package/src/types.ts +1 -3
- package/src/ui/agent-menu.ts +5 -6
- package/src/output-file.ts +0 -99
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.2...pi-subagents-v6.0.0) (2026-05-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* Subagent transcripts are now written in Pi's official JSONL session format via SessionManager.create() instead of the bespoke flat format. The output-file.ts module and its encodeCwd/createOutputFilePath/ writeInitialEntry/streamToOutputFile exports are removed. Transcript file paths change from /tmp/pi-subagents-<uid>/... to the Pi sessions directory.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add deriveSubagentSessionDir for session directory derivation ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([8442379](https://github.com/gotgenes/pi-packages/commit/8442379ae4433ba1428d703a24bef7b57c8624f2))
|
|
18
|
+
* remove bespoke output-file transcript format ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([1aab916](https://github.com/gotgenes/pi-packages/commit/1aab9166fc52e4f04e1d0a369788bf5c4a3da7c7))
|
|
19
|
+
* thread parent session info through spawn and run options ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([6f0d537](https://github.com/gotgenes/pi-packages/commit/6f0d537d745df63be43eb14de92caf42e65ab347))
|
|
20
|
+
* use persisted SessionManager for subagent sessions ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([ffafa69](https://github.com/gotgenes/pi-packages/commit/ffafa69d96068f881ec97e4f924245a308e542ba))
|
|
21
|
+
* wire session file path through agent-tool, remove output-file streaming ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([97acf0a](https://github.com/gotgenes/pi-packages/commit/97acf0a63cbe21e0954e79cd7db71e5632084454))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* use cwd in session-dir fallback path to namespace by project ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([0394420](https://github.com/gotgenes/pi-packages/commit/0394420237b9d23b35fe6c4e65b03fc267beaa0c))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
|
|
31
|
+
* plan session format transcript migration ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([68238c1](https://github.com/gotgenes/pi-packages/commit/68238c1c90c375c8469929399f18d0566e97a32c))
|
|
32
|
+
* **retro:** add retro notes for issue [#77](https://github.com/gotgenes/pi-packages/issues/77) ([004c99c](https://github.com/gotgenes/pi-packages/commit/004c99c4fba6b515360bb453eedbeb1218cebbc2))
|
|
33
|
+
* update architecture and package skill for session format migration ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([eef5e16](https://github.com/gotgenes/pi-packages/commit/eef5e16ad90118092badcbe2594c89c399919c15))
|
|
34
|
+
|
|
35
|
+
## [5.8.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.1...pi-subagents-v5.8.2) (2026-05-20)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Documentation
|
|
39
|
+
|
|
40
|
+
* plan inject projectAgentsDir into AgentMenuDeps ([#77](https://github.com/gotgenes/pi-packages/issues/77)) ([d8cf039](https://github.com/gotgenes/pi-packages/commit/d8cf03944d0abc6230541d67655d89582665a615))
|
|
41
|
+
* **retro:** add retro notes for issue [#66](https://github.com/gotgenes/pi-packages/issues/66) ([ce0f04a](https://github.com/gotgenes/pi-packages/commit/ce0f04a3d84523a4c2e8b7bc998b5bec0f16970f))
|
|
42
|
+
|
|
8
43
|
## [5.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.0...pi-subagents-v5.8.1) (2026-05-20)
|
|
9
44
|
|
|
10
45
|
|
|
@@ -37,7 +37,7 @@ worktree.ts — git worktree isolation
|
|
|
37
37
|
usage.ts — token usage tracking
|
|
38
38
|
model-resolver.ts — fuzzy model name resolution
|
|
39
39
|
invocation-config.ts — merge tool params with agent config
|
|
40
|
-
|
|
40
|
+
session-dir.ts — subagent session directory derivation
|
|
41
41
|
settings.ts — persistent operational settings
|
|
42
42
|
|
|
43
43
|
cross-extension-rpc.ts — RPC over pi.events ← replacing
|
|
@@ -114,18 +114,18 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
|
|
|
114
114
|
- **Group join** (`group-join.ts`) — 141 LOC removed.
|
|
115
115
|
The grouped notification batching adds complexity for a marginal UX improvement.
|
|
116
116
|
Individual completion notifications are sufficient.
|
|
117
|
-
- **Output file** (`output-file.ts`) —
|
|
118
|
-
|
|
117
|
+
- **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
|
|
118
|
+
Subagent transcripts are now written in Pi's official JSONL session format via the SDK's `SessionManager`, nested under the parent session directory.
|
|
119
119
|
|
|
120
120
|
### Estimated impact
|
|
121
121
|
|
|
122
|
-
| Subsystem removed | LOC removed
|
|
123
|
-
| ----------------- |
|
|
124
|
-
| Scheduling | 612
|
|
125
|
-
| Ad-hoc RPC | 80
|
|
126
|
-
| Group join | 141
|
|
127
|
-
| Output file | 83
|
|
128
|
-
| **Total** | **~916**
|
|
122
|
+
| Subsystem removed | LOC removed | LOC removed from index.ts |
|
|
123
|
+
| ----------------- | ------------- | ------------------------- |
|
|
124
|
+
| Scheduling | 612 | ~200 |
|
|
125
|
+
| Ad-hoc RPC | 80 | ~50 |
|
|
126
|
+
| Group join | 141 | ~100 |
|
|
127
|
+
| Output file | 83 (replaced) | ~50 |
|
|
128
|
+
| **Total** | **~916** | **~400** |
|
|
129
129
|
|
|
130
130
|
After removal and `index.ts` decomposition, the core shrinks from ~6,300 to ~5,400 LOC, and `index.ts` shrinks from ~1,894 to ~1,300 LOC.
|
|
131
131
|
|
|
@@ -314,9 +314,10 @@ Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
|
314
314
|
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
315
315
|
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
316
316
|
|
|
317
|
-
### Phase 3: Remove group-join,
|
|
317
|
+
### Phase 3: Remove group-join, ad-hoc RPC; replace output-file
|
|
318
318
|
|
|
319
|
-
Delete `group-join.ts`, `
|
|
319
|
+
Delete `group-join.ts`, `cross-extension-rpc.ts`.
|
|
320
|
+
Replace `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
320
321
|
Simplify `index.ts` to use direct individual notifications.
|
|
321
322
|
Emit lifecycle events on `pi.events` for external consumers.
|
|
322
323
|
|
|
@@ -395,8 +396,9 @@ Small cleanups that are safest after the structural changes settle.
|
|
|
395
396
|
|
|
396
397
|
### Phase 4: Features and cross-cutting concerns
|
|
397
398
|
|
|
398
|
-
1. **gotgenes/pi-packages#61** — Port transcript logging to Pi's official JSONL session format
|
|
399
|
-
-
|
|
399
|
+
1. **gotgenes/pi-packages#61** ✓ — Port transcript logging to Pi's official JSONL session format
|
|
400
|
+
- Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts`.
|
|
401
|
+
- Subagent sessions are persisted under `<parent-session-dir>/<parent-session-basename>/tasks/` with `parentSession` header linking.
|
|
400
402
|
|
|
401
403
|
2. **gotgenes/pi-packages#22** — Parent-session resolution for `nicobailon/pi-subagents` children
|
|
402
404
|
- Cross-extension issue that spans `pi-permission-system` and `pi-subagents`.
|
|
@@ -417,7 +419,7 @@ Small cleanups that are safest after the structural changes settle.
|
|
|
417
419
|
#66 (type casts) ◄─────(after structural changes settle)
|
|
418
420
|
#77 (projectAgentsDir) ◄─(after #66 or parallel)
|
|
419
421
|
|
|
420
|
-
#61 (transcript format)
|
|
422
|
+
#61 (transcript format) ✓
|
|
421
423
|
#22 (parent session) ◄──(cross-extension, independent)
|
|
422
424
|
```
|
|
423
425
|
|
|
@@ -426,10 +428,11 @@ Small cleanups that are safest after the structural changes settle.
|
|
|
426
428
|
The recommended sequence is:
|
|
427
429
|
|
|
428
430
|
```text
|
|
429
|
-
#69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61
|
|
431
|
+
#69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61 ✓
|
|
430
432
|
```
|
|
431
433
|
|
|
432
434
|
Phase 1 is complete; Phase 2 is complete.
|
|
435
|
+
Issue #61 (transcript format) is complete.
|
|
433
436
|
The next issue is #66 (replace `as any` casts with proper SDK types).
|
|
434
437
|
Issue #22 is a parallel cross-extension track and does not gate the structural work.
|
|
435
438
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 61
|
|
3
|
+
issue_title: "feat: port subagent transcript logging to Pi's official JSONL session format"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Port subagent transcripts to Pi's official session format
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Subagent conversation transcripts are written as JSONL by `output-file.ts`, but use a bespoke flat format (`{ isSidechain, agentId, type, message, timestamp, cwd }`) that does not conform to Pi's official session file format.
|
|
11
|
+
Pi's `SessionManager` writes tree-structured JSONL with a session header, UUIDv7 entry IDs, `parentId` tree structure, and typed entry discriminants (`"message"`, `"compaction"`, etc.).
|
|
12
|
+
The bespoke format cannot be loaded by Pi's session tooling (session selector, export-html, resume) and requires manual `jq` inspection for debugging.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Subagent transcripts written in Pi's official JSONL session format via `SessionManager`.
|
|
17
|
+
- Transcripts discoverable via the parent session path (nested under a parent-session-relative directory).
|
|
18
|
+
- Parent session linkage via the `parentSession` header field.
|
|
19
|
+
- Delete `output-file.ts` — the SDK's `SessionManager` handles all JSONL writing natively.
|
|
20
|
+
- Existing debugging use case preserved (full turn-by-turn history on disk).
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Making subagent sessions resumable via Pi's `/resume` command (future work; requires session selection UX changes).
|
|
25
|
+
- Cross-extension parent-session resolution (issue #22 — separate track).
|
|
26
|
+
- Changing the `SessionManager` usage for the child `AgentSession` itself (the child session already uses `SessionManager.inMemory()`; we replace it with a persisted one).
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
### Current flow
|
|
31
|
+
|
|
32
|
+
1. `agent-tool.ts` calls `createOutputFilePath(cwd, agentId, parentSessionId)` → creates `/tmp/pi-subagents-<uid>/<encoded-cwd>/<parentSessionId>/tasks/<agentId>.output`.
|
|
33
|
+
2. `writeInitialEntry(path, agentId, prompt, cwd)` writes the first user message in the bespoke format.
|
|
34
|
+
3. `streamToOutputFile(session, path, agentId, cwd)` subscribes to `turn_end` events and appends bespoke JSONL entries.
|
|
35
|
+
4. `agent-manager.ts` calls the cleanup function on completion/error to do a final flush and unsubscribe.
|
|
36
|
+
|
|
37
|
+
### Pi's SessionManager API
|
|
38
|
+
|
|
39
|
+
`SessionManager` from `@earendil-works/pi-coding-agent` provides:
|
|
40
|
+
|
|
41
|
+
- `SessionManager.create(cwd, sessionDir?)` — creates a persisted session file in the given directory.
|
|
42
|
+
- `newSession({ parentSession? })` — writes a `SessionHeader` with `parentSession` linking.
|
|
43
|
+
- `appendMessage(message)` — writes a `SessionMessageEntry` with auto-generated UUIDv7 `id` and `parentId` (tree structure).
|
|
44
|
+
- `appendCompaction(...)`, `appendCustomEntry(...)` — first-class support for all entry types.
|
|
45
|
+
- `getSessionFile()` — returns the path to the JSONL file on disk.
|
|
46
|
+
|
|
47
|
+
The child `AgentSession` (created by `createAgentSession`) accepts a `sessionManager` option.
|
|
48
|
+
Currently set to `SessionManager.inMemory(cwd)`, switching to `SessionManager.create(cwd, sessionDir)` makes the SDK write official-format JSONL automatically — no manual streaming code needed.
|
|
49
|
+
|
|
50
|
+
### Reference implementations
|
|
51
|
+
|
|
52
|
+
nicobailon/pi-subagents places subagent sessions under `<parent-session-dir>/<parent-session-basename>/<runId>/`.
|
|
53
|
+
This keeps them discoverable via the parent session path without cluttering the main session list.
|
|
54
|
+
Our approach follows the same pattern: derive a `sessionDir` from the parent session file.
|
|
55
|
+
|
|
56
|
+
### Constraints
|
|
57
|
+
|
|
58
|
+
- `agent-runner.ts` already imports `SessionManager` for the `inMemory()` call — switching to `create()` adds no new dependency.
|
|
59
|
+
- `ctx.sessionManager` is `ReadonlySessionManager` and provides `getSessionId()`, `getSessionFile()`, and `getSessionDir()`.
|
|
60
|
+
- The `code-style` skill requires keeping IO at the edges and not hiding dependencies.
|
|
61
|
+
The session directory derivation should be a pure function that receives the parent session file path.
|
|
62
|
+
|
|
63
|
+
## Design Overview
|
|
64
|
+
|
|
65
|
+
### Core change: persisted SessionManager in agent-runner
|
|
66
|
+
|
|
67
|
+
Replace `SessionManager.inMemory(cwd)` with `SessionManager.create(cwd, sessionDir)` inside `runAgent()`.
|
|
68
|
+
The `sessionDir` is derived from the parent's session file path, and the `parentSession` option links the child to the parent.
|
|
69
|
+
|
|
70
|
+
The key insight is that the SDK's `createAgentSession` already writes all messages through the `SessionManager` it receives.
|
|
71
|
+
By switching from in-memory to persisted, every message the child agent produces is automatically written in official JSONL format — no manual subscription or streaming code required.
|
|
72
|
+
|
|
73
|
+
### Session directory derivation
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
/**
|
|
77
|
+
* Derive the session directory for a subagent from the parent session file.
|
|
78
|
+
* Layout: <parent-dir>/<parent-basename>/tasks/
|
|
79
|
+
*
|
|
80
|
+
* Example:
|
|
81
|
+
* parent: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_.jsonl
|
|
82
|
+
* result: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_/tasks/
|
|
83
|
+
*
|
|
84
|
+
* Falls back to a temp directory when the parent session is not persisted.
|
|
85
|
+
*/
|
|
86
|
+
function deriveSubagentSessionDir(
|
|
87
|
+
parentSessionFile: string | undefined,
|
|
88
|
+
cwd: string,
|
|
89
|
+
): string;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Data flow (after)
|
|
93
|
+
|
|
94
|
+
1. `agent-tool.ts` passes `parentSessionFile` (from `ctx.sessionManager.getSessionFile()`) and `parentSessionId` to `agent-manager.ts` via `SpawnOptions`.
|
|
95
|
+
2. `agent-manager.ts` threads them to `agent-runner.ts` via `RunOptions`.
|
|
96
|
+
3. `agent-runner.ts` calls `deriveSubagentSessionDir(parentSessionFile, cwd)` and creates `SessionManager.create(cwd, sessionDir)` with `{ parentSession: parentSessionId }`.
|
|
97
|
+
4. The `createAgentSession` SDK call receives this persisted `SessionManager`.
|
|
98
|
+
5. All messages are written to disk automatically by the SDK.
|
|
99
|
+
6. `agent-runner.ts` returns the session file path in `RunResult` (via `sessionManager.getSessionFile()`).
|
|
100
|
+
7. `agent-tool.ts` stores the path on `AgentRecord.outputFile` for display in notifications/UI.
|
|
101
|
+
|
|
102
|
+
### What gets deleted
|
|
103
|
+
|
|
104
|
+
- `output-file.ts` — entirely replaced by the persisted `SessionManager`.
|
|
105
|
+
- `output-file.test.ts` — the `encodeCwd` tests are no longer needed (Pi's `SessionManager` handles directory encoding internally).
|
|
106
|
+
- `streamToOutputFile`, `writeInitialEntry`, `createOutputFilePath` imports from `agent-tool.ts`.
|
|
107
|
+
- `outputCleanup` field from `AgentRecord` (no manual cleanup needed; the `SessionManager` is append-only).
|
|
108
|
+
- The `onSessionCreated` wrapper in `agent-tool.ts` that wires output-file streaming.
|
|
109
|
+
|
|
110
|
+
### What changes
|
|
111
|
+
|
|
112
|
+
- `RunOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
|
|
113
|
+
- `RunResult` gains `sessionFile?: string` (path to the persisted session JSONL).
|
|
114
|
+
- `SpawnOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
|
|
115
|
+
- `AgentRecord.outputCleanup` is removed (no manual flush/unsubscribe lifecycle).
|
|
116
|
+
- `agent-manager.ts` removes the `outputCleanup` calls in completion/error paths.
|
|
117
|
+
- `agent-tool.ts` sets `record.outputFile` from the `RunResult.sessionFile` instead of calling `createOutputFilePath`.
|
|
118
|
+
- `notification.ts`, `renderer.ts`, `types.ts` — `outputFile` field semantics unchanged (still a path string); only the source changes.
|
|
119
|
+
|
|
120
|
+
### Edge cases
|
|
121
|
+
|
|
122
|
+
1. **Parent session not persisted** (e.g., API/headless mode where the parent uses `SessionManager.inMemory()`): `ctx.sessionManager.getSessionFile()` returns `undefined`.
|
|
123
|
+
Fallback: use a temp directory under `/tmp/pi-subagents-<uid>/` (similar to current behavior) so transcripts are still written to disk.
|
|
124
|
+
2. **Worktree isolation**: `effectiveCwd` differs from `ctx.cwd`.
|
|
125
|
+
The `SessionManager.create()` call uses `effectiveCwd` (same as current `inMemory` call), but the `sessionDir` is still derived from the parent session — the session header's `cwd` field correctly records the worktree path.
|
|
126
|
+
|
|
127
|
+
## Module-Level Changes
|
|
128
|
+
|
|
129
|
+
### New file: `src/session-dir.ts`
|
|
130
|
+
|
|
131
|
+
Pure function `deriveSubagentSessionDir(parentSessionFile, cwd)`.
|
|
132
|
+
Extracts the parent session basename, constructs `<parent-dir>/<parent-basename>/tasks/`.
|
|
133
|
+
Falls back to a temp directory when `parentSessionFile` is undefined.
|
|
134
|
+
|
|
135
|
+
### Modified: `src/agent-runner.ts`
|
|
136
|
+
|
|
137
|
+
- Import `deriveSubagentSessionDir` from `session-dir.ts`.
|
|
138
|
+
- Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions`.
|
|
139
|
+
- Add `sessionFile?: string` to `RunResult`.
|
|
140
|
+
- Replace `SessionManager.inMemory(cfg.effectiveCwd)` with:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
|
|
144
|
+
const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
|
|
145
|
+
sessionManager.newSession({ parentSession: options.parentSessionId });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- After `session.prompt()`, capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
|
|
149
|
+
|
|
150
|
+
### Modified: `src/agent-manager.ts`
|
|
151
|
+
|
|
152
|
+
- Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions`.
|
|
153
|
+
- Thread them to `RunOptions` in the `runner.run()` call.
|
|
154
|
+
- Remove `outputCleanup` calls from the completion and error handlers.
|
|
155
|
+
|
|
156
|
+
### Modified: `src/tools/agent-tool.ts`
|
|
157
|
+
|
|
158
|
+
- Remove import of `createOutputFilePath`, `streamToOutputFile`, `writeInitialEntry`.
|
|
159
|
+
- Remove the `onSessionCreated` wrapper that wired output-file streaming.
|
|
160
|
+
- Pass `parentSessionFile: ctx.sessionManager.getSessionFile()` and `parentSessionId: ctx.sessionManager.getSessionId()` in `SpawnOptions`.
|
|
161
|
+
- After spawn, set `record.outputFile` from the returned session file path (available via `onSessionCreated` callback which gives access to the session's `sessionManager`).
|
|
162
|
+
|
|
163
|
+
### Modified: `src/types.ts`
|
|
164
|
+
|
|
165
|
+
- Remove `outputCleanup?: () => void` from `AgentRecord`.
|
|
166
|
+
|
|
167
|
+
### Deleted: `src/output-file.ts`
|
|
168
|
+
|
|
169
|
+
Entire file removed.
|
|
170
|
+
|
|
171
|
+
### Deleted: `test/output-file.test.ts`
|
|
172
|
+
|
|
173
|
+
Entire file removed.
|
|
174
|
+
|
|
175
|
+
### Unchanged: `src/notification.ts`, `src/renderer.ts`
|
|
176
|
+
|
|
177
|
+
These read `record.outputFile` (a path string) — the field still exists, just populated differently.
|
|
178
|
+
The `NotificationDetails.outputFile` field is unchanged.
|
|
179
|
+
|
|
180
|
+
## Test Impact Analysis
|
|
181
|
+
|
|
182
|
+
### New tests enabled by extraction
|
|
183
|
+
|
|
184
|
+
1. `test/session-dir.test.ts` — unit tests for `deriveSubagentSessionDir`:
|
|
185
|
+
- Parent session file present → derives correct nested path.
|
|
186
|
+
- Parent session file undefined → falls back to temp directory.
|
|
187
|
+
- Various path shapes (POSIX, Windows-like).
|
|
188
|
+
|
|
189
|
+
2. `test/agent-runner.test.ts` — updated tests verifying:
|
|
190
|
+
- `SessionManager.create` is called (not `inMemory`) when `parentSessionFile` is provided.
|
|
191
|
+
- `newSession({ parentSession })` is called with the parent session ID.
|
|
192
|
+
- `RunResult.sessionFile` is populated from the session manager.
|
|
193
|
+
|
|
194
|
+
### Tests that become redundant
|
|
195
|
+
|
|
196
|
+
- `test/output-file.test.ts` (`encodeCwd` tests) — the directory encoding is now handled by `deriveSubagentSessionDir` with a different layout.
|
|
197
|
+
The `encodeCwd` function is deleted.
|
|
198
|
+
|
|
199
|
+
### Tests that stay as-is
|
|
200
|
+
|
|
201
|
+
- All `agent-manager.test.ts` tests that mock the runner — they don't depend on `output-file` internals.
|
|
202
|
+
- All `notification.test.ts` and `renderer.test.ts` tests — they read `record.outputFile` which remains a string.
|
|
203
|
+
- The `onSessionCreated` callback tests in `agent-tool.test.ts` need updating to remove the output-file wiring expectations.
|
|
204
|
+
|
|
205
|
+
## TDD Order
|
|
206
|
+
|
|
207
|
+
### Step 1: Add `deriveSubagentSessionDir` with tests
|
|
208
|
+
|
|
209
|
+
1. Create `src/session-dir.ts` with the pure function.
|
|
210
|
+
2. Create `test/session-dir.test.ts` with tests for parent-present and fallback cases.
|
|
211
|
+
3. Commit: `feat: add deriveSubagentSessionDir for session directory derivation (#61)`
|
|
212
|
+
|
|
213
|
+
### Step 2: Thread parent session info through SpawnOptions and RunOptions
|
|
214
|
+
|
|
215
|
+
1. Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions` in `agent-manager.ts`.
|
|
216
|
+
2. Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions` in `agent-runner.ts`.
|
|
217
|
+
3. Add `sessionFile?: string` to `RunResult` in `agent-runner.ts`.
|
|
218
|
+
4. Thread the new fields from `SpawnOptions` → `RunOptions` in `agent-manager.ts`.
|
|
219
|
+
5. Update tests to verify the new fields are threaded.
|
|
220
|
+
6. Commit: `feat: thread parent session info through spawn and run options (#61)`
|
|
221
|
+
|
|
222
|
+
### Step 3: Switch agent-runner to persisted SessionManager
|
|
223
|
+
|
|
224
|
+
1. In `runAgent()`, replace `SessionManager.inMemory(cfg.effectiveCwd)` with the persisted variant using `deriveSubagentSessionDir`.
|
|
225
|
+
2. Call `sessionManager.newSession({ parentSession: options.parentSessionId })`.
|
|
226
|
+
3. Capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
|
|
227
|
+
4. Update `agent-runner.test.ts` mocks to expect `SessionManager.create` instead of `SessionManager.inMemory`.
|
|
228
|
+
5. Commit: `feat: use persisted SessionManager for subagent sessions (#61)`
|
|
229
|
+
|
|
230
|
+
### Step 4: Remove output-file wiring from agent-tool and agent-manager
|
|
231
|
+
|
|
232
|
+
1. Remove the `onSessionCreated` output-file wrapper in `agent-tool.ts`.
|
|
233
|
+
2. Remove `createOutputFilePath`, `writeInitialEntry`, `streamToOutputFile` imports.
|
|
234
|
+
3. Set `record.outputFile` from the session manager's file path (via `onSessionCreated` callback).
|
|
235
|
+
4. Pass `parentSessionFile` and `parentSessionId` in spawn options.
|
|
236
|
+
5. Remove `outputCleanup` calls from `agent-manager.ts` completion/error handlers.
|
|
237
|
+
6. Remove `outputCleanup` from `AgentRecord` in `types.ts`.
|
|
238
|
+
7. Update agent-tool and agent-manager tests.
|
|
239
|
+
8. Commit: `feat: wire session file path through agent-tool, remove output-file streaming (#61)`
|
|
240
|
+
|
|
241
|
+
### Step 5: Delete output-file.ts and its tests
|
|
242
|
+
|
|
243
|
+
1. Delete `src/output-file.ts`.
|
|
244
|
+
2. Delete `test/output-file.test.ts`.
|
|
245
|
+
3. Run full test suite to verify no remaining references.
|
|
246
|
+
4. Commit: `feat!: remove bespoke output-file transcript format (#61)`
|
|
247
|
+
|
|
248
|
+
### Step 6: Documentation update
|
|
249
|
+
|
|
250
|
+
1. Update `docs/architecture/architecture.md` — remove `output-file.ts` from the module listing, add `session-dir.ts`, mark #61 as complete.
|
|
251
|
+
2. Commit: `docs: update architecture for session format migration (#61)`
|
|
252
|
+
|
|
253
|
+
## Risks and Mitigations
|
|
254
|
+
|
|
255
|
+
### Risk: SessionManager.create may fail in environments without a writable home directory
|
|
256
|
+
|
|
257
|
+
The temp-directory fallback in `deriveSubagentSessionDir` handles this case.
|
|
258
|
+
When the parent session file is unavailable (headless/API mode), we fall back to a temp directory just like the current implementation.
|
|
259
|
+
|
|
260
|
+
### Risk: Persisted sessions accumulate disk space over time
|
|
261
|
+
|
|
262
|
+
Pi's session tooling already manages session storage.
|
|
263
|
+
Subagent sessions nested under the parent session directory are cleaned up when the parent session is deleted.
|
|
264
|
+
This is an improvement over the current `/tmp` location where files persist until system cleanup.
|
|
265
|
+
|
|
266
|
+
### Risk: Tests that mock SessionManager.inMemory need updating
|
|
267
|
+
|
|
268
|
+
Step 3 explicitly plans for updating these mocks.
|
|
269
|
+
The change is localized to `agent-runner.ts` and its direct test file.
|
|
270
|
+
|
|
271
|
+
### Risk: Breaking change for consumers that read outputFile paths
|
|
272
|
+
|
|
273
|
+
The `outputFile` field changes from `/tmp/pi-subagents-<uid>/.../tasks/<agentId>.output` to `~/.pi/agent/sessions/.../<timestamp>_<uuid>.jsonl`.
|
|
274
|
+
Consumers that parse the path (unlikely) would break, but the field is internal to the extension and only used for display.
|
|
275
|
+
The format of the file content changes from bespoke JSONL to Pi's official format — this is the intentional breaking change.
|
|
276
|
+
|
|
277
|
+
## Open Questions
|
|
278
|
+
|
|
279
|
+
1. Should the plan add a `sessionDir` field to `AgentRecord` alongside `outputFile`, or is the session file path sufficient for all use cases?
|
|
280
|
+
Deferred — start with `outputFile` pointing to the session file; add `sessionDir` if needed later.
|
|
281
|
+
2. Should foreground agents also get persisted sessions, or only background agents?
|
|
282
|
+
The current `output-file` code only runs for background agents.
|
|
283
|
+
Persisted sessions are valuable for both — the plan applies the change in `agent-runner.ts` which serves both paths.
|
|
284
|
+
If this proves too noisy, a follow-up can gate it behind a setting.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 77
|
|
3
|
+
issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inject projectAgentsDir into AgentMenuDeps
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`createAgentsMenuHandler` computes the project agents directory by reading `process.cwd()` inline via a lambda on line 63 of `ui/agent-menu.ts`:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `AgentMenuDeps` interface already carries `personalAgentsDir: string` as an explicit field, but the project-side equivalent bypasses the injection boundary entirely.
|
|
17
|
+
`projectAgentsDir()` is called in at least five places inside the handler (`findAgentFile`, `ejectAgent`, `disableAgent`, `showCreateWizard`, `showManualWizard`).
|
|
18
|
+
This violates the code-style rule: "Do not read `process.cwd()` inside library/utility functions — accept the value as a parameter."
|
|
19
|
+
|
|
20
|
+
## Goals
|
|
21
|
+
|
|
22
|
+
- Add `projectAgentsDir: string` to `AgentMenuDeps`.
|
|
23
|
+
- Remove the inline `projectAgentsDir` lambda from `createAgentsMenuHandler`.
|
|
24
|
+
- Replace all five call sites with `deps.projectAgentsDir`.
|
|
25
|
+
- Wire the value at the call site in `index.ts` as `join(process.cwd(), ".pi", "agents")`.
|
|
26
|
+
- Update the test helper `makeDeps()` in `agent-menu.test.ts` to supply the new field.
|
|
27
|
+
|
|
28
|
+
## Non-Goals
|
|
29
|
+
|
|
30
|
+
- Removing other `process.cwd()` calls in `index.ts` (e.g., `loadCustomAgents`, `GitWorktreeManager`).
|
|
31
|
+
Those are separate concerns tracked or already addressed elsewhere (e.g., #76 for `AgentManager`).
|
|
32
|
+
- Changing `personalAgentsDir` wiring or any other `AgentMenuDeps` fields.
|
|
33
|
+
|
|
34
|
+
## Background
|
|
35
|
+
|
|
36
|
+
### Relevant modules
|
|
37
|
+
|
|
38
|
+
| Module | Role |
|
|
39
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `src/ui/agent-menu.ts` | Contains `AgentMenuDeps` interface and `createAgentsMenuHandler` factory. Owns the inline `projectAgentsDir` lambda. |
|
|
41
|
+
| `src/index.ts` | Extension entry point. Constructs the `AgentMenuDeps` object at line 228. Already passes `personalAgentsDir`. |
|
|
42
|
+
| `test/ui/agent-menu.test.ts` | Tests for the menu handler. Has a `makeDeps()` helper that constructs `AgentMenuDeps`. |
|
|
43
|
+
|
|
44
|
+
### Constraints
|
|
45
|
+
|
|
46
|
+
From code-style skill:
|
|
47
|
+
|
|
48
|
+
> Do not read `process.env`, `process.cwd()`, or `process.platform` inside library/utility functions — accept the value as a parameter.
|
|
49
|
+
|
|
50
|
+
This is the same pattern applied in #76 (inject `cwd` into `AgentManager`), now applied to the UI layer.
|
|
51
|
+
|
|
52
|
+
`AgentMenuDeps` is internal — the public API surface (`exports` in `package.json`) is `service.ts` only, so this is a non-breaking change for consumers.
|
|
53
|
+
|
|
54
|
+
## Design Overview
|
|
55
|
+
|
|
56
|
+
The change is mechanical — add a field, remove a lambda, replace call sites:
|
|
57
|
+
|
|
58
|
+
1. Add `projectAgentsDir: string` to `AgentMenuDeps` (mirrors the existing `personalAgentsDir` field).
|
|
59
|
+
2. Delete the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda inside `createAgentsMenuHandler`.
|
|
60
|
+
3. Replace all five `projectAgentsDir()` call sites with `deps.projectAgentsDir`:
|
|
61
|
+
- `findAgentFile` (line 68)
|
|
62
|
+
- `ejectAgent` (line 312)
|
|
63
|
+
- `disableAgent` (line 378)
|
|
64
|
+
- `showCreateWizard` (line 416)
|
|
65
|
+
- `showManualWizard` — uses the `targetDir` value from `showCreateWizard`, so not a direct call site
|
|
66
|
+
4. At the call site in `index.ts`, add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the deps object.
|
|
67
|
+
5. In `makeDeps()`, add `projectAgentsDir: "/test-project/.pi/agents"`.
|
|
68
|
+
|
|
69
|
+
No new types, no interface changes beyond the one added field.
|
|
70
|
+
|
|
71
|
+
## Module-Level Changes
|
|
72
|
+
|
|
73
|
+
### `src/ui/agent-menu.ts`
|
|
74
|
+
|
|
75
|
+
- Add `projectAgentsDir: string` to the `AgentMenuDeps` interface.
|
|
76
|
+
- Remove the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda.
|
|
77
|
+
- Replace `projectAgentsDir()` with `deps.projectAgentsDir` at all call sites inside the factory closure.
|
|
78
|
+
- The `join` import from `node:path` may become unused in this file if no other call uses it — verify and remove if so.
|
|
79
|
+
|
|
80
|
+
### `src/index.ts`
|
|
81
|
+
|
|
82
|
+
- Add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the `createAgentsMenuHandler({...})` call.
|
|
83
|
+
- Import `join` from `node:path` if not already imported.
|
|
84
|
+
|
|
85
|
+
### `test/ui/agent-menu.test.ts`
|
|
86
|
+
|
|
87
|
+
- Add `projectAgentsDir: "/test-project/.pi/agents"` to `makeDeps()`.
|
|
88
|
+
- No other test logic changes.
|
|
89
|
+
|
|
90
|
+
## Test Impact Analysis
|
|
91
|
+
|
|
92
|
+
1. No new unit tests are strictly required — the refactoring is mechanical and preserves behavior.
|
|
93
|
+
However, a targeted test verifying that `findAgentFile` uses the injected `projectAgentsDir` (not `process.cwd()`) is valuable to prevent regression.
|
|
94
|
+
2. No existing tests become redundant.
|
|
95
|
+
3. All existing tests stay as-is; only the `makeDeps()` helper needs the new field.
|
|
96
|
+
|
|
97
|
+
## TDD Order
|
|
98
|
+
|
|
99
|
+
1. **Red: test that the injected projectAgentsDir is used** — add a test in `agent-menu.test.ts` that mocks `existsSync` and verifies `findAgentFile` (exercised via the menu handler) checks a path under the injected `projectAgentsDir`, not under `process.cwd()`.
|
|
100
|
+
Commit message: `test: verify projectAgentsDir injection in agent menu (#77)`
|
|
101
|
+
|
|
102
|
+
2. **Green: add projectAgentsDir to AgentMenuDeps and wire it** — add the field to `AgentMenuDeps`, remove the inline lambda, replace all call sites with `deps.projectAgentsDir`, wire the value in `index.ts`, and update `makeDeps()` in the test helper.
|
|
103
|
+
Commit message: `refactor: inject projectAgentsDir into AgentMenuDeps (#77)`
|
|
104
|
+
|
|
105
|
+
## Risks and Mitigations
|
|
106
|
+
|
|
107
|
+
| Risk | Mitigation |
|
|
108
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| Missing a `projectAgentsDir()` call site | `grep 'projectAgentsDir'` in `agent-menu.ts` confirms exactly five call sites (one definition + four usages). After the change, grep again to verify no `process.cwd()` calls remain. |
|
|
110
|
+
| `join` import becomes unused in `agent-menu.ts` | Check whether `join` is used elsewhere in the file (it is — `findAgentFile` uses `join` for the personal path). The import stays. |
|
|
111
|
+
| Behavioral change | Production call site passes `join(process.cwd(), ".pi", "agents")`, which produces the same value the lambda computed. No behavioral change. |
|
|
112
|
+
|
|
113
|
+
## Open Questions
|
|
114
|
+
|
|
115
|
+
None — the issue's proposed change is unambiguous and mirrors the established `personalAgentsDir` pattern.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 66
|
|
3
|
+
issue_title: "refactor: replace `as any` casts in extracted tool/menu factories with proper SDK types"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #66 — replace `as any` casts with proper SDK types
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T18:50:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Replaced all 14 `as any` casts in `src/index.ts` by typing 5 factory dep interfaces with proper SDK types (`ExtensionContext`, `AgentSession`, `ExtensionAPI`, `ModelRegistry`) and the newly-exported `SpawnOptions`.
|
|
13
|
+
Released as `pi-subagents-v5.8.1` with zero behavioral change across 520 tests.
|
|
14
|
+
The plan, implementation, and shipping were completed in a single session with 6 implementation commits.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- Thorough context-gathering during planning paid off: reading all 5 dep interfaces, the SDK `.d.ts` files, test mock helpers, and the `ExtensionUIContext` shape before writing the plan meant most steps landed on first `pnpm run check`.
|
|
21
|
+
- The plan's risk table anticipated the cascading `as any` issue on `createAgentTool` and prepared a mitigation ("add explicit `satisfies ToolDefinition<…>` if needed"), which was close to the actual fix needed.
|
|
22
|
+
- Steps 4 and 5 (`GetResultDeps`, `SteerToolDeps`) were trivially clean — single-type-import changes that compiled immediately.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — The plan assumed `NotificationDeps.sendMessage.display` could be optional (`display?: boolean`) but the SDK's `CustomMessage.display` is required `boolean`.
|
|
27
|
+
First `pnpm run check` after step 2 failed; required one extra edit-verify cycle.
|
|
28
|
+
Impact: added friction but no rework (fixed in the same commit).
|
|
29
|
+
|
|
30
|
+
- `missing-context` — The plan did not check the `execute` function's full signature in `agent-tool.ts`.
|
|
31
|
+
Removing the outer `as any` on `createAgentTool({...})` exposed three additional type mismatches: `onUpdate` parameter type (`unknown` vs `AgentToolResult<any>`), `signal` optionality (`AbortSignal` vs `AbortSignal | undefined`), and `params.description` (`unknown` vs `string`).
|
|
32
|
+
The plan flagged cascading risk but assumed only the return type would be affected.
|
|
33
|
+
Impact: step 3 required three extra edits and two extra `pnpm run check` cycles, noted as a deviation in the commit body.
|
|
34
|
+
|
|
35
|
+
- `missing-context` — Steps 6 and 7 had to be folded into one commit.
|
|
36
|
+
The plan listed them as independent steps but typing `AgentMenuManager.spawnAndWait(ctx: ExtensionContext)` immediately made `MenuContext` incompatible — `showGenerateWizard` passes `ctx: MenuContext` to the dep callback that now expects `ExtensionContext`.
|
|
37
|
+
The testing skill already warns: "when a TDD plan lists separate steps that share a type definition, changing that type in step N breaks steps N+1…N+k."
|
|
38
|
+
The planner failed to recognize that `MenuContext` and `AgentMenuManager.spawnAndWait` share a type dependency through `ctx`.
|
|
39
|
+
Impact: no rework — the steps were folded successfully — but the plan's step count was inaccurate.
|
|
40
|
+
|
|
41
|
+
#### What caused friction (user side)
|
|
42
|
+
|
|
43
|
+
- No user-side friction observed.
|
|
44
|
+
The user ran three sequential prompts (`/plan-issue 66`, `/tdd-plan`, `/ship-issue`) with no corrections or redirects needed.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 77
|
|
3
|
+
issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #77 — inject projectAgentsDir into AgentMenuDeps
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T16:15:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned, implemented, and shipped a refactoring that adds `projectAgentsDir: string` to `AgentMenuDeps`, replacing the inline `process.cwd()` lambda in `createAgentsMenuHandler`.
|
|
13
|
+
The change mirrors the existing `personalAgentsDir` injection pattern.
|
|
14
|
+
Released as `pi-subagents-v5.8.2`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- Clean end-to-end execution: plan → TDD → ship with zero corrections or rework.
|
|
21
|
+
- The Red test was well-targeted: exercised `findAgentFile` through the menu navigation path and naturally failed because `process.cwd()` produced a different path than the injected `/test-project/.pi/agents`.
|
|
22
|
+
- Correctly identified at execution time that the plan's two-step TDD split was impractical (interface change is atomic) and combined into a single `refactor:` commit.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `instruction-violation` — The plan listed two TDD commits (step 1: `test:`, step 2: `refactor:`) but adding a required field to `AgentMenuDeps` is an atomic change — the test references the new field, so both must land together.
|
|
27
|
+
The testing skill's shared-type-definition rule ("changing that type in step N breaks steps N+1…N+k — fold them into one step") should have been applied during planning.
|
|
28
|
+
Impact: added friction but no rework — the deviation was self-identified at execution time and the steps were combined.
|
|
29
|
+
Self-identified.
|
|
30
|
+
|
|
31
|
+
#### What caused friction (user side)
|
|
32
|
+
|
|
33
|
+
- None observed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-subagents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/service.ts"
|
|
6
6
|
},
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@biomejs/biome": "^2.4.14",
|
|
46
|
+
"@earendil-works/pi-ai": "0.75.4",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
48
|
+
"@earendil-works/pi-tui": "0.75.4",
|
|
46
49
|
"@types/node": "^22.15.3",
|
|
50
|
+
"rumdl": "^0.1.93",
|
|
47
51
|
"typescript": "^6.0.3",
|
|
48
|
-
"vitest": "^4.1.5"
|
|
49
|
-
"rumdl": "^0.1.93"
|
|
52
|
+
"vitest": "^4.1.5"
|
|
50
53
|
},
|
|
51
54
|
"pi": {
|
|
52
55
|
"extensions": [
|
package/src/agent-manager.ts
CHANGED
|
@@ -74,6 +74,10 @@ export interface SpawnOptions {
|
|
|
74
74
|
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
75
75
|
/** Called when the session successfully compacts. */
|
|
76
76
|
onCompaction?: (info: CompactionInfo) => void;
|
|
77
|
+
/** Path to the parent session's JSONL file (for deriving the subagent session directory). */
|
|
78
|
+
parentSessionFile?: string;
|
|
79
|
+
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
80
|
+
parentSessionId?: string;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export class AgentManager {
|
|
@@ -205,6 +209,8 @@ export class AgentManager {
|
|
|
205
209
|
inheritContext: options.inheritContext,
|
|
206
210
|
thinkingLevel: options.thinkingLevel,
|
|
207
211
|
cwd: worktreeCwd,
|
|
212
|
+
parentSessionFile: options.parentSessionFile,
|
|
213
|
+
parentSessionId: options.parentSessionId,
|
|
208
214
|
signal: record.abortController!.signal,
|
|
209
215
|
onToolActivity: (activity) => {
|
|
210
216
|
if (activity.type === "end") record.toolUses++;
|
|
@@ -223,6 +229,10 @@ export class AgentManager {
|
|
|
223
229
|
},
|
|
224
230
|
onSessionCreated: (session) => {
|
|
225
231
|
record.session = session;
|
|
232
|
+
// Capture the session file path early so it's available for display
|
|
233
|
+
// before the run completes (e.g. in background agent status messages).
|
|
234
|
+
const file = session.sessionManager?.getSessionFile?.();
|
|
235
|
+
if (file) record.outputFile = file;
|
|
226
236
|
// Flush any steers that arrived before the session was ready
|
|
227
237
|
if (record.pendingSteers?.length) {
|
|
228
238
|
for (const msg of record.pendingSteers) {
|
|
@@ -233,7 +243,7 @@ export class AgentManager {
|
|
|
233
243
|
options.onSessionCreated?.(session);
|
|
234
244
|
},
|
|
235
245
|
})
|
|
236
|
-
.then(({ responseText, session, aborted, steered }) => {
|
|
246
|
+
.then(({ responseText, session, aborted, steered, sessionFile }) => {
|
|
237
247
|
// Don't overwrite status if externally stopped via abort()
|
|
238
248
|
if (record.status !== "stopped") {
|
|
239
249
|
record.status = aborted ? "aborted" : steered ? "steered" : "completed";
|
|
@@ -241,15 +251,10 @@ export class AgentManager {
|
|
|
241
251
|
record.result = responseText;
|
|
242
252
|
record.session = session;
|
|
243
253
|
record.completedAt ??= Date.now();
|
|
254
|
+
if (sessionFile) record.outputFile = sessionFile;
|
|
244
255
|
|
|
245
256
|
detach();
|
|
246
257
|
|
|
247
|
-
// Final flush of streaming output file
|
|
248
|
-
if (record.outputCleanup) {
|
|
249
|
-
try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup", err); }
|
|
250
|
-
record.outputCleanup = undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
258
|
// Clean up worktree if used
|
|
254
259
|
if (record.worktree) {
|
|
255
260
|
const wtResult = this.worktrees.cleanup(record.worktree, options.description);
|
|
@@ -277,11 +282,7 @@ export class AgentManager {
|
|
|
277
282
|
|
|
278
283
|
detach();
|
|
279
284
|
|
|
280
|
-
|
|
281
|
-
if (record.outputCleanup) {
|
|
282
|
-
try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup on error", err); }
|
|
283
|
-
record.outputCleanup = undefined;
|
|
284
|
-
}
|
|
285
|
+
|
|
285
286
|
|
|
286
287
|
// Best-effort worktree cleanup on error
|
|
287
288
|
if (record.worktree) {
|
package/src/agent-runner.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { buildParentContext, extractText } from "./context.js";
|
|
18
18
|
import { detectEnv } from "./env.js";
|
|
19
19
|
import { assembleSessionConfig } from "./session-config.js";
|
|
20
|
+
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
20
21
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
21
22
|
|
|
22
23
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
@@ -82,6 +83,10 @@ export interface RunOptions {
|
|
|
82
83
|
thinkingLevel?: ThinkingLevel;
|
|
83
84
|
/** Override working directory (e.g. for worktree isolation). */
|
|
84
85
|
cwd?: string;
|
|
86
|
+
/** Path to the parent session's JSONL file (for deriving the subagent session directory). */
|
|
87
|
+
parentSessionFile?: string;
|
|
88
|
+
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
89
|
+
parentSessionId?: string;
|
|
85
90
|
/** Called on tool start/end with activity info. */
|
|
86
91
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
87
92
|
/** Called on streaming text deltas from the assistant response. */
|
|
@@ -127,6 +132,8 @@ export interface RunResult {
|
|
|
127
132
|
aborted: boolean;
|
|
128
133
|
/** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
|
|
129
134
|
steered: boolean;
|
|
135
|
+
/** Path to the persisted session JSONL file, if the session was persisted. */
|
|
136
|
+
sessionFile?: string;
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
/** Options for resuming an existing agent session. */
|
|
@@ -240,10 +247,17 @@ export async function runAgent(
|
|
|
240
247
|
});
|
|
241
248
|
await loader.reload();
|
|
242
249
|
|
|
250
|
+
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
251
|
+
// official JSONL format. Falls back to a temp directory when the parent
|
|
252
|
+
// session is not persisted (e.g. headless/API mode).
|
|
253
|
+
const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
|
|
254
|
+
const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
|
|
255
|
+
sessionManager.newSession({ parentSession: options.parentSessionId });
|
|
256
|
+
|
|
243
257
|
const sessionOpts: Parameters<typeof createAgentSession>[0] = {
|
|
244
258
|
cwd: cfg.effectiveCwd,
|
|
245
259
|
agentDir,
|
|
246
|
-
sessionManager
|
|
260
|
+
sessionManager,
|
|
247
261
|
settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
|
|
248
262
|
modelRegistry: ctx.modelRegistry,
|
|
249
263
|
model: cfg.model as Model<any> | undefined,
|
|
@@ -383,7 +397,13 @@ export async function runAgent(
|
|
|
383
397
|
|
|
384
398
|
const responseText =
|
|
385
399
|
collector.getText().trim() || getLastAssistantText(session);
|
|
386
|
-
return {
|
|
400
|
+
return {
|
|
401
|
+
responseText,
|
|
402
|
+
session,
|
|
403
|
+
aborted,
|
|
404
|
+
steered: softLimitReached,
|
|
405
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
406
|
+
};
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
/**
|
package/src/index.ts
CHANGED
|
@@ -264,6 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
264
|
),
|
|
265
265
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
266
266
|
personalAgentsDir: join(getAgentDir(), 'agents'),
|
|
267
|
+
projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
pi.registerCommand('agents', {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-dir.ts — Pure function for deriving subagent session directories.
|
|
3
|
+
*
|
|
4
|
+
* Subagent sessions are nested under the parent session's basename so they are
|
|
5
|
+
* discoverable via the parent session path without cluttering the main session list.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Derive the session directory for a subagent from the parent session file.
|
|
13
|
+
*
|
|
14
|
+
* Layout: `<parent-dir>/<parent-basename>/tasks/`
|
|
15
|
+
*
|
|
16
|
+
* Example:
|
|
17
|
+
* parent: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_.jsonl`
|
|
18
|
+
* result: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_/tasks`
|
|
19
|
+
*
|
|
20
|
+
* Falls back to a temp directory when the parent session is not persisted
|
|
21
|
+
* (e.g. API/headless mode where the parent uses `SessionManager.inMemory()`).
|
|
22
|
+
*/
|
|
23
|
+
export function deriveSubagentSessionDir(
|
|
24
|
+
parentSessionFile: string | undefined,
|
|
25
|
+
cwd: string,
|
|
26
|
+
): string {
|
|
27
|
+
if (parentSessionFile) {
|
|
28
|
+
const dir = dirname(parentSessionFile);
|
|
29
|
+
const base = basename(parentSessionFile, ".jsonl");
|
|
30
|
+
return join(dir, base, "tasks");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback: use a temp directory keyed by uid and cwd so different
|
|
34
|
+
// projects don't collide when the parent session is not persisted.
|
|
35
|
+
const encoded = cwd.replace(/[/\\]/g, "-").replace(/^[A-Za-z]:-/, "").replace(/^-+/, "");
|
|
36
|
+
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
37
|
+
return join(root, encoded, "tasks");
|
|
38
|
+
}
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { normalizeMaxTurns } from "../agent-runner.js";
|
|
|
6
6
|
import { resolveAgentConfig, resolveType } from "../agent-types.js";
|
|
7
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
11
11
|
import {
|
|
12
12
|
type AgentActivity,
|
|
@@ -454,19 +454,12 @@ Guidelines:
|
|
|
454
454
|
const { state: bgState, callbacks: bgCallbacks } =
|
|
455
455
|
createActivityTracker(effectiveMaxTurns);
|
|
456
456
|
|
|
457
|
-
// Wrap onSessionCreated to wire output file streaming.
|
|
458
457
|
let id: string;
|
|
459
|
-
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
460
|
-
bgCallbacks.onSessionCreated = (session: any) => {
|
|
461
|
-
origBgOnSession(session);
|
|
462
|
-
const rec = deps.manager.getRecord(id);
|
|
463
|
-
if (rec?.outputFile) {
|
|
464
|
-
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
458
|
|
|
468
459
|
try {
|
|
469
460
|
id = deps.manager.spawn(ctx, subagentType, params.prompt as string, {
|
|
461
|
+
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
462
|
+
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
470
463
|
description: params.description as string,
|
|
471
464
|
model,
|
|
472
465
|
maxTurns: effectiveMaxTurns,
|
|
@@ -482,16 +475,9 @@ Guidelines:
|
|
|
482
475
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
483
476
|
}
|
|
484
477
|
|
|
485
|
-
// Set output file synchronously after spawn
|
|
486
478
|
const record = deps.manager.getRecord(id);
|
|
487
479
|
if (record) {
|
|
488
480
|
record.toolCallId = toolCallId;
|
|
489
|
-
record.outputFile = createOutputFilePath(
|
|
490
|
-
ctx.cwd,
|
|
491
|
-
id,
|
|
492
|
-
ctx.sessionManager.getSessionId(),
|
|
493
|
-
);
|
|
494
|
-
writeInitialEntry(record.outputFile, id, params.prompt as string, ctx.cwd);
|
|
495
481
|
}
|
|
496
482
|
|
|
497
483
|
deps.agentActivity.set(id, bgState);
|
|
@@ -596,6 +582,8 @@ Guidelines:
|
|
|
596
582
|
isolation,
|
|
597
583
|
invocation: agentInvocation,
|
|
598
584
|
signal,
|
|
585
|
+
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
586
|
+
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
599
587
|
...fgCallbacks,
|
|
600
588
|
},
|
|
601
589
|
);
|
package/src/types.ts
CHANGED
|
@@ -78,10 +78,8 @@ export interface AgentRecord {
|
|
|
78
78
|
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
79
79
|
/** The tool_use_id from the original Agent tool call. */
|
|
80
80
|
toolCallId?: string;
|
|
81
|
-
/** Path to the
|
|
81
|
+
/** Path to the persisted session transcript file. */
|
|
82
82
|
outputFile?: string;
|
|
83
|
-
/** Cleanup function for the output file stream subscription. */
|
|
84
|
-
outputCleanup?: () => void;
|
|
85
83
|
/**
|
|
86
84
|
* Lifetime usage breakdown, accumulated via `message_end` events. Survives
|
|
87
85
|
* compaction. Total = input + output + cacheWrite (cacheRead deliberately
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface AgentMenuDeps {
|
|
|
41
41
|
) => { message: string; level: string };
|
|
42
42
|
emitEvent: (name: string, data: unknown) => void;
|
|
43
43
|
personalAgentsDir: string;
|
|
44
|
+
projectAgentsDir: string;
|
|
44
45
|
/** Returns the runtime default max turns (undefined = unlimited). */
|
|
45
46
|
getDefaultMaxTurns: () => number | undefined;
|
|
46
47
|
/** Returns the runtime grace turns value. */
|
|
@@ -60,12 +61,10 @@ export interface AgentMenuDeps {
|
|
|
60
61
|
* Returns a function suitable for `pi.registerCommand("agents", { handler })`.
|
|
61
62
|
*/
|
|
62
63
|
export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
63
|
-
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
64
|
-
|
|
65
64
|
function findAgentFile(
|
|
66
65
|
name: string,
|
|
67
66
|
): { path: string; location: "project" | "personal" } | undefined {
|
|
68
|
-
const projectPath = join(projectAgentsDir
|
|
67
|
+
const projectPath = join(deps.projectAgentsDir, `${name}.md`);
|
|
69
68
|
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
70
69
|
const personalPath = join(deps.personalAgentsDir, `${name}.md`);
|
|
71
70
|
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
@@ -309,7 +308,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
309
308
|
if (!location) return;
|
|
310
309
|
|
|
311
310
|
const targetDir = location.startsWith("Project")
|
|
312
|
-
? projectAgentsDir
|
|
311
|
+
? deps.projectAgentsDir
|
|
313
312
|
: deps.personalAgentsDir;
|
|
314
313
|
mkdirSync(targetDir, { recursive: true });
|
|
315
314
|
|
|
@@ -375,7 +374,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
375
374
|
if (!location) return;
|
|
376
375
|
|
|
377
376
|
const targetDir = location.startsWith("Project")
|
|
378
|
-
? projectAgentsDir
|
|
377
|
+
? deps.projectAgentsDir
|
|
379
378
|
: deps.personalAgentsDir;
|
|
380
379
|
mkdirSync(targetDir, { recursive: true });
|
|
381
380
|
|
|
@@ -413,7 +412,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
413
412
|
if (!location) return;
|
|
414
413
|
|
|
415
414
|
const targetDir = location.startsWith("Project")
|
|
416
|
-
? projectAgentsDir
|
|
415
|
+
? deps.projectAgentsDir
|
|
417
416
|
: deps.personalAgentsDir;
|
|
418
417
|
|
|
419
418
|
const method = await ctx.ui.select("Creation method", [
|
package/src/output-file.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* output-file.ts — Streaming JSONL output file for agent transcripts.
|
|
3
|
-
*
|
|
4
|
-
* Creates a per-agent output file that streams conversation turns as JSONL,
|
|
5
|
-
* matching Claude Code's task output file format.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { debugLog } from "./debug.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
16
|
-
* - POSIX: "/home/user/project" → "home-user-project"
|
|
17
|
-
* - Windows: "C:\Users\foo\project" → "Users-foo-project"
|
|
18
|
-
* - UNC: "\\\\server\\share\\project" → "server-share-project"
|
|
19
|
-
*/
|
|
20
|
-
export function encodeCwd(cwd: string): string {
|
|
21
|
-
return cwd
|
|
22
|
-
.replace(/[/\\]/g, "-") // both separators → dash
|
|
23
|
-
.replace(/^[A-Za-z]:-/, "") // strip Windows drive prefix ("C:-")
|
|
24
|
-
.replace(/^-+/, ""); // strip leading dashes (POSIX root, UNC)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Create the output file path, ensuring the directory exists.
|
|
28
|
-
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
29
|
-
export function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string {
|
|
30
|
-
const encoded = encodeCwd(cwd);
|
|
31
|
-
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
32
|
-
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
33
|
-
// chmod is a no-op on Windows and throws on some Windows filesystems.
|
|
34
|
-
// On Unix we still want to enforce 0o700 past umask, so only swallow on Windows.
|
|
35
|
-
try {
|
|
36
|
-
chmodSync(root, 0o700);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if (process.platform !== "win32") throw err;
|
|
39
|
-
}
|
|
40
|
-
const dir = join(root, encoded, sessionId, "tasks");
|
|
41
|
-
mkdirSync(dir, { recursive: true });
|
|
42
|
-
return join(dir, `${agentId}.output`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Write the initial user prompt entry. */
|
|
46
|
-
export function writeInitialEntry(path: string, agentId: string, prompt: string, cwd: string): void {
|
|
47
|
-
const entry = {
|
|
48
|
-
isSidechain: true,
|
|
49
|
-
agentId,
|
|
50
|
-
type: "user",
|
|
51
|
-
message: { role: "user", content: prompt },
|
|
52
|
-
timestamp: new Date().toISOString(),
|
|
53
|
-
cwd,
|
|
54
|
-
};
|
|
55
|
-
writeFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Subscribe to session events and flush new messages to the output file on each turn_end.
|
|
60
|
-
* Returns a cleanup function that does a final flush and unsubscribes.
|
|
61
|
-
*/
|
|
62
|
-
export function streamToOutputFile(
|
|
63
|
-
session: AgentSession,
|
|
64
|
-
path: string,
|
|
65
|
-
agentId: string,
|
|
66
|
-
cwd: string,
|
|
67
|
-
): () => void {
|
|
68
|
-
let writtenCount = 1; // initial user prompt already written
|
|
69
|
-
|
|
70
|
-
const flush = () => {
|
|
71
|
-
const messages = session.messages;
|
|
72
|
-
while (writtenCount < messages.length) {
|
|
73
|
-
const msg = messages[writtenCount];
|
|
74
|
-
const entry = {
|
|
75
|
-
isSidechain: true,
|
|
76
|
-
agentId,
|
|
77
|
-
type: msg.role === "assistant" ? "assistant" : msg.role === "user" ? "user" : "toolResult",
|
|
78
|
-
message: msg,
|
|
79
|
-
timestamp: new Date().toISOString(),
|
|
80
|
-
cwd,
|
|
81
|
-
};
|
|
82
|
-
try {
|
|
83
|
-
appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
84
|
-
} catch (err) {
|
|
85
|
-
debugLog("write JSONL chunk", err);
|
|
86
|
-
}
|
|
87
|
-
writtenCount++;
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
92
|
-
if (event.type === "turn_end") flush();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return () => {
|
|
96
|
-
flush();
|
|
97
|
-
unsubscribe();
|
|
98
|
-
};
|
|
99
|
-
}
|