oira666_pi-subagent 0.1.7 → 0.1.8
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/LICENSE +21 -21
- package/README.md +57 -6
- package/agents/code-architect.md +24 -24
- package/agents/code-reviwer.md +23 -23
- package/agents/code-writer.md +18 -18
- package/agents.ts +188 -188
- package/index.ts +857 -880
- package/package.json +77 -72
- package/render.ts +344 -344
- package/runner-sdk.ts +733 -0
- package/runner.ts +729 -611
- package/shared.ts +53 -0
- package/types.ts +286 -286
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Michael Jakl
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Jakl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -22,9 +22,30 @@ pi remove npm:oira666_pi-subagent
|
|
|
22
22
|
|
|
23
23
|
## How It Works
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The extension supports two execution modes, selectable via `PI_SUBAGENTS_MODE`.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
### Execution mode: `subprocess` (default)
|
|
28
|
+
|
|
29
|
+
Each subagent runs as a **separate `pi` process** — fully isolated memory, its own model/tool loop.
|
|
30
|
+
Processes are spawned via the operating system and communicate through JSON-line stdout.
|
|
31
|
+
|
|
32
|
+
- Full OS-level isolation — a crashed subagent cannot affect the parent
|
|
33
|
+
- True parallel execution across all CPU cores
|
|
34
|
+
- Each subprocess boots a fresh Node.js runtime (adds ~200–500 ms per subagent)
|
|
35
|
+
- Requires the `pi` binary on `PATH`
|
|
36
|
+
|
|
37
|
+
### Execution mode: `sdk`
|
|
38
|
+
|
|
39
|
+
Each subagent runs as an **in-process `AgentSession`** created via the pi SDK — no new process is spawned.
|
|
40
|
+
|
|
41
|
+
- No spawn overhead — sessions start in milliseconds
|
|
42
|
+
- No temp files for system prompts or fork snapshots
|
|
43
|
+
- Concurrency through the Node.js event loop (fine for I/O-bound LLM work)
|
|
44
|
+
- All sessions share the same memory and event loop
|
|
45
|
+
|
|
46
|
+
### Context modes
|
|
47
|
+
|
|
48
|
+
**`spawn` (default)** — Child receives only the task string. Best for isolated work, lower cost.
|
|
28
49
|
**`fork`** — Child receives a snapshot of the current session context + task. Best for follow-up work.
|
|
29
50
|
|
|
30
51
|
The main agent receives only the **final text output** from subagents (no tool calls, no reasoning).
|
|
@@ -94,6 +115,32 @@ Available tools: `read`, `bash`, `edit`, `write`.
|
|
|
94
115
|
|
|
95
116
|
The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).
|
|
96
117
|
|
|
118
|
+
## Execution Mode
|
|
119
|
+
|
|
120
|
+
| Env Var | Default | Values | Description |
|
|
121
|
+
| --------------------- | ------------ | -------------------- | -------------------------------------- |
|
|
122
|
+
| `PI_SUBAGENTS_MODE` | `subprocess` | `subprocess` / `sdk` | How subagent sessions are created |
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Run subagents as in-process SDK sessions (faster startup, no spawn overhead)
|
|
126
|
+
PI_SUBAGENTS_MODE=sdk pi
|
|
127
|
+
|
|
128
|
+
# Run subagents as isolated subprocess pi instances (default, full isolation)
|
|
129
|
+
PI_SUBAGENTS_MODE=subprocess pi
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Comparison
|
|
133
|
+
|
|
134
|
+
| | `subprocess` | `sdk` |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| Session isolation | Full OS-level | Shared memory/event loop |
|
|
137
|
+
| Startup overhead | ~200–500 ms per agent | ~10–50 ms per agent |
|
|
138
|
+
| Parallelism | True OS parallelism | Event-loop concurrency |
|
|
139
|
+
| Fork mode context | Temp JSONL file | Temp JSONL file (same format) |
|
|
140
|
+
| Depth tracking | Env vars in child process | Closure parameters |
|
|
141
|
+
| `pi` binary required | Yes | No |
|
|
142
|
+
| Crash isolation | Yes — subprocess crash is contained | No — exception bubbles up |
|
|
143
|
+
|
|
97
144
|
## Delegation Guards
|
|
98
145
|
|
|
99
146
|
Depth and cycle guards prevent runaway recursive delegation.
|
|
@@ -124,9 +171,13 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
|
|
|
124
171
|
|
|
125
172
|
## CLI Argument Proxying
|
|
126
173
|
|
|
127
|
-
|
|
128
|
-
inherit
|
|
129
|
-
|
|
174
|
+
> **Note:** CLI argument proxying only applies to `subprocess` mode. In `sdk` mode, subagents
|
|
175
|
+
> inherit provider and model configuration directly from the calling session's model registry
|
|
176
|
+
> and API keys from environment variables, so no forwarding is needed.
|
|
177
|
+
|
|
178
|
+
In `subprocess` mode, all flags passed to the parent `pi` process are forwarded to subagent child
|
|
179
|
+
processes, so they inherit the same provider, API key, model, and other runtime settings. Flags the
|
|
180
|
+
extension manages itself are blocked from being forwarded.
|
|
130
181
|
|
|
131
182
|
**Always forwarded verbatim:**
|
|
132
183
|
|
|
@@ -421,7 +472,7 @@ for await (const line of jsonLines) {
|
|
|
421
472
|
|
|
422
473
|
Note: if you also track the main agent's own usage from `message_end` events, make sure **not** to
|
|
423
474
|
double-count the subagent costs there — the main agent's own token usage (from its own `message_end`
|
|
424
|
-
events) does not include subagent work
|
|
475
|
+
events) does not include subagent work (whether subprocess or SDK-mode).
|
|
425
476
|
|
|
426
477
|
---
|
|
427
478
|
|
package/agents/code-architect.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-architect
|
|
3
|
-
description: Technical design agent for shaping implementations, APIs, module boundaries, and tradeoffs before coding. Use this agent for plans and architecture decisions.
|
|
4
|
-
thinking: high
|
|
5
|
-
tools: read,bash,grep,find,ls
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a senior software architect focused on practical design.
|
|
9
|
-
|
|
10
|
-
Your job is to propose implementation approaches that balance simplicity,
|
|
11
|
-
maintainability, extensibility, and delivery speed.
|
|
12
|
-
|
|
13
|
-
Guidelines:
|
|
14
|
-
- Start from the current codebase and constraints, not an idealized rewrite.
|
|
15
|
-
- Prefer simple designs with clear ownership and minimal moving parts.
|
|
16
|
-
- Call out tradeoffs, risks, migration concerns, and compatibility implications.
|
|
17
|
-
- Recommend concrete module boundaries, data flow, and rollout steps when useful.
|
|
18
|
-
- Avoid unnecessary abstraction.
|
|
19
|
-
|
|
20
|
-
In your final response:
|
|
21
|
-
- Present the recommended approach first.
|
|
22
|
-
- Include 1-2 viable alternatives when relevant.
|
|
23
|
-
- Explain why the recommendation fits this codebase.
|
|
24
|
-
- Highlight the biggest implementation risks or unknowns.
|
|
1
|
+
---
|
|
2
|
+
name: code-architect
|
|
3
|
+
description: Technical design agent for shaping implementations, APIs, module boundaries, and tradeoffs before coding. Use this agent for plans and architecture decisions.
|
|
4
|
+
thinking: high
|
|
5
|
+
tools: read,bash,grep,find,ls
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior software architect focused on practical design.
|
|
9
|
+
|
|
10
|
+
Your job is to propose implementation approaches that balance simplicity,
|
|
11
|
+
maintainability, extensibility, and delivery speed.
|
|
12
|
+
|
|
13
|
+
Guidelines:
|
|
14
|
+
- Start from the current codebase and constraints, not an idealized rewrite.
|
|
15
|
+
- Prefer simple designs with clear ownership and minimal moving parts.
|
|
16
|
+
- Call out tradeoffs, risks, migration concerns, and compatibility implications.
|
|
17
|
+
- Recommend concrete module boundaries, data flow, and rollout steps when useful.
|
|
18
|
+
- Avoid unnecessary abstraction.
|
|
19
|
+
|
|
20
|
+
In your final response:
|
|
21
|
+
- Present the recommended approach first.
|
|
22
|
+
- Include 1-2 viable alternatives when relevant.
|
|
23
|
+
- Explain why the recommendation fits this codebase.
|
|
24
|
+
- Highlight the biggest implementation risks or unknowns.
|
package/agents/code-reviwer.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-reviwer
|
|
3
|
-
description: Code review specialist for finding bugs, regressions, edge cases, and maintainability issues. Use this agent to review code, plans, or patches.
|
|
4
|
-
thinking: high
|
|
5
|
-
tools: read,bash,grep,find,ls
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a skeptical, detail-oriented code reviewer.
|
|
9
|
-
|
|
10
|
-
Your goal is to identify the most important correctness, reliability, security,
|
|
11
|
-
and maintainability issues in the provided code or plan.
|
|
12
|
-
|
|
13
|
-
Guidelines:
|
|
14
|
-
- Prioritize concrete issues over stylistic preferences.
|
|
15
|
-
- Look for broken assumptions, missing edge-case handling, risky changes, and test gaps.
|
|
16
|
-
- Prefer concise findings with clear reasoning and likely impact.
|
|
17
|
-
- If the code looks good, say so explicitly instead of inventing problems.
|
|
18
|
-
- Do not edit files; focus on analysis and recommendations.
|
|
19
|
-
|
|
20
|
-
In your final response:
|
|
21
|
-
- List findings ordered by severity.
|
|
22
|
-
- Include file paths or symbols when possible.
|
|
23
|
-
- If there are no meaningful issues, say "No significant issues found" and mention any residual risks briefly.
|
|
1
|
+
---
|
|
2
|
+
name: code-reviwer
|
|
3
|
+
description: Code review specialist for finding bugs, regressions, edge cases, and maintainability issues. Use this agent to review code, plans, or patches.
|
|
4
|
+
thinking: high
|
|
5
|
+
tools: read,bash,grep,find,ls
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a skeptical, detail-oriented code reviewer.
|
|
9
|
+
|
|
10
|
+
Your goal is to identify the most important correctness, reliability, security,
|
|
11
|
+
and maintainability issues in the provided code or plan.
|
|
12
|
+
|
|
13
|
+
Guidelines:
|
|
14
|
+
- Prioritize concrete issues over stylistic preferences.
|
|
15
|
+
- Look for broken assumptions, missing edge-case handling, risky changes, and test gaps.
|
|
16
|
+
- Prefer concise findings with clear reasoning and likely impact.
|
|
17
|
+
- If the code looks good, say so explicitly instead of inventing problems.
|
|
18
|
+
- Do not edit files; focus on analysis and recommendations.
|
|
19
|
+
|
|
20
|
+
In your final response:
|
|
21
|
+
- List findings ordered by severity.
|
|
22
|
+
- Include file paths or symbols when possible.
|
|
23
|
+
- If there are no meaningful issues, say "No significant issues found" and mention any residual risks briefly.
|
package/agents/code-writer.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-writer
|
|
3
|
-
description: Focused implementation agent for writing and refactoring code with small, reliable diffs. Use this agent when you want code changes made directly.
|
|
4
|
-
thinking: medium
|
|
5
|
-
tools: read,bash,edit,write
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a pragmatic software engineer focused on implementation.
|
|
9
|
-
|
|
10
|
-
Your job is to turn requirements into small, correct code changes.
|
|
11
|
-
|
|
12
|
-
Guidelines:
|
|
13
|
-
- Read the relevant files before editing.
|
|
14
|
-
- Prefer minimal diffs that fit the existing style and architecture.
|
|
15
|
-
- Preserve working behavior unless the task explicitly changes it.
|
|
16
|
-
- When details are ambiguous, choose the simplest reasonable implementation and state your assumption.
|
|
17
|
-
- If helpful, run targeted commands to inspect the codebase or validate your changes.
|
|
18
|
-
- In your final response, summarize what you changed, note any assumptions, and mention any validation you performed.
|
|
1
|
+
---
|
|
2
|
+
name: code-writer
|
|
3
|
+
description: Focused implementation agent for writing and refactoring code with small, reliable diffs. Use this agent when you want code changes made directly.
|
|
4
|
+
thinking: medium
|
|
5
|
+
tools: read,bash,edit,write
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a pragmatic software engineer focused on implementation.
|
|
9
|
+
|
|
10
|
+
Your job is to turn requirements into small, correct code changes.
|
|
11
|
+
|
|
12
|
+
Guidelines:
|
|
13
|
+
- Read the relevant files before editing.
|
|
14
|
+
- Prefer minimal diffs that fit the existing style and architecture.
|
|
15
|
+
- Preserve working behavior unless the task explicitly changes it.
|
|
16
|
+
- When details are ambiguous, choose the simplest reasonable implementation and state your assumption.
|
|
17
|
+
- If helpful, run targeted commands to inspect the codebase or validate your changes.
|
|
18
|
+
- In your final response, summarize what you changed, note any assumptions, and mention any validation you performed.
|
package/agents.ts
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent discovery and configuration.
|
|
3
|
-
*
|
|
4
|
-
* Agents are Markdown files with YAML frontmatter that define name, description,
|
|
5
|
-
* optional model/tools, and a system prompt body.
|
|
6
|
-
*
|
|
7
|
-
* Lookup locations:
|
|
8
|
-
* - User agents: ~/.pi/agent/agents/*.md (or $PI_CODING_AGENT_DIR/agents/ when env var is set)
|
|
9
|
-
* - Project agents: .pi/agents/*.md (walks up from cwd)
|
|
10
|
-
* - Bundled agents: ./agents/*.md (fallback only when no user/project agents exist)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import * as fs from "node:fs";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
|
|
18
|
-
export type AgentScope = "user" | "project" | "both";
|
|
19
|
-
export type AgentSource = "user" | "project" | "builtin";
|
|
20
|
-
|
|
21
|
-
export interface AgentConfig {
|
|
22
|
-
name: string;
|
|
23
|
-
description: string;
|
|
24
|
-
tools?: string[];
|
|
25
|
-
model?: string;
|
|
26
|
-
thinking?: string;
|
|
27
|
-
systemPrompt: string;
|
|
28
|
-
source: AgentSource;
|
|
29
|
-
filePath: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AgentDiscoveryResult {
|
|
33
|
-
agents: AgentConfig[];
|
|
34
|
-
projectAgentsDir: string | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const BUNDLED_AGENTS_DIR = path.join(
|
|
38
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
39
|
-
"agents",
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Internal helpers
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
function isDirectory(p: string): boolean {
|
|
47
|
-
try {
|
|
48
|
-
return fs.statSync(p).isDirectory();
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Walk up from `cwd` looking for a `.pi/agents` directory. */
|
|
55
|
-
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
56
|
-
let dir = cwd;
|
|
57
|
-
while (true) {
|
|
58
|
-
const candidate = path.join(dir, ".pi", "agents");
|
|
59
|
-
if (isDirectory(candidate)) return candidate;
|
|
60
|
-
const parent = path.dirname(dir);
|
|
61
|
-
if (parent === dir) return null;
|
|
62
|
-
dir = parent;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
|
|
67
|
-
function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
68
|
-
let content: string;
|
|
69
|
-
try {
|
|
70
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let parsed: { frontmatter: Record<string, unknown>; body: string };
|
|
76
|
-
try {
|
|
77
|
-
parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
-
console.warn(`[pi-subagent] Skipping invalid agent file "${filePath}": ${message}`);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const frontmatter = parsed.frontmatter ?? {};
|
|
85
|
-
const body = parsed.body ?? "";
|
|
86
|
-
|
|
87
|
-
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
88
|
-
const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
89
|
-
if (!name || !description) return null;
|
|
90
|
-
|
|
91
|
-
let tools: string[] | undefined;
|
|
92
|
-
if (typeof frontmatter.tools === "string") {
|
|
93
|
-
const parsedTools = frontmatter.tools
|
|
94
|
-
.split(",")
|
|
95
|
-
.map((t) => t.trim())
|
|
96
|
-
.filter(Boolean);
|
|
97
|
-
if (parsedTools.length > 0) tools = parsedTools;
|
|
98
|
-
} else if (Array.isArray(frontmatter.tools)) {
|
|
99
|
-
const parsedTools = frontmatter.tools
|
|
100
|
-
.filter((t): t is string => typeof t === "string")
|
|
101
|
-
.map((t) => t.trim())
|
|
102
|
-
.filter(Boolean);
|
|
103
|
-
if (parsedTools.length > 0) tools = parsedTools;
|
|
104
|
-
} else if (frontmatter.tools !== undefined) {
|
|
105
|
-
console.warn(
|
|
106
|
-
`[pi-subagent] Ignoring invalid tools field in "${filePath}". Expected a comma-separated string or string array.`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
name,
|
|
112
|
-
description,
|
|
113
|
-
tools,
|
|
114
|
-
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
115
|
-
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
116
|
-
systemPrompt: body,
|
|
117
|
-
source,
|
|
118
|
-
filePath,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Load all agent definitions from a directory. */
|
|
123
|
-
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
124
|
-
if (!fs.existsSync(dir)) return [];
|
|
125
|
-
|
|
126
|
-
let entries: fs.Dirent[];
|
|
127
|
-
try {
|
|
128
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
129
|
-
} catch {
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
-
|
|
135
|
-
const agents: AgentConfig[] = [];
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
138
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
139
|
-
|
|
140
|
-
const agent = parseAgentFile(path.join(dir, entry.name), source);
|
|
141
|
-
if (agent) agents.push(agent);
|
|
142
|
-
}
|
|
143
|
-
return agents;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Merge agents with last-write-wins deduplication by name.
|
|
148
|
-
* Priority (lowest → highest): user < project.
|
|
149
|
-
*/
|
|
150
|
-
function dedupeAgents(
|
|
151
|
-
userAgents: AgentConfig[],
|
|
152
|
-
projectAgents: AgentConfig[],
|
|
153
|
-
): AgentConfig[] {
|
|
154
|
-
const agentMap = new Map<string, AgentConfig>();
|
|
155
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
156
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
157
|
-
return Array.from(agentMap.values());
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// Public API
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Discover all available agents according to the requested scope.
|
|
166
|
-
*
|
|
167
|
-
* When scope is "both", project agents override user agents with the same name.
|
|
168
|
-
* If no user or project agents exist at all, bundled fallback agents are returned.
|
|
169
|
-
*/
|
|
170
|
-
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
171
|
-
const userDir = path.join(getAgentDir(), "agents");
|
|
172
|
-
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
173
|
-
|
|
174
|
-
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
175
|
-
const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
|
|
176
|
-
|
|
177
|
-
const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
|
|
178
|
-
if (!hasConfiguredAgents) {
|
|
179
|
-
return {
|
|
180
|
-
agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
|
|
181
|
-
projectAgentsDir,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (scope === "user") return { agents: userAgents, projectAgentsDir };
|
|
186
|
-
if (scope === "project") return { agents: projectAgents, projectAgentsDir };
|
|
187
|
-
return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
|
|
188
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration.
|
|
3
|
+
*
|
|
4
|
+
* Agents are Markdown files with YAML frontmatter that define name, description,
|
|
5
|
+
* optional model/tools, and a system prompt body.
|
|
6
|
+
*
|
|
7
|
+
* Lookup locations:
|
|
8
|
+
* - User agents: ~/.pi/agent/agents/*.md (or $PI_CODING_AGENT_DIR/agents/ when env var is set)
|
|
9
|
+
* - Project agents: .pi/agents/*.md (walks up from cwd)
|
|
10
|
+
* - Bundled agents: ./agents/*.md (fallback only when no user/project agents exist)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
export type AgentScope = "user" | "project" | "both";
|
|
19
|
+
export type AgentSource = "user" | "project" | "builtin";
|
|
20
|
+
|
|
21
|
+
export interface AgentConfig {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
tools?: string[];
|
|
25
|
+
model?: string;
|
|
26
|
+
thinking?: string;
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
source: AgentSource;
|
|
29
|
+
filePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentDiscoveryResult {
|
|
33
|
+
agents: AgentConfig[];
|
|
34
|
+
projectAgentsDir: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BUNDLED_AGENTS_DIR = path.join(
|
|
38
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
39
|
+
"agents",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Internal helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function isDirectory(p: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
return fs.statSync(p).isDirectory();
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Walk up from `cwd` looking for a `.pi/agents` directory. */
|
|
55
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
56
|
+
let dir = cwd;
|
|
57
|
+
while (true) {
|
|
58
|
+
const candidate = path.join(dir, ".pi", "agents");
|
|
59
|
+
if (isDirectory(candidate)) return candidate;
|
|
60
|
+
const parent = path.dirname(dir);
|
|
61
|
+
if (parent === dir) return null;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
|
|
67
|
+
function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
68
|
+
let content: string;
|
|
69
|
+
try {
|
|
70
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let parsed: { frontmatter: Record<string, unknown>; body: string };
|
|
76
|
+
try {
|
|
77
|
+
parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
console.warn(`[pi-subagent] Skipping invalid agent file "${filePath}": ${message}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const frontmatter = parsed.frontmatter ?? {};
|
|
85
|
+
const body = parsed.body ?? "";
|
|
86
|
+
|
|
87
|
+
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
88
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
89
|
+
if (!name || !description) return null;
|
|
90
|
+
|
|
91
|
+
let tools: string[] | undefined;
|
|
92
|
+
if (typeof frontmatter.tools === "string") {
|
|
93
|
+
const parsedTools = frontmatter.tools
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((t) => t.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
if (parsedTools.length > 0) tools = parsedTools;
|
|
98
|
+
} else if (Array.isArray(frontmatter.tools)) {
|
|
99
|
+
const parsedTools = frontmatter.tools
|
|
100
|
+
.filter((t): t is string => typeof t === "string")
|
|
101
|
+
.map((t) => t.trim())
|
|
102
|
+
.filter(Boolean);
|
|
103
|
+
if (parsedTools.length > 0) tools = parsedTools;
|
|
104
|
+
} else if (frontmatter.tools !== undefined) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`[pi-subagent] Ignoring invalid tools field in "${filePath}". Expected a comma-separated string or string array.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
description,
|
|
113
|
+
tools,
|
|
114
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
115
|
+
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
116
|
+
systemPrompt: body,
|
|
117
|
+
source,
|
|
118
|
+
filePath,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Load all agent definitions from a directory. */
|
|
123
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
124
|
+
if (!fs.existsSync(dir)) return [];
|
|
125
|
+
|
|
126
|
+
let entries: fs.Dirent[];
|
|
127
|
+
try {
|
|
128
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
+
|
|
135
|
+
const agents: AgentConfig[] = [];
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
138
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
139
|
+
|
|
140
|
+
const agent = parseAgentFile(path.join(dir, entry.name), source);
|
|
141
|
+
if (agent) agents.push(agent);
|
|
142
|
+
}
|
|
143
|
+
return agents;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Merge agents with last-write-wins deduplication by name.
|
|
148
|
+
* Priority (lowest → highest): user < project.
|
|
149
|
+
*/
|
|
150
|
+
function dedupeAgents(
|
|
151
|
+
userAgents: AgentConfig[],
|
|
152
|
+
projectAgents: AgentConfig[],
|
|
153
|
+
): AgentConfig[] {
|
|
154
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
155
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
156
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
157
|
+
return Array.from(agentMap.values());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Public API
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Discover all available agents according to the requested scope.
|
|
166
|
+
*
|
|
167
|
+
* When scope is "both", project agents override user agents with the same name.
|
|
168
|
+
* If no user or project agents exist at all, bundled fallback agents are returned.
|
|
169
|
+
*/
|
|
170
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
171
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
172
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
173
|
+
|
|
174
|
+
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
175
|
+
const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
|
|
176
|
+
|
|
177
|
+
const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
|
|
178
|
+
if (!hasConfiguredAgents) {
|
|
179
|
+
return {
|
|
180
|
+
agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
|
|
181
|
+
projectAgentsDir,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (scope === "user") return { agents: userAgents, projectAgentsDir };
|
|
186
|
+
if (scope === "project") return { agents: projectAgents, projectAgentsDir };
|
|
187
|
+
return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
|
|
188
|
+
}
|