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 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
+ }