pi-herdr-subagents-lite 0.1.0 → 0.2.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/src/extension/index.ts +1 -2
- package/src/run/index.ts +0 -7
- package/src/run/poll.ts +0 -6
- package/src/run/resolve.ts +2 -1
- package/src/run/send.ts +0 -2
- package/src/run/start.ts +0 -1
- package/src/run/state.ts +0 -3
- package/src/session-file/index.ts +1 -2
- package/src/session-file/jsonl.ts +8 -21
- package/src/tool/index.ts +1 -1
- package/src/tool/message.ts +1 -12
- package/src/run/question.ts +0 -88
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Experimental; compatibility across versions is not promised. Exact behavior live
|
|
|
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
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
|
-
- **Questions stay with the human**: a child prompt
|
|
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
|
|
@@ -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
|
-
#
|
|
58
|
+
# Listed as "file (name)"; run takes the file name
|
|
59
59
|
name: Explore
|
|
60
60
|
description: "Read-only research: read code, answer questions, change nothing"
|
|
61
61
|
|
package/README.zh-CN.md
CHANGED
|
@@ -11,7 +11,7 @@ pi 扩展:让子代理作为独立 pi 进程跑在 herdr pane 里。一个工
|
|
|
11
11
|
- **独立进程,可人工接管**:每个子代理是 herdr pane 里独立的 pi 进程。cwd 与父相同则同 workspace 新开 tab,否则新建 workspace。随时切进去接管。
|
|
12
12
|
- **两层 profile**:Markdown + frontmatter,全局 `<agent-dir>/pi-herdr-subagents-lite/profiles/`,项目层 `.pi/pi-herdr-subagents-lite/profiles/`,同名时项目层覆盖。
|
|
13
13
|
- **异步、可合并的结果**:`run` 立即返回。每个子代理收工时投一条 `subagent_result`;落在 `aggregateWindowMs` 内的结果合并成一条。超长结果落 artifact 文件。
|
|
14
|
-
-
|
|
14
|
+
- **反问留在人这边**:子代理弹出对话框时只发出 `herdr:blocked` 事件,父侧什么也收不到。人在子 pane 里回答,不经模型转达。
|
|
15
15
|
- **默认不嵌套,也不是沙箱**:子代理的工具集默认去掉本工具,profile 里点名才授予。工具由 profile 决定,扩展不限制写操作。
|
|
16
16
|
|
|
17
17
|
## 要求
|
|
@@ -55,7 +55,7 @@ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # 只对当前进程
|
|
|
55
55
|
|
|
56
56
|
```markdown
|
|
57
57
|
---
|
|
58
|
-
#
|
|
58
|
+
# 列表写作「文件名 (显示名)」;run 取文件名
|
|
59
59
|
name: Explore
|
|
60
60
|
description: 只读调研:读代码、回答问题,不改动任何文件
|
|
61
61
|
|
package/package.json
CHANGED
package/src/extension/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { drain, hasInbox, pumpInbox } from "../inbox/index.ts";
|
|
|
6
6
|
import type { ProfileCatalog, ProfileSources } from "../profile/index.ts";
|
|
7
7
|
import { createRunService, type RunService } from "../run/index.ts";
|
|
8
8
|
import type { ExitReceiptReader, ExitReceiptWriter, SessionReader } from "../session-file/index.ts";
|
|
9
|
-
import { createTool, type ResultMessage, type SubagentTool,
|
|
9
|
+
import { createTool, type ResultMessage, type SubagentTool, sendResults } from "../tool/index.ts";
|
|
10
10
|
import { watchAutoExit } from "./auto-exit.ts";
|
|
11
11
|
import { type ShutdownHandler, watchShutdown } from "./shutdown.ts";
|
|
12
12
|
|
|
@@ -120,7 +120,6 @@ export function createExtension(deps: ExtensionDeps): (pi: ExtensionRegistrar) =
|
|
|
120
120
|
onDeliveryError: (error) => ctx.ui.notify(`delivery failed: ${errorText(error)}`, "error"),
|
|
121
121
|
});
|
|
122
122
|
service.onResult((results) => sendResults(pi, results));
|
|
123
|
-
service.onQuestion((question) => sendQuestion(pi, question));
|
|
124
123
|
};
|
|
125
124
|
|
|
126
125
|
pi.on("session_start", (_event, ctx) => {
|
package/src/run/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { createAggregator } from "./aggregate.ts";
|
|
|
5
5
|
import { type CloseChildrenResult, closeRuns } from "./close.ts";
|
|
6
6
|
import { createNamer } from "./naming.ts";
|
|
7
7
|
import { stopPoll } from "./poll.ts";
|
|
8
|
-
import type { QuestionHandler } from "./question.ts";
|
|
9
8
|
import type { ResultHandler, RunResult } from "./result.ts";
|
|
10
9
|
import { type SendResult, send } from "./send.ts";
|
|
11
10
|
import { type RunStarted, start } from "./start.ts";
|
|
@@ -15,7 +14,6 @@ export interface RunService {
|
|
|
15
14
|
run(profile: string, task: string, cwd?: string): Promise<RunStarted>;
|
|
16
15
|
send(name: string, message: string): Promise<SendResult>;
|
|
17
16
|
onResult(handler: ResultHandler): void;
|
|
18
|
-
onQuestion(handler: QuestionHandler): void;
|
|
19
17
|
shutdown(): void;
|
|
20
18
|
closeChildren(): Promise<CloseChildrenResult>;
|
|
21
19
|
active(): string[];
|
|
@@ -42,7 +40,6 @@ export function createRunService(deps: RunServiceDeps): RunService {
|
|
|
42
40
|
runs: [],
|
|
43
41
|
resultHandler: undefined,
|
|
44
42
|
results: undefined,
|
|
45
|
-
questionHandler: undefined,
|
|
46
43
|
pollBusy: false,
|
|
47
44
|
nextName: createNamer(),
|
|
48
45
|
active() {
|
|
@@ -88,9 +85,6 @@ export function createRunService(deps: RunServiceDeps): RunService {
|
|
|
88
85
|
onResult(handler) {
|
|
89
86
|
state.resultHandler = handler;
|
|
90
87
|
},
|
|
91
|
-
onQuestion(handler) {
|
|
92
|
-
state.questionHandler = handler;
|
|
93
|
-
},
|
|
94
88
|
shutdown() {
|
|
95
89
|
stopService(state);
|
|
96
90
|
takeRuns(state);
|
|
@@ -105,7 +99,6 @@ export function createRunService(deps: RunServiceDeps): RunService {
|
|
|
105
99
|
|
|
106
100
|
export type { CloseChildrenResult } from "./close.ts";
|
|
107
101
|
export { ToolError } from "./errors.ts";
|
|
108
|
-
export type { QuestionHandler, RunQuestion } from "./question.ts";
|
|
109
102
|
export { profileLabel } from "./resolve.ts";
|
|
110
103
|
export type { ResultHandler, RunResult } from "./result.ts";
|
|
111
104
|
export type { SendResult } from "./send.ts";
|
package/src/run/poll.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentEntry } from "../herdr/index.ts";
|
|
2
2
|
import { closeTab, shouldClose } from "./close.ts";
|
|
3
3
|
import { findAgent } from "./find.ts";
|
|
4
|
-
import { askBlocked } from "./question.ts";
|
|
5
4
|
import type { RunResult } from "./result.ts";
|
|
6
5
|
import { doneResult, unknownResult } from "./result.ts";
|
|
7
6
|
import type { RunRecord, RunState } from "./state.ts";
|
|
@@ -66,11 +65,6 @@ async function pollRuns(state: RunState): Promise<void> {
|
|
|
66
65
|
await exited(state, run);
|
|
67
66
|
continue;
|
|
68
67
|
}
|
|
69
|
-
if (agent.status === "blocked") {
|
|
70
|
-
await askBlocked(state, run);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
run.blocked = false;
|
|
74
68
|
run.pollFailures = 0;
|
|
75
69
|
if (agent.completionSeq > run.lastHandledSeq && (agent.status === "idle" || agent.status === "done")) {
|
|
76
70
|
await readResult(state, run, agent.completionSeq, 1);
|
package/src/run/resolve.ts
CHANGED
|
@@ -2,7 +2,8 @@ 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
|
-
|
|
5
|
+
const label = profile.displayName === profile.name ? profile.name : `${profile.name} (${profile.displayName})`;
|
|
6
|
+
return profile.description === "" ? label : `${label} — ${profile.description}`;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function resolveProfile({ profiles, problems }: ProfileCatalog, name: string): Profile {
|
package/src/run/send.ts
CHANGED
|
@@ -40,7 +40,6 @@ export async function send(state: RunState, name: string, message: string): Prom
|
|
|
40
40
|
run.lastHandledSeq = agent.completionSeq;
|
|
41
41
|
run.live = true;
|
|
42
42
|
run.pollFailures = 0;
|
|
43
|
-
run.blocked = false;
|
|
44
43
|
ensurePoll(state);
|
|
45
44
|
}
|
|
46
45
|
await enqueue(run.sessionFile, inboxMessage(message));
|
|
@@ -94,7 +93,6 @@ export async function send(state: RunState, name: string, message: string): Prom
|
|
|
94
93
|
|
|
95
94
|
run.live = true;
|
|
96
95
|
run.pollFailures = 0;
|
|
97
|
-
run.blocked = false;
|
|
98
96
|
ensurePoll(state);
|
|
99
97
|
await enqueue(run.sessionFile, inboxMessage(message));
|
|
100
98
|
return { state: "restarted", name: run.name, paneId: run.paneId };
|
package/src/run/start.ts
CHANGED
package/src/run/state.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { HerdrHost } from "../herdr/index.ts";
|
|
|
2
2
|
import type { ProfileCatalog } from "../profile/index.ts";
|
|
3
3
|
import type { ExitReceiptReader, SessionReader } from "../session-file/index.ts";
|
|
4
4
|
import type { Aggregator } from "./aggregate.ts";
|
|
5
|
-
import type { QuestionHandler } from "./question.ts";
|
|
6
5
|
import type { ResultHandler, RunResult } from "./result.ts";
|
|
7
6
|
|
|
8
7
|
export interface RunServiceDeps {
|
|
@@ -32,7 +31,6 @@ export interface RunRecord {
|
|
|
32
31
|
artifactSeq: number;
|
|
33
32
|
timer?: ReturnType<typeof setTimeout>;
|
|
34
33
|
pollFailures: number;
|
|
35
|
-
blocked: boolean;
|
|
36
34
|
live: boolean;
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -41,7 +39,6 @@ export interface RunState extends RunServiceDeps {
|
|
|
41
39
|
runs: RunRecord[];
|
|
42
40
|
resultHandler: ResultHandler | undefined;
|
|
43
41
|
results: Aggregator<RunResult> | undefined;
|
|
44
|
-
questionHandler: QuestionHandler | undefined;
|
|
45
42
|
pollTimer?: ReturnType<typeof setTimeout>;
|
|
46
43
|
pollBusy: boolean;
|
|
47
44
|
nextName(profile: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { parseLastAssistant, type SessionSnapshot
|
|
2
|
+
import { parseLastAssistant, type SessionSnapshot } from "./jsonl.ts";
|
|
3
3
|
|
|
4
4
|
export {
|
|
5
5
|
type ExitReceipt,
|
|
@@ -8,7 +8,6 @@ export {
|
|
|
8
8
|
readExitReceipt,
|
|
9
9
|
writeExitReceipt,
|
|
10
10
|
} from "./receipt.ts";
|
|
11
|
-
export type { ToolCall };
|
|
12
11
|
|
|
13
12
|
export type SessionReader = (path: string) => SessionSnapshot;
|
|
14
13
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
export interface ToolCall {
|
|
2
|
-
name: string;
|
|
3
|
-
arguments: unknown;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
1
|
export interface SessionSnapshot {
|
|
7
2
|
text: string | null;
|
|
8
3
|
pendingToolCall: boolean;
|
|
9
|
-
toolCall?: ToolCall;
|
|
10
4
|
empty: boolean;
|
|
11
5
|
}
|
|
12
6
|
|
|
@@ -26,21 +20,17 @@ function isTextBlock(block: unknown): block is { type: "text"; text: string } {
|
|
|
26
20
|
return candidate.type === "text" && typeof candidate.text === "string";
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
function
|
|
23
|
+
function hasToolCall(content: unknown): boolean {
|
|
30
24
|
if (!Array.isArray(content)) {
|
|
31
|
-
return
|
|
25
|
+
return false;
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
const block = content[index];
|
|
27
|
+
return content.some((block) => {
|
|
35
28
|
if (typeof block !== "object" || block === null) {
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const candidate = block as { type?: unknown; name?: unknown; arguments?: unknown };
|
|
39
|
-
if (candidate.type === "toolCall" && typeof candidate.name === "string") {
|
|
40
|
-
return { name: candidate.name, arguments: candidate.arguments };
|
|
29
|
+
return false;
|
|
41
30
|
}
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
const candidate = block as { type?: unknown; name?: unknown };
|
|
32
|
+
return candidate.type === "toolCall" && typeof candidate.name === "string";
|
|
33
|
+
});
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
function textOf(entry: SessionEntry): string | null {
|
|
@@ -77,10 +67,7 @@ export function parseLastAssistant(raw: string): SessionSnapshot {
|
|
|
77
67
|
if (entry.type !== "message" || entry.message?.role !== "assistant") {
|
|
78
68
|
continue;
|
|
79
69
|
}
|
|
80
|
-
|
|
81
|
-
return toolCall === undefined
|
|
82
|
-
? { text: textOf(entry), pendingToolCall: false, empty: false }
|
|
83
|
-
: { text: textOf(entry), pendingToolCall: true, toolCall, empty: false };
|
|
70
|
+
return { text: textOf(entry), pendingToolCall: hasToolCall(entry.message?.content), empty: false };
|
|
84
71
|
}
|
|
85
72
|
return { text: null, pendingToolCall: false, empty: true };
|
|
86
73
|
}
|
package/src/tool/index.ts
CHANGED
package/src/tool/message.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunResult } from "../run/index.ts";
|
|
2
2
|
|
|
3
3
|
export interface ResultMessage {
|
|
4
4
|
customType: string;
|
|
@@ -19,17 +19,6 @@ function resultText(result: RunResult): string {
|
|
|
19
19
|
return `[subagent ${result.name}] done\n\n${result.text}${artifact}${warning}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const QUESTION_FOOTER = [
|
|
23
|
-
"The human answers it in the child's pane. A follow-up sent with send is",
|
|
24
|
-
"queued until the dialog closes.",
|
|
25
|
-
].join("\n");
|
|
26
|
-
|
|
27
|
-
export function sendQuestion(sink: MessageSink, question: RunQuestion): void {
|
|
28
|
-
const body = question.text.trim() === "" ? "" : `\n\n${question.text.trim()}`;
|
|
29
|
-
const content = `[subagent ${question.name}] blocked (question)${body}\n\n${QUESTION_FOOTER}`;
|
|
30
|
-
sink.sendMessage({ customType: "subagent_question", content, display: true }, { triggerTurn: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
export function sendResults(sink: MessageSink, results: RunResult[]): void {
|
|
34
23
|
const content = results.map(resultText).join("\n\n");
|
|
35
24
|
sink.sendMessage({ customType: "subagent_result", content, display: true }, { triggerTurn: true });
|
package/src/run/question.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { ToolCall } from "../session-file/index.ts";
|
|
2
|
-
import type { RunRecord, RunState } from "./state.ts";
|
|
3
|
-
|
|
4
|
-
export interface RunQuestion {
|
|
5
|
-
name: string;
|
|
6
|
-
text: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type QuestionHandler = (question: RunQuestion) => void | Promise<void>;
|
|
10
|
-
|
|
11
|
-
function record(value: unknown): Record<string, unknown> {
|
|
12
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function line(value: unknown): string | undefined {
|
|
16
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function optionLines(options: unknown): string[] {
|
|
20
|
-
if (!Array.isArray(options)) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
const lines: string[] = [];
|
|
24
|
-
for (const entry of options) {
|
|
25
|
-
const label = line(record(entry).label);
|
|
26
|
-
if (label === undefined) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
const description = line(record(entry).description);
|
|
30
|
-
lines.push(description === undefined ? `- ${label}` : `- ${label} — ${description}`);
|
|
31
|
-
}
|
|
32
|
-
return lines;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function groupLines(questions: unknown): string[] {
|
|
36
|
-
if (!Array.isArray(questions)) {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
const lines: string[] = [];
|
|
40
|
-
for (const entry of questions) {
|
|
41
|
-
const group = record(entry);
|
|
42
|
-
const question = line(group.question);
|
|
43
|
-
if (question === undefined) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const header = line(group.header);
|
|
47
|
-
lines.push(header === undefined ? question : `${header}: ${question}`, ...optionLines(group.options), "");
|
|
48
|
-
}
|
|
49
|
-
return lines;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function plainLine(args: unknown): string | undefined {
|
|
53
|
-
const values = record(args);
|
|
54
|
-
for (const key of ["question", "title", "message", "prompt", "text"]) {
|
|
55
|
-
const found = line(values[key]);
|
|
56
|
-
if (found !== undefined) {
|
|
57
|
-
return found;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function questionText(call: ToolCall | undefined): string {
|
|
64
|
-
if (call === undefined) {
|
|
65
|
-
return "";
|
|
66
|
-
}
|
|
67
|
-
const lines = groupLines(record(call.arguments).questions);
|
|
68
|
-
if (lines.length > 0) {
|
|
69
|
-
return lines.join("\n").trim();
|
|
70
|
-
}
|
|
71
|
-
return plainLine(call.arguments) ?? "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function askBlocked(state: RunState, run: RunRecord): Promise<void> {
|
|
75
|
-
if (run.blocked) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
run.blocked = true;
|
|
79
|
-
try {
|
|
80
|
-
await state.questionHandler?.({
|
|
81
|
-
name: run.name,
|
|
82
|
-
text: questionText(state.readLastAssistant(run.sessionFile).toolCall),
|
|
83
|
-
});
|
|
84
|
-
run.pollFailures = 0;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
state.onDeliveryError(error);
|
|
87
|
-
}
|
|
88
|
-
}
|