@tintinweb/pi-subagents 0.2.3 → 0.2.5
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 +24 -0
- package/README.md +31 -6
- package/package.json +1 -1
- package/src/agent-runner.ts +14 -4
- package/src/custom-agents.ts +21 -10
- package/src/index.ts +400 -119
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.5] - 2026-03-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Interactive `/agents` menu** — single command replaces `/agent` and `/agents` with a full management wizard
|
|
12
|
+
- Browse and manage running agents
|
|
13
|
+
- Custom agents submenu — edit or delete existing agents
|
|
14
|
+
- Create new custom agents via manual wizard or AI-generated (with comprehensive frontmatter documentation for the generator)
|
|
15
|
+
- Settings: configure max concurrency, default max turns, and grace turns at runtime
|
|
16
|
+
- Built-in agent types shown with model info (e.g. `Explore · haiku`)
|
|
17
|
+
- Aligned formatting for agent lists
|
|
18
|
+
- **Configurable turn limits** — `defaultMaxTurns` and `graceTurns` are now runtime-adjustable via `/agents` → Settings
|
|
19
|
+
- Sub-menus return to main menu instead of exiting
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- `/agent <type> <prompt>` command (use `Agent` tool directly, or create custom agents via `/agents`)
|
|
23
|
+
|
|
24
|
+
## [0.2.4] - 2026-03-06
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Global custom agents** — agents in `~/.pi/agent/agents/*.md` are now discovered automatically and available across all projects
|
|
28
|
+
- Two-tier discovery hierarchy: project-level (`.pi/agents/`) overrides global (`~/.pi/agent/agents/`)
|
|
29
|
+
|
|
8
30
|
## [0.2.3] - 2026-03-05
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -82,6 +104,8 @@ Initial release.
|
|
|
82
104
|
- **Thinking level** — per-agent extended thinking control
|
|
83
105
|
- **`/agent` and `/agents` commands**
|
|
84
106
|
|
|
107
|
+
[0.2.5]: https://github.com/tintinweb/pi-subagents/compare/v0.2.4...v0.2.5
|
|
108
|
+
[0.2.4]: https://github.com/tintinweb/pi-subagents/compare/v0.2.3...v0.2.4
|
|
85
109
|
[0.2.3]: https://github.com/tintinweb/pi-subagents/compare/v0.2.2...v0.2.3
|
|
86
110
|
[0.2.2]: https://github.com/tintinweb/pi-subagents/compare/v0.2.1...v0.2.2
|
|
87
111
|
[0.2.1]: https://github.com/tintinweb/pi-subagents/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-
|
|
|
6
6
|
|
|
7
7
|
<img width="600" alt="pi-subagents screenshot" src="https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png" />
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
https://github.com/user-attachments/assets/5d1331e8-6d02-420b-b30a-dcbf838b1660
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
15
|
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
@@ -83,7 +87,16 @@ Completed results can be expanded (ctrl+o in pi) to show the full agent output i
|
|
|
83
87
|
|
|
84
88
|
## Custom Agents
|
|
85
89
|
|
|
86
|
-
Define custom agent types by creating `.
|
|
90
|
+
Define custom agent types by creating `.md` files. The filename becomes the agent type name.
|
|
91
|
+
|
|
92
|
+
Custom agents are discovered from two locations (higher priority wins):
|
|
93
|
+
|
|
94
|
+
| Priority | Location | Scope |
|
|
95
|
+
|----------|----------|-------|
|
|
96
|
+
| 1 (highest) | `.pi/agents/<name>.md` | Project — per-repo agents |
|
|
97
|
+
| 2 | `~/.pi/agent/agents/<name>.md` | Global — available everywhere |
|
|
98
|
+
|
|
99
|
+
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project.
|
|
87
100
|
|
|
88
101
|
### Example: `.pi/agents/auditor.md`
|
|
89
102
|
|
|
@@ -173,15 +186,27 @@ Send a steering message to a running agent. The message interrupts after the cur
|
|
|
173
186
|
|
|
174
187
|
| Command | Description |
|
|
175
188
|
|---------|-------------|
|
|
176
|
-
| `/
|
|
177
|
-
|
|
189
|
+
| `/agents` | Interactive agent management menu |
|
|
190
|
+
|
|
191
|
+
The `/agents` command opens an interactive menu:
|
|
178
192
|
|
|
179
193
|
```
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
Running agents (2) — 1 running, 1 done ← only shown when agents exist
|
|
195
|
+
Custom agents (3) ← submenu: edit or delete agents
|
|
196
|
+
Create new agent ← manual wizard or AI-generated
|
|
197
|
+
Settings ← max concurrency, max turns, grace turns
|
|
198
|
+
|
|
199
|
+
Built-in (always available):
|
|
200
|
+
general-purpose · inherit
|
|
201
|
+
Explore · haiku
|
|
202
|
+
Plan · inherit
|
|
203
|
+
...
|
|
183
204
|
```
|
|
184
205
|
|
|
206
|
+
- **Custom agents submenu** — select an agent to edit (opens editor) or delete
|
|
207
|
+
- **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file)
|
|
208
|
+
- **Settings** — configure max concurrency, default max turns, and grace turns at runtime
|
|
209
|
+
|
|
185
210
|
## Graceful Max Turns
|
|
186
211
|
|
|
187
212
|
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
package/package.json
CHANGED
package/src/agent-runner.ts
CHANGED
|
@@ -23,10 +23,20 @@ import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
|
23
23
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
24
24
|
|
|
25
25
|
/** Default max turns to prevent subagents from looping indefinitely. */
|
|
26
|
-
|
|
26
|
+
let defaultMaxTurns = 50;
|
|
27
|
+
|
|
28
|
+
/** Get the default max turns value. */
|
|
29
|
+
export function getDefaultMaxTurns(): number { return defaultMaxTurns; }
|
|
30
|
+
/** Set the default max turns value (minimum 1). */
|
|
31
|
+
export function setDefaultMaxTurns(n: number): void { defaultMaxTurns = Math.max(1, n); }
|
|
27
32
|
|
|
28
33
|
/** Additional turns allowed after the soft limit steer message. */
|
|
29
|
-
|
|
34
|
+
let graceTurns = 5;
|
|
35
|
+
|
|
36
|
+
/** Get the grace turns value. */
|
|
37
|
+
export function getGraceTurns(): number { return graceTurns; }
|
|
38
|
+
/** Set the grace turns value (minimum 1). */
|
|
39
|
+
export function setGraceTurns(n: number): void { graceTurns = Math.max(1, n); }
|
|
30
40
|
|
|
31
41
|
/** Haiku model IDs to try for Explore agents (in preference order). */
|
|
32
42
|
const HAIKU_MODEL_IDS = [
|
|
@@ -215,7 +225,7 @@ export async function runAgent(
|
|
|
215
225
|
|
|
216
226
|
// Track turns for graceful max_turns enforcement
|
|
217
227
|
let turnCount = 0;
|
|
218
|
-
const maxTurns = options.maxTurns ?? customConfig?.maxTurns ??
|
|
228
|
+
const maxTurns = options.maxTurns ?? customConfig?.maxTurns ?? defaultMaxTurns;
|
|
219
229
|
let softLimitReached = false;
|
|
220
230
|
let aborted = false;
|
|
221
231
|
|
|
@@ -226,7 +236,7 @@ export async function runAgent(
|
|
|
226
236
|
if (!softLimitReached && turnCount >= maxTurns) {
|
|
227
237
|
softLimitReached = true;
|
|
228
238
|
session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
|
|
229
|
-
} else if (softLimitReached && turnCount >= maxTurns +
|
|
239
|
+
} else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
230
240
|
aborted = true;
|
|
231
241
|
session.abort();
|
|
232
242
|
}
|
package/src/custom-agents.ts
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from .pi/agents
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
7
7
|
import { join, basename } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { SUBAGENT_TYPES, type CustomAgentConfig, type ThinkingLevel } from "./types.js";
|
|
9
10
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Scan
|
|
13
|
-
*
|
|
13
|
+
* Scan for custom agent .md files from multiple locations.
|
|
14
|
+
* Discovery hierarchy (higher priority wins):
|
|
15
|
+
* 1. Project: <cwd>/.pi/agents/*.md
|
|
16
|
+
* 2. Global: ~/.pi/agent/agents/*.md
|
|
17
|
+
*
|
|
18
|
+
* Project-level agents override global ones with the same name.
|
|
14
19
|
*/
|
|
15
20
|
export function loadCustomAgents(cwd: string): Map<string, CustomAgentConfig> {
|
|
16
|
-
const
|
|
17
|
-
|
|
21
|
+
const globalDir = join(homedir(), ".pi", "agent", "agents");
|
|
22
|
+
const projectDir = join(cwd, ".pi", "agents");
|
|
23
|
+
|
|
24
|
+
const agents = new Map<string, CustomAgentConfig>();
|
|
25
|
+
loadFromDir(globalDir, agents); // lower priority
|
|
26
|
+
loadFromDir(projectDir, agents); // higher priority (overwrites)
|
|
27
|
+
return agents;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Load agent configs from a directory into the map. */
|
|
31
|
+
function loadFromDir(dir: string, agents: Map<string, CustomAgentConfig>): void {
|
|
32
|
+
if (!existsSync(dir)) return;
|
|
18
33
|
|
|
19
34
|
let files: string[];
|
|
20
35
|
try {
|
|
21
36
|
files = readdirSync(dir).filter(f => f.endsWith(".md"));
|
|
22
37
|
} catch {
|
|
23
|
-
return
|
|
38
|
+
return;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
const agents = new Map<string, CustomAgentConfig>();
|
|
27
|
-
|
|
28
41
|
for (const file of files) {
|
|
29
42
|
const name = basename(file, ".md");
|
|
30
43
|
if ((SUBAGENT_TYPES as readonly string[]).includes(name)) continue;
|
|
@@ -54,8 +67,6 @@ export function loadCustomAgents(cwd: string): Map<string, CustomAgentConfig> {
|
|
|
54
67
|
isolated: fm.isolated === true,
|
|
55
68
|
});
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
return agents;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
// ---- Field parsers ----
|
package/src/index.ts
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
* steer_subagent — LLM-callable: send a steering message to a running agent
|
|
8
8
|
*
|
|
9
9
|
* Commands:
|
|
10
|
-
* /
|
|
11
|
-
* /agents — List all agents with status
|
|
10
|
+
* /agents — Interactive agent management menu
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { existsSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
15
17
|
import { Text } from "@mariozechner/pi-tui";
|
|
16
18
|
import { Type } from "@sinclair/typebox";
|
|
17
19
|
import { AgentManager } from "./agent-manager.js";
|
|
18
|
-
import { steerAgent, getAgentConversation } from "./agent-runner.js";
|
|
20
|
+
import { steerAgent, getAgentConversation, getDefaultMaxTurns, setDefaultMaxTurns, getGraceTurns, setGraceTurns } from "./agent-runner.js";
|
|
19
21
|
import { SUBAGENT_TYPES, type SubagentType, type ThinkingLevel, type CustomAgentConfig } from "./types.js";
|
|
20
|
-
import {
|
|
22
|
+
import { getAvailableTypes, getCustomAgentNames, getCustomAgentConfig, isValidType, registerCustomAgents, BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
21
23
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
24
|
import {
|
|
23
25
|
AgentWidget,
|
|
@@ -256,7 +258,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
256
258
|
...builtinDescs,
|
|
257
259
|
...(customDescs.length > 0 ? ["", "Custom types:", ...customDescs] : []),
|
|
258
260
|
"",
|
|
259
|
-
"Custom agents can be defined in .pi/agents/<name>.md — they are picked up automatically.",
|
|
261
|
+
"Custom agents can be defined in .pi/agents/<name>.md (project) or ~/.pi/agent/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones.",
|
|
260
262
|
].join("\n");
|
|
261
263
|
};
|
|
262
264
|
|
|
@@ -295,7 +297,7 @@ Guidelines:
|
|
|
295
297
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
296
298
|
}),
|
|
297
299
|
subagent_type: Type.String({
|
|
298
|
-
description: `The type of specialized agent to use. Built-in: ${SUBAGENT_TYPES.join(", ")}. Custom agents from .pi/agents/*.md are also available.`,
|
|
300
|
+
description: `The type of specialized agent to use. Built-in: ${SUBAGENT_TYPES.join(", ")}. Custom agents from .pi/agents/*.md (project) or ~/.pi/agent/agents/*.md (global) are also available.`,
|
|
299
301
|
}),
|
|
300
302
|
model: Type.Optional(
|
|
301
303
|
Type.String({
|
|
@@ -717,139 +719,418 @@ Guidelines:
|
|
|
717
719
|
},
|
|
718
720
|
});
|
|
719
721
|
|
|
720
|
-
// ---- /
|
|
721
|
-
|
|
722
|
-
pi.registerCommand("agent", {
|
|
723
|
-
description: "Spawn a sub-agent: /agent <type> <prompt>",
|
|
724
|
-
handler: async (args, ctx) => {
|
|
725
|
-
const trimmed = args?.trim() ?? "";
|
|
726
|
-
|
|
727
|
-
if (!trimmed) {
|
|
728
|
-
const lines = [
|
|
729
|
-
"Usage: /agent <type> <prompt>",
|
|
730
|
-
"",
|
|
731
|
-
"Agent types:",
|
|
732
|
-
...getAvailableTypes().map(
|
|
733
|
-
(t) => ` ${t.padEnd(20)} ${getConfig(t).description}`,
|
|
734
|
-
),
|
|
735
|
-
"",
|
|
736
|
-
"Examples:",
|
|
737
|
-
" /agent Explore Find all TypeScript files that handle authentication",
|
|
738
|
-
" /agent Plan Design a caching layer for the API",
|
|
739
|
-
" /agent general-purpose Refactor the auth module to use JWT",
|
|
740
|
-
];
|
|
741
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
722
|
+
// ---- /agents interactive menu ----
|
|
744
723
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (spaceIdx === -1) {
|
|
748
|
-
ctx.ui.notify(
|
|
749
|
-
`Missing prompt. Usage: /agent <type> <prompt>\nTypes: ${getAvailableTypes().join(", ")}`,
|
|
750
|
-
"warning",
|
|
751
|
-
);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
724
|
+
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
725
|
+
const personalAgentsDir = () => join(homedir(), ".pi", "agent", "agents");
|
|
754
726
|
|
|
755
|
-
|
|
756
|
-
|
|
727
|
+
/** Find the file path of a custom agent by name (project first, then global). */
|
|
728
|
+
function findAgentFile(name: string): { path: string; location: "project" | "personal" } | undefined {
|
|
729
|
+
const projectPath = join(projectAgentsDir(), `${name}.md`);
|
|
730
|
+
if (existsSync(projectPath)) return { path: projectPath, location: "project" };
|
|
731
|
+
const personalPath = join(personalAgentsDir(), `${name}.md`);
|
|
732
|
+
if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
757
735
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
736
|
+
/** Model label for display: built-in types have known defaults, custom agents show their config. */
|
|
737
|
+
const BUILTIN_MODEL_LABELS: Record<string, string> = {
|
|
738
|
+
"general-purpose": "inherit",
|
|
739
|
+
"Explore": "haiku",
|
|
740
|
+
"Plan": "inherit",
|
|
741
|
+
"statusline-setup": "inherit",
|
|
742
|
+
"claude-code-guide": "inherit",
|
|
743
|
+
};
|
|
765
744
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
745
|
+
function getModelLabel(type: string): string {
|
|
746
|
+
const builtin = BUILTIN_MODEL_LABELS[type];
|
|
747
|
+
if (builtin) return builtin;
|
|
748
|
+
const custom = getCustomAgentConfig(type);
|
|
749
|
+
if (custom?.model) {
|
|
750
|
+
// Show short form: "anthropic/claude-haiku-4-5-20251001" → "haiku"
|
|
751
|
+
const id = custom.model.toLowerCase();
|
|
752
|
+
if (id.includes("haiku")) return "haiku";
|
|
753
|
+
if (id.includes("sonnet")) return "sonnet";
|
|
754
|
+
if (id.includes("opus")) return "opus";
|
|
755
|
+
return custom.model;
|
|
756
|
+
}
|
|
757
|
+
return "inherit";
|
|
758
|
+
}
|
|
770
759
|
|
|
771
|
-
|
|
772
|
-
|
|
760
|
+
async function showAgentsMenu(ctx: ExtensionCommandContext) {
|
|
761
|
+
reloadCustomAgents();
|
|
762
|
+
const customNames = getCustomAgentNames();
|
|
773
763
|
|
|
774
|
-
|
|
775
|
-
|
|
764
|
+
// Build select options
|
|
765
|
+
const options: string[] = [];
|
|
776
766
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
});
|
|
767
|
+
// Running agents entry (only if there are active agents)
|
|
768
|
+
const agents = manager.listAgents();
|
|
769
|
+
if (agents.length > 0) {
|
|
770
|
+
const running = agents.filter(a => a.status === "running" || a.status === "queued").length;
|
|
771
|
+
const done = agents.filter(a => a.status === "completed" || a.status === "steered").length;
|
|
772
|
+
options.push(`Running agents (${agents.length}) — ${running} running, ${done} done`);
|
|
773
|
+
}
|
|
783
774
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
775
|
+
// Custom agents submenu (only if there are custom agents)
|
|
776
|
+
if (customNames.length > 0) {
|
|
777
|
+
options.push(`Custom agents (${customNames.length})`);
|
|
778
|
+
}
|
|
788
779
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
// Send the result as a message so it appears in the conversation
|
|
793
|
-
pi.sendMessage(
|
|
794
|
-
{
|
|
795
|
-
customType: "agent-result",
|
|
796
|
-
content: [
|
|
797
|
-
{
|
|
798
|
-
type: "text",
|
|
799
|
-
text:
|
|
800
|
-
`**${displayName}** agent completed in ${duration} (${record.toolUses} tool uses)${statusNote}\n\n` +
|
|
801
|
-
(record.result ?? "No output."),
|
|
802
|
-
},
|
|
803
|
-
],
|
|
804
|
-
display: true,
|
|
805
|
-
},
|
|
806
|
-
{ triggerTurn: false },
|
|
807
|
-
);
|
|
808
|
-
},
|
|
809
|
-
});
|
|
780
|
+
// Actions
|
|
781
|
+
options.push("Create new agent");
|
|
782
|
+
options.push("Settings");
|
|
810
783
|
|
|
811
|
-
|
|
784
|
+
// Show built-in types below the select as informational text (like Claude does)
|
|
785
|
+
const maxBuiltin = Math.max(...SUBAGENT_TYPES.map(t => t.length));
|
|
786
|
+
const builtinLines = SUBAGENT_TYPES.map(t => {
|
|
787
|
+
const model = BUILTIN_MODEL_LABELS[t] ?? "inherit";
|
|
788
|
+
return ` ${t.padEnd(maxBuiltin)} · ${model}`;
|
|
789
|
+
});
|
|
812
790
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
791
|
+
const noAgentsMsg = customNames.length === 0 && agents.length === 0
|
|
792
|
+
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
793
|
+
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
794
|
+
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
795
|
+
: "";
|
|
796
|
+
|
|
797
|
+
ctx.ui.notify(
|
|
798
|
+
`${noAgentsMsg}Built-in (always available):\n${builtinLines.join("\n")}`,
|
|
799
|
+
"info",
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
const choice = await ctx.ui.select("Agents", options);
|
|
803
|
+
if (!choice) return;
|
|
804
|
+
|
|
805
|
+
if (choice.startsWith("Running agents (")) {
|
|
806
|
+
await showRunningAgents(ctx);
|
|
807
|
+
await showAgentsMenu(ctx);
|
|
808
|
+
} else if (choice.startsWith("Custom agents (")) {
|
|
809
|
+
await showCustomAgentsList(ctx);
|
|
810
|
+
await showAgentsMenu(ctx);
|
|
811
|
+
} else if (choice === "Create new agent") {
|
|
812
|
+
await showCreateWizard(ctx);
|
|
813
|
+
} else if (choice === "Settings") {
|
|
814
|
+
await showSettings(ctx);
|
|
815
|
+
await showAgentsMenu(ctx);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function showCustomAgentsList(ctx: ExtensionCommandContext) {
|
|
820
|
+
const customNames = getCustomAgentNames();
|
|
821
|
+
if (customNames.length === 0) {
|
|
822
|
+
ctx.ui.notify("No custom agents.", "info");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Compute max width of "name · model" for alignment
|
|
827
|
+
const entries = customNames.map(name => {
|
|
828
|
+
const cfg = getCustomAgentConfig(name);
|
|
829
|
+
const model = getModelLabel(name);
|
|
830
|
+
const prefix = `${name} · ${model}`;
|
|
831
|
+
return { prefix, desc: cfg?.description ?? name };
|
|
832
|
+
});
|
|
833
|
+
const maxPrefix = Math.max(...entries.map(e => e.prefix.length));
|
|
817
834
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
835
|
+
const options = entries.map(({ prefix, desc }) =>
|
|
836
|
+
`${prefix.padEnd(maxPrefix)} — ${desc}`,
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const choice = await ctx.ui.select("Custom agents", options);
|
|
840
|
+
if (!choice) return;
|
|
841
|
+
|
|
842
|
+
const agentName = choice.split(" · ")[0];
|
|
843
|
+
if (getCustomAgentConfig(agentName)) {
|
|
844
|
+
await showAgentDetail(ctx, agentName);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function showRunningAgents(ctx: ExtensionCommandContext) {
|
|
849
|
+
const agents = manager.listAgents();
|
|
850
|
+
if (agents.length === 0) {
|
|
851
|
+
ctx.ui.notify("No agents.", "info");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Show as a selectable list for potential future actions
|
|
856
|
+
const options = agents.map(a => {
|
|
857
|
+
const dn = getDisplayName(a.type);
|
|
858
|
+
const dur = formatDuration(a.startedAt, a.completedAt);
|
|
859
|
+
return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
await ctx.ui.select("Running agents", options);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
866
|
+
const file = findAgentFile(name);
|
|
867
|
+
if (!file) {
|
|
868
|
+
ctx.ui.notify(`Agent file not found for "${name}".`, "warning");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const choice = await ctx.ui.select(name, ["Edit", "Delete", "Back"]);
|
|
873
|
+
if (!choice || choice === "Back") return;
|
|
874
|
+
|
|
875
|
+
if (choice === "Edit") {
|
|
876
|
+
const content = readFileSync(file.path, "utf-8");
|
|
877
|
+
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
878
|
+
if (edited !== undefined && edited !== content) {
|
|
879
|
+
const { writeFileSync } = await import("node:fs");
|
|
880
|
+
writeFileSync(file.path, edited, "utf-8");
|
|
881
|
+
reloadCustomAgents();
|
|
882
|
+
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
821
883
|
}
|
|
884
|
+
} else if (choice === "Delete") {
|
|
885
|
+
const confirmed = await ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`);
|
|
886
|
+
if (confirmed) {
|
|
887
|
+
unlinkSync(file.path);
|
|
888
|
+
reloadCustomAgents();
|
|
889
|
+
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
822
893
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
894
|
+
async function showCreateWizard(ctx: ExtensionCommandContext) {
|
|
895
|
+
const location = await ctx.ui.select("Choose location", [
|
|
896
|
+
"Project (.pi/agents/)",
|
|
897
|
+
"Personal (~/.pi/agent/agents/)",
|
|
898
|
+
]);
|
|
899
|
+
if (!location) return;
|
|
900
|
+
|
|
901
|
+
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
902
|
+
|
|
903
|
+
const method = await ctx.ui.select("Creation method", [
|
|
904
|
+
"Generate with Claude (recommended)",
|
|
905
|
+
"Manual configuration",
|
|
906
|
+
]);
|
|
907
|
+
if (!method) return;
|
|
908
|
+
|
|
909
|
+
if (method.startsWith("Generate")) {
|
|
910
|
+
await showGenerateWizard(ctx, targetDir);
|
|
911
|
+
} else {
|
|
912
|
+
await showManualWizard(ctx, targetDir);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
826
915
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
lines.push("");
|
|
916
|
+
async function showGenerateWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
917
|
+
const description = await ctx.ui.input("Describe what this agent should do");
|
|
918
|
+
if (!description) return;
|
|
831
919
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const connector = i === agents.length - 1 ? "└─" : "├─";
|
|
835
|
-
const displayName = getDisplayName(a.type);
|
|
836
|
-
const duration = formatDuration(a.startedAt, a.completedAt);
|
|
920
|
+
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
921
|
+
if (!name) return;
|
|
837
922
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
923
|
+
// Validate name
|
|
924
|
+
if (isValidType(name) && !getCustomAgentConfig(name)) {
|
|
925
|
+
ctx.ui.notify(`"${name}" conflicts with a built-in agent type.`, "warning");
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!mkdirSync(targetDir, { recursive: true }) && !existsSync(targetDir)) {
|
|
930
|
+
mkdirSync(targetDir, { recursive: true });
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
934
|
+
if (existsSync(targetPath)) {
|
|
935
|
+
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
936
|
+
if (!overwrite) return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
ctx.ui.notify("Generating agent definition...", "info");
|
|
940
|
+
|
|
941
|
+
const generatePrompt = `Create a custom pi sub-agent definition file based on this description: "${description}"
|
|
942
|
+
|
|
943
|
+
Write a markdown file to: ${targetPath}
|
|
944
|
+
|
|
945
|
+
The file format is a markdown file with YAML frontmatter and a system prompt body:
|
|
946
|
+
|
|
947
|
+
\`\`\`markdown
|
|
948
|
+
---
|
|
949
|
+
description: <one-line description shown in UI>
|
|
950
|
+
tools: <comma-separated built-in tools: read, bash, edit, write, grep, find, ls. Use "none" for no tools. Omit for all tools>
|
|
951
|
+
model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5-20251001". Omit to inherit parent model>
|
|
952
|
+
thinking: <optional thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit>
|
|
953
|
+
max_turns: <optional max agentic turns, default 50. Omit for default>
|
|
954
|
+
prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is appended to default prompt). Default: replace>
|
|
955
|
+
extensions: <true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>
|
|
956
|
+
skills: <true (inherit all), false (none). Default: true>
|
|
957
|
+
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
958
|
+
run_in_background: <true to run in background by default. Default: false>
|
|
959
|
+
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
<system prompt body — instructions for the agent>
|
|
963
|
+
\`\`\`
|
|
964
|
+
|
|
965
|
+
Guidelines for choosing settings:
|
|
966
|
+
- For read-only tasks (review, analysis): tools: read, bash, grep, find, ls
|
|
967
|
+
- For code modification tasks: include edit, write
|
|
968
|
+
- Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
|
|
969
|
+
- Use prompt_mode: replace for fully custom agents with their own personality/instructions
|
|
970
|
+
- Set inherit_context: true if the agent needs to know what was discussed in the parent conversation
|
|
971
|
+
- Set isolated: true if the agent should NOT have access to MCP servers or other extensions
|
|
972
|
+
- Only include frontmatter fields that differ from defaults — omit fields where the default is fine
|
|
973
|
+
|
|
974
|
+
Write the file using the write tool. Only write the file, nothing else.`;
|
|
975
|
+
|
|
976
|
+
const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
|
|
977
|
+
description: `Generate ${name} agent`,
|
|
978
|
+
maxTurns: 5,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
if (record.status === "error") {
|
|
982
|
+
ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
reloadCustomAgents();
|
|
987
|
+
|
|
988
|
+
if (existsSync(targetPath)) {
|
|
989
|
+
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
990
|
+
} else {
|
|
991
|
+
ctx.ui.notify("Agent generation completed but file was not created. Check the agent output.", "warning");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function showManualWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
996
|
+
// 1. Name
|
|
997
|
+
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
998
|
+
if (!name) return;
|
|
999
|
+
|
|
1000
|
+
if (isValidType(name) && !getCustomAgentConfig(name)) {
|
|
1001
|
+
ctx.ui.notify(`"${name}" conflicts with a built-in agent type.`, "warning");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 2. Description
|
|
1006
|
+
const description = await ctx.ui.input("Description (one line)");
|
|
1007
|
+
if (!description) return;
|
|
1008
|
+
|
|
1009
|
+
// 3. Tools
|
|
1010
|
+
const toolChoice = await ctx.ui.select("Tools", ["all", "none", "read-only (read, bash, grep, find, ls)", "custom..."]);
|
|
1011
|
+
if (!toolChoice) return;
|
|
1012
|
+
|
|
1013
|
+
let tools: string;
|
|
1014
|
+
if (toolChoice === "all") {
|
|
1015
|
+
tools = BUILTIN_TOOL_NAMES.join(", ");
|
|
1016
|
+
} else if (toolChoice === "none") {
|
|
1017
|
+
tools = "none";
|
|
1018
|
+
} else if (toolChoice.startsWith("read-only")) {
|
|
1019
|
+
tools = "read, bash, grep, find, ls";
|
|
1020
|
+
} else {
|
|
1021
|
+
const customTools = await ctx.ui.input("Tools (comma-separated)", BUILTIN_TOOL_NAMES.join(", "));
|
|
1022
|
+
if (!customTools) return;
|
|
1023
|
+
tools = customTools;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// 4. Model
|
|
1027
|
+
const modelChoice = await ctx.ui.select("Model", [
|
|
1028
|
+
"inherit (parent model)",
|
|
1029
|
+
"haiku",
|
|
1030
|
+
"sonnet",
|
|
1031
|
+
"opus",
|
|
1032
|
+
"custom...",
|
|
1033
|
+
]);
|
|
1034
|
+
if (!modelChoice) return;
|
|
1035
|
+
|
|
1036
|
+
let modelLine = "";
|
|
1037
|
+
if (modelChoice === "haiku") modelLine = "\nmodel: anthropic/claude-haiku-4-5-20251001";
|
|
1038
|
+
else if (modelChoice === "sonnet") modelLine = "\nmodel: anthropic/claude-sonnet-4-6";
|
|
1039
|
+
else if (modelChoice === "opus") modelLine = "\nmodel: anthropic/claude-opus-4-6";
|
|
1040
|
+
else if (modelChoice === "custom...") {
|
|
1041
|
+
const customModel = await ctx.ui.input("Model (provider/modelId)");
|
|
1042
|
+
if (customModel) modelLine = `\nmodel: ${customModel}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// 5. Thinking
|
|
1046
|
+
const thinkingChoice = await ctx.ui.select("Thinking level", [
|
|
1047
|
+
"inherit",
|
|
1048
|
+
"off",
|
|
1049
|
+
"minimal",
|
|
1050
|
+
"low",
|
|
1051
|
+
"medium",
|
|
1052
|
+
"high",
|
|
1053
|
+
"xhigh",
|
|
1054
|
+
]);
|
|
1055
|
+
if (!thinkingChoice) return;
|
|
1056
|
+
|
|
1057
|
+
let thinkingLine = "";
|
|
1058
|
+
if (thinkingChoice !== "inherit") thinkingLine = `\nthinking: ${thinkingChoice}`;
|
|
1059
|
+
|
|
1060
|
+
// 6. System prompt
|
|
1061
|
+
const systemPrompt = await ctx.ui.editor("System prompt", "");
|
|
1062
|
+
if (systemPrompt === undefined) return;
|
|
1063
|
+
|
|
1064
|
+
// Build the file
|
|
1065
|
+
const content = `---
|
|
1066
|
+
description: ${description}
|
|
1067
|
+
tools: ${tools}${modelLine}${thinkingLine}
|
|
1068
|
+
prompt_mode: replace
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
${systemPrompt}
|
|
1072
|
+
`;
|
|
1073
|
+
|
|
1074
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1075
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1076
|
+
|
|
1077
|
+
if (existsSync(targetPath)) {
|
|
1078
|
+
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1079
|
+
if (!overwrite) return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const { writeFileSync } = await import("node:fs");
|
|
1083
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
1084
|
+
reloadCustomAgents();
|
|
1085
|
+
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1086
|
+
}
|
|
841
1087
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1088
|
+
async function showSettings(ctx: ExtensionCommandContext) {
|
|
1089
|
+
const choice = await ctx.ui.select("Settings", [
|
|
1090
|
+
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
1091
|
+
`Default max turns (current: ${getDefaultMaxTurns()})`,
|
|
1092
|
+
`Grace turns (current: ${getGraceTurns()})`,
|
|
1093
|
+
]);
|
|
1094
|
+
if (!choice) return;
|
|
1095
|
+
|
|
1096
|
+
if (choice.startsWith("Max concurrency")) {
|
|
1097
|
+
const val = await ctx.ui.input("Max concurrent background agents", String(manager.getMaxConcurrent()));
|
|
1098
|
+
if (val) {
|
|
1099
|
+
const n = parseInt(val, 10);
|
|
1100
|
+
if (n >= 1) {
|
|
1101
|
+
manager.setMaxConcurrent(n);
|
|
1102
|
+
ctx.ui.notify(`Max concurrency set to ${n}`, "info");
|
|
1103
|
+
} else {
|
|
1104
|
+
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
845
1105
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1106
|
+
}
|
|
1107
|
+
} else if (choice.startsWith("Default max turns")) {
|
|
1108
|
+
const val = await ctx.ui.input("Default max turns before wrap-up", String(getDefaultMaxTurns()));
|
|
1109
|
+
if (val) {
|
|
1110
|
+
const n = parseInt(val, 10);
|
|
1111
|
+
if (n >= 1) {
|
|
1112
|
+
setDefaultMaxTurns(n);
|
|
1113
|
+
ctx.ui.notify(`Default max turns set to ${n}`, "info");
|
|
1114
|
+
} else {
|
|
1115
|
+
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
849
1116
|
}
|
|
850
1117
|
}
|
|
1118
|
+
} else if (choice.startsWith("Grace turns")) {
|
|
1119
|
+
const val = await ctx.ui.input("Grace turns after wrap-up steer", String(getGraceTurns()));
|
|
1120
|
+
if (val) {
|
|
1121
|
+
const n = parseInt(val, 10);
|
|
1122
|
+
if (n >= 1) {
|
|
1123
|
+
setGraceTurns(n);
|
|
1124
|
+
ctx.ui.notify(`Grace turns set to ${n}`, "info");
|
|
1125
|
+
} else {
|
|
1126
|
+
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
851
1131
|
|
|
852
|
-
|
|
853
|
-
|
|
1132
|
+
pi.registerCommand("agents", {
|
|
1133
|
+
description: "Manage agents",
|
|
1134
|
+
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
|
854
1135
|
});
|
|
855
1136
|
}
|