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 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 produces `subagent_question` plus a `herdr:blocked` event. The human answers in the child pane; the parent model never relays the answer.
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
- # Display name only; run takes the file name
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
- - **反问留在人这边**:子代理弹出对话框时,父侧收到 `subagent_question` 并发出 `herdr:blocked` 事件。人在子 pane 里回答,父模型不转达。
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
- # 只作显示名;run 取文件名
58
+ # 列表写作「文件名 (显示名)」;run 取文件名
59
59
  name: Explore
60
60
  description: 只读调研:读代码、回答问题,不改动任何文件
61
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-subagents-lite",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Run pi subagents in herdr panes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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, sendQuestion, sendResults } from "../tool/index.ts";
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);
@@ -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
- return profile.description === "" ? profile.displayName : `${profile.displayName} — ${profile.description}`;
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
@@ -70,7 +70,6 @@ export async function start(state: RunState, profileName: string, task: string,
70
70
  lastDeliveredText: null,
71
71
  artifactSeq: 0,
72
72
  pollFailures: 0,
73
- blocked: false,
74
73
  live: true,
75
74
  };
76
75
  // 先占槽再 await:并发的 run() 必须立刻看到它。
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, type ToolCall } from "./jsonl.ts";
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 lastToolCall(content: unknown): ToolCall | undefined {
23
+ function hasToolCall(content: unknown): boolean {
30
24
  if (!Array.isArray(content)) {
31
- return undefined;
25
+ return false;
32
26
  }
33
- for (let index = content.length - 1; index >= 0; index--) {
34
- const block = content[index];
27
+ return content.some((block) => {
35
28
  if (typeof block !== "object" || block === null) {
36
- continue;
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
- return undefined;
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
- const toolCall = lastToolCall(entry.message?.content);
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
@@ -55,4 +55,4 @@ export function createTool({ config, profiles, service }: ToolDeps) {
55
55
  };
56
56
  }
57
57
 
58
- export { type ResultMessage, sendQuestion, sendResults } from "./message.ts";
58
+ export { type ResultMessage, sendResults } from "./message.ts";
@@ -1,4 +1,4 @@
1
- import type { RunQuestion, RunResult } from "../run/index.ts";
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 });
@@ -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
- }