pi-herdr-subagents-lite 0.2.0 → 0.3.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 +8 -7
- package/README.zh-CN.md +8 -7
- package/package.json +1 -1
- package/src/profile/examples.ts +2 -2
- package/src/profile/frontmatter.ts +18 -1
- package/src/profile/layers.ts +43 -10
- package/src/run/resolve.ts +1 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Experimental; compatibility across versions is not promised. Exact behavior live
|
|
|
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
|
|
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
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.
|
|
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.
|
|
@@ -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); its frontmatter `name` is the key `run` takes. The file name is not used for lookup.
|
|
40
40
|
3. Ask the model to call the tool:
|
|
41
41
|
|
|
42
42
|
```json
|
|
@@ -51,12 +51,12 @@ It returns `{"state":"running",...}`, runs in its own tab, and a `[subagent expl
|
|
|
51
51
|
|
|
52
52
|
## Profile
|
|
53
53
|
|
|
54
|
-
Global profiles live in `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`, project profiles in `.pi/pi-herdr-subagents-lite/profiles/*.md` (read once the project is trusted); the project wins on the same name
|
|
54
|
+
Global profiles live in `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`, project profiles in `.pi/pi-herdr-subagents-lite/profiles/*.md` (read once the project is trusted); the project wins on the same `name`.
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
57
|
---
|
|
58
|
-
#
|
|
59
|
-
name:
|
|
58
|
+
# Key run takes; keep it equal to the file name
|
|
59
|
+
name: explore
|
|
60
60
|
description: "Read-only research: read code, answer questions, change nothing"
|
|
61
61
|
|
|
62
62
|
# Omit to inherit from the parent
|
|
@@ -84,9 +84,9 @@ 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
|
|
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.
|
|
88
88
|
|
|
89
|
-
A bundled example ships with the extension
|
|
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
92
|
cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
|
|
@@ -176,6 +176,7 @@ All docs are in Chinese.
|
|
|
176
176
|
- [ARCHITECTURE](docs/ARCHITECTURE.md): module boundaries, dependency direction, test seams
|
|
177
177
|
- [GLOSSARY](docs/GLOSSARY.md): the only source for terms
|
|
178
178
|
- [Modules](docs/modules/): usage and trade-offs per module
|
|
179
|
+
- [ADR](docs/decisions/): decisions and rejected proposals, with reasons
|
|
179
180
|
|
|
180
181
|
## Development
|
|
181
182
|
|
package/README.zh-CN.md
CHANGED
|
@@ -9,7 +9,7 @@ pi 扩展:让子代理作为独立 pi 进程跑在 herdr pane 里。一个工
|
|
|
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
|
|
12
|
+
- **两层 profile**:Markdown + frontmatter,全局 `<agent-dir>/pi-herdr-subagents-lite/profiles/`,项目层 `.pi/pi-herdr-subagents-lite/profiles/`,`name` 相同时项目层覆盖。
|
|
13
13
|
- **异步、可合并的结果**:`run` 立即返回。每个子代理收工时投一条 `subagent_result`;落在 `aggregateWindowMs` 内的结果合并成一条。超长结果落 artifact 文件。
|
|
14
14
|
- **反问留在人这边**:子代理弹出对话框时只发出 `herdr:blocked` 事件,父侧什么也收不到。人在子 pane 里回答,不经模型转达。
|
|
15
15
|
- **默认不嵌套,也不是沙箱**:子代理的工具集默认去掉本工具,profile 里点名才授予。工具由 profile 决定,扩展不限制写操作。
|
|
@@ -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
|
|
39
|
+
2. 写一个 profile(下一节);frontmatter `name` 就是 `run` 的寻址名,文件名不参与查找。
|
|
40
40
|
3. 让模型调用工具:
|
|
41
41
|
|
|
42
42
|
```json
|
|
@@ -51,12 +51,12 @@ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # 只对当前进程
|
|
|
51
51
|
|
|
52
52
|
## Profile
|
|
53
53
|
|
|
54
|
-
全局 profile 在 `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`,项目 profile 在 `.pi/pi-herdr-subagents-lite/profiles/*.md
|
|
54
|
+
全局 profile 在 `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`,项目 profile 在 `.pi/pi-herdr-subagents-lite/profiles/*.md`(项目受信任后才读);`name` 相同时项目覆盖全局。
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
57
|
---
|
|
58
|
-
#
|
|
59
|
-
name:
|
|
58
|
+
# run 的寻址名;建议与文件名保持一致
|
|
59
|
+
name: explore
|
|
60
60
|
description: 只读调研:读代码、回答问题,不改动任何文件
|
|
61
61
|
|
|
62
62
|
# 省略即继承父会话
|
|
@@ -84,9 +84,9 @@ disable-model-invocation: false
|
|
|
84
84
|
你是一名只读探索者,回答时给出文件路径与行号。
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
字段默认值与错误码见 [契约 §2](docs/specs/0001-v1-contract.md#2-profile)
|
|
87
|
+
字段默认值与错误码见 [契约 §2](docs/specs/0001-v1-contract.md#2-profile)。没写 `name`、字段非法或同层同名的文件都不进列表;对它 `run` 返回 `profile_invalid`,带文件、字段与原因。
|
|
88
88
|
|
|
89
|
-
扩展自带一个示例 profile
|
|
89
|
+
扩展自带一个示例 profile,文件名和 `name` 都是 `domain`。加载只读 `profiles/*.md`,示例也只在缺失时写入,所以复制一层上去即可:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
|
|
@@ -174,6 +174,7 @@ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.
|
|
|
174
174
|
- [ARCHITECTURE](docs/ARCHITECTURE.md) — 模块边界与 seam,改结构前看
|
|
175
175
|
- [GLOSSARY](docs/GLOSSARY.md) — 术语唯一来源,命名前看
|
|
176
176
|
- [模块](docs/modules/) — 各模块的用法与取舍
|
|
177
|
+
- [ADR](docs/decisions/) — 已决选型与理由,改设计前扫一遍
|
|
177
178
|
|
|
178
179
|
## 开发
|
|
179
180
|
|
package/package.json
CHANGED
package/src/profile/examples.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.
|
|
5
|
-
name:
|
|
4
|
+
# Copy this file to profiles/domain.md to use it. \`name\` is what \`run\` takes.
|
|
5
|
+
name: domain
|
|
6
6
|
description: Take one domain of work into a fresh context. Invoke by name.
|
|
7
7
|
|
|
8
8
|
# User-invoked: hidden from the tool guidelines, so only an explicit run uses it.
|
|
@@ -8,7 +8,6 @@ export type SystemPromptMode = (typeof SYSTEM_PROMPT_MODES)[number];
|
|
|
8
8
|
|
|
9
9
|
export interface Profile {
|
|
10
10
|
name: string;
|
|
11
|
-
displayName: string;
|
|
12
11
|
description: string;
|
|
13
12
|
body: string;
|
|
14
13
|
model: string | undefined;
|
|
@@ -48,6 +47,24 @@ function complain(fields: Fields, field: string, reason: string) {
|
|
|
48
47
|
fields.problems.push({ name: fields.name, file: fields.file, field, reason });
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
export function nameField(fields: Fields): string | undefined {
|
|
51
|
+
const value = fields.raw.name;
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
complain(fields, "name", "required");
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== "string") {
|
|
57
|
+
complain(fields, "name", `expected string, got ${typeName(value)}`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const name = value.trim();
|
|
61
|
+
if (name === "") {
|
|
62
|
+
complain(fields, "name", "expected a non-empty name");
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
export function stringField(fields: Fields, key: string): string | undefined {
|
|
52
69
|
const value = fields.raw[key];
|
|
53
70
|
if (value === undefined) {
|
package/src/profile/layers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
booleanField,
|
|
8
8
|
enumField,
|
|
9
9
|
type Fields,
|
|
10
|
+
nameField,
|
|
10
11
|
type Profile,
|
|
11
12
|
type ProfileProblem,
|
|
12
13
|
SYSTEM_PROMPT_MODES,
|
|
@@ -27,24 +28,27 @@ export interface ProfileSources {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function parseProfile(file: string, source: string): { profile?: Profile; problems: ProfileProblem[] } {
|
|
30
|
-
const name = basename(file, ".md");
|
|
31
31
|
let frontmatter: unknown;
|
|
32
32
|
let body: string;
|
|
33
33
|
try {
|
|
34
34
|
({ frontmatter, body } = parseFrontmatter(source));
|
|
35
35
|
} catch (error) {
|
|
36
36
|
const reason = error instanceof Error ? error.message : "parse failed";
|
|
37
|
-
return { problems: [{ name, file, field: "frontmatter", reason }] };
|
|
37
|
+
return { problems: [{ name: basename(file, ".md"), file, field: "frontmatter", reason }] };
|
|
38
38
|
}
|
|
39
39
|
if (!isRaw(frontmatter)) {
|
|
40
|
-
return { problems: [{ name, file, field: "frontmatter", reason: "expected a mapping" }] };
|
|
40
|
+
return { problems: [{ name: basename(file, ".md"), file, field: "frontmatter", reason: "expected a mapping" }] };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const problems: ProfileProblem[] = [];
|
|
44
|
-
const fields: Fields = { raw: frontmatter, name, file, problems };
|
|
44
|
+
const fields: Fields = { raw: frontmatter, name: basename(file, ".md"), file, problems };
|
|
45
|
+
const name = nameField(fields);
|
|
46
|
+
if (name === undefined) {
|
|
47
|
+
return { problems };
|
|
48
|
+
}
|
|
49
|
+
fields.name = name;
|
|
45
50
|
const profile: Profile = {
|
|
46
51
|
name,
|
|
47
|
-
displayName: stringField(fields, "name") ?? name,
|
|
48
52
|
description: stringField(fields, "description") ?? "",
|
|
49
53
|
body,
|
|
50
54
|
model: stringField(fields, "model"),
|
|
@@ -69,7 +73,7 @@ function readProfilesDir(dir: string): ProfileCatalog {
|
|
|
69
73
|
throw error;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const
|
|
76
|
+
const parsed: Array<{ profile: Profile; file: string }> = [];
|
|
73
77
|
const problems: ProfileProblem[] = [];
|
|
74
78
|
for (const entry of entries.sort()) {
|
|
75
79
|
const file = join(dir, entry);
|
|
@@ -81,12 +85,41 @@ function readProfilesDir(dir: string): ProfileCatalog {
|
|
|
81
85
|
problems.push({ name: basename(entry, ".md"), file, field: "file", reason });
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
|
|
88
|
+
const result = parseProfile(file, source);
|
|
89
|
+
if (result.profile) {
|
|
90
|
+
parsed.push({ profile: result.profile, file });
|
|
91
|
+
}
|
|
92
|
+
problems.push(...result.problems);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const byName = new Map<string, Array<{ profile: Profile; file: string }>>();
|
|
96
|
+
for (const entry of parsed) {
|
|
97
|
+
const group = byName.get(entry.profile.name);
|
|
98
|
+
if (group) {
|
|
99
|
+
group.push(entry);
|
|
100
|
+
} else {
|
|
101
|
+
byName.set(entry.profile.name, [entry]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const profiles: Profile[] = [];
|
|
106
|
+
for (const [name, group] of byName) {
|
|
107
|
+
const [only] = group;
|
|
108
|
+
if (only && group.length === 1) {
|
|
109
|
+
profiles.push(only.profile);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of group) {
|
|
113
|
+
const others = group.filter((other) => other.file !== entry.file).map((other) => other.file);
|
|
114
|
+
problems.push({
|
|
115
|
+
name,
|
|
116
|
+
file: entry.file,
|
|
117
|
+
field: "name",
|
|
118
|
+
reason: `duplicate profile name; also in ${others.join(", ")}`,
|
|
119
|
+
});
|
|
87
120
|
}
|
|
88
|
-
problems.push(...parsed.problems);
|
|
89
121
|
}
|
|
122
|
+
profiles.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
90
123
|
return { profiles, problems };
|
|
91
124
|
}
|
|
92
125
|
|
package/src/run/resolve.ts
CHANGED
|
@@ -2,8 +2,7 @@ import type { Profile, ProfileCatalog } from "../profile/index.ts";
|
|
|
2
2
|
import { ToolError } from "./errors.ts";
|
|
3
3
|
|
|
4
4
|
export function profileLabel(profile: Profile): string {
|
|
5
|
-
|
|
6
|
-
return profile.description === "" ? label : `${label} — ${profile.description}`;
|
|
5
|
+
return profile.description === "" ? profile.name : `${profile.name} — ${profile.description}`;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
export function resolveProfile({ profiles, problems }: ProfileCatalog, name: string): Profile {
|