@harms-haus/pi-subagents 0.1.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.
@@ -0,0 +1,519 @@
1
+ # Tools Reference
2
+
3
+ Complete reference for all tools provided by the pi-subagents extension.
4
+
5
+ > For profile configuration details, see [docs/profiles.md](profiles.md).
6
+ > For internal architecture (session lifecycle, process spawning, TUI rendering), see [docs/architecture.md](architecture.md).
7
+
8
+ ## 1. Overview
9
+
10
+ | Tool | Description |
11
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
12
+ | [`delegate_to_subagents`](#2-delegate_to_subagents) | Spawn one or more parallel sub-agents to work on separate tasks concurrently. |
13
+ | [`get_subagent_output`](#3-get_subagent_output) | Retrieve the final assistant text output from a completed sub-agent session. |
14
+ | [`get_subagent_session`](#4-get_subagent_session) | Retrieve the complete session transcript, including all messages, tool calls, and tool results. |
15
+ | [`list_subagent_profiles`](#5-list_subagent_profiles) | List all available named sub-agent profiles and their configurations. |
16
+
17
+ All tools are registered on the pi-coding-agent `ExtensionAPI` via `pi.registerTool()`.
18
+
19
+ ---
20
+
21
+ ## 2. delegate_to_subagents
22
+
23
+ ### Purpose
24
+
25
+ Spawn one or more parallel sub-agents to work on independent tasks. Each sub-agent runs in an isolated `pi` process with its own context window, tool access, and working directory. Live progress from each sub-agent is displayed in a rolling TUI window.
26
+
27
+ The tool returns session IDs for each task, which can be used with `get_subagent_output` and `get_subagent_session` to retrieve results after completion.
28
+
29
+ ### Parameters
30
+
31
+ Defined by `DelegateParams` (TypeBox schema in [`schemas.ts`](../src/schemas.ts)):
32
+
33
+ | Parameter | Type | Required | Default | Description |
34
+ | --------- | ------------------- | -------- | ------- | -------------------------------------------------------------------------------------- |
35
+ | `tasks` | `Array<TaskObject>` | Yes | — | Array of 1–16 tasks to execute in parallel. See Task Object below. |
36
+ | `profile` | `string` | No | — | Default profile name applied to all tasks. Overridden by any per-task `profile` field. |
37
+
38
+ ### Task Object
39
+
40
+ Each element of the `tasks` array has the following fields:
41
+
42
+ | Field | Type | Required | Default | Description |
43
+ | --------- | ----------------- | -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `name` | `string` | Yes | — | Display name for this sub-agent's TUI window. |
45
+ | `prompt` | `string` | Yes | — | The task/prompt sent to the sub-agent. When `resume` is set, the prior transcript is prepended (see [Resume Mechanics](#resume-mechanics)). |
46
+ | `cwd` | `string` | No | `process.cwd()` | Working directory for this sub-agent. Must be an absolute path and must not contain `..` segments. |
47
+ | `profile` | `string` | No | — | Named sub-agent profile for this specific task (overrides the top-level `profile`). See [docs/profiles.md](profiles.md). |
48
+ | `timeout` | `number` | No | `600` | Timeout in seconds. Must be ≥ 1. Aborts the sub-agent when exceeded. |
49
+ | `resume` | `string` | No | — | A previous session ID to continue work from. The resumed agent receives the prior session's transcript as context. |
50
+ | `files` | `Array<FileSpec>` | No | — | File paths to read and prepend to the sub-agent's prompt. See [Files Parameter](#files-parameter). |
51
+
52
+ ### Return Value
53
+
54
+ Returns an object with:
55
+
56
+ - **`content`**: Array with a single `{ type: "text", text: string }` entry containing a summary line per task. Each line includes status (✓/✗), task name, session ID, error message (if any), and profile info.
57
+ - **`details`**: A `WindowedSubagentDetails` object containing live window state, max lines per window, global status (`"running"` or `"done"`), and all session IDs.
58
+
59
+ **Summary text format:**
60
+
61
+ ```
62
+ ✓ build-assets: completed (session: a3f7b9c2d1e8f4a1)
63
+ ✗ run-tests: error — Timed out after 300s. Consider resuming with a longer timeout. (session: f4e1d8a7c0b39265)
64
+ ```
65
+
66
+ ### Behavior Details
67
+
68
+ #### Concurrency
69
+
70
+ - Maximum of **4 sub-agents** run simultaneously (`MAX_CONCURRENCY = 4`).
71
+ - Additional tasks are queued and started as earlier ones complete.
72
+ - Up to **16 tasks** total can be specified (`MAX_PARALLEL_TASKS = 16`).
73
+
74
+ #### Profile Resolution
75
+
76
+ Profiles are resolved **per-task** before execution begins. The resolution order is:
77
+
78
+ 1. Per-task `profile` field (highest priority)
79
+ 2. Top-level `profile` field (fallback)
80
+ 3. No profile (if neither is set)
81
+
82
+ Resolution happens synchronously at the start of `execute()`, before any sub-agents are spawned.
83
+
84
+ After profile resolution, a skill validation and resolution pipeline runs:
85
+
86
+ 1. **`validateProfileSkills()`** — Checks each resolved profile for mutually exclusive field combinations (`suggestedSkills` + `noSkills`, or `loadSkills` + `noSkills`). If a conflict is found, the entire tool call throws — no sub-agents are spawned.
87
+ 2. **`discoverSkills()`** — Called **once** (result cached) if any profile references `suggestedSkills` or `loadSkills`. Scans global and project-local skill directories.
88
+ 3. **`resolveProfileSkills()`** — Called once per **unique** profile (deduplicated to avoid repeated file reads). Resolves skill names to file paths (`suggestedSkills`) or reads skill content into `appendSystemPrompt` (`loadSkills`). Unknown skill names cause the task to fail with an error, but other tasks continue normally.
89
+
90
+ #### Resume Mechanics
91
+
92
+ When a task specifies `resume`:
93
+
94
+ 1. The session ID is validated against the session store — the session must exist and have at least one run.
95
+ 2. The session must **not** be currently running (checked against the active session set).
96
+ 3. If valid, all previous runs from the session are formatted into a human-readable transcript via `formatRunsForResume()` and prepended to the task's prompt:
97
+
98
+ ```
99
+ Previously:
100
+
101
+ --- Run 1 (completed, 42 messages) ---
102
+ User: Analyze the codebase structure...
103
+ Assistant: The project has 3 main modules...
104
+ Tool Call: read(src/index.ts)
105
+ Tool Result: export interface Config {...}
106
+ ...
107
+
108
+ Instructions:
109
+
110
+ Continue by refactoring the main loop.
111
+ ```
112
+
113
+ 4. The resumed task uses the **same session ID** as the original (not a new UUID).
114
+ 5. The session store records the new run as an additional entry in the `SessionRecord.runs` array.
115
+
116
+ ### Files Parameter
117
+
118
+ When a task specifies `files`, each file is read from disk and its contents are prepended to the prompt before the sub-agent is spawned. This provides context to the sub-agent without embedding large file contents directly in the prompt string.
119
+
120
+ Each entry in the `files` array can be:
121
+
122
+ - **A string** — Just a file path (relative to the task's `cwd` or absolute):
123
+
124
+ ```json
125
+ "src/main.ts"
126
+ ```
127
+
128
+ - **A range object** — Read specific lines (1-indexed, inclusive):
129
+
130
+ ```json
131
+ { "path": "src/main.ts", "start": 10, "end": 50 }
132
+ ```
133
+
134
+ If `start` is omitted, defaults to line 1. If `end` is omitted, reads to end of file.
135
+
136
+ - **A head object** — Read the first N lines (minimum: 1):
137
+
138
+ ```json
139
+ { "path": "src/main.ts", "head": 20 }
140
+ ```
141
+
142
+ - **A tail object** — Read the last N lines (minimum: 1):
143
+ ```json
144
+ { "path": "src/main.ts", "tail": 30 }
145
+ ```
146
+
147
+ Files are resolved relative to the task's `cwd` (or the extension's working directory if not set). The file size limit is **1 MB** per file — larger files produce a placeholder message.
148
+
149
+ File contents are formatted with a header and prepended before the prompt (and before any resume context):
150
+
151
+ ```
152
+ === src/main.ts ===
153
+ import { foo } from "bar";
154
+ ...
155
+
156
+ === README.md ===
157
+ # My Project
158
+ ...
159
+
160
+ [task prompt here]
161
+ ```
162
+
163
+ Missing or unreadable files produce a placeholder instead of failing the entire task:
164
+
165
+ - `[file not found: path]` — File does not exist
166
+ - `[could not read file: path]` — File exists but cannot be read (permissions, encoding)
167
+ - `[file too large: path (NKB, limit 1024KB)]` — File exceeds the 1 MB size limit
168
+
169
+ #### Timeout Behavior
170
+
171
+ - Each task gets its own `AbortController` and timer based on its `timeout` value (or the 600s default).
172
+ - When the timer fires, the task's `AbortController` is triggered, aborting the sub-agent process.
173
+ - The abort handler sends `SIGTERM`, escalating to `SIGKILL` after 5 seconds if the process hasn't exited.
174
+ - The window and session status are set to `"error"` with the message:
175
+ ```
176
+ Timed out after {N}s. Consider resuming with a longer timeout.
177
+ ```
178
+ - If the parent tool call's signal is aborted, all task controllers are also aborted (but this does not produce the "timed out" message).
179
+
180
+ **Timeout Extension:** When a sub-agent's timeout expires but the agent is still actively working (making tool calls), the timeout is automatically extended rather than immediately killing the process. A two-timer mechanism is used: when the original `timeout` elapses, an idle timer is started instead of aborting. Each tool call resets this idle timer. The sub-agent is only killed when no tool calls have been made for `extend_timeout_debounce` seconds. This setting defaults to **30** and is loaded from the settings file (project-local overrides global). The value is clamped to the range 0–300 seconds. The TUI always displays the original timeout value.
181
+
182
+ **Loop Detection:** To prevent runaway sub-agents, pi-subagents monitors tool call patterns. Each tool call's name and arguments are serialized as JSON into a signature string. If `looping_tool_count` (default **5**, range 0–50) consecutive tool call signatures are identical strings (exact match), the sub-agent is immediately killed with `SIGTERM` and marked as errored:
183
+
184
+ ```
185
+ Loop detected: sub-agent is repeating the same tool calls
186
+ ```
187
+
188
+ `looping_tool_count` is configurable via the settings file under `subagents` (project-local overrides global). Set `looping_tool_count` to `0` to disable loop detection entirely.
189
+
190
+ #### Unknown Profile Handling
191
+
192
+ Profile resolution happens before any sub-agents are spawned. If a task references a profile that cannot be found:
193
+
194
+ - The task's window and session are immediately marked as `"error"`.
195
+ - The error message lists available profiles:
196
+ ```
197
+ Unknown profile: "code-reviewer". Available profiles: researcher, writer
198
+ ```
199
+ If no profiles are configured, the message reads:
200
+ ```
201
+ Unknown profile: "code-reviewer". Available profiles: (none)
202
+ ```
203
+ - Other tasks continue to execute normally — one bad profile does not cancel remaining tasks.
204
+
205
+ #### Resume Validation
206
+
207
+ Two checks are performed during `execute()`, before any sub-agents are spawned:
208
+
209
+ 1. **Session exists**: `sessionStore.get(resumeId)` must return a record with at least one run. Otherwise:
210
+ ```
211
+ Cannot resume: session "a1b2c3d4e5f6a7b8" not found. The session may have expired or the ID is incorrect.
212
+ ```
213
+ 2. **Session not running**: The resume ID must not be in the active session set. Otherwise:
214
+ ```
215
+ Cannot resume: session "a1b2c3d4e5f6a7b8" is still running. Wait for it to complete before resuming.
216
+ ```
217
+
218
+ If validation fails for **any** task, the entire tool call throws — no sub-agents are spawned.
219
+
220
+ #### Session Persistence
221
+
222
+ Sessions are persisted to the main agent's session log immediately after each sub-agent completes (or errors). This enables session data to survive agent restarts.
223
+
224
+ **How it works:**
225
+
226
+ 1. After each task finishes — whether successfully, due to an error (unknown profile, loop detection, timeout), or any other reason — `persistSession()` calls `pi.appendEntry()` with the custom entry type `"pi-subagents"` and the serialized `SubagentSessionData`.
227
+ 2. Each task is persisted independently. For multiple tasks, `appendEntry` is called once per task.
228
+ 3. Persistence is **fault-tolerant**: if `appendEntry()` throws, the error is caught and logged as a warning. It does not break delegation or affect other tasks.
229
+
230
+ **Session reconstruction on restart:**
231
+
232
+ - When the main agent session restarts or resumes (`session_start` event with `reason !== "new"`), the extension iterates over all custom entries in the session log and reconstructs the in-memory session store via `deserializeSessionData()`.
233
+ - Only entries with `customType === "pi-subagents"` are processed. Malformed entries are silently skipped.
234
+ - **Stale "running" sessions** — sessions that were still `"running"` when the main agent session ended (e.g., due to a crash) — are automatically converted to `"error"` status with the message:
235
+ ```
236
+ Session was interrupted (main agent session ended unexpectedly)
237
+ ```
238
+ This ensures that `get_subagent_output` and `get_subagent_session` can always return meaningful data for persisted sessions.
239
+ - New sessions (`reason === "new"`) skip reconstruction entirely — there is no prior data to load.
240
+ - On `session_shutdown`, the in-memory session store is cleared.
241
+
242
+ ### TUI Rendering
243
+
244
+ The tool provides custom `renderCall` and `renderResult` implementations:
245
+
246
+ - **`renderCall`**: Displays the tool name, task count, and profile information.
247
+
248
+ ```
249
+ delegate_to_subagents 3 sub-agents (default profile: researcher) profiles: [writer, researcher]
250
+ ```
251
+
252
+ - **`renderResult`**: Shows a live rolling window display with:
253
+ - **Global status header**: `Sub-agents: 2 running, 1 done, 1 error`
254
+ - **Per-agent windows**: Each with a condensed header: `{icon} {bold name} • {profile-name} ({provider}/{model} {thinking-level}) • {n} tools • [{completed}/{total}] • {elapsed}s/{timeout}s`. Tool calls are rendered in muted color. `ls` and `find` calls display condensed summaries instead of raw output:
255
+ - `ls → src/` (call) → ` 2 files, 1 dir` (result); empty results show ` (empty)`
256
+ - `find → *.ts in src/` (call) → ` 3 matches` (result); empty results show ` 0 matches`
257
+ - Truncated results show a `+` suffix (e.g., ` 500 files, 3 dirs+`)
258
+ The todo segment `[completed/total]` only appears when todos are active and incomplete. Elapsed time updates live every second and freezes when the subagent completes.
259
+ - **Expanded mode** (Ctrl+O): Shows all captured messages instead of just the latest N lines.
260
+ - **Error display**: Red-colored error message beneath the agent's output.
261
+ - **Footer**: When all agents are done, displays session IDs for use with retrieval tools.
262
+
263
+ ### Examples
264
+
265
+ **Basic — two tasks, no profiles:**
266
+
267
+ ```json
268
+ {
269
+ "tasks": [
270
+ {
271
+ "name": "review-pr",
272
+ "prompt": "Review the latest pull request and provide constructive feedback."
273
+ },
274
+ {
275
+ "name": "update-docs",
276
+ "prompt": "Update the API documentation to reflect the new /v2 endpoints."
277
+ }
278
+ ]
279
+ }
280
+ ```
281
+
282
+ **With profiles — default and per-task overrides:**
283
+
284
+ ```json
285
+ {
286
+ "profile": "researcher",
287
+ "tasks": [
288
+ {
289
+ "name": "market-analysis",
290
+ "prompt": "Research competitor pricing for SaaS analytics platforms.",
291
+ "profile": "analyst"
292
+ },
293
+ {
294
+ "name": "tech-survey",
295
+ "prompt": "Survey the latest developments in RAG architectures."
296
+ }
297
+ ]
298
+ }
299
+ ```
300
+
301
+ **With resume — continue a timed-out session:**
302
+
303
+ ```json
304
+ {
305
+ "tasks": [
306
+ {
307
+ "name": "refactor-core",
308
+ "prompt": "Continue refactoring the core module. Focus on the event dispatcher.",
309
+ "resume": "a3f7b9c2d1e8f4a1",
310
+ "timeout": 900
311
+ }
312
+ ]
313
+ }
314
+ ```
315
+
316
+ **With files — provide context to sub-agents:**
317
+
318
+ ```json
319
+ {
320
+ "tasks": [
321
+ {
322
+ "name": "review-api",
323
+ "prompt": "Review the API routes for security issues.",
324
+ "files": ["src/routes.ts", { "path": "src/middleware.ts", "head": 50 }]
325
+ },
326
+ {
327
+ "name": "review-tests",
328
+ "prompt": "Check test coverage for the auth module.",
329
+ "files": [{ "path": "src/auth.ts", "tail": 30 }, "tests/auth.test.ts"]
330
+ }
331
+ ]
332
+ }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## 3. get_subagent_output
338
+
339
+ ### Purpose
340
+
341
+ Retrieve the final assistant text output from a completed sub-agent session. This is the primary way to get sub-agent results without requiring the sub-agent to write to files.
342
+
343
+ For **resumed sessions** (multiple runs), returns the output from the **latest** run only.
344
+
345
+ ### Parameters
346
+
347
+ | Parameter | Type | Required | Description |
348
+ | ----------- | -------- | -------- | --------------------------------------------------- |
349
+ | `sessionId` | `string` | Yes | The session ID returned by `delegate_to_subagents`. |
350
+
351
+ Defined inline as `Type.Object({ sessionId: Type.String(...) })`.
352
+
353
+ ### Return Value
354
+
355
+ - **`content`**: Array with a single `{ type: "text", text: string }` entry containing the last assistant text message from the latest run.
356
+ - **`details`**:
357
+ - `sessionId`: The queried session ID.
358
+ - `status`: `"running"`, `"completed"`, or `"error"`.
359
+ - `taskName`: The task name from the session.
360
+ - `runCount`: Total number of runs (1 for normal sessions, 2+ for resumed sessions).
361
+ - `maxLines`: TUI truncation limit.
362
+
363
+ If no assistant text was produced, returns `"(no text output from sub-agent)"`.
364
+
365
+ ### Resumed Sessions
366
+
367
+ For sessions with multiple runs (created via `resume`), this tool returns output from `record.runs[record.runs.length - 1]` — the **latest** run only. It does **not** concatenate output from previous runs.
368
+
369
+ ### Error Handling
370
+
371
+ If the session ID is not found or the session has no runs:
372
+
373
+ ```
374
+ Session "c4d5e6f7a8b9c0d1" not found. The session may have expired or the ID is incorrect.
375
+ ```
376
+
377
+ ### TUI Rendering
378
+
379
+ Uses a **truncating renderer**: output is displayed in the TUI up to `maxLinesPerWindow` lines (loaded from profile config). Excess lines are indicated with:
380
+
381
+ ```
382
+ ... (47 more lines)
383
+ ```
384
+
385
+ The **full content** is still injected into the LLM's context — truncation only affects TUI display.
386
+
387
+ ---
388
+
389
+ ## 4. get_subagent_session
390
+
391
+ ### Purpose
392
+
393
+ Retrieve the **complete** session transcript from a sub-agent, including all messages: assistant text, tool calls, and tool results. Use this for detailed debugging or when you need the full conversation history.
394
+
395
+ For **resumed sessions** (multiple runs), returns **all runs' data concatenated** with run separators.
396
+
397
+ ### Parameters
398
+
399
+ | Parameter | Type | Required | Description |
400
+ | ----------- | -------- | -------- | --------------------------------------------------- |
401
+ | `sessionId` | `string` | Yes | The session ID returned by `delegate_to_subagents`. |
402
+
403
+ Defined inline as `Type.Object({ sessionId: Type.String(...) })`.
404
+
405
+ ### Return Value
406
+
407
+ - **`content`**: Array with a single `{ type: "text", text: string }` entry containing the full transcript.
408
+
409
+ Each message is processed as follows:
410
+ - **Text content**: Extracted and added verbatim.
411
+ - **Tool calls**: Formatted as `→ toolName: {JSON args preview (120 chars)}`.
412
+ - **Tool results**: Formatted as `[tool result]: {text}` — truncated to 500 characters with `...` appended if longer.
413
+ - **Error messages**: Formatted as `[Error: {message}]`.
414
+
415
+ Runs are separated by `\n---\n`. For multi-run (resumed) sessions, each run is prefixed with a header:
416
+
417
+ ```
418
+ === Run 1/3 (completed) ===
419
+ ...
420
+ ---
421
+ === Run 2/3 (completed) ===
422
+ ...
423
+ ```
424
+
425
+ - **`details`**:
426
+ - `sessionId`: The queried session ID.
427
+ - `status`: Status of the latest run (`"running"`, `"completed"`, or `"error"`).
428
+ - `taskName`: Task name from the latest run.
429
+ - `messageCount`: Total messages across **all** runs (sum of `r.messages.length` for each run).
430
+ - `exitCode`: Exit code from the latest run.
431
+ - `model`: Model identifier from the latest run.
432
+ - `runCount`: Number of runs (1 for normal, 2+ for resumed sessions).
433
+ - `maxLines`: TUI truncation limit.
434
+
435
+ ### Resumed Sessions
436
+
437
+ For sessions with multiple runs, `get_subagent_session` iterates over **all** runs in `record.runs` and concatenates their messages into a single transcript. Each run is clearly labeled with its run number and status.
438
+
439
+ ### Error Handling
440
+
441
+ If the session ID is not found or the session has no runs:
442
+
443
+ ```
444
+ Session "c4d5e6f7a8b9c0d1" not found. The session may have expired or the ID is incorrect.
445
+ ```
446
+
447
+ ### TUI Rendering
448
+
449
+ Uses the same **truncating renderer** as `get_subagent_output` — display is limited to `maxLinesPerWindow` lines with a truncation indicator. Full content is available in the LLM context.
450
+
451
+ ---
452
+
453
+ ## 5. list_subagent_profiles
454
+
455
+ ### Purpose
456
+
457
+ List all available named sub-agent profiles that can be used with `delegate_to_subagents`. Profiles are loaded from two locations:
458
+
459
+ - **Global**: `~/.pi/agent/agent-profiles/*.md`
460
+ - **Project-local**: `.pi/agent-profiles/*.md`
461
+
462
+ Project-local profiles override global profiles with the same name.
463
+
464
+ ### Parameters
465
+
466
+ None. The parameters schema is `Type.Object({})` — an empty object.
467
+
468
+ ### Return Value
469
+
470
+ - **`content`**: Array with a single `{ type: "text", text: string }` entry. Each line is a profile summary generated by `profileSummary()`, containing the profile name, provider/model, system prompt preview, and key settings.
471
+ - **`details`**:
472
+ - `count`: Number of profiles found.
473
+ - `profiles`: Object mapping profile names to their summary strings.
474
+
475
+ ### Empty Case
476
+
477
+ When no profiles are found, returns:
478
+
479
+ ```
480
+ No subagent profiles found. Add .md files to ~/.pi/agent/agent-profiles/ or .pi/agent-profiles/.
481
+ ```
482
+
483
+ With `details: { count: 0 }`.
484
+
485
+ ### TUI Rendering
486
+
487
+ Uses a **simple (non-truncating) renderer** — the full profile list is displayed without truncation.
488
+
489
+ ---
490
+
491
+ ## 6. Error Reference
492
+
493
+ All error conditions and their messages:
494
+
495
+ | Condition | Tool(s) | Error Message | When |
496
+ | ------------------------------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
497
+ | **Session not found** | `get_subagent_output`, `get_subagent_session` | `Session "{id}" not found. The session may have expired or the ID is incorrect.` | Session ID not in store or has zero runs. |
498
+ | **Resume session still running** | `delegate_to_subagents` | `Cannot resume: session "{id}" is still running. Wait for it to complete before resuming.` | `resume` references a session currently in the active set. |
499
+ | **Resume session not found** | `delegate_to_subagents` | `Cannot resume: session "{id}" not found. The session may have expired or the ID is incorrect.` | `resume` references a non-existent session or one with zero runs. |
500
+ | **Unknown profile** | `delegate_to_subagents` | `Unknown profile: "{name}". Available profiles: {list}` | Task specifies a profile name not found in global or project-local directories. Other tasks continue normally. |
501
+ | **Sub-agent spawn failure** | `delegate_to_subagents` (internal) | `Failed to spawn sub-agent process` | The child `pi` process fails to start (e.g., binary not found, permission denied). Caught by the `error` event on `spawn()`. |
502
+ | **Timeout** | `delegate_to_subagents` (internal) | `Timed out after {N}s. Consider resuming with a longer timeout.` | A task exceeds its `timeout` value (default 600s). The sub-agent is aborted via `SIGTERM` → `SIGKILL`. |
503
+ | **Invalid cwd — relative path** | `delegate_to_subagents` (internal) | `cwd must be an absolute path` | The `cwd` parameter is not an absolute path. Thrown before the sub-agent is spawned. |
504
+ | **Invalid cwd — path traversal** | `delegate_to_subagents` (internal) | `cwd must not contain '..' path segments` | The resolved `cwd` contains `..` segments. Thrown before the sub-agent is spawned. |
505
+ | **Unknown skill (suggestedSkills)** | `delegate_to_subagents` | `Unknown skills: "bad-name". Available skills: skill-a, skill-b` | Profile specifies a skill name not found by skill discovery. The task is marked as error; other tasks continue normally. |
506
+ | **Skills + noSkills conflict (suggestedSkills)** | `delegate_to_subagents` | `Profile "name" has both "suggestedSkills" and "noSkills" set. These are mutually exclusive — --no-skills would override --skill flags.` | Profile validation throws during `validateProfileSkills()`. The entire tool call fails — no sub-agents are spawned. |
507
+ | **Skills + noSkills conflict (loadSkills)** | `delegate_to_subagents` | `Profile "name" has both "loadSkills" and "noSkills" set. These are mutually exclusive — --no-skills disables skill discovery.` | Profile validation throws during `validateProfileSkills()`. The entire tool call fails — no sub-agents are spawned. |
508
+ | **Loop detected** | `delegate_to_subagents` (internal) | `Loop detected: sub-agent is repeating the same tool calls` | `looping_tool_count` consecutive tool call signatures (serialized as JSON) were identical. The sub-agent is killed via `SIGTERM`. |
509
+ | **File not found** | `delegate_to_subagents` (files) | `[file not found: {path}]` | A file specified in `files` does not exist. The task continues with a placeholder in the prompt. |
510
+ | **File unreadable** | `delegate_to_subagents` (files) | `[could not read file: {path}]` | A file exists but cannot be read (permissions, encoding). The task continues with a placeholder. |
511
+ | **File too large** | `delegate_to_subagents` (files) | `[file too large: {path} ({size}KB, limit 1024KB)]` | A file exceeds the 1 MB size limit. The task continues with a placeholder. |
512
+ | **Tool restriction override guard** | `delegate_to_subagents` | `Refusing extraArg "${arg}" which would override profile tool restrictions. Use the dedicated profile fields instead.` | Thrown when `extraArgs` contains `--tools`, `-t`, `--no-tools`, or `-nt` (including their `=` forms) while the profile has tool restrictions active. |
513
+
514
+ ---
515
+
516
+ ## Cross-References
517
+
518
+ - **Profile configuration**: See [docs/profiles.md](profiles.md) for profile file format, frontmatter schema, and profile resolution order.
519
+ - **Architecture**: See [docs/architecture.md](architecture.md) for session lifecycle, process management, and TUI rendering internals.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@harms-haus/pi-subagents",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension for spawning parallel sub-agents with live TUI windows",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "keywords": [
8
+ "pi-package"
9
+ ],
10
+ "author": "harms-haus",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/harms-haus/pi-subagents.git"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.0.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "src/**/*.ts",
24
+ "!src/__tests__/**",
25
+ "docs/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "pi": {
30
+ "extensions": [
31
+ "./src/index.ts"
32
+ ]
33
+ },
34
+ "peerDependencies": {
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "typebox": "*"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^10.0.1",
40
+ "@types/node": "^22.0.0",
41
+ "@vitest/coverage-v8": "^4.1.6",
42
+ "eslint": "^10.4.0",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "prettier": "^3.8.3",
45
+ "typescript": "^6.0.3",
46
+ "typescript-eslint": "^8.59.3",
47
+ "vitest": "^4.1.6"
48
+ },
49
+ "scripts": {
50
+ "lint": "eslint src/",
51
+ "lint:fix": "eslint --fix src/",
52
+ "format": "prettier --write src/",
53
+ "format:check": "prettier --check src/",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage"
58
+ }
59
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Generic TTL-based cache for single-key lookups */
2
+ export class TtlCache<T> {
3
+ private entry: { key: string; data: T; timestamp: number } | null = null;
4
+
5
+ constructor(private ttl: number) {}
6
+
7
+ get(key: string): T | undefined {
8
+ if (this.entry && this.entry.key === key) {
9
+ if (Date.now() - this.entry.timestamp < this.ttl) {
10
+ return this.entry.data;
11
+ }
12
+ this.entry = null; // Allow GC of stale data
13
+ }
14
+ return undefined;
15
+ }
16
+
17
+ set(key: string, data: T): void {
18
+ this.entry = { key, data, timestamp: Date.now() };
19
+ }
20
+
21
+ invalidate(): void {
22
+ this.entry = null;
23
+ }
24
+ }