pi-herdr-subagents-lite 0.4.0 → 0.5.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 +29 -64
- package/README.zh-CN.md +33 -66
- package/package.json +1 -1
- package/src/config/index.ts +2 -0
- package/src/config/schema.ts +1 -0
- package/src/error/index.ts +3 -0
- package/src/extension/index.ts +3 -4
- package/src/herdr/agent.ts +10 -10
- package/src/herdr/index.ts +2 -3
- package/src/herdr/pane.ts +11 -7
- package/src/inbox/index.ts +4 -11
- package/src/profile/index.ts +3 -18
- package/src/profile/layers.ts +4 -16
- package/src/profile/seed.ts +18 -0
- package/src/profile/skills.ts +25 -0
- package/src/run/aggregate.ts +9 -4
- package/src/run/artifact.ts +11 -3
- package/src/run/close.ts +29 -18
- package/src/run/errors.ts +8 -4
- package/src/run/find.ts +8 -1
- package/src/run/index.ts +12 -16
- package/src/run/pane.ts +16 -0
- package/src/run/paths.ts +5 -0
- package/src/run/poll-result.ts +84 -0
- package/src/run/poll.ts +82 -94
- package/src/run/resolve.ts +13 -1
- package/src/run/result.ts +5 -8
- package/src/run/send.ts +46 -22
- package/src/run/slot.ts +9 -0
- package/src/run/spawn-args.ts +25 -10
- package/src/run/start.ts +40 -35
- package/src/run/state.ts +22 -5
- package/src/session-file/index.ts +13 -5
- package/src/session-file/jsonl.ts +8 -2
- package/src/session-file/receipt.ts +18 -4
- package/src/session-file/tail.ts +33 -0
package/README.md
CHANGED
|
@@ -4,19 +4,19 @@ English | [简体中文](README.zh-CN.md)
|
|
|
4
4
|
|
|
5
5
|
A pi extension that runs subagents as separate pi processes in herdr panes. One tool, two actions: `run` starts a subagent, `send` continues it, results come back asynchronously.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Behavior is defined by the [v1 contract](docs/specs/0001-v1-contract.md). Project docs are in Chinese.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Separate process, human takeover**: each subagent is its own pi process in a herdr pane. Same cwd as the parent opens a tab in the same workspace, otherwise a new workspace. Switch into the pane and take over at any time.
|
|
12
|
-
- **Profile layers**: Markdown + frontmatter
|
|
13
|
-
- **Async, merged results**: `run` returns at once. Each finished
|
|
12
|
+
- **Profile layers**: Markdown + frontmatter profiles, global and per project.
|
|
13
|
+
- **Async, merged results**: `run` returns at once. Each finished turn reports one `subagent_result` message; results inside `aggregateWindowMs` merge into one. Oversize results go to an artifact file.
|
|
14
14
|
- **Questions stay with the human**: a child prompt marks its pane `herdr:blocked` and nothing goes to the parent. The human answers in the child pane; no model relays the answer.
|
|
15
15
|
- **No nesting by default, no sandbox**: child tool sets drop this tool unless the profile names it. The profile picks tools; the extension does not restrict writes.
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
|
-
herdr `0.9.5
|
|
19
|
+
Tested with herdr `0.9.5` and pi `1.4.0`. Node `>= 22.19`. The parent must run inside a herdr pane, otherwise calls fail with `not_in_herdr`.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ Upgrade with `pi update --extensions`, remove with `pi remove <source>`. After a
|
|
|
36
36
|
## Quick start
|
|
37
37
|
|
|
38
38
|
1. Start `pi` in a herdr pane.
|
|
39
|
-
2. Write a profile (next section);
|
|
39
|
+
2. Write a profile (next section); `run` takes its frontmatter `name`.
|
|
40
40
|
3. Ask the model to call the tool:
|
|
41
41
|
|
|
42
42
|
```json
|
|
@@ -55,7 +55,7 @@ Global profiles live in `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`, pro
|
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
57
|
---
|
|
58
|
-
# Key run takes;
|
|
58
|
+
# Key run takes; the file name is not used
|
|
59
59
|
name: explore
|
|
60
60
|
description: "Read-only research: read code, answer questions, change nothing"
|
|
61
61
|
|
|
@@ -70,7 +70,7 @@ tools: read, bash
|
|
|
70
70
|
# Omitted = pi discovery; "" = no skills; a list = only those
|
|
71
71
|
skills: ""
|
|
72
72
|
|
|
73
|
-
# append
|
|
73
|
+
# append / replace: body is appended to or replaces the system prompt
|
|
74
74
|
# task: body is the task text, no identity file
|
|
75
75
|
system-prompt: append
|
|
76
76
|
|
|
@@ -84,18 +84,20 @@ disable-model-invocation: false
|
|
|
84
84
|
You are a read-only explorer. Answer with file paths and line numbers.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Error codes: [contract section 2](docs/specs/0001-v1-contract.md#2-profile). Defaults: `src/profile/frontmatter.ts` and `test/profile/`. A file without `name`, or with an invalid field, stays out of the list; duplicate names in one layer are all rejected. `run` returns `profile_invalid` with the file, field and reason.
|
|
88
88
|
|
|
89
89
|
A bundled example ships with the extension; its file name and `name` are both `domain`. Loading reads `profiles/*.md` only and the example is written only when missing, so copy it one level up:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
cd
|
|
92
|
+
cd <agent-dir>/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
## Configuration
|
|
96
96
|
|
|
97
97
|
One global file: `<agent-dir>/pi-herdr-subagents-lite/config.json`. `<agent-dir>` defaults to `~/.pi/agent` and follows `PI_CODING_AGENT_DIR`. Project config is not read.
|
|
98
98
|
|
|
99
|
+
Defaults and clamp boundaries: `src/config/index.ts`, `test/config/config.test.ts`. Key meaning: [contract §1](docs/specs/0001-v1-contract.md#1-配置).
|
|
100
|
+
|
|
99
101
|
### Minimal config
|
|
100
102
|
|
|
101
103
|
A missing file means all defaults. To change one value, write only that value:
|
|
@@ -106,58 +108,24 @@ A missing file means all defaults. To change one value, write only that value:
|
|
|
106
108
|
}
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
Every key with its default. Strict JSON: an unknown key, a wrong type or a bad enum makes the extension fail to load and pi exits naming the field. The `//` lines are comments; delete them before use.
|
|
112
|
-
|
|
113
|
-
```jsonc
|
|
114
|
-
{
|
|
115
|
-
// Gates tool registration only; the bridge has its own switch
|
|
116
|
-
"enabled": true,
|
|
117
|
-
|
|
118
|
-
// Name the model sees; ^[a-z][a-z0-9_]{0,31}$
|
|
119
|
-
"toolName": "subagent",
|
|
120
|
-
|
|
121
|
-
// Resolved from PATH; set an absolute path to pin a version
|
|
122
|
-
"herdrBin": "herdr",
|
|
123
|
-
|
|
124
|
-
// Live children per parent session; auto-exit false keeps its slot after settling
|
|
125
|
-
"maxConcurrent": 16,
|
|
126
|
-
|
|
127
|
-
// Per result (1024-1048576); beyond it the full text goes to an artifact and the message keeps a truncated head plus the path
|
|
128
|
-
"maxResultChars": 16384,
|
|
129
|
-
|
|
130
|
-
// Ms (0-10000); results inside the window merge into one message, 0 = deliver each immediately
|
|
131
|
-
"aggregateWindowMs": 500,
|
|
111
|
+
### Keys
|
|
132
112
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
// Turns pi prompt events into herdr:blocked, for native notifications
|
|
152
|
-
"bridge": {
|
|
153
|
-
"enabled": true,
|
|
154
|
-
// Which prompt kinds count as blocked
|
|
155
|
-
"blockedOn": ["custom", "select", "confirm", "input", "editor"]
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Out-of-range numbers are clamped to the boundary, not rejected. The config is read once, so restart pi after editing.
|
|
113
|
+
| Key | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `enabled` | Gates tool registration only; the bridge has its own switch |
|
|
116
|
+
| `toolName` | Name the model sees; `^[a-z][a-z0-9_]{0,31}$` |
|
|
117
|
+
| `herdrBin` | herdr executable; a plain name resolves from PATH, an absolute path pins a version |
|
|
118
|
+
| `maxConcurrent` | Live children per parent session; an `auto-exit: false` child keeps its slot after settling |
|
|
119
|
+
| `maxResultChars` | Per-result character limit; beyond it the full text goes to an artifact and the message keeps a truncated head plus the path |
|
|
120
|
+
| `aggregateWindowMs` | Merge window in milliseconds; 0 = deliver each result at once |
|
|
121
|
+
| `closeChildrenOn` | Parent shutdown reasons that close its child tabs: `new` / `resume` / `fork` / `quit` / `reload`; empty = never |
|
|
122
|
+
| `inboxPollMs` | Child inbox polling interval |
|
|
123
|
+
| `pollIntervalMs` | Parent `agent list` interval while a child is working or blocked, or within 30 s of `run` / `send` |
|
|
124
|
+
| `idlePollIntervalMs` | Parent interval when every live child only waits for `send`; it bounds how late a vanished pane is noticed (a killed child whose pane lives is fixed at 10 s) |
|
|
125
|
+
| `prompts` | Tool text overrides: `description`, `snippet`, `guidelines`; omitted keys keep the built-in text; `snippet: ""` hides the tool from Available tools; keep `{{profiles}}` to list profiles |
|
|
126
|
+
| `bridge` | `enabled` turns pi prompt events into `herdr:blocked`; `blockedOn` lists the prompt kinds that count as blocked |
|
|
127
|
+
|
|
128
|
+
Strict JSON: an unknown key, a wrong type or a bad enum makes the extension fail to load, and pi exits naming the field. Out-of-range numbers clamp to the boundary. The config is read once, so restart pi after editing.
|
|
161
129
|
|
|
162
130
|
## Reference projects
|
|
163
131
|
|
|
@@ -172,18 +140,15 @@ The design is based on these projects. Thank you.
|
|
|
172
140
|
|
|
173
141
|
## Docs
|
|
174
142
|
|
|
175
|
-
All docs are in Chinese.
|
|
176
|
-
|
|
177
143
|
- [ARCHITECTURE](docs/ARCHITECTURE.md): module boundaries, dependency direction, test seams
|
|
178
144
|
- [GLOSSARY](docs/GLOSSARY.md): the only source for terms
|
|
179
145
|
- [Modules](docs/modules/): usage and trade-offs per module
|
|
180
|
-
- [ADR](docs/decisions/): decisions and rejected proposals, with reasons
|
|
181
146
|
|
|
182
147
|
## Development
|
|
183
148
|
|
|
184
149
|
```bash
|
|
185
150
|
pnpm install # dependencies
|
|
186
|
-
pnpm check #
|
|
151
|
+
pnpm check # the four checks below
|
|
187
152
|
pnpm test # unit tests (vitest)
|
|
188
153
|
pnpm lint # biome
|
|
189
154
|
pnpm typecheck # tsc --noEmit
|
package/README.zh-CN.md
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
pi 扩展:让子代理作为独立 pi 进程跑在 herdr pane 里。一个工具,两个 action:`run` 起子代理,`send` 续聊,结果异步回投。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
行为以 [v1 契约](docs/specs/0001-v1-contract.md) 为准。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
11
|
- **独立进程,可人工接管**:每个子代理是 herdr pane 里独立的 pi 进程。cwd 与父相同则同 workspace 新开 tab,否则新建 workspace。随时切进去接管。
|
|
12
|
-
- **两层 profile**:Markdown + frontmatter
|
|
13
|
-
- **异步、可合并的结果**:`run`
|
|
12
|
+
- **两层 profile**:Markdown + frontmatter,全局一层、项目一层。
|
|
13
|
+
- **异步、可合并的结果**:`run` 立即返回。每次收工投一条 `subagent_result`;落在 `aggregateWindowMs` 内的结果合并成一条。超长结果落 artifact 文件。
|
|
14
14
|
- **反问留在人这边**:子代理弹出对话框时只发出 `herdr:blocked` 事件,父侧什么也收不到。人在子 pane 里回答,不经模型转达。
|
|
15
15
|
- **默认不嵌套,也不是沙箱**:子代理的工具集默认去掉本工具,profile 里点名才授予。工具由 profile 决定,扩展不限制写操作。
|
|
16
16
|
|
|
17
17
|
## 要求
|
|
18
18
|
|
|
19
|
-
herdr `0.9.5
|
|
19
|
+
在 herdr `0.9.5` 与 pi `1.4.0` 上测过。Node `>= 22.19`。父会话必须在 herdr pane 里,否则调用返回 `not_in_herdr`。
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # 只对当前进程
|
|
|
36
36
|
## 快速开始
|
|
37
37
|
|
|
38
38
|
1. 在 herdr pane 里启动 `pi`。
|
|
39
|
-
2. 写一个 profile(下一节);frontmatter `name` 就是 `run`
|
|
39
|
+
2. 写一个 profile(下一节);frontmatter `name` 就是 `run` 的寻址名。
|
|
40
40
|
3. 让模型调用工具:
|
|
41
41
|
|
|
42
42
|
```json
|
|
@@ -55,7 +55,7 @@ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # 只对当前进程
|
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
57
|
---
|
|
58
|
-
# run
|
|
58
|
+
# run 的寻址名;文件名不参与查找
|
|
59
59
|
name: explore
|
|
60
60
|
description: 只读调研:读代码、回答问题,不改动任何文件
|
|
61
61
|
|
|
@@ -70,32 +70,34 @@ tools: read, bash
|
|
|
70
70
|
# 省略 = 由 pi 发现;"" = 不加载;列表 = 只加载列出的
|
|
71
71
|
skills: ""
|
|
72
72
|
|
|
73
|
-
# append
|
|
73
|
+
# append / replace:正文追加到或替换系统提示
|
|
74
74
|
# task:正文就是任务文本,不写身份文件
|
|
75
75
|
system-prompt: append
|
|
76
76
|
|
|
77
77
|
# 收工后自行退出并关掉 tab;有打开的对话框或 inbox 待投递时会等
|
|
78
78
|
auto-exit: true
|
|
79
79
|
|
|
80
|
-
# true =
|
|
80
|
+
# true = 不出现在 guidelines 的 profile 列表里;显式 run 仍然可用
|
|
81
81
|
disable-model-invocation: false
|
|
82
82
|
---
|
|
83
83
|
|
|
84
84
|
你是一名只读探索者,回答时给出文件路径与行号。
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
错误码见 [契约 §2](docs/specs/0001-v1-contract.md#2-profile);默认值见 `src/profile/frontmatter.ts` 与 `test/profile/`。没写 `name`、字段非法或同层同名的文件都不进列表;对它 `run` 返回 `profile_invalid`,带文件、字段与原因。
|
|
88
88
|
|
|
89
89
|
扩展自带一个示例 profile,文件名和 `name` 都是 `domain`。加载只读 `profiles/*.md`,示例也只在缺失时写入,所以复制一层上去即可:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
cd
|
|
92
|
+
cd <agent-dir>/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
## 配置
|
|
96
96
|
|
|
97
97
|
只有一个全局文件:`<agent-dir>/pi-herdr-subagents-lite/config.json`。`<agent-dir>` 默认 `~/.pi/agent`,跟随 `PI_CODING_AGENT_DIR`。项目级配置不读。
|
|
98
98
|
|
|
99
|
+
默认值与 clamp 边界:`src/config/index.ts`、`test/config/config.test.ts`。键与语义:[契约 §1](docs/specs/0001-v1-contract.md#1-配置)。
|
|
100
|
+
|
|
99
101
|
### 最小配置
|
|
100
102
|
|
|
101
103
|
文件不存在就全用默认值。只改一项,就只写这一项:
|
|
@@ -106,58 +108,24 @@ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.
|
|
|
106
108
|
}
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
逐项列出,值是默认值。必须是严格 JSON:未知键、类型不对、枚举非法都会让扩展加载失败,pi 报出字段名退出。`//` 行是注释,用之前删掉。
|
|
112
|
-
|
|
113
|
-
```jsonc
|
|
114
|
-
{
|
|
115
|
-
// 只管工具注册;bridge 有自己的开关
|
|
116
|
-
"enabled": true,
|
|
117
|
-
|
|
118
|
-
// 模型看到的工具名;^[a-z][a-z0-9_]{0,31}$
|
|
119
|
-
"toolName": "subagent",
|
|
120
|
-
|
|
121
|
-
// 从 PATH 解析;写绝对路径可锁定版本
|
|
122
|
-
"herdrBin": "herdr",
|
|
123
|
-
|
|
124
|
-
// 每个父会话并存的子代理数;auto-exit 为 false 的子代理收工后仍占槽
|
|
125
|
-
"maxConcurrent": 16,
|
|
126
|
-
|
|
127
|
-
// 单条结果上限(1024-1048576);超了全文落 artifact,消息里留截断的头和路径
|
|
128
|
-
"maxResultChars": 16384,
|
|
129
|
-
|
|
130
|
-
// 合并窗口,毫秒(0-10000);窗口内的结果并成一条,0 = 逐条立即投递
|
|
131
|
-
"aggregateWindowMs": 500,
|
|
111
|
+
### 键
|
|
132
112
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
// 把 pi 的对话框事件转成 herdr:blocked,供原生通知使用
|
|
152
|
-
"bridge": {
|
|
153
|
-
"enabled": true,
|
|
154
|
-
// 哪些对话框算 blocked
|
|
155
|
-
"blockedOn": ["custom", "select", "confirm", "input", "editor"]
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
超范围的数字会被夹到边界,不报错。配置只读一次,改完重启 pi。
|
|
113
|
+
| 键 | 含义 |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `enabled` | 只管工具注册;bridge 有自己的开关 |
|
|
116
|
+
| `toolName` | 模型看到的工具名;`^[a-z][a-z0-9_]{0,31}$` |
|
|
117
|
+
| `herdrBin` | herdr 可执行文件;裸名从 PATH 解析,绝对路径锁定版本 |
|
|
118
|
+
| `maxConcurrent` | 每个父会话并存的子代理数;`auto-exit: false` 的子代理收工后仍占槽 |
|
|
119
|
+
| `maxResultChars` | 单条结果字符上限;超了全文落 artifact,消息里留截断的头和路径 |
|
|
120
|
+
| `aggregateWindowMs` | 合并窗口,毫秒;0 = 逐条立即投递 |
|
|
121
|
+
| `closeChildrenOn` | 父会话以这些原因退出时关掉子 tab:`new` / `resume` / `fork` / `quit` / `reload`;空 = 从不关 |
|
|
122
|
+
| `inboxPollMs` | 子侧 inbox 轮询间隔 |
|
|
123
|
+
| `pollIntervalMs` | 父侧 `agent list` 间隔;有子代理在 working 或 blocked,或刚 `run` / `send` 的 30 秒内用 |
|
|
124
|
+
| `idlePollIntervalMs` | 所有 live 子代理都只是在等 `send` 时的间隔;它决定 pane 消失最晚多久被发现(pane 还在、进程被杀的那类固定 10 秒) |
|
|
125
|
+
| `prompts` | 覆盖工具文案:`description`、`snippet`、`guidelines`;省略的键用内置文案;`snippet: ""` 让工具从 Available tools 消失;保留 `{{profiles}}` 才会列出 profile |
|
|
126
|
+
| `bridge` | `enabled` 把 pi 对话框事件转成 `herdr:blocked`;`blockedOn` 列出算 blocked 的对话框类型 |
|
|
127
|
+
|
|
128
|
+
必须是严格 JSON:未知键、类型不对、枚举非法都会让扩展加载失败,pi 报出字段名退出。超范围的数字会被夹到边界,不报错。配置只读一次,改完重启 pi。
|
|
161
129
|
|
|
162
130
|
## 参考项目
|
|
163
131
|
|
|
@@ -172,16 +140,15 @@ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.
|
|
|
172
140
|
|
|
173
141
|
## 文档
|
|
174
142
|
|
|
175
|
-
- [ARCHITECTURE](docs/ARCHITECTURE.md)
|
|
176
|
-
- [GLOSSARY](docs/GLOSSARY.md)
|
|
177
|
-
- [模块](docs/modules/)
|
|
178
|
-
- [ADR](docs/decisions/) — 已决选型与理由,改设计前扫一遍
|
|
143
|
+
- [ARCHITECTURE](docs/ARCHITECTURE.md):模块边界、依赖方向、测试 seam
|
|
144
|
+
- [GLOSSARY](docs/GLOSSARY.md):术语唯一来源
|
|
145
|
+
- [模块](docs/modules/):各模块的用法与取舍
|
|
179
146
|
|
|
180
147
|
## 开发
|
|
181
148
|
|
|
182
149
|
```bash
|
|
183
150
|
pnpm install # 装依赖
|
|
184
|
-
pnpm check #
|
|
151
|
+
pnpm check # 下面四项
|
|
185
152
|
pnpm test # 单测(vitest)
|
|
186
153
|
pnpm lint # biome
|
|
187
154
|
pnpm typecheck # tsc --noEmit
|
package/package.json
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface Config {
|
|
|
41
41
|
closeChildrenOn: ShutdownReason[];
|
|
42
42
|
inboxPollMs: number;
|
|
43
43
|
pollIntervalMs: number;
|
|
44
|
+
idlePollIntervalMs: number;
|
|
44
45
|
prompts: PromptOverrides;
|
|
45
46
|
bridge: { enabled: boolean; blockedOn: BlockedKind[] };
|
|
46
47
|
}
|
|
@@ -77,6 +78,7 @@ function parse(raw: Raw, file: string): Config {
|
|
|
77
78
|
closeChildrenOn: enumList(top, "closeChildrenOn", SHUTDOWN_REASONS, []),
|
|
78
79
|
inboxPollMs: integer(top, "inboxPollMs", 250, 50, 5000),
|
|
79
80
|
pollIntervalMs: integer(top, "pollIntervalMs", 1000, 100, 60000),
|
|
81
|
+
idlePollIntervalMs: integer(top, "idlePollIntervalMs", 8000, 100, 60000),
|
|
80
82
|
prompts: promptOverrides(top),
|
|
81
83
|
bridge: {
|
|
82
84
|
enabled: boolean(bridge, "enabled", true),
|
package/src/config/schema.ts
CHANGED
package/src/extension/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createBridge } from "../bridge/index.ts";
|
|
3
3
|
import type { BlockedKind, Config } from "../config/index.ts";
|
|
4
|
+
import { errorText } from "../error/index.ts";
|
|
4
5
|
import type { HerdrHost } from "../herdr/index.ts";
|
|
5
6
|
import { drain, hasInbox, pumpInbox } from "../inbox/index.ts";
|
|
6
7
|
import type { ProfileCatalog, ProfileSources } from "../profile/index.ts";
|
|
@@ -30,10 +31,6 @@ export interface SessionContext {
|
|
|
30
31
|
|
|
31
32
|
export type { SubagentTool };
|
|
32
33
|
|
|
33
|
-
function errorText(error: unknown): string {
|
|
34
|
-
return error instanceof Error ? error.message : String(error);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
type SessionStartHandler = (event: SessionStartEvent, ctx: SessionContext) => void;
|
|
38
35
|
type SettledHandler = (event: { type: "agent_settled" }, ctx: SessionContext) => void;
|
|
39
36
|
type PromptHandler = (event: { kind: BlockedKind; title?: string }) => void;
|
|
@@ -112,10 +109,12 @@ export function createExtension(deps: ExtensionDeps): (pi: ExtensionRegistrar) =
|
|
|
112
109
|
readLastAssistant: deps.readLastAssistant,
|
|
113
110
|
readExitReceipt: deps.readExitReceipt,
|
|
114
111
|
loadProfiles: () => deps.loadProfiles(sources),
|
|
112
|
+
trusted: sources.trusted,
|
|
115
113
|
maxConcurrent: config.maxConcurrent,
|
|
116
114
|
maxResultChars: config.maxResultChars,
|
|
117
115
|
aggregateWindowMs: config.aggregateWindowMs,
|
|
118
116
|
pollIntervalMs: config.pollIntervalMs,
|
|
117
|
+
idlePollIntervalMs: config.idlePollIntervalMs,
|
|
119
118
|
toolName: config.toolName,
|
|
120
119
|
onDeliveryError: (error) => ctx.ui.notify(`subagent result not delivered: ${errorText(error)}`, "error"),
|
|
121
120
|
});
|
package/src/herdr/agent.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface AgentSnapshot {
|
|
|
10
10
|
export interface AgentEntry extends AgentSnapshot {
|
|
11
11
|
name: string | null;
|
|
12
12
|
paneId: string;
|
|
13
|
+
sessionFile: string | null;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface StartAgentInput {
|
|
@@ -17,20 +18,18 @@ export interface StartAgentInput {
|
|
|
17
18
|
paneId: string;
|
|
18
19
|
sessionFile: string;
|
|
19
20
|
agentArgs: string[];
|
|
20
|
-
briefFile: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export type ResumeAgentInput = Omit<StartAgentInput, "briefFile">;
|
|
24
|
-
|
|
25
23
|
interface AgentInfo {
|
|
26
24
|
agent_status: AgentStatus;
|
|
27
25
|
completion_seq?: number | null;
|
|
28
26
|
state_change_seq?: number;
|
|
29
27
|
name?: string | null;
|
|
30
28
|
pane_id: string;
|
|
29
|
+
agent_session?: { kind?: string; value?: string } | null;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
function agentStartCommand(input:
|
|
32
|
+
function agentStartCommand(input: StartAgentInput): string[] {
|
|
34
33
|
return [
|
|
35
34
|
"agent",
|
|
36
35
|
"start",
|
|
@@ -48,14 +47,14 @@ function agentStartCommand(input: ResumeAgentInput): string[] {
|
|
|
48
47
|
];
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
function sessionPath(agent: AgentInfo): string | null {
|
|
51
|
+
const session = agent.agent_session;
|
|
52
|
+
return session?.kind === "path" && typeof session.value === "string" ? session.value : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
export function createAgentCommands(exec: ExecHerdr) {
|
|
52
56
|
return {
|
|
53
|
-
async startAgent(
|
|
54
|
-
await exec(agentStartCommand({ name, ...input }));
|
|
55
|
-
await exec(["agent", "prompt", name, `@${briefFile}`]);
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
async resumeAgent(input: ResumeAgentInput) {
|
|
57
|
+
async startAgent(input: StartAgentInput) {
|
|
59
58
|
await exec(agentStartCommand(input));
|
|
60
59
|
},
|
|
61
60
|
|
|
@@ -64,6 +63,7 @@ export function createAgentCommands(exec: ExecHerdr) {
|
|
|
64
63
|
return result.agents.map((agent) => ({
|
|
65
64
|
name: agent.name ?? null,
|
|
66
65
|
paneId: agent.pane_id,
|
|
66
|
+
sessionFile: sessionPath(agent),
|
|
67
67
|
status: agent.agent_status,
|
|
68
68
|
completionSeq: agent.completion_seq ?? agent.state_change_seq ?? 0,
|
|
69
69
|
}));
|
package/src/herdr/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEntry, AgentSnapshot,
|
|
1
|
+
import type { AgentEntry, AgentSnapshot, StartAgentInput } from "./agent.ts";
|
|
2
2
|
import { createAgentCommands } from "./agent.ts";
|
|
3
3
|
import { createHerdrExec, HerdrHostError } from "./cli.ts";
|
|
4
4
|
import type {
|
|
@@ -21,13 +21,12 @@ export interface HerdrHost {
|
|
|
21
21
|
renameTab(input: RenameTabInput): Promise<void>;
|
|
22
22
|
renamePane(input: RenamePaneInput): Promise<void>;
|
|
23
23
|
startAgent(input: StartAgentInput): Promise<void>;
|
|
24
|
-
resumeAgent(input: ResumeAgentInput): Promise<void>;
|
|
25
24
|
hasPane(paneId: string): Promise<boolean>;
|
|
26
25
|
listAgents(): Promise<AgentEntry[]>;
|
|
27
26
|
closeTabIfOnlyPane(input: CloseTabInput): Promise<CloseTabResult>;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export type { AgentEntry, AgentSnapshot, CloseTabResult,
|
|
29
|
+
export type { AgentEntry, AgentSnapshot, CloseTabResult, StartAgentInput };
|
|
31
30
|
|
|
32
31
|
export function createHerdrHost(bin: string): HerdrHost {
|
|
33
32
|
const { execJson } = createHerdrExec(bin);
|
package/src/herdr/pane.ts
CHANGED
|
@@ -9,15 +9,15 @@ export interface CreateTabInput {
|
|
|
9
9
|
cwd: string;
|
|
10
10
|
label: string;
|
|
11
11
|
env: Record<string, string>;
|
|
12
|
-
workspaceId?: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export interface CreateWorkspaceInput {
|
|
16
15
|
cwd: string;
|
|
17
16
|
label: string;
|
|
17
|
+
env: Record<string, string>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface CreatedWorkspace {
|
|
20
|
+
export interface CreatedWorkspace extends CreatedTab {
|
|
21
21
|
workspaceId: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -59,20 +59,25 @@ interface PaneInfo {
|
|
|
59
59
|
|
|
60
60
|
export function createPaneCommands(exec: ExecHerdr) {
|
|
61
61
|
return {
|
|
62
|
-
async createWorkspace({ cwd, label }: CreateWorkspaceInput): Promise<CreatedWorkspace> {
|
|
63
|
-
const result = await exec<{ workspace: WorkspaceInfo }>([
|
|
62
|
+
async createWorkspace({ cwd, label, env }: CreateWorkspaceInput): Promise<CreatedWorkspace> {
|
|
63
|
+
const result = await exec<{ workspace: WorkspaceInfo; tab: TabInfo; root_pane: PaneInfo }>([
|
|
64
64
|
"workspace",
|
|
65
65
|
"create",
|
|
66
66
|
"--cwd",
|
|
67
67
|
cwd,
|
|
68
68
|
"--label",
|
|
69
69
|
label,
|
|
70
|
+
...envArgs(env),
|
|
70
71
|
"--no-focus",
|
|
71
72
|
]);
|
|
72
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
workspaceId: result.workspace.workspace_id,
|
|
75
|
+
tabId: result.tab.tab_id,
|
|
76
|
+
paneId: result.root_pane.pane_id,
|
|
77
|
+
};
|
|
73
78
|
},
|
|
74
79
|
|
|
75
|
-
async createTab({ cwd, label, env
|
|
80
|
+
async createTab({ cwd, label, env }: CreateTabInput): Promise<CreatedTab> {
|
|
76
81
|
const result = await exec<{ tab: TabInfo; root_pane: PaneInfo }>([
|
|
77
82
|
"tab",
|
|
78
83
|
"create",
|
|
@@ -81,7 +86,6 @@ export function createPaneCommands(exec: ExecHerdr) {
|
|
|
81
86
|
"--label",
|
|
82
87
|
label,
|
|
83
88
|
...envArgs(env),
|
|
84
|
-
...(workspaceId === undefined ? [] : ["--workspace", workspaceId]),
|
|
85
89
|
"--no-focus",
|
|
86
90
|
]);
|
|
87
91
|
return { tabId: result.tab.tab_id, paneId: result.root_pane.pane_id };
|
package/src/inbox/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, wri
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
const MESSAGE_SUFFIX = ".md";
|
|
6
|
-
const DELIVERED_DIR = ".delivered";
|
|
7
6
|
|
|
8
7
|
function inboxDir(sessionFile: string): string {
|
|
9
8
|
return `${sessionFile}.inbox`;
|
|
@@ -56,18 +55,12 @@ export function enqueue(sessionFile: string, text: string): void {
|
|
|
56
55
|
|
|
57
56
|
export function drain(sessionFile: string, deliver: (text: string) => void): number {
|
|
58
57
|
const dir = inboxDir(sessionFile);
|
|
59
|
-
const names = pendingNames(dir);
|
|
60
|
-
if (names.length === 0) {
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const deliveredDir = join(dir, DELIVERED_DIR);
|
|
65
|
-
mkdirSync(deliveredDir, { recursive: true });
|
|
66
58
|
let delivered = 0;
|
|
67
|
-
for (const name of
|
|
68
|
-
const
|
|
59
|
+
for (const name of pendingNames(dir)) {
|
|
60
|
+
const path = join(dir, name);
|
|
61
|
+
const text = readFileSync(path, "utf8");
|
|
69
62
|
deliver(text);
|
|
70
|
-
|
|
63
|
+
rmSync(path, { force: true });
|
|
71
64
|
delivered += 1;
|
|
72
65
|
}
|
|
73
66
|
return delivered;
|
package/src/profile/index.ts
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
|
|
4
1
|
export { EXAMPLE_PROFILE, EXAMPLE_PROFILE_FILE } from "./examples.ts";
|
|
5
2
|
export type { Profile } from "./frontmatter.ts";
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type ProfileCatalog,
|
|
10
|
-
type ProfileSources,
|
|
11
|
-
seedExampleProfile,
|
|
12
|
-
} from "./layers.ts";
|
|
13
|
-
|
|
14
|
-
export function agentDir(): string {
|
|
15
|
-
return getAgentDir();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function skillPath(name: string): string {
|
|
19
|
-
return join(agentDir(), "skills", name);
|
|
20
|
-
}
|
|
3
|
+
export { globalProfilesDir, loadProfiles, type ProfileCatalog, type ProfileSources } from "./layers.ts";
|
|
4
|
+
export { seedExampleProfile } from "./seed.ts";
|
|
5
|
+
export { agentDir, resolveSkillPaths } from "./skills.ts";
|