pi-subagents 0.8.2 → 0.8.4
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 +30 -1
- package/README.md +43 -18
- package/agent-management.ts +7 -0
- package/agent-manager-detail.ts +4 -0
- package/agent-manager-edit.ts +4 -2
- package/agent-scope.ts +6 -0
- package/agent-selection.ts +20 -0
- package/agent-serializer.ts +6 -0
- package/agents.ts +13 -12
- package/artifacts.ts +23 -1
- package/async-execution.ts +8 -18
- package/chain-execution.ts +15 -7
- package/completion-dedupe.ts +63 -0
- package/execution.ts +36 -13
- package/file-coalescer.ts +40 -0
- package/index.ts +54 -24
- package/jsonl-writer.ts +81 -0
- package/notify.ts +4 -13
- package/package.json +2 -2
- package/pi-spawn.ts +77 -0
- package/render.ts +44 -18
- package/schemas.ts +5 -4
- package/settings.ts +18 -2
- package/single-output.ts +55 -0
- package/skills.ts +221 -80
- package/subagent-runner.ts +32 -6
- package/types.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.4] - 2026-02-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- JSONL artifact files no longer written by default — they duplicated pi's own session files and were the sole cause of `subagent-artifacts` directories growing to 10+ GB. Changed `includeJsonl` default from `true` to `false`. `_output.md` and `_meta.json` still capture the useful data.
|
|
9
|
+
- Artifact cleanup now covers session-based directories, not just the temp dir. Previously `cleanupOldArtifacts` only ran on `os.tmpdir()/pi-subagent-artifacts` at startup, while sync runs (the common path) wrote to `<session-dir>/subagent-artifacts/` which was never cleaned. Now scans all `~/.pi/agent/sessions/*/subagent-artifacts/` dirs on startup and cleans the current session's artifacts dir on session lifecycle events.
|
|
10
|
+
- JSONL writer now enforces a 50 MB size cap (`maxBytes` on `JsonlWriterDeps`) as defense-in-depth for users who opt into JSONL. Silently stops writing at the cap without pausing the source stream, so the progress tracker keeps working.
|
|
11
|
+
|
|
12
|
+
## [0.8.3] - 2026-02-11
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Agent `extensions` frontmatter support for extension sandboxing: absent field keeps default extension discovery, empty value disables all extensions, and comma-separated values create an explicit extension allowlist.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Parallel chain aggregation now surfaces step failures and warnings in `{previous}` instead of silently passing empty output.
|
|
19
|
+
- Empty-output warnings are now context-aware: runs that intentionally write to explicit output paths are not flagged as warning-only successes in the renderer.
|
|
20
|
+
- Async execution now respects agent `extensions` sandbox settings, matching sync behavior.
|
|
21
|
+
- Single-mode `output` now resolves explicit paths correctly: absolute paths are used directly, and relative paths resolve against `cwd`.
|
|
22
|
+
- Single-mode output persistence is now caller-side in both sync and async execution, so output files are still written when agents run with read-only tools.
|
|
23
|
+
- Pi process spawning now uses a shared cross-platform helper in sync and async paths; on Windows it prefers direct Node + CLI invocation to avoid `ENOENT` and argument fragmentation.
|
|
24
|
+
- Sync JSONL artifact capture now streams lines directly to disk with backpressure handling, preventing unbounded memory growth in long or parallel runs.
|
|
25
|
+
- Execution now defaults `agentScope` to `both`, aligning run behavior with management `list` so project agents shown in discovery execute without explicit scope overrides.
|
|
26
|
+
- Async completion notifications now dedupe at source and notify layers, eliminating duplicate/triple "Background task completed" messages.
|
|
27
|
+
- Async notifications now standardize on canonical `subagent:started` and `subagent:complete` events (legacy enhanced event emissions removed).
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Reworked `skills.ts` to resolve skills through Pi core skill loading with explicit project-first precedence and support for project/user package and settings skill paths.
|
|
31
|
+
- Skill discovery now normalizes and prioritizes collisions by source so project-scoped skills consistently win over user-scoped skills.
|
|
32
|
+
- Documentation now references `<tmpdir>` instead of hardcoded `/tmp` paths for cross-platform clarity.
|
|
33
|
+
|
|
5
34
|
## [0.8.2] - 2026-02-11
|
|
6
35
|
|
|
7
36
|
### Added
|
|
@@ -10,7 +39,7 @@
|
|
|
10
39
|
## [0.8.1] - 2026-02-10
|
|
11
40
|
|
|
12
41
|
### Added
|
|
13
|
-
- **`chainDir` param** for persistent chain artifacts — specify a directory to keep artifacts beyond the default 24-hour
|
|
42
|
+
- **`chainDir` param** for persistent chain artifacts — specify a directory to keep artifacts beyond the default 24-hour temp-directory cleanup. Relative paths are resolved to absolute via `path.resolve()` for safe use in `{chain_dir}` template substitutions.
|
|
14
43
|
|
|
15
44
|
## [0.8.0] - 2026-02-09
|
|
16
45
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
|
|
|
31
31
|
| User | `~/.pi/agent/agents/{name}.md` |
|
|
32
32
|
| Project | `.pi/agents/{name}.md` (searches up directory tree) |
|
|
33
33
|
|
|
34
|
-
Use `agentScope` parameter to control discovery: `"user"
|
|
34
|
+
Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
|
|
35
35
|
|
|
36
36
|
**Agent frontmatter:**
|
|
37
37
|
|
|
@@ -40,6 +40,7 @@ Use `agentScope` parameter to control discovery: `"user"` (default), `"project"`
|
|
|
40
40
|
name: scout
|
|
41
41
|
description: Fast codebase recon
|
|
42
42
|
tools: read, grep, find, ls, bash, mcp:chrome-devtools # mcp: requires pi-mcp-adapter
|
|
43
|
+
extensions: # absent=all, empty=none, csv=allowlist
|
|
43
44
|
model: claude-haiku-4-5
|
|
44
45
|
thinking: high # off, minimal, low, medium, high, xhigh
|
|
45
46
|
skill: safe-bash, chrome-devtools # comma-separated skills to inject
|
|
@@ -54,6 +55,27 @@ Your system prompt goes here (the markdown body after frontmatter).
|
|
|
54
55
|
|
|
55
56
|
The `thinking` field sets a default extended thinking level for the agent. At runtime it's appended as a `:level` suffix to the model string (e.g., `claude-sonnet-4-5:high`). If the model already has a thinking suffix (from a chain-clarify override), the agent's default is not double-applied.
|
|
56
57
|
|
|
58
|
+
**Extension sandboxing**
|
|
59
|
+
|
|
60
|
+
Use `extensions` in frontmatter to control which extensions a subagent can access:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# Field absent: all extensions load (default behavior)
|
|
64
|
+
|
|
65
|
+
# Empty field: no extensions
|
|
66
|
+
extensions:
|
|
67
|
+
|
|
68
|
+
# Allowlist specific extensions
|
|
69
|
+
extensions: /abs/path/to/ext-a.ts, /abs/path/to/ext-b.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Semantics:
|
|
73
|
+
- `extensions` absent → all extensions load
|
|
74
|
+
- `extensions:` empty → `--no-extensions`
|
|
75
|
+
- `extensions: a,b` → `--no-extensions --extension a --extension b`
|
|
76
|
+
|
|
77
|
+
When `extensions` is present, it takes precedence over extension paths implied by `tools` entries.
|
|
78
|
+
|
|
57
79
|
**MCP Tools**
|
|
58
80
|
|
|
59
81
|
Agents can use MCP server tools directly (requires the [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) extension). Add `mcp:` prefixed entries to the `tools` field:
|
|
@@ -127,7 +149,7 @@ Append `[key=value,...]` to any agent name to override its defaults:
|
|
|
127
149
|
|
|
128
150
|
| Key | Example | Description |
|
|
129
151
|
|-----|---------|-------------|
|
|
130
|
-
| `output` | `output=context.md` | Write results to file (relative to chain dir for `/chain`/`/parallel`,
|
|
152
|
+
| `output` | `output=context.md` | Write results to file (relative to chain dir for `/chain`/`/parallel`; for `/run`, absolute paths are used as-is and relative paths resolve against cwd) |
|
|
131
153
|
| `reads` | `reads=a.md+b.md` | Read files before executing (`+` separates multiple) |
|
|
132
154
|
| `model` | `model=anthropic/claude-sonnet-4` | Override model for this step |
|
|
133
155
|
| `skills` | `skills=planning+review` | Override skills (`+` separates multiple) |
|
|
@@ -228,7 +250,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
228
250
|
- **Parallel-in-Chain**: Fan-out/fan-in patterns with `{ parallel: [...] }` steps within chains
|
|
229
251
|
- **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
|
|
230
252
|
- **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`)
|
|
231
|
-
- **Chain Artifacts**: Shared directory at
|
|
253
|
+
- **Chain Artifacts**: Shared directory at `<tmpdir>/pi-chain-runs/{runId}/` for inter-step files
|
|
232
254
|
- **Solo Agent Output**: Agents with `output` write to temp dir and return path to caller
|
|
233
255
|
- **Live Progress Display**: Real-time visibility during sync execution showing current tool, recent output, tokens, and duration
|
|
234
256
|
- **Output Truncation**: Configurable byte/line limits via `maxOutput`
|
|
@@ -310,9 +332,13 @@ Single and parallel modes also support the clarify TUI for previewing/editing pa
|
|
|
310
332
|
|
|
311
333
|
Skills are specialized instructions loaded from SKILL.md files and injected into the agent's system prompt.
|
|
312
334
|
|
|
313
|
-
**Skill locations:**
|
|
314
|
-
- Project: `.pi/skills/{name}/SKILL.md`
|
|
335
|
+
**Skill locations (project-first precedence):**
|
|
336
|
+
- Project: `.pi/skills/{name}/SKILL.md`
|
|
337
|
+
- Project packages: `.pi/npm/node_modules/*` via `package.json -> pi.skills`
|
|
338
|
+
- Project settings: `.pi/settings.json -> skills`
|
|
315
339
|
- User: `~/.pi/agent/skills/{name}/SKILL.md`
|
|
340
|
+
- User packages: `~/.pi/agent/npm/node_modules/*` via `package.json -> pi.skills`
|
|
341
|
+
- User settings: `~/.pi/agent/settings.json -> skills`
|
|
316
342
|
|
|
317
343
|
**Usage:**
|
|
318
344
|
```typescript
|
|
@@ -397,7 +423,7 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
397
423
|
**subagent_status tool:**
|
|
398
424
|
```typescript
|
|
399
425
|
{ id: "a53ebe46" }
|
|
400
|
-
{ dir: "
|
|
426
|
+
{ dir: "<tmpdir>/pi-async-subagent-runs/a53ebe46-..." }
|
|
401
427
|
```
|
|
402
428
|
|
|
403
429
|
## Management Actions
|
|
@@ -421,6 +447,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
421
447
|
systemPrompt: "You are a code scout...",
|
|
422
448
|
model: "anthropic/claude-sonnet-4",
|
|
423
449
|
tools: "read, bash, mcp:github/search_repositories",
|
|
450
|
+
extensions: "", // empty = no extensions
|
|
424
451
|
skills: "parallel-scout",
|
|
425
452
|
thinking: "high",
|
|
426
453
|
output: "context.md",
|
|
@@ -457,7 +484,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
457
484
|
Notes:
|
|
458
485
|
- `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
|
|
459
486
|
- `update`/`delete` use `agentScope` only for scope disambiguation when the same name exists in both scopes.
|
|
460
|
-
- Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, and `tools` supports `mcp:` entries that map to direct MCP tools.
|
|
487
|
+
- Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, and `tools` supports `mcp:` entries that map to direct MCP tools.
|
|
461
488
|
- To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
|
|
462
489
|
|
|
463
490
|
## Parameters
|
|
@@ -469,14 +496,14 @@ Notes:
|
|
|
469
496
|
| `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
|
|
470
497
|
| `chainName` | string | - | Chain name for management get/update/delete |
|
|
471
498
|
| `config` | object | - | Agent or chain config for management create/update |
|
|
472
|
-
| `output` | `string \| false` | agent default | Override output file for single agent |
|
|
499
|
+
| `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
|
|
473
500
|
| `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
|
|
474
501
|
| `model` | string | agent default | Override model for single agent |
|
|
475
502
|
| `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
|
|
476
503
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
477
|
-
| `chainDir` | string |
|
|
504
|
+
| `chainDir` | string | `<tmpdir>/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
478
505
|
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
479
|
-
| `agentScope` | `"user" \| "project" \| "both"` | `
|
|
506
|
+
| `agentScope` | `"user" \| "project" \| "both"` | `both` | Agent discovery scope (project wins on name collisions) |
|
|
480
507
|
| `async` | boolean | false | Background execution (requires `clarify: false` for chains) |
|
|
481
508
|
| `cwd` | string | - | Override working directory |
|
|
482
509
|
| `maxOutput` | `{bytes?, lines?}` | 200KB, 5000 lines | Truncation limits for final output |
|
|
@@ -551,7 +578,7 @@ This aggregated output becomes `{previous}` for the next step.
|
|
|
551
578
|
|
|
552
579
|
## Chain Directory
|
|
553
580
|
|
|
554
|
-
Each chain run creates
|
|
581
|
+
Each chain run creates `<tmpdir>/pi-chain-runs/{runId}/` containing:
|
|
555
582
|
- `context.md` - Scout/context-builder output
|
|
556
583
|
- `plan.md` - Planner output
|
|
557
584
|
- `progress.md` - Worker/reviewer shared progress
|
|
@@ -564,7 +591,7 @@ Directories older than 24 hours are cleaned up on extension startup.
|
|
|
564
591
|
|
|
565
592
|
## Artifacts
|
|
566
593
|
|
|
567
|
-
Location: `{sessionDir}/subagent-artifacts/` or
|
|
594
|
+
Location: `{sessionDir}/subagent-artifacts/` or `<tmpdir>/pi-subagent-artifacts/`
|
|
568
595
|
|
|
569
596
|
Files per task:
|
|
570
597
|
- `{runId}_{agent}_input.md` - Task prompt
|
|
@@ -574,7 +601,7 @@ Files per task:
|
|
|
574
601
|
|
|
575
602
|
## Session Logs
|
|
576
603
|
|
|
577
|
-
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
|
|
604
|
+
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 `<tmpdir>`.
|
|
578
605
|
|
|
579
606
|
## Live progress (sync mode)
|
|
580
607
|
|
|
@@ -616,7 +643,7 @@ export PI_SUBAGENT_MAX_DEPTH=0 # disable the subagent tool entirely
|
|
|
616
643
|
Async runs write a dedicated observability folder:
|
|
617
644
|
|
|
618
645
|
```
|
|
619
|
-
|
|
646
|
+
<tmpdir>/pi-async-subagent-runs/<id>/
|
|
620
647
|
status.json
|
|
621
648
|
events.jsonl
|
|
622
649
|
subagent-log-<id>.md
|
|
@@ -627,7 +654,7 @@ Async runs write a dedicated observability folder:
|
|
|
627
654
|
|
|
628
655
|
```typescript
|
|
629
656
|
subagent_status({ id: "<id>" })
|
|
630
|
-
subagent_status({ dir: "
|
|
657
|
+
subagent_status({ dir: "<tmpdir>/pi-async-subagent-runs/<id>" })
|
|
631
658
|
```
|
|
632
659
|
|
|
633
660
|
## Events
|
|
@@ -636,9 +663,7 @@ Async events:
|
|
|
636
663
|
- `subagent:started`
|
|
637
664
|
- `subagent:complete`
|
|
638
665
|
|
|
639
|
-
|
|
640
|
-
- `subagent_enhanced:started`
|
|
641
|
-
- `subagent_enhanced:complete`
|
|
666
|
+
`notify.ts` consumes `subagent:complete` as the canonical completion channel.
|
|
642
667
|
|
|
643
668
|
## Files
|
|
644
669
|
|
package/agent-management.ts
CHANGED
|
@@ -203,6 +203,12 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
203
203
|
else if (typeof cfg.skills === "string") { const skills = parseCsv(cfg.skills); target.skills = skills.length ? skills : undefined; }
|
|
204
204
|
else return "config.skills must be a comma-separated string or false when provided.";
|
|
205
205
|
}
|
|
206
|
+
if (hasKey(cfg, "extensions")) {
|
|
207
|
+
if (cfg.extensions === false) target.extensions = undefined;
|
|
208
|
+
else if (cfg.extensions === "") target.extensions = [];
|
|
209
|
+
else if (typeof cfg.extensions === "string") target.extensions = parseCsv(cfg.extensions);
|
|
210
|
+
else return "config.extensions must be a comma-separated string, empty string, or false when provided.";
|
|
211
|
+
}
|
|
206
212
|
if (hasKey(cfg, "thinking")) {
|
|
207
213
|
if (cfg.thinking === false || cfg.thinking === "") target.thinking = undefined;
|
|
208
214
|
else if (typeof cfg.thinking === "string") target.thinking = cfg.thinking.trim() || undefined;
|
|
@@ -273,6 +279,7 @@ export function formatAgentDetail(agent: AgentConfig): string {
|
|
|
273
279
|
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
274
280
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
275
281
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
282
|
+
if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
|
|
276
283
|
if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
|
|
277
284
|
if (agent.output) lines.push(`Output: ${agent.output}`);
|
|
278
285
|
if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
|
package/agent-manager-detail.ts
CHANGED
|
@@ -64,6 +64,10 @@ function buildDetailLines(
|
|
|
64
64
|
lines.push(renderFieldLine("Tools:", tools, contentWidth, theme));
|
|
65
65
|
lines.push(renderFieldLine("MCP:", mcp, contentWidth, theme));
|
|
66
66
|
lines.push(renderFieldLine("Skills:", skillsList, contentWidth, theme));
|
|
67
|
+
const extensionsList = agent.extensions !== undefined
|
|
68
|
+
? (agent.extensions.length > 0 ? agent.extensions.join(", ") : "(none)")
|
|
69
|
+
: "(all)";
|
|
70
|
+
lines.push(renderFieldLine("Extensions:", extensionsList, contentWidth, theme));
|
|
67
71
|
lines.push(renderFieldLine("Output:", output, contentWidth, theme));
|
|
68
72
|
lines.push(renderFieldLine("Reads:", reads, contentWidth, theme));
|
|
69
73
|
lines.push(renderFieldLine("Progress:", progress, contentWidth, theme));
|
package/agent-manager-edit.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface EditState {
|
|
|
16
16
|
export interface EditInputResult { action?: "save" | "discard"; nextScreen?: EditScreen; }
|
|
17
17
|
|
|
18
18
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
19
|
-
const FIELD_ORDER = ["name", "description", "model", "thinking", "tools", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
19
|
+
const FIELD_ORDER = ["name", "description", "model", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
20
20
|
type EditField = typeof FIELD_ORDER[number];
|
|
21
21
|
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
22
22
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
@@ -29,7 +29,7 @@ function parseCommaList(value: string): string[] | undefined { const items = val
|
|
|
29
29
|
|
|
30
30
|
export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[]): EditState {
|
|
31
31
|
return {
|
|
32
|
-
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
32
|
+
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
33
33
|
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
34
34
|
modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
35
35
|
};
|
|
@@ -43,6 +43,7 @@ function renderFieldValue(field: EditField, state: EditState): string {
|
|
|
43
43
|
case "model": return draft.model ?? "default";
|
|
44
44
|
case "thinking": return draft.thinking ?? "off";
|
|
45
45
|
case "tools": return formatTools(draft);
|
|
46
|
+
case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
|
|
46
47
|
case "skills": return draft.skills && draft.skills.length > 0 ? draft.skills.join(", ") : "";
|
|
47
48
|
case "output": return draft.output ?? "";
|
|
48
49
|
case "reads": return draft.defaultReads && draft.defaultReads.length > 0 ? draft.defaultReads.join(", ") : "";
|
|
@@ -59,6 +60,7 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
|
|
|
59
60
|
case "description": draft.description = value.trim(); break;
|
|
60
61
|
case "model": draft.model = value.trim() || undefined; break;
|
|
61
62
|
case "tools": { const parsed = parseTools(value); draft.tools = parsed.tools; draft.mcpDirectTools = parsed.mcp; break; }
|
|
63
|
+
case "extensions": { const trimmed = value.trim(); draft.extensions = trimmed === "(all)" ? undefined : parseCommaList(trimmed) ?? []; break; }
|
|
62
64
|
case "skills": draft.skills = parseCommaList(value); break;
|
|
63
65
|
case "output": { const trimmed = value.trim(); draft.output = trimmed.length > 0 ? trimmed : undefined; break; }
|
|
64
66
|
case "reads": draft.defaultReads = parseCommaList(value); break;
|
package/agent-scope.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentScope, AgentConfig } from "./agents.js";
|
|
2
|
+
|
|
3
|
+
export function mergeAgentsForScope(
|
|
4
|
+
scope: AgentScope,
|
|
5
|
+
userAgents: AgentConfig[],
|
|
6
|
+
projectAgents: AgentConfig[],
|
|
7
|
+
): AgentConfig[] {
|
|
8
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
9
|
+
|
|
10
|
+
if (scope === "both") {
|
|
11
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
12
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
13
|
+
} else if (scope === "user") {
|
|
14
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
15
|
+
} else {
|
|
16
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Array.from(agentMap.values());
|
|
20
|
+
}
|
package/agent-serializer.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
9
9
|
"thinking",
|
|
10
10
|
"skill",
|
|
11
11
|
"skills",
|
|
12
|
+
"extensions",
|
|
12
13
|
"output",
|
|
13
14
|
"defaultReads",
|
|
14
15
|
"defaultProgress",
|
|
@@ -39,6 +40,11 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
39
40
|
const skillsValue = joinComma(config.skills);
|
|
40
41
|
if (skillsValue) lines.push(`skills: ${skillsValue}`);
|
|
41
42
|
|
|
43
|
+
if (config.extensions !== undefined) {
|
|
44
|
+
const extensionsValue = joinComma(config.extensions);
|
|
45
|
+
lines.push(`extensions: ${extensionsValue ?? ""}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
if (config.output) lines.push(`output: ${config.output}`);
|
|
43
49
|
|
|
44
50
|
const readsValue = joinComma(config.defaultReads);
|
package/agents.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as os from "node:os";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { KNOWN_FIELDS } from "./agent-serializer.js";
|
|
9
9
|
import { parseChain } from "./chain-serializer.js";
|
|
10
|
+
import { mergeAgentsForScope } from "./agent-selection.js";
|
|
10
11
|
|
|
11
12
|
export type AgentScope = "user" | "project" | "both";
|
|
12
13
|
|
|
@@ -21,6 +22,7 @@ export interface AgentConfig {
|
|
|
21
22
|
source: "user" | "project";
|
|
22
23
|
filePath: string;
|
|
23
24
|
skills?: string[];
|
|
25
|
+
extensions?: string[];
|
|
24
26
|
// Chain behavior fields
|
|
25
27
|
output?: string;
|
|
26
28
|
defaultReads?: string[];
|
|
@@ -145,6 +147,14 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
145
147
|
.map((s) => s.trim())
|
|
146
148
|
.filter(Boolean);
|
|
147
149
|
|
|
150
|
+
let extensions: string[] | undefined;
|
|
151
|
+
if (frontmatter.extensions !== undefined) {
|
|
152
|
+
extensions = frontmatter.extensions
|
|
153
|
+
.split(",")
|
|
154
|
+
.map((e) => e.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
const extraFields: Record<string, string> = {};
|
|
149
159
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
150
160
|
if (!KNOWN_FIELDS.has(key)) extraFields[key] = value;
|
|
@@ -161,6 +171,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
161
171
|
source,
|
|
162
172
|
filePath,
|
|
163
173
|
skills: skills && skills.length > 0 ? skills : undefined,
|
|
174
|
+
extensions,
|
|
164
175
|
// Chain behavior fields
|
|
165
176
|
output: frontmatter.output,
|
|
166
177
|
defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
|
|
@@ -235,19 +246,9 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
235
246
|
|
|
236
247
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
237
248
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
249
|
+
const agents = mergeAgentsForScope(scope, userAgents, projectAgents);
|
|
238
250
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (scope === "both") {
|
|
242
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
243
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
244
|
-
} else if (scope === "user") {
|
|
245
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
246
|
-
} else {
|
|
247
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
251
|
+
return { agents, projectAgentsDir };
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
export function discoverAgentsAll(cwd: string): {
|
package/artifacts.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ArtifactPaths } from "./types.js";
|
|
4
5
|
|
|
5
|
-
const TEMP_ARTIFACTS_DIR = "
|
|
6
|
+
const TEMP_ARTIFACTS_DIR = path.join(os.tmpdir(), "pi-subagent-artifacts");
|
|
6
7
|
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
7
8
|
|
|
8
9
|
export function getArtifactsDir(sessionFile: string | null): string {
|
|
@@ -68,3 +69,24 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
|
68
69
|
|
|
69
70
|
fs.writeFileSync(markerPath, String(now));
|
|
70
71
|
}
|
|
72
|
+
|
|
73
|
+
export function cleanupAllArtifactDirs(maxAgeDays: number): void {
|
|
74
|
+
cleanupOldArtifacts(TEMP_ARTIFACTS_DIR, maxAgeDays);
|
|
75
|
+
|
|
76
|
+
const sessionsBase = path.join(os.homedir(), ".pi", "agent", "sessions");
|
|
77
|
+
if (!fs.existsSync(sessionsBase)) return;
|
|
78
|
+
|
|
79
|
+
let dirs: string[];
|
|
80
|
+
try {
|
|
81
|
+
dirs = fs.readdirSync(sessionsBase);
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
const artifactsDir = path.join(sessionsBase, dir, "subagent-artifacts");
|
|
88
|
+
try {
|
|
89
|
+
cleanupOldArtifacts(artifactsDir, maxAgeDays);
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/async-execution.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "./agents.js";
|
|
13
13
|
import { applyThinkingSuffix } from "./execution.js";
|
|
14
|
+
import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
|
|
14
15
|
import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
|
|
15
16
|
import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
|
|
16
17
|
import {
|
|
@@ -61,6 +62,7 @@ export interface AsyncSingleParams {
|
|
|
61
62
|
shareEnabled: boolean;
|
|
62
63
|
sessionRoot?: string;
|
|
63
64
|
skills?: string[];
|
|
65
|
+
output?: string | false;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export interface AsyncExecutionResult {
|
|
@@ -153,6 +155,7 @@ export function executeAsyncChain(
|
|
|
153
155
|
cwd: s.cwd,
|
|
154
156
|
model: applyThinkingSuffix(s.model ?? a.model, a.thinking),
|
|
155
157
|
tools: a.tools,
|
|
158
|
+
extensions: a.extensions,
|
|
156
159
|
mcpDirectTools: a.mcpDirectTools,
|
|
157
160
|
systemPrompt,
|
|
158
161
|
skills: resolvedSkills.map((r) => r.name),
|
|
@@ -181,15 +184,6 @@ export function executeAsyncChain(
|
|
|
181
184
|
|
|
182
185
|
if (pid) {
|
|
183
186
|
const firstAgent = chain[0] as SequentialStep;
|
|
184
|
-
ctx.pi.events.emit("subagent_enhanced:started", {
|
|
185
|
-
id,
|
|
186
|
-
pid,
|
|
187
|
-
agent: firstAgent.agent,
|
|
188
|
-
task: firstAgent.task?.slice(0, 50),
|
|
189
|
-
chain: chain.map((s) => (s as SequentialStep).agent),
|
|
190
|
-
cwd: runnerCwd,
|
|
191
|
-
asyncDir,
|
|
192
|
-
});
|
|
193
187
|
ctx.pi.events.emit("subagent:started", {
|
|
194
188
|
id,
|
|
195
189
|
pid,
|
|
@@ -231,19 +225,23 @@ export function executeAsyncSingle(
|
|
|
231
225
|
} catch {}
|
|
232
226
|
|
|
233
227
|
const runnerCwd = cwd ?? ctx.cwd;
|
|
228
|
+
const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, cwd);
|
|
229
|
+
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
|
|
234
230
|
const pid = spawnRunner(
|
|
235
231
|
{
|
|
236
232
|
id,
|
|
237
233
|
steps: [
|
|
238
234
|
{
|
|
239
235
|
agent,
|
|
240
|
-
task,
|
|
236
|
+
task: taskWithOutputInstruction,
|
|
241
237
|
cwd,
|
|
242
238
|
model: applyThinkingSuffix(agentConfig.model, agentConfig.thinking),
|
|
243
239
|
tools: agentConfig.tools,
|
|
240
|
+
extensions: agentConfig.extensions,
|
|
244
241
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
245
242
|
systemPrompt,
|
|
246
243
|
skills: resolvedSkills.map((r) => r.name),
|
|
244
|
+
outputPath,
|
|
247
245
|
},
|
|
248
246
|
],
|
|
249
247
|
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
@@ -262,14 +260,6 @@ export function executeAsyncSingle(
|
|
|
262
260
|
);
|
|
263
261
|
|
|
264
262
|
if (pid) {
|
|
265
|
-
ctx.pi.events.emit("subagent_enhanced:started", {
|
|
266
|
-
id,
|
|
267
|
-
pid,
|
|
268
|
-
agent,
|
|
269
|
-
task: task?.slice(0, 50),
|
|
270
|
-
cwd: runnerCwd,
|
|
271
|
-
asyncDir,
|
|
272
|
-
});
|
|
273
263
|
ctx.pi.events.emit("subagent:started", {
|
|
274
264
|
id,
|
|
275
265
|
pid,
|
package/chain-execution.ts
CHANGED
|
@@ -376,13 +376,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
// Aggregate outputs for {previous}
|
|
379
|
-
const taskResults: ParallelTaskResult[] = parallelResults.map((r, i) =>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
const taskResults: ParallelTaskResult[] = parallelResults.map((r, i) => {
|
|
380
|
+
const outputTarget = parallelBehaviors[i]?.output;
|
|
381
|
+
const outputTargetPath = typeof outputTarget === "string"
|
|
382
|
+
? (path.isAbsolute(outputTarget) ? outputTarget : path.join(chainDir, outputTarget))
|
|
383
|
+
: undefined;
|
|
384
|
+
return {
|
|
385
|
+
agent: r.agent,
|
|
386
|
+
taskIndex: i,
|
|
387
|
+
output: getFinalOutput(r.messages),
|
|
388
|
+
exitCode: r.exitCode,
|
|
389
|
+
error: r.error,
|
|
390
|
+
outputTargetPath,
|
|
391
|
+
outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
386
394
|
prev = aggregateParallelOutputs(taskResults);
|
|
387
395
|
} else {
|
|
388
396
|
// === SEQUENTIAL STEP EXECUTION ===
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface CompletionDataLike {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
agent?: unknown;
|
|
4
|
+
timestamp?: unknown;
|
|
5
|
+
sessionId?: unknown;
|
|
6
|
+
taskIndex?: unknown;
|
|
7
|
+
totalTasks?: unknown;
|
|
8
|
+
success?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
12
|
+
if (typeof value !== "string") return undefined;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
18
|
+
if (typeof value !== "number") return undefined;
|
|
19
|
+
return Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
|
+
const id = asNonEmptyString(data.id);
|
|
24
|
+
if (id) return `id:${id}`;
|
|
25
|
+
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
26
|
+
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
|
+
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
|
+
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
29
|
+
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
30
|
+
const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
|
|
31
|
+
return [
|
|
32
|
+
"meta",
|
|
33
|
+
sessionId,
|
|
34
|
+
agent,
|
|
35
|
+
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
36
|
+
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
37
|
+
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
38
|
+
success,
|
|
39
|
+
fallback,
|
|
40
|
+
].join(":");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
|
|
44
|
+
for (const [key, ts] of seen.entries()) {
|
|
45
|
+
if (now - ts > ttlMs) seen.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
|
|
50
|
+
pruneSeenMap(seen, now, ttlMs);
|
|
51
|
+
if (seen.has(key)) return true;
|
|
52
|
+
seen.set(key, now);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
57
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
58
|
+
const existing = globalStore[storeKey];
|
|
59
|
+
if (existing instanceof Map) return existing as Map<string, number>;
|
|
60
|
+
const map = new Map<string, number>();
|
|
61
|
+
globalStore[storeKey] = map;
|
|
62
|
+
return map;
|
|
63
|
+
}
|