pi-subagents 0.3.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 +94 -0
- package/README.md +300 -0
- package/agents.ts +172 -0
- package/artifacts.ts +70 -0
- package/chain-clarify.ts +612 -0
- package/index.ts +2186 -0
- package/install.mjs +93 -0
- package/notify.ts +87 -0
- package/package.json +38 -0
- package/settings.ts +492 -0
- package/subagent-runner.ts +608 -0
- package/types.ts +114 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0] - 2026-01-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Full edit mode for chain TUI** - Press `e`, `o`, or `r` to enter a full-screen editor with:
|
|
7
|
+
- Word wrapping for long text that spans multiple display lines
|
|
8
|
+
- Scrolling viewport (12 lines visible) with scroll indicators (↑↓)
|
|
9
|
+
- Full cursor navigation: Up/Down move by display line, Page Up/Down by viewport
|
|
10
|
+
- Home/End go to start/end of current display line, Ctrl+Home/End for start/end of text
|
|
11
|
+
- Auto-scroll to keep cursor visible
|
|
12
|
+
- Esc saves, Ctrl+C discards changes
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- **Tool description now explicitly shows the three modes** (SINGLE, CHAIN, PARALLEL) with syntax - helps agents pick the right mode when user says "scout → planner"
|
|
16
|
+
- **Chain execution observability** - Now shows:
|
|
17
|
+
- Chain visualization with status icons: `✓scout → ●planner` (✓=done, ●=running, ○=pending, ✗=failed) - sequential chains only
|
|
18
|
+
- Accurate step counter: "step 1/2" instead of misleading "1/1"
|
|
19
|
+
- Current tool and recent output for running step
|
|
20
|
+
|
|
21
|
+
## [0.2.0] - 2026-01-24
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Rebranded to `pi-subagents`** (was `pi-async-subagents`)
|
|
25
|
+
- Now installable via `npx pi-subagents`
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Chain TUI now supports editing output paths, reads lists, and toggling progress per step
|
|
29
|
+
- New keybindings: `o` (output), `r` (reads), `p` (progress toggle)
|
|
30
|
+
- Output and reads support full file paths, not just relative to chain_dir
|
|
31
|
+
- Each step shows all editable fields: task, output, reads, progress
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Chain clarification TUI edit mode now properly re-renders after state changes (was unresponsive)
|
|
35
|
+
- Changed edit shortcut from Tab to 'e' (Tab can be problematic in terminals)
|
|
36
|
+
- Edit mode cursor now starts at beginning of first line for better UX
|
|
37
|
+
- Footer shows context-sensitive keybinding hints for navigation vs edit mode
|
|
38
|
+
- Edit mode is now single-line only (Enter disabled) - UI only displays first line, so multi-line was confusing
|
|
39
|
+
- Added Ctrl+C in edit mode to discard changes (Esc saves, Ctrl+C discards)
|
|
40
|
+
- Footer now shows "Done" instead of "Save" for clarity
|
|
41
|
+
- Absolute paths for output/reads now work correctly (were incorrectly prepended with chainDir)
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Parallel-in-chain execution with `{ parallel: [...] }` step syntax for fan-out/fan-in patterns
|
|
45
|
+
- Configurable concurrency and fail-fast options for parallel steps
|
|
46
|
+
- Output aggregation with clear separators (`=== Parallel Task N (agent) ===`) for `{previous}`
|
|
47
|
+
- Namespaced artifact directories for parallel tasks (`parallel-{step}/{index}-{agent}/`)
|
|
48
|
+
- Pre-created progress.md for parallel steps to avoid race conditions
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- TUI clarification skipped for chains with parallel steps (runs directly in sync mode)
|
|
52
|
+
- Async mode rejects chains with parallel steps with clear error message
|
|
53
|
+
- Chain completion now returns summary blurb with progress.md and artifacts paths instead of raw output
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Live progress display for sync subagents (single and chain modes)
|
|
57
|
+
- Shows current tool, recent output lines, token count, and duration during execution
|
|
58
|
+
- Ctrl+O hint during sync execution to expand full streaming view
|
|
59
|
+
- Throttled updates (150ms) for smoother progress display
|
|
60
|
+
- Updates on tool_execution_start/end events for more responsive feedback
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Async widget elapsed time now freezes when job completes instead of continuing to count up
|
|
64
|
+
- Progress data now correctly linked to results during execution (was showing "ok" instead of "...")
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- Extension API support (registerTool) with `subagent` tool name
|
|
68
|
+
- Session logs (JSONL + HTML export) and optional share links via GitHub Gist
|
|
69
|
+
- `share` and `sessionDir` parameters for session retention control
|
|
70
|
+
- Async events: `subagent:started`/`subagent:complete` (legacy events still emitted)
|
|
71
|
+
- Share info surfaced in TUI and async notifications
|
|
72
|
+
- Async observability folder with `status.json`, `events.jsonl`, and `subagent-log-*.md`
|
|
73
|
+
- `subagent_status` tool for inspecting async run state
|
|
74
|
+
- Async TUI widget for background runs
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
- Parallel mode auto-downgrades to sync when async:true is passed (with note in output)
|
|
78
|
+
- TUI now shows "parallel (no live progress)" label to set expectations
|
|
79
|
+
- Tools passed via agent config can include extension paths (forwarded via `--extension`)
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- Chain mode now sums step durations instead of taking max (was showing incorrect total time)
|
|
83
|
+
- Async notifications no longer leak across pi sessions in different directories
|
|
84
|
+
|
|
85
|
+
## [0.1.0] - 2026-01-03
|
|
86
|
+
|
|
87
|
+
Initial release forked from async-subagent example.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
- Output truncation with configurable byte/line limits
|
|
91
|
+
- Real-time progress tracking (tools, tokens, duration)
|
|
92
|
+
- Debug artifacts (input, output, JSONL, metadata)
|
|
93
|
+
- Session-tied artifact storage for sync mode
|
|
94
|
+
- Per-step duration tracking for chains
|
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<img src="banner.png" alt="pi-subagents" width="1100">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# pi-subagents
|
|
6
|
+
|
|
7
|
+
Pi extension for delegating tasks to subagents with chains, parallel execution, TUI clarification, and async support.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx pi-subagents
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This clones the extension to `~/.pi/agent/extensions/subagent/`. To update, run the same command. To remove:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx pi-subagents --remove
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features (beyond base)
|
|
22
|
+
|
|
23
|
+
- **Parallel-in-Chain**: Fan-out/fan-in patterns with `{ parallel: [...] }` steps within chains
|
|
24
|
+
- **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
|
|
25
|
+
- **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`)
|
|
26
|
+
- **Chain Artifacts**: Shared directory at `/tmp/pi-chain-runs/{runId}/` for inter-step files
|
|
27
|
+
- **Solo Agent Output**: Agents with `output` write to temp dir and return path to caller
|
|
28
|
+
- **Live Progress Display**: Real-time visibility during sync execution showing current tool, recent output, tokens, and duration
|
|
29
|
+
- **Output Truncation**: Configurable byte/line limits via `maxOutput`
|
|
30
|
+
- **Debug Artifacts**: Input/output/JSONL/metadata files per task
|
|
31
|
+
- **Session Logs**: JSONL session files with paths shown in output
|
|
32
|
+
- **Async Status Files**: Durable `status.json`, `events.jsonl`, and markdown logs for async runs
|
|
33
|
+
- **Async Widget**: Lightweight TUI widget shows background run progress
|
|
34
|
+
- **Session-scoped Notifications**: Async completions only notify the originating session
|
|
35
|
+
|
|
36
|
+
## Modes
|
|
37
|
+
|
|
38
|
+
| Mode | Async Support | Notes |
|
|
39
|
+
|------|---------------|-------|
|
|
40
|
+
| Single | Yes | `{ agent, task }` - agents with `output` write to temp dir |
|
|
41
|
+
| Chain | Yes* | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
|
|
42
|
+
| Parallel | Sync only | `{ tasks: [{agent, task}...] }` - auto-downgrades if async requested |
|
|
43
|
+
|
|
44
|
+
*Chain defaults to sync with TUI clarification. Use `clarify: false` to enable async (sequential-only chains; parallel-in-chain requires sync mode).
|
|
45
|
+
|
|
46
|
+
**Chain clarification TUI keybindings:**
|
|
47
|
+
|
|
48
|
+
*Navigation mode:*
|
|
49
|
+
- `Enter` - Run the chain
|
|
50
|
+
- `Esc` - Cancel
|
|
51
|
+
- `↑↓` - Navigate between steps
|
|
52
|
+
- `e` - Edit task/template
|
|
53
|
+
- `o` - Edit output path
|
|
54
|
+
- `r` - Edit reads list
|
|
55
|
+
- `p` - Toggle progress tracking on/off
|
|
56
|
+
|
|
57
|
+
*Edit mode (full-screen editor with word wrapping):*
|
|
58
|
+
- `Esc` - Save changes and exit
|
|
59
|
+
- `Ctrl+C` - Discard changes and exit
|
|
60
|
+
- `←→` - Move cursor left/right
|
|
61
|
+
- `↑↓` - Move cursor up/down by display line (auto-scrolls)
|
|
62
|
+
- `Page Up/Down` or `Shift+↑↓` - Move cursor by viewport (12 lines)
|
|
63
|
+
- `Home/End` - Start/end of current display line
|
|
64
|
+
- `Ctrl+Home/End` - Start/end of text
|
|
65
|
+
|
|
66
|
+
## Agent Frontmatter
|
|
67
|
+
|
|
68
|
+
Agents can declare default chain behavior in their frontmatter:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
---
|
|
72
|
+
name: scout
|
|
73
|
+
description: Fast codebase recon
|
|
74
|
+
tools: read, grep, find, ls, bash
|
|
75
|
+
model: claude-haiku-4-5
|
|
76
|
+
output: context.md # writes to {chain_dir}/context.md
|
|
77
|
+
defaultReads: context.md # comma-separated files to read
|
|
78
|
+
defaultProgress: true # maintain progress.md
|
|
79
|
+
interactive: true # (parsed but not enforced in v1)
|
|
80
|
+
---
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Resolution priority:** step override > agent frontmatter > disabled
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
**subagent tool:**
|
|
88
|
+
```typescript
|
|
89
|
+
// Single agent
|
|
90
|
+
{ agent: "worker", task: "refactor auth" }
|
|
91
|
+
{ agent: "scout", task: "find todos", maxOutput: { lines: 1000 } }
|
|
92
|
+
{ agent: "scout", task: "investigate", output: false } // disable file output
|
|
93
|
+
|
|
94
|
+
// Parallel (sync only)
|
|
95
|
+
{ tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
|
|
96
|
+
|
|
97
|
+
// Chain with TUI clarification (default)
|
|
98
|
+
{ chain: [
|
|
99
|
+
{ agent: "scout", task: "Gather context for auth refactor" },
|
|
100
|
+
{ agent: "planner" }, // task defaults to {previous}
|
|
101
|
+
{ agent: "worker" }, // uses agent defaults for reads/progress
|
|
102
|
+
{ agent: "reviewer" }
|
|
103
|
+
]}
|
|
104
|
+
|
|
105
|
+
// Chain without TUI (enables async)
|
|
106
|
+
{ chain: [...], clarify: false, async: true }
|
|
107
|
+
|
|
108
|
+
// Chain with behavior overrides
|
|
109
|
+
{ chain: [
|
|
110
|
+
{ agent: "scout", task: "find issues", output: false }, // text-only, no file
|
|
111
|
+
{ agent: "worker", progress: false } // disable progress tracking
|
|
112
|
+
]}
|
|
113
|
+
|
|
114
|
+
// Chain with parallel step (fan-out/fan-in)
|
|
115
|
+
{ chain: [
|
|
116
|
+
{ agent: "scout", task: "Gather context for the codebase" },
|
|
117
|
+
{ parallel: [
|
|
118
|
+
{ agent: "worker", task: "Implement auth based on {previous}" },
|
|
119
|
+
{ agent: "worker", task: "Implement API based on {previous}" }
|
|
120
|
+
]},
|
|
121
|
+
{ agent: "reviewer", task: "Review all changes from {previous}" }
|
|
122
|
+
]}
|
|
123
|
+
|
|
124
|
+
// Parallel step with options
|
|
125
|
+
{ chain: [
|
|
126
|
+
{ agent: "scout", task: "Find all modules" },
|
|
127
|
+
{ parallel: [
|
|
128
|
+
{ agent: "worker", task: "Refactor module A" },
|
|
129
|
+
{ agent: "worker", task: "Refactor module B" },
|
|
130
|
+
{ agent: "worker", task: "Refactor module C" }
|
|
131
|
+
], concurrency: 2, failFast: true } // limit concurrency, stop on first failure
|
|
132
|
+
]}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**subagent_status tool:**
|
|
136
|
+
```typescript
|
|
137
|
+
{ id: "a53ebe46" }
|
|
138
|
+
{ dir: "/tmp/pi-async-subagent-runs/a53ebe46-..." }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Parameters
|
|
142
|
+
|
|
143
|
+
| Param | Type | Default | Description |
|
|
144
|
+
|-------|------|---------|-------------|
|
|
145
|
+
| `agent` | string | - | Agent name (single mode) |
|
|
146
|
+
| `task` | string | - | Task string (single mode) |
|
|
147
|
+
| `output` | `string \| false` | agent default | Override output file for single agent |
|
|
148
|
+
| `tasks` | `{agent, task, cwd?}[]` | - | Parallel tasks (sync only) |
|
|
149
|
+
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
150
|
+
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
151
|
+
| `agentScope` | `"user" \| "project" \| "both"` | `user` | Agent discovery scope |
|
|
152
|
+
| `async` | boolean | false | Background execution (requires `clarify: false` for chains) |
|
|
153
|
+
| `cwd` | string | - | Override working directory |
|
|
154
|
+
| `maxOutput` | `{bytes?, lines?}` | 200KB, 5000 lines | Truncation limits for final output |
|
|
155
|
+
| `artifacts` | boolean | true | Write debug artifacts |
|
|
156
|
+
| `includeProgress` | boolean | false | Include full progress in result |
|
|
157
|
+
| `share` | boolean | true | Create shareable session log |
|
|
158
|
+
| `sessionDir` | string | temp | Directory to store session logs |
|
|
159
|
+
|
|
160
|
+
**ChainItem** can be either a sequential step or a parallel step:
|
|
161
|
+
|
|
162
|
+
*Sequential step fields:*
|
|
163
|
+
|
|
164
|
+
| Field | Type | Default | Description |
|
|
165
|
+
|-------|------|---------|-------------|
|
|
166
|
+
| `agent` | string | required | Agent name |
|
|
167
|
+
| `task` | string | `{task}` or `{previous}` | Task template (required for first step) |
|
|
168
|
+
| `cwd` | string | - | Override working directory |
|
|
169
|
+
| `output` | `string \| false` | agent default | Override output filename or disable |
|
|
170
|
+
| `reads` | `string[] \| false` | agent default | Override files to read from chain dir |
|
|
171
|
+
| `progress` | boolean | agent default | Override progress.md tracking |
|
|
172
|
+
|
|
173
|
+
*Parallel step fields:*
|
|
174
|
+
|
|
175
|
+
| Field | Type | Default | Description |
|
|
176
|
+
|-------|------|---------|-------------|
|
|
177
|
+
| `parallel` | ParallelTask[] | required | Array of tasks to run concurrently |
|
|
178
|
+
| `concurrency` | number | 4 | Max concurrent tasks |
|
|
179
|
+
| `failFast` | boolean | false | Stop remaining tasks on first failure |
|
|
180
|
+
|
|
181
|
+
*ParallelTask fields:* (same as sequential step)
|
|
182
|
+
|
|
183
|
+
| Field | Type | Default | Description |
|
|
184
|
+
|-------|------|---------|-------------|
|
|
185
|
+
| `agent` | string | required | Agent name |
|
|
186
|
+
| `task` | string | `{previous}` | Task template |
|
|
187
|
+
| `cwd` | string | - | Override working directory |
|
|
188
|
+
| `output` | `string \| false` | agent default | Override output (namespaced to parallel-N/M-agent/) |
|
|
189
|
+
| `reads` | `string[] \| false` | agent default | Override files to read |
|
|
190
|
+
| `progress` | boolean | agent default | Override progress tracking |
|
|
191
|
+
|
|
192
|
+
Status tool:
|
|
193
|
+
|
|
194
|
+
| Tool | Description |
|
|
195
|
+
|------|-------------|
|
|
196
|
+
| `subagent_status` | Inspect async run status by id or dir |
|
|
197
|
+
|
|
198
|
+
## Chain Variables
|
|
199
|
+
|
|
200
|
+
Templates support three variables:
|
|
201
|
+
|
|
202
|
+
| Variable | Description |
|
|
203
|
+
|----------|-------------|
|
|
204
|
+
| `{task}` | Original task from first step (use in subsequent steps) |
|
|
205
|
+
| `{previous}` | Output from prior step (or aggregated outputs from parallel step) |
|
|
206
|
+
| `{chain_dir}` | Path to chain artifacts directory |
|
|
207
|
+
|
|
208
|
+
**Parallel output aggregation:** When a parallel step completes, all outputs are concatenated with clear separators:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
=== Parallel Task 1 (worker) ===
|
|
212
|
+
[output from first task]
|
|
213
|
+
|
|
214
|
+
=== Parallel Task 2 (worker) ===
|
|
215
|
+
[output from second task]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This aggregated output becomes `{previous}` for the next step.
|
|
219
|
+
|
|
220
|
+
## Chain Directory
|
|
221
|
+
|
|
222
|
+
Each chain run creates `/tmp/pi-chain-runs/{runId}/` containing:
|
|
223
|
+
- `context.md` - Scout/context-builder output
|
|
224
|
+
- `plan.md` - Planner output
|
|
225
|
+
- `progress.md` - Worker/reviewer shared progress
|
|
226
|
+
- `parallel-{stepIndex}/` - Subdirectories for parallel step outputs
|
|
227
|
+
- `0-{agent}/output.md` - First parallel task output
|
|
228
|
+
- `1-{agent}/output.md` - Second parallel task output
|
|
229
|
+
- Additional files as written by agents
|
|
230
|
+
|
|
231
|
+
Directories older than 24 hours are cleaned up on extension startup.
|
|
232
|
+
|
|
233
|
+
## Artifacts
|
|
234
|
+
|
|
235
|
+
Location: `{sessionDir}/subagent-artifacts/` or `/tmp/pi-subagent-artifacts/`
|
|
236
|
+
|
|
237
|
+
Files per task:
|
|
238
|
+
- `{runId}_{agent}_input.md` - Task prompt
|
|
239
|
+
- `{runId}_{agent}_output.md` - Full output (untruncated)
|
|
240
|
+
- `{runId}_{agent}.jsonl` - Event stream (sync only)
|
|
241
|
+
- `{runId}_{agent}_meta.json` - Timing, usage, exit code
|
|
242
|
+
|
|
243
|
+
## Session Logs
|
|
244
|
+
|
|
245
|
+
Session files (JSONL) are stored under a per-run session dir (temp by default). The session file path is shown in output. Set `sessionDir` to keep session logs outside `/tmp`.
|
|
246
|
+
|
|
247
|
+
## Live progress (sync mode)
|
|
248
|
+
|
|
249
|
+
During sync execution, the collapsed view shows:
|
|
250
|
+
- Header: `... chain 1/2 | 8 tools, 1.4k tok, 38s`
|
|
251
|
+
- Chain visualization with status: `✓scout → ●planner` (✓=done, ●=running, ○=pending, ✗=failed)
|
|
252
|
+
- Current tool: `> read: packages/tui/src/...`
|
|
253
|
+
- Recent output lines (last 2-3 lines)
|
|
254
|
+
- Hint: `(ctrl+o to expand)`
|
|
255
|
+
|
|
256
|
+
Press **Ctrl+O** to expand the full streaming view with complete output per step.
|
|
257
|
+
|
|
258
|
+
> **Note:** Chain visualization is only shown for sequential chains. Chains with parallel steps show the header and progress but not the step-by-step visualization.
|
|
259
|
+
|
|
260
|
+
## Async observability
|
|
261
|
+
|
|
262
|
+
Async runs write a dedicated observability folder:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
/tmp/pi-async-subagent-runs/<id>/
|
|
266
|
+
status.json
|
|
267
|
+
events.jsonl
|
|
268
|
+
subagent-log-<id>.md
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`status.json` is the source of truth for async progress and powers the TUI widget. If you already use
|
|
272
|
+
`/status <id>` you can keep doing that; otherwise use:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
subagent_status({ id: "<id>" })
|
|
276
|
+
subagent_status({ dir: "/tmp/pi-async-subagent-runs/<id>" })
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Events
|
|
280
|
+
|
|
281
|
+
Async events:
|
|
282
|
+
- `subagent:started`
|
|
283
|
+
- `subagent:complete`
|
|
284
|
+
|
|
285
|
+
Legacy events (still emitted):
|
|
286
|
+
- `subagent_enhanced:started`
|
|
287
|
+
- `subagent_enhanced:complete`
|
|
288
|
+
|
|
289
|
+
## Files
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
├── index.ts # Main extension (registerTool)
|
|
293
|
+
├── agents.ts # Agent discovery + frontmatter parsing
|
|
294
|
+
├── settings.ts # Chain behavior resolution, templates, chain dir
|
|
295
|
+
├── chain-clarify.ts # TUI component for chain clarification
|
|
296
|
+
├── artifacts.ts # Artifact management
|
|
297
|
+
├── types.ts # Shared types
|
|
298
|
+
├── subagent-runner.ts # Async runner
|
|
299
|
+
└── notify.ts # Async completion notifications
|
|
300
|
+
```
|
package/agents.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
export type AgentScope = "user" | "project" | "both";
|
|
10
|
+
|
|
11
|
+
export interface AgentConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
tools?: string[];
|
|
15
|
+
model?: string;
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
source: "user" | "project";
|
|
18
|
+
filePath: string;
|
|
19
|
+
// Chain behavior fields
|
|
20
|
+
output?: string;
|
|
21
|
+
defaultReads?: string[];
|
|
22
|
+
defaultProgress?: boolean;
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AgentDiscoveryResult {
|
|
27
|
+
agents: AgentConfig[];
|
|
28
|
+
projectAgentsDir: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
32
|
+
const frontmatter: Record<string, string> = {};
|
|
33
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
34
|
+
|
|
35
|
+
if (!normalized.startsWith("---")) {
|
|
36
|
+
return { frontmatter, body: normalized };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
40
|
+
if (endIndex === -1) {
|
|
41
|
+
return { frontmatter, body: normalized };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
45
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
46
|
+
|
|
47
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
48
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
49
|
+
if (match) {
|
|
50
|
+
let value = match[2].trim();
|
|
51
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
52
|
+
value = value.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
frontmatter[match[1]] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { frontmatter, body };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
62
|
+
const agents: AgentConfig[] = [];
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
return agents;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let entries: fs.Dirent[];
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return agents;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
77
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
78
|
+
|
|
79
|
+
const filePath = path.join(dir, entry.name);
|
|
80
|
+
let content: string;
|
|
81
|
+
try {
|
|
82
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
88
|
+
|
|
89
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tools = frontmatter.tools
|
|
94
|
+
?.split(",")
|
|
95
|
+
.map((t) => t.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
|
|
98
|
+
// Parse defaultReads as comma-separated list (like tools)
|
|
99
|
+
const defaultReads = frontmatter.defaultReads
|
|
100
|
+
?.split(",")
|
|
101
|
+
.map((f) => f.trim())
|
|
102
|
+
.filter(Boolean);
|
|
103
|
+
|
|
104
|
+
agents.push({
|
|
105
|
+
name: frontmatter.name,
|
|
106
|
+
description: frontmatter.description,
|
|
107
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
108
|
+
model: frontmatter.model,
|
|
109
|
+
systemPrompt: body,
|
|
110
|
+
source,
|
|
111
|
+
filePath,
|
|
112
|
+
// Chain behavior fields
|
|
113
|
+
output: frontmatter.output,
|
|
114
|
+
defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
|
|
115
|
+
defaultProgress: frontmatter.defaultProgress === "true",
|
|
116
|
+
interactive: frontmatter.interactive === "true",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return agents;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isDirectory(p: string): boolean {
|
|
124
|
+
try {
|
|
125
|
+
return fs.statSync(p).isDirectory();
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
132
|
+
let currentDir = cwd;
|
|
133
|
+
while (true) {
|
|
134
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
135
|
+
if (isDirectory(candidate)) return candidate;
|
|
136
|
+
|
|
137
|
+
const parentDir = path.dirname(currentDir);
|
|
138
|
+
if (parentDir === currentDir) return null;
|
|
139
|
+
currentDir = parentDir;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
144
|
+
const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
145
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
146
|
+
|
|
147
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
148
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
149
|
+
|
|
150
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
151
|
+
|
|
152
|
+
if (scope === "both") {
|
|
153
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
154
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
155
|
+
} else if (scope === "user") {
|
|
156
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
157
|
+
} else {
|
|
158
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
165
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
166
|
+
const listed = agents.slice(0, maxItems);
|
|
167
|
+
const remaining = agents.length - listed.length;
|
|
168
|
+
return {
|
|
169
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
170
|
+
remaining,
|
|
171
|
+
};
|
|
172
|
+
}
|
package/artifacts.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ArtifactPaths } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const TEMP_ARTIFACTS_DIR = "/tmp/pi-subagent-artifacts";
|
|
6
|
+
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
7
|
+
|
|
8
|
+
export function getArtifactsDir(sessionFile: string | null): string {
|
|
9
|
+
if (sessionFile) {
|
|
10
|
+
const sessionDir = path.dirname(sessionFile);
|
|
11
|
+
return path.join(sessionDir, "subagent-artifacts");
|
|
12
|
+
}
|
|
13
|
+
return TEMP_ARTIFACTS_DIR;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getArtifactPaths(artifactsDir: string, runId: string, agent: string, index?: number): ArtifactPaths {
|
|
17
|
+
const suffix = index !== undefined ? `_${index}` : "";
|
|
18
|
+
const safeAgent = agent.replace(/[^\w.-]/g, "_");
|
|
19
|
+
const base = `${runId}_${safeAgent}${suffix}`;
|
|
20
|
+
return {
|
|
21
|
+
inputPath: path.join(artifactsDir, `${base}_input.md`),
|
|
22
|
+
outputPath: path.join(artifactsDir, `${base}_output.md`),
|
|
23
|
+
jsonlPath: path.join(artifactsDir, `${base}.jsonl`),
|
|
24
|
+
metadataPath: path.join(artifactsDir, `${base}_meta.json`),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ensureArtifactsDir(dir: string): void {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeArtifact(filePath: string, content: string): void {
|
|
33
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeMetadata(filePath: string, metadata: object): void {
|
|
37
|
+
fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function appendJsonl(filePath: string, line: string): void {
|
|
41
|
+
fs.appendFileSync(filePath, `${line}\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
45
|
+
if (!fs.existsSync(dir)) return;
|
|
46
|
+
|
|
47
|
+
const markerPath = path.join(dir, CLEANUP_MARKER_FILE);
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(markerPath)) {
|
|
51
|
+
const stat = fs.statSync(markerPath);
|
|
52
|
+
if (now - stat.mtimeMs < 24 * 60 * 60 * 1000) return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
56
|
+
const cutoff = now - maxAgeMs;
|
|
57
|
+
|
|
58
|
+
for (const file of fs.readdirSync(dir)) {
|
|
59
|
+
if (file === CLEANUP_MARKER_FILE) continue;
|
|
60
|
+
const filePath = path.join(dir, file);
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.statSync(filePath);
|
|
63
|
+
if (stat.mtimeMs < cutoff) {
|
|
64
|
+
fs.unlinkSync(filePath);
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.writeFileSync(markerPath, String(now));
|
|
70
|
+
}
|