pi-herdr-subagents-lite 0.3.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 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
- Experimental; compatibility across versions is not promised. Exact behavior lives in the [v1 contract](docs/specs/0001-v1-contract.md). Project docs are in Chinese.
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 under `<agent-dir>/pi-herdr-subagents-lite/profiles/`, plus a project layer under `.pi/pi-herdr-subagents-lite/profiles/` that wins on the same `name`.
13
- - **Async, merged results**: `run` returns at once. Each finished child reports one `subagent_result` message; results inside `aggregateWindowMs` merge into one. Oversize results go to an artifact file.
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`, pi `1.4.0`, Node `>= 22.19`. The parent must run inside a herdr pane, otherwise calls fail with `not_in_herdr`.
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); its frontmatter `name` is the key `run` takes. The file name is not used for lookup.
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; keep it equal to the file name
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 (default) / replace: body is appended to or replaces the system prompt
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
- Defaults and error codes: [contract section 2](docs/specs/0001-v1-contract.md#2-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.
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 ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
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,57 +108,24 @@ A missing file means all defaults. To change one value, write only that value:
106
108
  }
107
109
  ```
108
110
 
109
- ### Full config
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
- // Parent shutdown reasons that close its child tabs: new / resume / fork / quit / reload; empty = never
134
- "closeChildrenOn": [],
135
-
136
- // Child inbox polling interval in ms (50-5000)
137
- "inboxPollMs": 250,
138
-
139
- // Parent agent list polling in ms (100-60000); larger saves processes, smaller spots finished children sooner
140
- "pollIntervalMs": 1000,
141
-
142
- // Override tool text. Omitted keys keep the built-in text; the values are placeholders, drop the whole block if unused
143
- "prompts": {
144
- "description": "One line about what this tool does",
145
- "snippet": "One line shown in Available tools",
146
- // Keep a line containing {{profiles}} to keep the profile list
147
- "guidelines": ["Do not wait for a result in this turn.", "Available profiles:\n{{profiles}}"]
148
- },
149
-
150
- // Turns pi prompt events into herdr:blocked, for native notifications
151
- "bridge": {
152
- "enabled": true,
153
- // Which prompt kinds count as blocked
154
- "blockedOn": ["custom", "select", "confirm", "input", "editor"]
155
- }
156
- }
157
- ```
158
-
159
- 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.
160
129
 
161
130
  ## Reference projects
162
131
 
@@ -171,18 +140,15 @@ The design is based on these projects. Thank you.
171
140
 
172
141
  ## Docs
173
142
 
174
- All docs are in Chinese.
175
-
176
143
  - [ARCHITECTURE](docs/ARCHITECTURE.md): module boundaries, dependency direction, test seams
177
144
  - [GLOSSARY](docs/GLOSSARY.md): the only source for terms
178
145
  - [Modules](docs/modules/): usage and trade-offs per module
179
- - [ADR](docs/decisions/): decisions and rejected proposals, with reasons
180
146
 
181
147
  ## Development
182
148
 
183
149
  ```bash
184
150
  pnpm install # dependencies
185
- pnpm check # lint + import boundaries + types + unit tests
151
+ pnpm check # the four checks below
186
152
  pnpm test # unit tests (vitest)
187
153
  pnpm lint # biome
188
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
- 实验阶段,不保证跨版本兼容。行为以 [v1 契约](docs/specs/0001-v1-contract.md) 为准。
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,全局 `<agent-dir>/pi-herdr-subagents-lite/profiles/`,项目层 `.pi/pi-herdr-subagents-lite/profiles/`,`name` 相同时项目层覆盖。
13
- - **异步、可合并的结果**:`run` 立即返回。每个子代理收工时投一条 `subagent_result`;落在 `aggregateWindowMs` 内的结果合并成一条。超长结果落 artifact 文件。
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`、pi `1.4.0`、Node `>= 22.19`。父会话必须在 herdr pane 里,否则调用返回 `not_in_herdr`。
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(默认)/ replace:正文追加到或替换系统提示
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 = 从工具说明里隐藏;显式 run 仍然可用
80
+ # true = 不出现在 guidelines 的 profile 列表里;显式 run 仍然可用
81
81
  disable-model-invocation: false
82
82
  ---
83
83
 
84
84
  你是一名只读探索者,回答时给出文件路径与行号。
85
85
  ```
86
86
 
87
- 字段默认值与错误码见 [契约 §2](docs/specs/0001-v1-contract.md#2-profile)。没写 `name`、字段非法或同层同名的文件都不进列表;对它 `run` 返回 `profile_invalid`,带文件、字段与原因。
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 ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
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,57 +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
- // 父会话以这些原因退出时关掉子 tab:new / resume / fork / quit / reload;空 = 从不关
134
- "closeChildrenOn": [],
135
-
136
- // 子侧 inbox 轮询间隔,毫秒(50-5000)
137
- "inboxPollMs": 250,
138
-
139
- // 父侧 agent list 轮询间隔,毫秒(100-60000);调大省进程,调小更早发现收工
140
- "pollIntervalMs": 1000,
141
-
142
- // 覆盖工具文案。省略的键用内置文案;下面的值是占位符,不用就整段删掉
143
- "prompts": {
144
- "description": "一句话说明这个工具做什么",
145
- "snippet": "Available tools 里的一句话",
146
- // 保留含 {{profiles}} 的行才会保留 profile 列表
147
- "guidelines": ["不要在本轮等待结果。", "可用 profile:\n{{profiles}}"]
148
- },
149
-
150
- // 把 pi 的对话框事件转成 herdr:blocked,供原生通知使用
151
- "bridge": {
152
- "enabled": true,
153
- // 哪些对话框算 blocked
154
- "blockedOn": ["custom", "select", "confirm", "input", "editor"]
155
- }
156
- }
157
- ```
158
-
159
- 超范围的数字会被夹到边界,不报错。配置只读一次,改完重启 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。
160
129
 
161
130
  ## 参考项目
162
131
 
@@ -171,16 +140,15 @@ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.
171
140
 
172
141
  ## 文档
173
142
 
174
- - [ARCHITECTURE](docs/ARCHITECTURE.md) — 模块边界与 seam,改结构前看
175
- - [GLOSSARY](docs/GLOSSARY.md) — 术语唯一来源,命名前看
176
- - [模块](docs/modules/) — 各模块的用法与取舍
177
- - [ADR](docs/decisions/) — 已决选型与理由,改设计前扫一遍
143
+ - [ARCHITECTURE](docs/ARCHITECTURE.md):模块边界、依赖方向、测试 seam
144
+ - [GLOSSARY](docs/GLOSSARY.md):术语唯一来源
145
+ - [模块](docs/modules/):各模块的用法与取舍
178
146
 
179
147
  ## 开发
180
148
 
181
149
  ```bash
182
150
  pnpm install # 装依赖
183
- pnpm check # lint + 导入边界 + 类型 + 单测
151
+ pnpm check # 下面四项
184
152
  pnpm test # 单测(vitest)
185
153
  pnpm lint # biome
186
154
  pnpm typecheck # tsc --noEmit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-subagents-lite",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Run pi subagents in herdr panes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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),
@@ -8,6 +8,7 @@ export const CONFIG_KEYS = [
8
8
  "closeChildrenOn",
9
9
  "inboxPollMs",
10
10
  "pollIntervalMs",
11
+ "idlePollIntervalMs",
11
12
  "prompts",
12
13
  "bridge",
13
14
  ];
@@ -0,0 +1,3 @@
1
+ export function errorText(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
@@ -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;
@@ -100,7 +97,7 @@ export function createExtension(deps: ExtensionDeps): (pi: ExtensionRegistrar) =
100
97
  }
101
98
  stopInbox();
102
99
  const detail = errorText(error);
103
- ctx.ui.notify(`inbox: stopped after ${INBOX_FAILURE_LIMIT} delivery failures: ${detail}`, "error");
100
+ ctx.ui.notify(`subagent messages stopped after ${INBOX_FAILURE_LIMIT} delivery failures: ${detail}`, "error");
104
101
  },
105
102
  });
106
103
  };
@@ -112,12 +109,14 @@ 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
- onDeliveryError: (error) => ctx.ui.notify(`delivery failed: ${errorText(error)}`, "error"),
119
+ onDeliveryError: (error) => ctx.ui.notify(`subagent result not delivered: ${errorText(error)}`, "error"),
121
120
  });
122
121
  service.onResult((results) => sendResults(pi, results));
123
122
  };
@@ -27,10 +27,10 @@ export function watchShutdown(pi: ExtensionRegistrar, deps: ShutdownDeps): void
27
27
  if (closing && deps.config.closeChildrenOn.includes(event.reason)) {
28
28
  const { kept, failed } = await closing.closeChildren();
29
29
  for (const name of kept) {
30
- ctx.ui.notify(`child "${name}" kept: its tab was not a single pane`, "warning");
30
+ ctx.ui.notify(`child "${name}" kept running: its tab has more than one pane`, "warning");
31
31
  }
32
32
  for (const failure of failed) {
33
- ctx.ui.notify(`child "${failure.name}" close failed: ${failure.reason}`, "error");
33
+ ctx.ui.notify(`could not close child "${failure.name}": ${failure.reason}`, "error");
34
34
  }
35
35
  } else {
36
36
  closing?.shutdown();
@@ -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: ResumeAgentInput): string[] {
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({ name, briefFile, ...input }: StartAgentInput) {
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
  }));
@@ -1,4 +1,4 @@
1
- import type { AgentEntry, AgentSnapshot, ResumeAgentInput, StartAgentInput } from "./agent.ts";
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, ResumeAgentInput, StartAgentInput };
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 { workspaceId: result.workspace.workspace_id };
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, workspaceId }: CreateTabInput): Promise<CreatedTab> {
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 };
@@ -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 names) {
68
- const text = readFileSync(join(dir, name), "utf8");
59
+ for (const name of pendingNames(dir)) {
60
+ const path = join(dir, name);
61
+ const text = readFileSync(path, "utf8");
69
62
  deliver(text);
70
- renameSync(join(dir, name), join(deliveredDir, name));
63
+ rmSync(path, { force: true });
71
64
  delivered += 1;
72
65
  }
73
66
  return delivered;
@@ -1,20 +1,21 @@
1
1
  export const EXAMPLE_PROFILE_FILE = "domain.md";
2
2
 
3
3
  export const EXAMPLE_PROFILE = `---
4
- # Copy this file to profiles/domain.md to use it. \`name\` is what \`run\` takes.
4
+ # Copy this file to profiles/domain.md to use it.
5
5
  name: domain
6
- description: Take one domain of work into a fresh context. Invoke by name.
7
-
8
- # User-invoked: hidden from the tool guidelines, so only an explicit run uses it.
9
- disable-model-invocation: true
10
-
11
- # Close the tab once the work is done; set false to keep it for follow-ups.
12
- auto-exit: true
6
+ description: Use when the task covers another domain.
7
+ # model: provider/model
8
+ # thinking: low
9
+ tools: -subagent
10
+ skills: []
11
+ system-prompt: append
12
+ auto-exit: false
13
+ disable-model-invocation: false
13
14
  ---
14
- You work on one domain per task.
15
+ Work on one domain per task.
15
16
 
16
- - The task names your domain. Stay inside it and do not touch files of other domains.
17
- - Read the code you need yourself; do not ask the parent for context you can read.
18
- - Report results, not exploration: what changed, what is left, what blocks you.
19
- - Need a decision only the user can make? Stop and report it.
17
+ - Change only files in that domain.
18
+ - Read the code yourself. Do not ask the parent for context.
19
+ - Report what changed, what is left, and what blocks you.
20
+ - Stop if only the user can decide. Report the question.
20
21
  `;