pi-subagents 0.21.3 → 0.21.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 +10 -0
- package/README.md +23 -13
- package/package.json +15 -1
- package/skills/pi-subagents/SKILL.md +18 -12
- package/src/agents/agent-management.ts +58 -16
- package/src/agents/agent-serializer.ts +4 -1
- package/src/agents/agents.ts +39 -30
- package/src/agents/chain-serializer.ts +16 -3
- package/src/agents/identity.ts +30 -0
- package/src/extension/index.ts +5 -5
- package/src/extension/schemas.ts +11 -2
- package/src/manager-ui/agent-manager-chain-detail.ts +4 -0
- package/src/manager-ui/agent-manager-detail.ts +4 -0
- package/src/manager-ui/agent-manager-edit.ts +25 -6
- package/src/manager-ui/agent-manager.ts +28 -8
- package/src/runs/background/async-execution.ts +12 -2
- package/src/runs/background/subagent-runner.ts +12 -10
- package/src/runs/foreground/chain-clarify.ts +2 -0
- package/src/runs/foreground/chain-execution.ts +36 -0
- package/src/runs/foreground/execution.ts +26 -4
- package/src/runs/foreground/subagent-executor.ts +24 -1
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/single-output.ts +47 -3
- package/src/shared/settings.ts +9 -3
- package/src/shared/types.ts +12 -0
- package/src/slash/slash-commands.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.21.4] - 2026-05-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added explicit frontmatter `package` identifiers for agents and saved chains, registering runtime names like `code-analysis.scout` while preserving separate `name` and `package` fields on save.
|
|
9
|
+
- Added recursive subdirectory discovery for user and project agent and chain definitions.
|
|
10
|
+
- Added `outputMode: "inline" | "file-only"` for saved subagent outputs. `inline` remains the default, while `file-only` returns a concise saved-file reference instead of injecting full saved output back into the parent context.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Marked Pi runtime peer dependencies as optional so npm package installs do not auto-install duplicate Pi packages or emit unrelated transitive dependency warnings.
|
|
14
|
+
|
|
5
15
|
## [0.21.3] - 2026-04-30
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -298,12 +298,13 @@ Append `[key=value,...]` to an agent name to override defaults for that step:
|
|
|
298
298
|
| Key | Example | Description |
|
|
299
299
|
|-----|---------|-------------|
|
|
300
300
|
| `output` | `output=context.md` | Write results to a file. For `/chain` and `/parallel`, relative paths live under the chain directory; for `/run`, relative paths resolve against cwd. |
|
|
301
|
+
| `outputMode` | `outputMode=file-only` | Return only a concise file reference for saved output instead of the full saved content. Requires `output`; default is `inline`. |
|
|
301
302
|
| `reads` | `reads=a.md+b.md` | Read files before executing. `+` separates multiple paths. |
|
|
302
303
|
| `model` | `model=anthropic/claude-sonnet-4` | Override model for this step. |
|
|
303
304
|
| `skills` | `skills=planning+review` | Override injected skills. `+` separates multiple skills. |
|
|
304
305
|
| `progress` | `progress` | Enable progress tracking. |
|
|
305
306
|
|
|
306
|
-
Set `output=false`, `reads=false`, or `skills=false` to disable that behavior explicitly.
|
|
307
|
+
Set `output=false`, `reads=false`, or `skills=false` to disable that behavior explicitly. Do not use `output=false` for file-only returns; use `outputMode=file-only` with an `output` path.
|
|
307
308
|
|
|
308
309
|
### Background and forked runs
|
|
309
310
|
|
|
@@ -396,10 +397,10 @@ Agent locations, lowest to highest priority:
|
|
|
396
397
|
| Scope | Path |
|
|
397
398
|
|-------|------|
|
|
398
399
|
| Builtin | `~/.pi/agent/extensions/subagent/agents/` |
|
|
399
|
-
| User | `~/.pi/agent/agents
|
|
400
|
-
| Project | `.pi/agents
|
|
400
|
+
| User | `~/.pi/agent/agents/**/*.md` |
|
|
401
|
+
| Project | `.pi/agents/**/*.md` |
|
|
401
402
|
|
|
402
|
-
Project discovery also reads legacy `.agents
|
|
403
|
+
Project discovery also reads legacy `.agents/**/*.md` files. Nested subdirectories are discovered recursively. `.chain.md` files are treated as chains, not agents. If both `.agents/` and `.pi/agents/` define the same parsed runtime agent name, `.pi/agents/` wins. Use `agentScope: "user" | "project" | "both"` to control discovery; `both` is the default and project definitions win runtime-name collisions.
|
|
403
404
|
|
|
404
405
|
Builtin agents load at the lowest priority, so a user or project agent with the same name overrides them. `oracle` is an advisory reviewer that critiques direction and proposes an execution prompt without editing files. `worker` is the implementation agent for normal tasks and approved oracle handoffs.
|
|
405
406
|
|
|
@@ -458,6 +459,8 @@ A typical agent looks like this:
|
|
|
458
459
|
```yaml
|
|
459
460
|
---
|
|
460
461
|
name: scout
|
|
462
|
+
# Optional: registers this as code-analysis.scout while preserving name: scout
|
|
463
|
+
package: code-analysis
|
|
461
464
|
description: Fast codebase recon
|
|
462
465
|
tools: read, grep, find, ls, bash, mcp:chrome-devtools
|
|
463
466
|
extensions:
|
|
@@ -482,6 +485,7 @@ Important fields:
|
|
|
482
485
|
|
|
483
486
|
| Field | Notes |
|
|
484
487
|
|-------|-------|
|
|
488
|
+
| `package` | Optional package identifier. A file with `name: scout` and `package: code-analysis` registers as `code-analysis.scout`; serialization keeps `name` and `package` separate. |
|
|
485
489
|
| `tools` | Builtin tool allowlist. `mcp:` entries select direct MCP tools when `pi-mcp-adapter` is installed. |
|
|
486
490
|
| `extensions` | Omitted means normal extensions; empty means no extensions; comma-separated values allowlist specific extensions. |
|
|
487
491
|
| `model` | Default model. Bare ids prefer the current provider when possible, then unique registry matches. |
|
|
@@ -530,10 +534,10 @@ Chains are reusable `.chain.md` workflows stored next to agent files.
|
|
|
530
534
|
|
|
531
535
|
| Scope | Path |
|
|
532
536
|
|-------|------|
|
|
533
|
-
| User | `~/.pi/agent/agents
|
|
534
|
-
| Project | `.pi/agents
|
|
537
|
+
| User | `~/.pi/agent/agents/**/*.chain.md` |
|
|
538
|
+
| Project | `.pi/agents/**/*.chain.md` |
|
|
535
539
|
|
|
536
|
-
Project discovery also reads legacy `.agents
|
|
540
|
+
Project discovery also reads legacy `.agents/**/*.chain.md` files. Nested subdirectories are discovered recursively. If both locations define the same parsed runtime chain name, `.pi/agents/` wins. Chains support the same optional `package` frontmatter as agents; `name: review-flow` plus `package: code-analysis` runs as `code-analysis.review-flow`.
|
|
537
541
|
|
|
538
542
|
Example:
|
|
539
543
|
|
|
@@ -556,9 +560,9 @@ progress: true
|
|
|
556
560
|
Create an implementation plan based on {previous}
|
|
557
561
|
```
|
|
558
562
|
|
|
559
|
-
Each `## agent-name` section is a step. Config lines such as `output`, `reads`, `model`, `skills`, and `progress` go immediately after the header. A blank line separates config from task text.
|
|
563
|
+
Each `## agent-name` section is a step. Config lines such as `output`, `outputMode`, `reads`, `model`, `skills`, and `progress` go immediately after the header. A blank line separates config from task text.
|
|
560
564
|
|
|
561
|
-
|
|
565
|
+
For `output`, `reads`, `skills`, and `progress`, chain behavior is three-state: omitted inherits from the agent, a value overrides, and `false` disables.
|
|
562
566
|
|
|
563
567
|
Create chains from the Agents Manager template picker, save them from the chain-clarify TUI, or write them by hand. Run them with natural language, `/agents`, or:
|
|
564
568
|
|
|
@@ -645,6 +649,7 @@ These are the parameters the LLM passes when it calls the `subagent` tool. Most
|
|
|
645
649
|
{ agent: "worker", task: "refactor auth" }
|
|
646
650
|
{ agent: "scout", task: "find todos", maxOutput: { lines: 1000 } }
|
|
647
651
|
{ agent: "scout", task: "investigate", output: false }
|
|
652
|
+
{ agent: "scout", task: "write a large report", output: "reports/scout.md", outputMode: "file-only" }
|
|
648
653
|
|
|
649
654
|
// Forked context
|
|
650
655
|
{ agent: "worker", task: "continue this thread", context: "fork" }
|
|
@@ -690,10 +695,12 @@ Agent definitions are not loaded into context by default. Management actions let
|
|
|
690
695
|
{ action: "list" }
|
|
691
696
|
{ action: "list", agentScope: "project" }
|
|
692
697
|
{ action: "get", agent: "scout" }
|
|
698
|
+
{ action: "get", agent: "code-analysis.scout" }
|
|
693
699
|
{ action: "get", chainName: "review-pipeline" }
|
|
694
700
|
|
|
695
701
|
{ action: "create", config: {
|
|
696
702
|
name: "Code Scout",
|
|
703
|
+
package: "code-analysis",
|
|
697
704
|
description: "Scans codebases for patterns and issues",
|
|
698
705
|
scope: "user",
|
|
699
706
|
systemPrompt: "You are a code scout...",
|
|
@@ -721,13 +728,13 @@ Agent definitions are not loaded into context by default. Management actions let
|
|
|
721
728
|
]
|
|
722
729
|
}}
|
|
723
730
|
|
|
724
|
-
{ action: "update", agent: "scout", config: { model: "openai/gpt-4o" } }
|
|
731
|
+
{ action: "update", agent: "code-analysis.scout", config: { model: "openai/gpt-4o" } }
|
|
725
732
|
{ action: "update", chainName: "review-pipeline", config: { steps: [...] } }
|
|
726
733
|
{ action: "delete", agent: "scout" }
|
|
727
734
|
{ action: "delete", chainName: "review-pipeline" }
|
|
728
735
|
```
|
|
729
736
|
|
|
730
|
-
`create` uses `config.scope`, not `agentScope`. `update` and `delete` use `agentScope` only when the same name exists in multiple scopes. To clear optional string fields, set them to `false` or `""`.
|
|
737
|
+
`create` uses `config.scope`, not `agentScope`. `config.name` is the local frontmatter name; optional `config.package` registers the runtime name as `{package}.{name}` and is saved as separate `name` and `package` frontmatter. `update` and `delete` use the runtime name and `agentScope` only when the same runtime name exists in multiple scopes. To clear optional string fields, including `package`, set them to `false` or `""`.
|
|
731
738
|
|
|
732
739
|
### Parameter reference
|
|
733
740
|
|
|
@@ -739,9 +746,10 @@ Agent definitions are not loaded into context by default. Management actions let
|
|
|
739
746
|
| `chainName` | string | - | Chain name for management actions. |
|
|
740
747
|
| `config` | object/string | - | Agent or chain config for create/update. |
|
|
741
748
|
| `output` | `string \| false` | agent default | Override single-agent output file. |
|
|
749
|
+
| `outputMode` | `"inline" \| "file-only"` | `inline` | Return saved output inline or as a concise saved-file reference. `file-only` requires an `output` path. |
|
|
742
750
|
| `skill` | `string \| string[] \| false` | agent default | Override skills or disable all. |
|
|
743
751
|
| `model` | string | agent default | Override model. |
|
|
744
|
-
| `tasks` | array | - | Top-level parallel tasks. Supports `agent`, `task`, `cwd`, `count`, `output`, `reads`, `progress`, `skill`, and `model`. |
|
|
752
|
+
| `tasks` | array | - | Top-level parallel tasks. Supports `agent`, `task`, `cwd`, `count`, `output`, `outputMode`, `reads`, `progress`, `skill`, and `model`. |
|
|
745
753
|
| `concurrency` | number | config or `4` | Top-level parallel concurrency. |
|
|
746
754
|
| `worktree` | boolean | false | Create isolated git worktrees for parallel tasks. |
|
|
747
755
|
| `chain` | array | - | Sequential and parallel chain steps. |
|
|
@@ -759,7 +767,9 @@ Agent definitions are not loaded into context by default. Management actions let
|
|
|
759
767
|
|
|
760
768
|
`context: "fork"` fails fast when the parent session is not persisted, the current leaf is missing, or the branched child session cannot be created. It never silently downgrades to `fresh`. In multi-agent runs, if any requested agent has `defaultContext: fork` and the launch omits `context`, the whole invocation uses forked context; pass `context: "fresh"` when you intentionally want a fresh run.
|
|
761
769
|
|
|
762
|
-
|
|
770
|
+
Use `outputMode: "file-only"` when a saved output may be large and the parent only needs a pointer. The returned text is a compact reference like `Output saved to: /abs/report.md (48.2 KB, 2847 lines). Read this file if needed.` Failed runs and save errors still return normal inline output for debugging. In chains, later `{previous}` steps receive the same compact reference when the prior step used file-only mode.
|
|
771
|
+
|
|
772
|
+
Sequential and parallel chain tasks accept `agent`, `task`, `cwd`, `output`, `outputMode`, `reads`, `progress`, `skill`, and `model`. Parallel tasks also accept `count`. Parallel step groups accept `parallel`, `concurrency`, `failFast`, and `worktree`.
|
|
763
773
|
|
|
764
774
|
Status and control actions:
|
|
765
775
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,20 @@
|
|
|
57
57
|
"@mariozechner/pi-coding-agent": "*",
|
|
58
58
|
"@mariozechner/pi-tui": "*"
|
|
59
59
|
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@mariozechner/pi-agent-core": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@mariozechner/pi-ai": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@mariozechner/pi-coding-agent": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@mariozechner/pi-tui": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
60
74
|
"dependencies": {
|
|
61
75
|
"typebox": "^1.1.24"
|
|
62
76
|
},
|
|
@@ -178,16 +178,18 @@ agent with the same name only when you want a substantially different agent.
|
|
|
178
178
|
## Discovery and Scope Rules
|
|
179
179
|
|
|
180
180
|
Agent files can live in:
|
|
181
|
-
- `~/.pi/agent/agents
|
|
182
|
-
- `.pi/agents
|
|
183
|
-
- legacy `.agents
|
|
181
|
+
- `~/.pi/agent/agents/**/*.md` — user scope
|
|
182
|
+
- `.pi/agents/**/*.md` — canonical project scope
|
|
183
|
+
- legacy `.agents/**/*.md` — still read for compatibility, but `.pi/agents/` wins on conflicts
|
|
184
184
|
|
|
185
185
|
Chains live in:
|
|
186
|
-
- `~/.pi/agent/agents
|
|
187
|
-
- `.pi/agents
|
|
188
|
-
- legacy `.agents
|
|
186
|
+
- `~/.pi/agent/agents/**/*.chain.md`
|
|
187
|
+
- `.pi/agents/**/*.chain.md`
|
|
188
|
+
- legacy `.agents/**/*.chain.md`
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
Discovery is recursive. `.chain.md` files are chains, not agents. Agents and chains can set optional frontmatter `package: code-analysis`; `name: scout` plus `package: code-analysis` registers as runtime name `code-analysis.scout` while serialization keeps `name` and `package` separate.
|
|
191
|
+
|
|
192
|
+
Precedence is by parsed runtime name:
|
|
191
193
|
1. project scope
|
|
192
194
|
2. user scope
|
|
193
195
|
3. builtin agents
|
|
@@ -241,7 +243,7 @@ subagent({
|
|
|
241
243
|
})
|
|
242
244
|
```
|
|
243
245
|
|
|
244
|
-
Avoid duplicate output paths in parallel tasks. Concurrent children should not write to the same file.
|
|
246
|
+
Avoid duplicate output paths in parallel tasks. Concurrent children should not write to the same file. For large saved outputs, set `outputMode: "file-only"` together with an `output` path. The parent result then contains only a compact reference like `Output saved to: /abs/report.md (48.2 KB, 2847 lines). Read this file if needed.` instead of the full saved content. Do not use `output: false` for this; `output: false` means no file output. Failed runs and save errors still return inline details for debugging.
|
|
245
247
|
|
|
246
248
|
### Chain execution
|
|
247
249
|
|
|
@@ -271,6 +273,8 @@ subagent({
|
|
|
271
273
|
})
|
|
272
274
|
```
|
|
273
275
|
|
|
276
|
+
File-only output mode also works for async single runs, top-level parallel task items, sequential chain steps, and chain parallel task items. In chains, `{previous}` receives the compact saved-file reference when the prior step used file-only mode.
|
|
277
|
+
|
|
274
278
|
For review fanout where the parent continues a local audit:
|
|
275
279
|
|
|
276
280
|
```typescript
|
|
@@ -451,6 +455,7 @@ subagent({
|
|
|
451
455
|
action: "create",
|
|
452
456
|
config: {
|
|
453
457
|
name: "my-agent",
|
|
458
|
+
package: "code-analysis",
|
|
454
459
|
description: "Project-specific implementation helper",
|
|
455
460
|
systemPrompt: "Your system prompt here.",
|
|
456
461
|
systemPromptMode: "replace",
|
|
@@ -465,7 +470,7 @@ subagent({
|
|
|
465
470
|
```typescript
|
|
466
471
|
subagent({
|
|
467
472
|
action: "update",
|
|
468
|
-
agent: "my-agent",
|
|
473
|
+
agent: "code-analysis.my-agent",
|
|
469
474
|
config: {
|
|
470
475
|
thinking: "high"
|
|
471
476
|
}
|
|
@@ -475,13 +480,13 @@ subagent({
|
|
|
475
480
|
### Delete an agent
|
|
476
481
|
|
|
477
482
|
```typescript
|
|
478
|
-
subagent({ action: "delete", agent: "my-agent" })
|
|
483
|
+
subagent({ action: "delete", agent: "code-analysis.my-agent" })
|
|
479
484
|
```
|
|
480
485
|
|
|
481
486
|
Use management actions when the system needs to create or edit subagents on
|
|
482
487
|
demand without dropping into raw file editing.
|
|
483
488
|
|
|
484
|
-
Management actions create or update user/project agent files. For small builtin changes such as a model swap, prefer `/agents` builtin overrides or `subagents.agentOverrides` in settings.
|
|
489
|
+
Management actions create or update user/project agent files. `config.name` is the local frontmatter name; optional `config.package` registers and looks up the runtime name as `{package}.{name}`. Use the dotted runtime name for `get`, `update`, `delete`, slash commands, and chain steps. For small builtin changes such as a model swap, prefer `/agents` builtin overrides or `subagents.agentOverrides` in settings.
|
|
485
490
|
|
|
486
491
|
## Creating and Editing Agents by File
|
|
487
492
|
|
|
@@ -490,6 +495,7 @@ A minimal agent file looks like this:
|
|
|
490
495
|
```markdown
|
|
491
496
|
---
|
|
492
497
|
name: my-agent
|
|
498
|
+
package: code-analysis
|
|
493
499
|
description: What this agent does
|
|
494
500
|
model: openai-codex/gpt-5.4
|
|
495
501
|
thinking: high
|
|
@@ -502,7 +508,7 @@ inheritSkills: false
|
|
|
502
508
|
Your system prompt here.
|
|
503
509
|
```
|
|
504
510
|
|
|
505
|
-
That is only a starting point. Common optional fields include:
|
|
511
|
+
That is only a starting point. Omit `package` for the traditional unqualified runtime name. Common optional fields include:
|
|
506
512
|
- `defaultProgress`
|
|
507
513
|
- `defaultReads`
|
|
508
514
|
- `output`
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
defaultInheritSkills,
|
|
13
13
|
defaultSystemPromptMode,
|
|
14
14
|
discoverAgentsAll,
|
|
15
|
+
buildRuntimeName,
|
|
16
|
+
frontmatterNameForConfig,
|
|
17
|
+
parsePackageName,
|
|
15
18
|
} from "./agents.ts";
|
|
16
19
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
17
20
|
import { serializeChain } from "./chain-serializer.ts";
|
|
@@ -71,6 +74,10 @@ function sanitizeName(name: string): string {
|
|
|
71
74
|
return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
function parsePackageConfig(value: unknown): { packageName?: string; error?: string } {
|
|
78
|
+
return parsePackageName(value, "config.package");
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
function allAgents(d: { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[] }): AgentConfig[] {
|
|
75
82
|
return [...d.builtin, ...d.user, ...d.project];
|
|
76
83
|
}
|
|
@@ -167,6 +174,10 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
|
|
|
167
174
|
else if (typeof s.output === "string") step.output = s.output;
|
|
168
175
|
else return { error: `config.steps[${i}].output must be a string or false.` };
|
|
169
176
|
}
|
|
177
|
+
if (hasKey(s, "outputMode")) {
|
|
178
|
+
if (s.outputMode === "inline" || s.outputMode === "file-only") step.outputMode = s.outputMode;
|
|
179
|
+
else return { error: `config.steps[${i}].outputMode must be 'inline' or 'file-only'.` };
|
|
180
|
+
}
|
|
170
181
|
if (hasKey(s, "reads")) {
|
|
171
182
|
if (s.reads === false) step.reads = false;
|
|
172
183
|
else if (Array.isArray(s.reads)) step.reads = s.reads.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
|
|
@@ -336,6 +347,10 @@ function renamePath(
|
|
|
336
347
|
function formatAgentDetail(agent: AgentConfig): string {
|
|
337
348
|
const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
|
|
338
349
|
const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
|
|
350
|
+
if (agent.packageName) {
|
|
351
|
+
lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
|
|
352
|
+
lines.push(`Package: ${agent.packageName}`);
|
|
353
|
+
}
|
|
339
354
|
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
340
355
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
341
356
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
@@ -356,13 +371,19 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
356
371
|
}
|
|
357
372
|
|
|
358
373
|
function formatChainDetail(chain: ChainConfig): string {
|
|
359
|
-
const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}
|
|
374
|
+
const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}`];
|
|
375
|
+
if (chain.packageName) {
|
|
376
|
+
lines.push(`Local name: ${frontmatterNameForConfig(chain)}`);
|
|
377
|
+
lines.push(`Package: ${chain.packageName}`);
|
|
378
|
+
}
|
|
379
|
+
lines.push("", "Steps:");
|
|
360
380
|
for (let i = 0; i < chain.steps.length; i++) {
|
|
361
381
|
const s = chain.steps[i]!;
|
|
362
382
|
lines.push(`${i + 1}. ${s.agent}`);
|
|
363
383
|
if (s.task.trim()) lines.push(` Task: ${s.task}`);
|
|
364
384
|
if (s.output === false) lines.push(" Output: false");
|
|
365
385
|
else if (s.output) lines.push(` Output: ${s.output}`);
|
|
386
|
+
if (s.outputMode) lines.push(` Output mode: ${s.outputMode}`);
|
|
366
387
|
if (s.reads === false) lines.push(" Reads: false");
|
|
367
388
|
else if (Array.isArray(s.reads) && s.reads.length > 0) lines.push(` Reads: ${s.reads.join(", ")}`);
|
|
368
389
|
if (s.model) lines.push(` Model: ${s.model}`);
|
|
@@ -430,6 +451,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
430
451
|
if (typeof cfg.description !== "string" || !cfg.description.trim()) return result("config.description is required and must be a non-empty string.", true);
|
|
431
452
|
const name = sanitizeName(cfg.name);
|
|
432
453
|
if (!name) return result("config.name is invalid after sanitization. Use letters, numbers, spaces, or hyphens.", true);
|
|
454
|
+
const parsedPackage = parsePackageConfig(cfg.package);
|
|
455
|
+
if (parsedPackage.error) return result(parsedPackage.error, true);
|
|
456
|
+
const runtimeName = buildRuntimeName(name, parsedPackage.packageName);
|
|
433
457
|
const scopeRaw = cfg.scope ?? "user";
|
|
434
458
|
if (scopeRaw !== "user" && scopeRaw !== "project") return result("config.scope must be 'user' or 'project'.", true);
|
|
435
459
|
const scope = scopeRaw as ManagementScope;
|
|
@@ -437,23 +461,25 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
437
461
|
const d = discoverAgentsAll(ctx.cwd);
|
|
438
462
|
const targetDir = scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
|
|
439
463
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
440
|
-
if (nameExistsInScope(ctx.cwd, scope,
|
|
441
|
-
const targetPath = path.join(targetDir, isChain ? `${
|
|
464
|
+
if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
|
|
465
|
+
const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
|
|
442
466
|
if (fs.existsSync(targetPath)) return result(`File already exists at ${targetPath} but is not a valid ${isChain ? "chain" : "agent"} definition. Remove or rename it first.`, true);
|
|
443
467
|
const warnings: string[] = [];
|
|
444
|
-
if (!isChain && d.builtin.some((a) => a.name ===
|
|
468
|
+
if (!isChain && d.builtin.some((a) => a.name === runtimeName)) warnings.push(`Note: this shadows the builtin agent '${runtimeName}'.`);
|
|
445
469
|
if (isChain) {
|
|
446
470
|
const parsed = parseStepList(cfg.steps);
|
|
447
471
|
if (parsed.error) return result(parsed.error, true);
|
|
448
|
-
const chain: ChainConfig = { name, description: cfg.description.trim(), source: scope, filePath: targetPath, steps: parsed.steps! };
|
|
472
|
+
const chain: ChainConfig = { name: runtimeName, localName: name, packageName: parsedPackage.packageName, description: cfg.description.trim(), source: scope, filePath: targetPath, steps: parsed.steps! };
|
|
449
473
|
fs.writeFileSync(targetPath, serializeChain(chain), "utf-8");
|
|
450
474
|
const missing = unknownChainAgents(ctx.cwd, chain.steps);
|
|
451
475
|
if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
|
|
452
476
|
warnings.push(...chainStepWarnings(ctx, chain.steps));
|
|
453
|
-
return result([`Created chain '${
|
|
477
|
+
return result([`Created chain '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
|
|
454
478
|
}
|
|
455
479
|
const agent: AgentConfig = {
|
|
456
|
-
name,
|
|
480
|
+
name: runtimeName,
|
|
481
|
+
localName: name,
|
|
482
|
+
packageName: parsedPackage.packageName,
|
|
457
483
|
description: cfg.description.trim(),
|
|
458
484
|
source: scope,
|
|
459
485
|
filePath: targetPath,
|
|
@@ -471,7 +497,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
471
497
|
const sw = skillsWarning(ctx.cwd, agent.skills);
|
|
472
498
|
if (sw) warnings.push(sw);
|
|
473
499
|
fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
|
|
474
|
-
return result([`Created agent '${
|
|
500
|
+
return result([`Created agent '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
|
|
475
501
|
}
|
|
476
502
|
|
|
477
503
|
export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
@@ -491,14 +517,22 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
491
517
|
const oldName = target.name;
|
|
492
518
|
if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
|
|
493
519
|
if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
|
|
494
|
-
let
|
|
520
|
+
let newLocalName = target.localName ?? frontmatterNameForConfig(target);
|
|
495
521
|
if (hasKey(cfg, "name")) {
|
|
496
|
-
|
|
497
|
-
if (!
|
|
522
|
+
newLocalName = sanitizeName(cfg.name as string);
|
|
523
|
+
if (!newLocalName) return result("config.name is invalid after sanitization.", true);
|
|
524
|
+
}
|
|
525
|
+
let newPackageName = target.packageName;
|
|
526
|
+
if (hasKey(cfg, "package")) {
|
|
527
|
+
const parsedPackage = parsePackageConfig(cfg.package);
|
|
528
|
+
if (parsedPackage.error) return result(parsedPackage.error, true);
|
|
529
|
+
newPackageName = parsedPackage.packageName;
|
|
498
530
|
}
|
|
499
531
|
const applyError = applyAgentConfig(updated, cfg);
|
|
500
532
|
if (applyError) return result(applyError, true);
|
|
501
|
-
|
|
533
|
+
updated.localName = newLocalName;
|
|
534
|
+
updated.packageName = newPackageName;
|
|
535
|
+
updated.name = buildRuntimeName(newLocalName, newPackageName);
|
|
502
536
|
if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
|
|
503
537
|
if (hasKey(cfg, "model")) {
|
|
504
538
|
const mw = modelWarning(ctx, updated.model);
|
|
@@ -535,10 +569,16 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
535
569
|
const oldName = target.name;
|
|
536
570
|
if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
|
|
537
571
|
if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
|
|
538
|
-
let
|
|
572
|
+
let newLocalName = target.localName ?? frontmatterNameForConfig(target);
|
|
539
573
|
if (hasKey(cfg, "name")) {
|
|
540
|
-
|
|
541
|
-
if (!
|
|
574
|
+
newLocalName = sanitizeName(cfg.name as string);
|
|
575
|
+
if (!newLocalName) return result("config.name is invalid after sanitization.", true);
|
|
576
|
+
}
|
|
577
|
+
let newPackageName = target.packageName;
|
|
578
|
+
if (hasKey(cfg, "package")) {
|
|
579
|
+
const parsedPackage = parsePackageConfig(cfg.package);
|
|
580
|
+
if (parsedPackage.error) return result(parsedPackage.error, true);
|
|
581
|
+
newPackageName = parsedPackage.packageName;
|
|
542
582
|
}
|
|
543
583
|
let parsedSteps: ChainStepConfig[] | undefined;
|
|
544
584
|
if (hasKey(cfg, "steps")) {
|
|
@@ -546,7 +586,9 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
546
586
|
if (parsed.error) return result(parsed.error, true);
|
|
547
587
|
parsedSteps = parsed.steps!;
|
|
548
588
|
}
|
|
549
|
-
|
|
589
|
+
updated.localName = newLocalName;
|
|
590
|
+
updated.packageName = newPackageName;
|
|
591
|
+
updated.name = buildRuntimeName(newLocalName, newPackageName);
|
|
550
592
|
if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
|
|
551
593
|
if (parsedSteps) {
|
|
552
594
|
updated.steps = parsedSteps;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentConfig } from "./agents.ts";
|
|
3
|
+
import { frontmatterNameForConfig } from "./identity.ts";
|
|
3
4
|
|
|
4
5
|
export const KNOWN_FIELDS = new Set([
|
|
5
6
|
"name",
|
|
7
|
+
"package",
|
|
6
8
|
"description",
|
|
7
9
|
"tools",
|
|
8
10
|
"model",
|
|
@@ -30,7 +32,8 @@ function joinComma(values: string[] | undefined): string | undefined {
|
|
|
30
32
|
export function serializeAgent(config: AgentConfig): string {
|
|
31
33
|
const lines: string[] = [];
|
|
32
34
|
lines.push("---");
|
|
33
|
-
lines.push(`name: ${config
|
|
35
|
+
lines.push(`name: ${frontmatterNameForConfig(config)}`);
|
|
36
|
+
if (config.packageName) lines.push(`package: ${config.packageName}`);
|
|
34
37
|
lines.push(`description: ${config.description}`);
|
|
35
38
|
|
|
36
39
|
const tools = [
|
package/src/agents/agents.ts
CHANGED
|
@@ -6,10 +6,13 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { OutputMode } from "../shared/types.ts";
|
|
9
10
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
10
11
|
import { parseChain } from "./chain-serializer.ts";
|
|
11
12
|
import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
12
13
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
14
|
+
import { buildRuntimeName, parsePackageName } from "./identity.ts";
|
|
15
|
+
export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
13
16
|
|
|
14
17
|
export type AgentScope = "user" | "project" | "both";
|
|
15
18
|
|
|
@@ -66,6 +69,8 @@ interface BuiltinAgentOverrideInfo {
|
|
|
66
69
|
|
|
67
70
|
export interface AgentConfig {
|
|
68
71
|
name: string;
|
|
72
|
+
localName?: string;
|
|
73
|
+
packageName?: string;
|
|
69
74
|
description: string;
|
|
70
75
|
tools?: string[];
|
|
71
76
|
mcpDirectTools?: string[];
|
|
@@ -102,6 +107,7 @@ export interface ChainStepConfig {
|
|
|
102
107
|
agent: string;
|
|
103
108
|
task: string;
|
|
104
109
|
output?: string | false;
|
|
110
|
+
outputMode?: OutputMode;
|
|
105
111
|
reads?: string[] | false;
|
|
106
112
|
model?: string;
|
|
107
113
|
skills?: string[] | false;
|
|
@@ -110,6 +116,8 @@ export interface ChainStepConfig {
|
|
|
110
116
|
|
|
111
117
|
export interface ChainConfig {
|
|
112
118
|
name: string;
|
|
119
|
+
localName?: string;
|
|
120
|
+
packageName?: string;
|
|
113
121
|
description: string;
|
|
114
122
|
source: AgentSource;
|
|
115
123
|
filePath: string;
|
|
@@ -505,26 +513,34 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
|
|
|
505
513
|
return filePath;
|
|
506
514
|
}
|
|
507
515
|
|
|
508
|
-
function
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
if (!fs.existsSync(dir)) {
|
|
512
|
-
return agents;
|
|
513
|
-
}
|
|
516
|
+
function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
|
|
517
|
+
const files: string[] = [];
|
|
518
|
+
if (!fs.existsSync(dir)) return files;
|
|
514
519
|
|
|
515
520
|
let entries: fs.Dirent[];
|
|
516
521
|
try {
|
|
517
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
522
|
+
entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
518
523
|
} catch {
|
|
519
|
-
return
|
|
524
|
+
return files;
|
|
520
525
|
}
|
|
521
526
|
|
|
522
527
|
for (const entry of entries) {
|
|
523
|
-
|
|
524
|
-
if (entry.
|
|
528
|
+
const filePath = path.join(dir, entry.name);
|
|
529
|
+
if (entry.isDirectory()) {
|
|
530
|
+
files.push(...listMarkdownFilesRecursive(filePath, predicate));
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
525
533
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
534
|
+
if (!predicate(entry.name)) continue;
|
|
535
|
+
files.push(filePath);
|
|
536
|
+
}
|
|
537
|
+
return files;
|
|
538
|
+
}
|
|
526
539
|
|
|
527
|
-
|
|
540
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
541
|
+
const agents: AgentConfig[] = [];
|
|
542
|
+
|
|
543
|
+
for (const filePath of listMarkdownFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
528
544
|
let content: string;
|
|
529
545
|
try {
|
|
530
546
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -538,6 +554,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
538
554
|
continue;
|
|
539
555
|
}
|
|
540
556
|
|
|
557
|
+
const localName = frontmatter.name;
|
|
558
|
+
const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
|
|
559
|
+
if (parsedPackage.error) continue;
|
|
560
|
+
const packageName = parsedPackage.packageName;
|
|
561
|
+
const runtimeName = buildRuntimeName(localName, packageName);
|
|
562
|
+
|
|
541
563
|
const rawTools = frontmatter.tools
|
|
542
564
|
?.split(",")
|
|
543
565
|
.map((t) => t.trim())
|
|
@@ -573,12 +595,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
573
595
|
? "replace"
|
|
574
596
|
: frontmatter.systemPromptMode === "append"
|
|
575
597
|
? "append"
|
|
576
|
-
: defaultSystemPromptMode(
|
|
598
|
+
: defaultSystemPromptMode(localName);
|
|
577
599
|
const inheritProjectContext = frontmatter.inheritProjectContext === "true"
|
|
578
600
|
? true
|
|
579
601
|
: frontmatter.inheritProjectContext === "false"
|
|
580
602
|
? false
|
|
581
|
-
: defaultInheritProjectContext(
|
|
603
|
+
: defaultInheritProjectContext(localName);
|
|
582
604
|
const inheritSkills = frontmatter.inheritSkills === "true"
|
|
583
605
|
? true
|
|
584
606
|
: frontmatter.inheritSkills === "false"
|
|
@@ -606,7 +628,9 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
606
628
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
607
629
|
|
|
608
630
|
agents.push({
|
|
609
|
-
name:
|
|
631
|
+
name: runtimeName,
|
|
632
|
+
localName,
|
|
633
|
+
packageName,
|
|
610
634
|
description: frontmatter.description,
|
|
611
635
|
tools: tools.length > 0 ? tools : undefined,
|
|
612
636
|
mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
|
|
@@ -640,22 +664,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
640
664
|
function loadChainsFromDir(dir: string, source: AgentSource): ChainConfig[] {
|
|
641
665
|
const chains: ChainConfig[] = [];
|
|
642
666
|
|
|
643
|
-
|
|
644
|
-
return chains;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
let entries: fs.Dirent[];
|
|
648
|
-
try {
|
|
649
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
650
|
-
} catch {
|
|
651
|
-
return chains;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
for (const entry of entries) {
|
|
655
|
-
if (!entry.name.endsWith(".chain.md")) continue;
|
|
656
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
657
|
-
|
|
658
|
-
const filePath = path.join(dir, entry.name);
|
|
667
|
+
for (const filePath of listMarkdownFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md"))) {
|
|
659
668
|
let content: string;
|
|
660
669
|
try {
|
|
661
670
|
content = fs.readFileSync(filePath, "utf-8");
|