@yandy0725/pi-subagents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/README.zh.md +155 -0
- package/index.ts +1 -0
- package/package.json +49 -0
- package/src/config/agent-types.ts +127 -0
- package/src/config/custom-agents.ts +109 -0
- package/src/config/default-agents.ts +117 -0
- package/src/config/invocation-config.ts +30 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/handlers/lifecycle.ts +63 -0
- package/src/handlers/tool-start.ts +32 -0
- package/src/index.ts +186 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +88 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +240 -0
- package/src/lifecycle/parent-snapshot.ts +45 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/subagent-manager.ts +353 -0
- package/src/lifecycle/subagent-session.ts +232 -0
- package/src/lifecycle/subagent-state.ts +216 -0
- package/src/lifecycle/subagent.ts +498 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +65 -0
- package/src/lifecycle/workspace-bracket.ts +59 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification-state.ts +27 -0
- package/src/observation/notification.ts +186 -0
- package/src/observation/record-observer.ts +75 -0
- package/src/observation/renderer.ts +63 -0
- package/src/observation/subagent-events-observer.ts +94 -0
- package/src/runtime.ts +77 -0
- package/src/service/service-adapter.ts +131 -0
- package/src/service/service.ts +123 -0
- package/src/session/content-items.ts +51 -0
- package/src/session/context.ts +78 -0
- package/src/session/conversation.ts +44 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +121 -0
- package/src/session/prompts.ts +83 -0
- package/src/session/session-config.ts +172 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +227 -0
- package/src/tools/agent-tool.ts +220 -0
- package/src/tools/background-spawner.ts +66 -0
- package/src/tools/foreground-runner.ts +114 -0
- package/src/tools/get-result-tool.ts +120 -0
- package/src/tools/helpers.ts +105 -0
- package/src/tools/result-renderer.ts +109 -0
- package/src/tools/spawn-config.ts +150 -0
- package/src/tools/steer-tool.ts +90 -0
- package/src/types.ts +115 -0
- package/src/ui/agent-widget.ts +311 -0
- package/src/ui/display.ts +174 -0
- package/src/ui/session-navigation.ts +147 -0
- package/src/ui/session-navigator.ts +406 -0
- package/src/ui/subagents-settings.ts +77 -0
- package/src/ui/widget-renderer.ts +296 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# pi-subagents
|
|
2
|
+
|
|
3
|
+
A [pi](https://pi.dev) extension providing a focused, in-process sub-agent core — autonomous agents that run inside the same pi runtime (no spawned subprocesses), plus a typed API and lifecycle events other extensions build on.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **In-process & native** — agents share the same pi runtime: same tool names, calling conventions, and UI patterns
|
|
8
|
+
- **Parallel background agents** — spawn multiple agents with automatic queuing (configurable concurrency, default 4)
|
|
9
|
+
- **Live widget UI** — persistent widget showing animated spinners, live tool activity, token counts, and colored status icons
|
|
10
|
+
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: system prompts, model, thinking, tools
|
|
11
|
+
- **Mid-run steering** — inject messages into running agents to redirect work without restarting
|
|
12
|
+
- **Session resume** — pick up where an agent left off, preserving full conversation context
|
|
13
|
+
- **Graceful turn limits** — agents get a "wrap up" warning before hard abort
|
|
14
|
+
- **Case-insensitive types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work
|
|
15
|
+
- **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs
|
|
16
|
+
- **Context inheritance** — optionally fork the parent conversation into a sub-agent
|
|
17
|
+
- **Styled notifications** — background results render as themed notification boxes
|
|
18
|
+
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) via `pi.events`
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install npm:@yandy0725/pi-subagents
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
The parent agent spawns sub-agents using the `subagent` tool:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
subagent({
|
|
32
|
+
subagent_type: "Explore",
|
|
33
|
+
prompt: "Find all files that handle authentication",
|
|
34
|
+
description: "Find auth files",
|
|
35
|
+
run_in_background: true,
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Foreground agents block until complete. Background agents return an ID immediately and notify on completion.
|
|
40
|
+
|
|
41
|
+
## Default Agent Types
|
|
42
|
+
|
|
43
|
+
| Type | Tools | Model | Description |
|
|
44
|
+
|------|-------|-------|-------------|
|
|
45
|
+
| `general-purpose` | all | inherit | Full parent system prompt — same rules, same conventions |
|
|
46
|
+
| `Explore` | read, bash, grep, find, ls | haiku (fallback: inherit) | Fast codebase exploration (read-only) |
|
|
47
|
+
| `Plan` | read, bash, grep, find, ls | inherit | Software architect for implementation planning (read-only) |
|
|
48
|
+
|
|
49
|
+
## Custom Agents
|
|
50
|
+
|
|
51
|
+
Define custom agent types by creating `.md` files in `.pi/agents/<name>.md`:
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
---
|
|
55
|
+
description: Security Code Reviewer
|
|
56
|
+
tools: read, grep, find, bash
|
|
57
|
+
model: anthropic/claude-opus-4-6
|
|
58
|
+
thinking: high
|
|
59
|
+
max_turns: 30
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
You are a security auditor. Review code for vulnerabilities...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Agents are discovered from `.pi/agents/<name>.md` (project) and `~/.pi/agent/agents/<name>.md` (global). Project-level overrides global.
|
|
66
|
+
|
|
67
|
+
## Tools
|
|
68
|
+
|
|
69
|
+
### `subagent`
|
|
70
|
+
|
|
71
|
+
Launch a sub-agent.
|
|
72
|
+
|
|
73
|
+
| Parameter | Type | Required | Description |
|
|
74
|
+
|-----------|------|----------|-------------|
|
|
75
|
+
| `prompt` | string | yes | The task for the agent |
|
|
76
|
+
| `description` | string | yes | Short 3-5 word summary (shown in UI) |
|
|
77
|
+
| `subagent_type` | string | yes | Agent type (built-in or custom) |
|
|
78
|
+
| `model` | string | no | Model override (`provider/modelId` or fuzzy name) |
|
|
79
|
+
| `thinking` | string | no | off / minimal / low / medium / high / xhigh |
|
|
80
|
+
| `max_turns` | number | no | Max agentic turns (unlimited by default) |
|
|
81
|
+
| `run_in_background` | boolean | no | Run without blocking |
|
|
82
|
+
| `resume` | string | no | Agent ID to resume a previous session |
|
|
83
|
+
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
84
|
+
|
|
85
|
+
### `get_subagent_result`
|
|
86
|
+
|
|
87
|
+
Check status and retrieve results from a background agent.
|
|
88
|
+
|
|
89
|
+
| Parameter | Type | Required | Description |
|
|
90
|
+
|-----------|------|----------|-------------|
|
|
91
|
+
| `agent_id` | string | yes | Agent ID to check |
|
|
92
|
+
| `wait` | boolean | no | Wait for completion |
|
|
93
|
+
| `verbose` | boolean | no | Include full conversation log |
|
|
94
|
+
|
|
95
|
+
### `steer_subagent`
|
|
96
|
+
|
|
97
|
+
Send a steering message to a running agent.
|
|
98
|
+
|
|
99
|
+
| Parameter | Type | Required | Description |
|
|
100
|
+
|-----------|------|----------|-------------|
|
|
101
|
+
| `agent_id` | string | yes | Agent ID to steer |
|
|
102
|
+
| `message` | string | yes | Message to inject into agent conversation |
|
|
103
|
+
|
|
104
|
+
## Commands
|
|
105
|
+
|
|
106
|
+
| Command | Description |
|
|
107
|
+
|---------|-------------|
|
|
108
|
+
| `/subagents:settings` | Configure concurrency, turn limits |
|
|
109
|
+
| `/subagents:sessions` | View a subagent's session transcript |
|
|
110
|
+
|
|
111
|
+
## Concurrency
|
|
112
|
+
|
|
113
|
+
Background agents are subject to a configurable concurrency limit (default: 4). Excess agents queue automatically. Foreground agents bypass the queue.
|
|
114
|
+
|
|
115
|
+
## Persistent Settings
|
|
116
|
+
|
|
117
|
+
Settings set via `/subagents:settings` persist in `<cwd>/.pi/subagents.json` (project) and can be overridden globally in `~/.pi/agent/subagents.json`.
|
|
118
|
+
|
|
119
|
+
## Events
|
|
120
|
+
|
|
121
|
+
Lifecycle events emitted via `pi.events` for other extensions to consume:
|
|
122
|
+
|
|
123
|
+
| Event | When |
|
|
124
|
+
|-------|------|
|
|
125
|
+
| `subagents:created` | Background agent registered |
|
|
126
|
+
| `subagents:started` | Agent transitions to running |
|
|
127
|
+
| `subagents:completed` | Agent finished successfully |
|
|
128
|
+
| `subagents:failed` | Agent errored, stopped, or aborted |
|
|
129
|
+
| `subagents:steered` | Steering message sent |
|
|
130
|
+
| `subagents:compacted` | Agent session compacted |
|
|
131
|
+
| `subagents:settings_loaded` | Persisted settings applied |
|
|
132
|
+
| `subagents:settings_changed` | Settings mutation applied |
|
|
133
|
+
|
|
134
|
+
## Permission System Integration
|
|
135
|
+
|
|
136
|
+
When `@yandy0725/pi-permission-system` is installed, the package integrates automatically:
|
|
137
|
+
- Per-agent permission policies via YAML frontmatter
|
|
138
|
+
- Tool filtering before agent start
|
|
139
|
+
- `ask`-state forwarding from child to parent UI
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm run typecheck # tsc --noEmit
|
|
145
|
+
npm run check # biome check
|
|
146
|
+
npm test # vitest run
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Acknowledgments
|
|
150
|
+
|
|
151
|
+
This project is a friendly fork of [@gotgenes/pi-subagents](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-subagents) by [Chris Lasher](https://github.com/gotgenes), which began as a fork of [tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents). Thank you to all original authors for their work that made this package possible.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# pi-subagents
|
|
2
|
+
|
|
3
|
+
[Pi](https://pi.dev) 扩展,提供聚焦的进程内子代理核心——在同一 pi 运行时内运行的自主代理(无需生成子进程),外加类型化 API 和生命周期事件供其他扩展构建。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **进程内原生** — 代理共享同一 pi 运行时:相同的工具名、调用约定和 UI 模式
|
|
8
|
+
- **并行后台代理** — 同时启动多个代理,自动排队(可配置并发数,默认 4)
|
|
9
|
+
- **实时 Widget UI** — 持久化的编辑器上方组件,带动态旋转指示器、实时工具活动、token 计数和彩色状态图标
|
|
10
|
+
- **自定义代理类型** — 在 `.pi/agents/<name>.md` 中通过 YAML frontmatter 定义:系统提示词、模型、思考等级、工具限制
|
|
11
|
+
- **运行中转向** — 向运行中的代理注入消息,无需重启即可改变工作方向
|
|
12
|
+
- **会话恢复** — 从上次中断处继续,保留完整对话上下文
|
|
13
|
+
- **优雅轮次限制** — 代理在硬中止前收到"收尾"警告
|
|
14
|
+
- **大小写不敏感** — `"explore"`、`"Explore"`、`"EXPLORE"` 均可
|
|
15
|
+
- **模糊模型选择** — 按名称(`"haiku"`、`"sonnet"`)而非完整 ID 指定模型
|
|
16
|
+
- **上下文继承** — 可选择将父对话分叉给子代理
|
|
17
|
+
- **风格化通知** — 后台结果渲染为主题化通知框
|
|
18
|
+
- **事件总线** — 通过 `pi.events` 发出生命周期事件(`subagents:created`、`started`、`completed`、`failed`、`steered`、`compacted`)
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install npm:@yandy0725/pi-subagents
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
父代理使用 `subagent` 工具启动子代理:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
subagent({
|
|
32
|
+
subagent_type: "Explore",
|
|
33
|
+
prompt: "Find all files that handle authentication",
|
|
34
|
+
description: "Find auth files",
|
|
35
|
+
run_in_background: true,
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
前台代理阻塞直到完成。后台代理立即返回 ID,完成后通知。
|
|
40
|
+
|
|
41
|
+
## 默认代理类型
|
|
42
|
+
|
|
43
|
+
| 类型 | 工具 | 模型 | 描述 |
|
|
44
|
+
|------|------|------|------|
|
|
45
|
+
| `general-purpose` | 全部 | 继承父级 | 完整父级系统提示词——相同规则、相同约定 |
|
|
46
|
+
| `Explore` | read, bash, grep, find, ls | haiku(回退:继承) | 快速代码库探索(只读) |
|
|
47
|
+
| `Plan` | read, bash, grep, find, ls | 继承父级 | 软件架构师,实现方案设计(只读) |
|
|
48
|
+
|
|
49
|
+
## 自定义代理
|
|
50
|
+
|
|
51
|
+
在 `.pi/agents/<name>.md` 中创建 `.md` 文件定义自定义代理类型:
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
---
|
|
55
|
+
description: 安全代码审查
|
|
56
|
+
tools: read, grep, find, bash
|
|
57
|
+
model: anthropic/claude-opus-4-6
|
|
58
|
+
thinking: high
|
|
59
|
+
max_turns: 30
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
你是一名安全审计员。审查代码漏洞...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
代理从 `.pi/agents/<name>.md`(项目级)和 `~/.pi/agent/agents/<name>.md`(全局)发现。项目级覆盖全局。
|
|
66
|
+
|
|
67
|
+
## 工具
|
|
68
|
+
|
|
69
|
+
### `subagent`
|
|
70
|
+
|
|
71
|
+
启动子代理。
|
|
72
|
+
|
|
73
|
+
| 参数 | 类型 | 必填 | 描述 |
|
|
74
|
+
|------|------|------|------|
|
|
75
|
+
| `prompt` | string | 是 | 代理要执行的任务 |
|
|
76
|
+
| `description` | string | 是 | 3-5 词简短描述(UI 中显示) |
|
|
77
|
+
| `subagent_type` | string | 是 | 代理类型(内置或自定义) |
|
|
78
|
+
| `model` | string | 否 | 模型覆盖(`provider/modelId` 或模糊名称) |
|
|
79
|
+
| `thinking` | string | 否 | off / minimal / low / medium / high / xhigh |
|
|
80
|
+
| `max_turns` | number | 否 | 最大代理轮次(默认无限制) |
|
|
81
|
+
| `run_in_background` | boolean | 否 | 后台运行不阻塞 |
|
|
82
|
+
| `resume` | string | 否 | 恢复之前会话的代理 ID |
|
|
83
|
+
| `inherit_context` | boolean | 否 | 将父对话分叉给子代理 |
|
|
84
|
+
|
|
85
|
+
### `get_subagent_result`
|
|
86
|
+
|
|
87
|
+
检查后台代理的状态并获取结果。
|
|
88
|
+
|
|
89
|
+
| 参数 | 类型 | 必填 | 描述 |
|
|
90
|
+
|------|------|------|------|
|
|
91
|
+
| `agent_id` | string | 是 | 要检查的代理 ID |
|
|
92
|
+
| `wait` | boolean | 否 | 等待完成 |
|
|
93
|
+
| `verbose` | boolean | 否 | 包含完整对话日志 |
|
|
94
|
+
|
|
95
|
+
### `steer_subagent`
|
|
96
|
+
|
|
97
|
+
向运行中的代理发送转向消息。
|
|
98
|
+
|
|
99
|
+
| 参数 | 类型 | 必填 | 描述 |
|
|
100
|
+
|------|------|------|------|
|
|
101
|
+
| `agent_id` | string | 是 | 要转向的代理 ID |
|
|
102
|
+
| `message` | string | 是 | 注入代理对话的消息 |
|
|
103
|
+
|
|
104
|
+
## 命令
|
|
105
|
+
|
|
106
|
+
| 命令 | 描述 |
|
|
107
|
+
|------|------|
|
|
108
|
+
| `/subagents:settings` | 配置并发数、轮次限制 |
|
|
109
|
+
| `/subagents:sessions` | 查看子代理的会话记录 |
|
|
110
|
+
|
|
111
|
+
## 并发控制
|
|
112
|
+
|
|
113
|
+
后台代理受可配置并发限制约束(默认 4)。超出限制的代理自动排队。前台代理绕过队列。
|
|
114
|
+
|
|
115
|
+
## 持久化设置
|
|
116
|
+
|
|
117
|
+
通过 `/subagents:settings` 设置的参数持久化到 `<cwd>/.pi/subagents.json`(项目级),也可在 `~/.pi/agent/subagents.json` 中设置全局默认值。
|
|
118
|
+
|
|
119
|
+
## 事件
|
|
120
|
+
|
|
121
|
+
通过 `pi.events` 发出的生命周期事件,供其他扩展消费:
|
|
122
|
+
|
|
123
|
+
| 事件 | 触发时机 |
|
|
124
|
+
|------|----------|
|
|
125
|
+
| `subagents:created` | 后台代理已注册 |
|
|
126
|
+
| `subagents:started` | 代理转入运行状态 |
|
|
127
|
+
| `subagents:completed` | 代理成功完成 |
|
|
128
|
+
| `subagents:failed` | 代理出错/停止/中止 |
|
|
129
|
+
| `subagents:steered` | 转向消息已发送 |
|
|
130
|
+
| `subagents:compacted` | 代理会话已压缩 |
|
|
131
|
+
| `subagents:settings_loaded` | 持久化设置已应用 |
|
|
132
|
+
| `subagents:settings_changed` | 设置变更已生效 |
|
|
133
|
+
|
|
134
|
+
## 权限系统集成
|
|
135
|
+
|
|
136
|
+
当安装了 `@yandy0725/pi-permission-system` 时,本包自动集成:
|
|
137
|
+
- 通过 YAML frontmatter 为各代理类型设置权限策略
|
|
138
|
+
- Agent 启动前过滤工具
|
|
139
|
+
- `ask` 状态从子代理转发到父级 UI
|
|
140
|
+
|
|
141
|
+
## 开发
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm run typecheck # tsc --noEmit
|
|
145
|
+
npm run check # biome check
|
|
146
|
+
npm test # vitest run
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 致谢
|
|
150
|
+
|
|
151
|
+
本项目是 [@gotgenes/pi-subagents](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-subagents)(作者 [Chris Lasher](https://github.com/gotgenes))的友好分支,后者源自 [tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents)。感谢所有原作者的工作使本包成为可能。
|
|
152
|
+
|
|
153
|
+
## 许可证
|
|
154
|
+
|
|
155
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yandy0725/pi-subagents",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Focused, in-process sub-agent core for pi — autonomous agents plus a typed API and lifecycle events",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/yandy/pi-packages",
|
|
12
|
+
"directory": "pi-subagents"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"index.ts",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "biome lint .",
|
|
27
|
+
"format": "biome format --write .",
|
|
28
|
+
"check": "biome check ."
|
|
29
|
+
},
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./index.ts"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-ai": ">=0.74.0",
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
38
|
+
"@earendil-works/pi-tui": ">=0.74.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@sinclair/typebox": "^0.34.49"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "^2.5.0",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"typescript": "~5.7.0",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-types.ts — Unified agent type registry.
|
|
3
|
+
*
|
|
4
|
+
* Merges embedded default agents with user-defined agents from .pi/agents/*.md.
|
|
5
|
+
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_AGENTS } from "../config/default-agents";
|
|
9
|
+
import type { AgentConfig } from "../types";
|
|
10
|
+
|
|
11
|
+
// ── AgentConfigLookup interface ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Narrow registry interface for consumers that only need config resolution.
|
|
15
|
+
* Prefer this over the full `AgentTypeRegistry` in function signatures (ISP).
|
|
16
|
+
*/
|
|
17
|
+
export interface AgentConfigLookup {
|
|
18
|
+
resolveAgentConfig(type: string): AgentConfig;
|
|
19
|
+
getToolNamesForType(type: string): string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── AgentTypeRegistry class ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Injectable registry of all agent configurations (defaults + user-defined).
|
|
26
|
+
*
|
|
27
|
+
* Replaces the module-scoped `agents` Map and its companion free functions.
|
|
28
|
+
* The constructor accepts a `loadUserAgents` callback to defer disk I/O to the
|
|
29
|
+
* call site, keeping this class side-effect-free and easy to test.
|
|
30
|
+
*/
|
|
31
|
+
export class AgentTypeRegistry implements AgentConfigLookup {
|
|
32
|
+
private agents = new Map<string, AgentConfig>();
|
|
33
|
+
|
|
34
|
+
/** The three embedded default agent names. */
|
|
35
|
+
static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
36
|
+
|
|
37
|
+
constructor(private loadUserAgents: () => Map<string, AgentConfig>) {
|
|
38
|
+
this.reload();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Re-scan user agents and rebuild the registry.
|
|
43
|
+
* Starts with DEFAULT_AGENTS, then overlays whatever `loadUserAgents()` returns.
|
|
44
|
+
*/
|
|
45
|
+
reload(): void {
|
|
46
|
+
this.agents.clear();
|
|
47
|
+
for (const [name, config] of DEFAULT_AGENTS) {
|
|
48
|
+
this.agents.set(name, config);
|
|
49
|
+
}
|
|
50
|
+
for (const [name, config] of this.loadUserAgents()) {
|
|
51
|
+
this.agents.set(name, config);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
|
|
56
|
+
resolveType(name: string): string | undefined {
|
|
57
|
+
return this.resolveKey(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Get all enabled type names (for spawning and tool descriptions). */
|
|
61
|
+
getAvailableTypes(): string[] {
|
|
62
|
+
return [...this.agents.entries()].filter(([_, config]) => config.enabled !== false).map(([name]) => name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Get all type names including disabled (for UI listing). */
|
|
66
|
+
getAllTypes(): string[] {
|
|
67
|
+
return [...this.agents.keys()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Get names of default agents currently in the registry. */
|
|
71
|
+
getDefaultAgentNames(): string[] {
|
|
72
|
+
return [...this.agents.entries()].filter(([_, config]) => config.isDefault === true).map(([name]) => name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get names of user-defined agents (non-defaults) currently in the registry. */
|
|
76
|
+
getUserAgentNames(): string[] {
|
|
77
|
+
return [...this.agents.entries()].filter(([_, config]) => config.isDefault !== true).map(([name]) => name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Check if a type is valid and enabled (case-insensitive). */
|
|
81
|
+
isValidType(type: string): boolean {
|
|
82
|
+
const key = this.resolveKey(type);
|
|
83
|
+
if (!key) return false;
|
|
84
|
+
return this.agents.get(key)?.enabled !== false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
88
|
+
getToolNamesForType(type: string): string[] {
|
|
89
|
+
const key = this.resolveKey(type);
|
|
90
|
+
const raw = key ? this.agents.get(key) : undefined;
|
|
91
|
+
const config = raw?.enabled !== false ? raw : undefined;
|
|
92
|
+
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
97
|
+
resolveAgentConfig(type: string): AgentConfig {
|
|
98
|
+
const key = this.resolveKey(type);
|
|
99
|
+
const config = key ? this.agents.get(key) : undefined;
|
|
100
|
+
if (config) return config;
|
|
101
|
+
|
|
102
|
+
const gp = this.agents.get("general-purpose");
|
|
103
|
+
if (gp) return gp;
|
|
104
|
+
|
|
105
|
+
// Absolute fallback (should never happen in practice)
|
|
106
|
+
return {
|
|
107
|
+
name: type,
|
|
108
|
+
displayName: "Agent",
|
|
109
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
110
|
+
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
111
|
+
systemPrompt: "",
|
|
112
|
+
promptMode: "append",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private resolveKey(name: string): string | undefined {
|
|
117
|
+
if (this.agents.has(name)) return name;
|
|
118
|
+
const lower = name.toLowerCase();
|
|
119
|
+
for (const key of this.agents.keys()) {
|
|
120
|
+
if (key.toLowerCase() === lower) return key;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** All known built-in tool names. */
|
|
127
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { BUILTIN_TOOL_NAMES } from "../config/agent-types";
|
|
9
|
+
import { debugLog } from "../debug";
|
|
10
|
+
import type { AgentConfig, ThinkingLevel } from "../types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
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_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
17
|
+
*
|
|
18
|
+
* Project-level agents override global ones with the same name.
|
|
19
|
+
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
20
|
+
*/
|
|
21
|
+
export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
22
|
+
const globalDir = join(getAgentDir(), "agents");
|
|
23
|
+
const projectDir = join(cwd, ".pi", "agents");
|
|
24
|
+
|
|
25
|
+
const agents = new Map<string, AgentConfig>();
|
|
26
|
+
loadFromDir(globalDir, agents, "global"); // lower priority
|
|
27
|
+
loadFromDir(projectDir, agents, "project"); // higher priority (overwrites)
|
|
28
|
+
return agents;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Load agent configs from a directory into the map. */
|
|
32
|
+
function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "project" | "global"): void {
|
|
33
|
+
if (!existsSync(dir)) return;
|
|
34
|
+
|
|
35
|
+
let files: string[];
|
|
36
|
+
try {
|
|
37
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
debugLog("readdirSync agents dir", err);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const name = basename(file, ".md");
|
|
45
|
+
|
|
46
|
+
let content: string;
|
|
47
|
+
try {
|
|
48
|
+
content = readFileSync(join(dir, file), "utf-8");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
debugLog("readFileSync agent file", err);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { frontmatter: fm, body } = parseFrontmatter(content);
|
|
55
|
+
|
|
56
|
+
agents.set(name, {
|
|
57
|
+
name,
|
|
58
|
+
displayName: str(fm.display_name),
|
|
59
|
+
description: str(fm.description) ?? name,
|
|
60
|
+
builtinToolNames: csvList(fm.tools, BUILTIN_TOOL_NAMES),
|
|
61
|
+
model: str(fm.model),
|
|
62
|
+
thinking: str(fm.thinking) as ThinkingLevel | undefined,
|
|
63
|
+
maxTurns: nonNegativeInt(fm.max_turns),
|
|
64
|
+
systemPrompt: body.trim(),
|
|
65
|
+
promptMode: fm.prompt_mode === "replace" ? "replace" : "append",
|
|
66
|
+
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
67
|
+
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
68
|
+
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
69
|
+
source,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- Field parsers ----
|
|
75
|
+
// All follow the same convention: omitted → default, "none"/empty → nothing, value → exact.
|
|
76
|
+
|
|
77
|
+
/** Extract a string or undefined. */
|
|
78
|
+
function str(val: unknown): string | undefined {
|
|
79
|
+
return typeof val === "string" ? val : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract a non-negative integer or undefined. 0 means unlimited for max_turns. */
|
|
83
|
+
function nonNegativeInt(val: unknown): number | undefined {
|
|
84
|
+
return typeof val === "number" && val >= 0 ? val : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse a raw CSV field value into items, or undefined if absent/empty/"none".
|
|
89
|
+
*/
|
|
90
|
+
function parseCsvField(val: unknown): string[] | undefined {
|
|
91
|
+
if (val === undefined || val === null) return undefined;
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- val is already narrowed past null/undefined; String() is the intended coercion here
|
|
93
|
+
const s = String(val).trim();
|
|
94
|
+
if (!s || s === "none") return undefined;
|
|
95
|
+
const items = s
|
|
96
|
+
.split(",")
|
|
97
|
+
.map((t) => t.trim())
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
return items.length > 0 ? items : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse a comma-separated list field with defaults.
|
|
104
|
+
* omitted → defaults; "none"/empty → []; csv → listed items.
|
|
105
|
+
*/
|
|
106
|
+
function csvList(val: unknown, defaults: string[]): string[] {
|
|
107
|
+
if (val === undefined || val === null) return defaults;
|
|
108
|
+
return parseCsvField(val) ?? [];
|
|
109
|
+
}
|