@oldflag2333333/pi-subagent 0.1.5 → 0.1.7
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 +4 -2
- package/README.zh-CN.md +4 -2
- package/docs/messaging.md +9 -0
- package/package.json +1 -1
- package/src/adapters/herdr.ts +63 -17
- package/src/adapters/launch-script.ts +71 -0
- package/src/profiles/system-prompt.ts +3 -1
- package/src/tools/main.ts +2 -1
- package/src/tools/sub.ts +8 -4
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Ask the reviewer profile to review src/auth for bugs and missing tests.
|
|
|
77
77
|
| Finish with a specialist | Ask Pi to close the Sub, or close its Herdr tab |
|
|
78
78
|
| Remove Sub sessions and communication files left by deleted Mains | `/clean-sub` |
|
|
79
79
|
|
|
80
|
-
Within the same Main session, repeated `/sub:reviewer` commands reuse that profile's open Sub. If it is busy, your next task is queued. Finishing a task does not automatically close the tab.
|
|
80
|
+
Within the same Main session, repeated `/sub:reviewer` commands reuse that profile's open Sub. If it is busy, your next task is queued. Finishing a task does not automatically close the tab. Subs can report progress through `talk` without stopping; `endTurn: true` explicitly ends their current turn when finished or waiting for Main.
|
|
81
81
|
|
|
82
82
|
The **Sub-agents** list shows only assistants created by the current Main: all **Open** Subs (including idle or unknown status) and **Resumable** persistent sessions confirmed closed with saved files still available. Other Main sessions and forks do not inherit them or gain access to resume them. Reloading or resuming the same Main preserves ownership.
|
|
83
83
|
|
|
@@ -191,7 +191,9 @@ Messages use custom inbox cards and Pi’s native run preparation, without extra
|
|
|
191
191
|
- **Subs have separate conversations.** They receive their task, profile, and normal project context—not Main's chat history. Include the details they need in the task.
|
|
192
192
|
- **Reloading Main does not close Subs.** Close tabs you no longer need. Hard crashes may require manual cleanup.
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Fresh and resumed Subs start from a private `0600` POSIX shell script in their existing runtime communication directory. The terminal receives only the fixed short command `sh "$PI_SUBAGENT_LAUNCH"`; the script path travels through the tab environment, and Pi's full arguments are read from the file. No shell probe or long terminal input is needed, avoiding the macOS canonical line-input limit even while the shell initializes. The plugin waits up to 60 seconds for the expected Pi in the same pane and terminal to become idle before naming it and submitting the task. Startup remains cancellable, and failures attempt to close the new tab. See [Herdr issue #4950](https://github.com/herdrdev/herdr/issues/4950).
|
|
195
|
+
|
|
196
|
+
The script unlinks itself after the shell opens it; Main also removes it once Pi is ready or a launch fails, times out, or is cancelled—even if tab rollback fails. Each new launch reclaims leftover launch scripts at least five minutes old only when their creator process is confirmed gone. Live processes, uncertain liveness, symlinks, unrelated files, and communication directories are preserved. Failures to inspect or remove abandoned scripts are warned about and skipped without blocking new launches or the rest of the sweep. Normal channel cleanup is unchanged; script cleanup does not depend on the operating system purging temporary files.
|
|
195
197
|
|
|
196
198
|
If a profile is missing or fails to start, run `/profiles` first. Check its directory/name match, project trust, and any referenced tools, skills, or extensions. If Herdr is unavailable, confirm Pi is running inside a Herdr workspace.
|
|
197
199
|
|
package/README.zh-CN.md
CHANGED
|
@@ -77,7 +77,7 @@ mkdir -p ~/.pi/agent/subagent/profiles/reviewer
|
|
|
77
77
|
| 结束使用 | 让 Pi 关闭子助手,或手动关闭其 Herdr 标签页 |
|
|
78
78
|
| 清理已删除 Main 遗留的 Sub 会话和通信文件 | `/clean-sub` |
|
|
79
79
|
|
|
80
|
-
在同一个主会话(Main)中,多次调用 `/sub:reviewer` 会复用该角色已打开的子助手(Sub)。如果它正在工作,新任务会排队。任务完成后,标签页不会自动关闭。
|
|
80
|
+
在同一个主会话(Main)中,多次调用 `/sub:reviewer` 会复用该角色已打开的子助手(Sub)。如果它正在工作,新任务会排队。任务完成后,标签页不会自动关闭。Sub 通过 `talk` 汇报进度后默认继续工作;只有完成任务或需要等待 Main 回复时,才用 `endTurn: true` 显式结束本轮。
|
|
81
81
|
|
|
82
82
|
**Sub-agents(子助手)** 列表只显示当前 Main 创建的助手:**Open** 包含所有已打开的子助手,包括空闲或状态未知的;**Resumable** 包含已确认关闭、且保存文件仍存在的持久化会话。其他 Main 或分叉会话不会继承这些助手,也不能恢复它们。重载或恢复同一个 Main 则会保留关联。
|
|
83
83
|
|
|
@@ -191,7 +191,9 @@ reviewer/
|
|
|
191
191
|
- **子助手拥有独立对话。** 它会收到任务、角色配置和常规项目上下文,但不会收到主对话的聊天记录。请在任务中提供必要信息。
|
|
192
192
|
- **重载主 Pi 不会关闭子助手。** 不再需要时请关闭标签页;异常崩溃后可能需要手动清理。
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
新建或恢复子助手时,插件会在已有的运行通信目录中创建权限为 `0600` 的私有 POSIX shell 启动脚本。终端只接收固定短命令 `sh "$PI_SUBAGENT_LAUNCH"`,脚本路径通过标签页环境变量传入,完整 Pi 参数从文件读取。不再使用 shell 探针,也不再向终端输入长行,因此即使 shell 仍在初始化,也不会触发 macOS 的 canonical 行输入长度限制。插件等待最多 60 秒,确认同一 pane 和 terminal 中检测到的 Pi 已进入空闲状态,再命名并提交任务。等待支持取消,启动失败会尝试关闭新建标签页。参见 [Herdr 已知问题 #4950](https://github.com/herdrdev/herdr/issues/4950)。
|
|
195
|
+
|
|
196
|
+
脚本在被 shell 打开后自行删除;Main 也会在 Pi 就绪或启动失败、超时、取消时兜底删除,即使标签页关闭失败也会尝试回收。每次新启动还会回收已超过 5 分钟、且创建进程确认不存在的残留启动脚本;仍存活的进程、存活状态不明的进程、符号链接、其他文件和通信目录不会被扫描清理逻辑删除。检查或删除遗留脚本失败时会输出警告并跳过,不会阻塞新启动或其他条目的回收。原有通信目录的生命周期保持不变,脚本回收不依赖操作系统定期清空临时目录。
|
|
195
197
|
|
|
196
198
|
如果找不到角色或启动失败,先运行 `/profiles`,检查目录名与 `name` 是否一致、项目是否受信任,以及引用的工具、技能和扩展是否可用。如果提示 Herdr 不可用,请确认 Pi 正在 Herdr 工作区内运行。
|
|
197
199
|
|
package/docs/messaging.md
CHANGED
|
@@ -7,6 +7,15 @@ Pi Subagent requires **Pi 1.2.0 or newer**. Main ↔ Sub messages use `pi.sendMe
|
|
|
7
7
|
|
|
8
8
|
There are no synthetic user-role wake messages, version fallbacks, or patches to Pi internals. Custom messages do not pass through user-input processing or execute peer text as slash commands or templates.
|
|
9
9
|
|
|
10
|
+
## Sub talk and turn control
|
|
11
|
+
|
|
12
|
+
Sub's `talk` accepts an optional `endTurn` boolean, defaulting to `false`:
|
|
13
|
+
|
|
14
|
+
- Omitted or `false`: send the message and continue working. Use this for progress updates; no acknowledgement or "continue" reply from Main is required.
|
|
15
|
+
- `true`: send the message and end the current turn. Use this for completed work or a blocking question that requires Main's reply. Send a terminating `talk` on its own: Pi only ends the tool batch early when every tool result in it is terminating.
|
|
16
|
+
|
|
17
|
+
Ending a turn does not close the Sub or discard queued follow-ups. Main can still send further work through `talk`. Message text should make clear whether it reports progress, asks a blocking question, or delivers completed work; Main must not treat every message as task completion. Main's own `talk` does not end its turn.
|
|
18
|
+
|
|
10
19
|
## Delivery guarantees
|
|
11
20
|
|
|
12
21
|
- `content` carries the peer message and routing instructions; `details` carries rendering and receipt metadata and is not sent to the model.
|
package/package.json
CHANGED
package/src/adapters/herdr.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { subSessionName } from "../manual-context.js";
|
|
6
6
|
import { subCapabilityArgs } from "../profiles/launch-args.js";
|
|
7
|
+
import { LAUNCH_COMMAND, launchScriptContent, launchScriptPath, reapAbandonedLaunchScripts } from "./launch-script.js";
|
|
7
8
|
import type { SubAgentStatus, SubLaunchSpec, SubSurfaceAdapter, SurfaceHandle } from "../types.js";
|
|
8
9
|
|
|
9
10
|
function parseEnvelope(stdout: string): Record<string, unknown> {
|
|
@@ -70,6 +71,8 @@ export class HerdrTabAdapter implements SubSurfaceAdapter {
|
|
|
70
71
|
const workspaceId = process.env.HERDR_WORKSPACE_ID;
|
|
71
72
|
if (!workspaceId) throw new Error("HERDR_WORKSPACE_ID is unavailable; Pi is not running in a Herdr workspace.");
|
|
72
73
|
signal?.throwIfAborted();
|
|
74
|
+
reapAbandonedLaunchScripts();
|
|
75
|
+
const launcher = launchScriptPath(spec.channelDir);
|
|
73
76
|
const label = `↳ pi · ${safeLabel(spec.title)}`;
|
|
74
77
|
const created = await this.pi.exec("herdr", [
|
|
75
78
|
"tab", "create",
|
|
@@ -79,6 +82,7 @@ export class HerdrTabAdapter implements SubSurfaceAdapter {
|
|
|
79
82
|
"--env", "PI_SUBAGENT_ROLE=sub",
|
|
80
83
|
"--env", `PI_SUBAGENT_CHANNEL=${spec.channelDir}`,
|
|
81
84
|
"--env", `PI_SUBAGENT_TOKEN=${spec.token}`,
|
|
85
|
+
"--env", `PI_SUBAGENT_LAUNCH=${launcher}`,
|
|
82
86
|
"--no-focus",
|
|
83
87
|
], { timeout: 15_000, signal });
|
|
84
88
|
let payload: Record<string, unknown>;
|
|
@@ -90,29 +94,34 @@ export class HerdrTabAdapter implements SubSurfaceAdapter {
|
|
|
90
94
|
const paneId = nestedString(payload, "root_pane", "pane_id") ?? nestedString(payload, "pane", "pane_id");
|
|
91
95
|
if (!tabId) throw new Error("Herdr tab creation response did not contain a tab ID; automatic cleanup is unavailable.");
|
|
92
96
|
const handle: SurfaceHandle = { adapter: "herdr", tabId, ...(paneId ? { paneId } : {}) };
|
|
97
|
+
const terminalId = nestedString(payload, "root_pane", "terminal_id") ?? nestedString(payload, "pane", "terminal_id");
|
|
98
|
+
let launcherOwned = false;
|
|
99
|
+
const removeLauncher = () => {
|
|
100
|
+
if (!launcherOwned) return;
|
|
101
|
+
fs.rmSync(launcher, { force: true });
|
|
102
|
+
launcherOwned = false;
|
|
103
|
+
};
|
|
93
104
|
const step = async (args: string[], timeout: number, failure: string) => {
|
|
94
105
|
signal?.throwIfAborted();
|
|
95
106
|
const result = await this.pi.exec("herdr", args, { timeout, signal });
|
|
96
|
-
if (result.code !== 0 || result.killed) throw new Error(result.stderr || failure);
|
|
97
107
|
signal?.throwIfAborted();
|
|
108
|
+
if (result.code !== 0 || result.killed) throw new Error(result.stderr || failure);
|
|
98
109
|
};
|
|
99
110
|
try {
|
|
100
111
|
if (created.code !== 0 || created.killed) throw new Error(created.stderr || "Failed to create Herdr tab.");
|
|
101
112
|
if (!paneId) throw new Error("Herdr tab creation response did not contain a root pane ID.");
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await step([
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
// The file is private and exclusive; even a partial write is owned and
|
|
114
|
+
// cleaned up on failure. Its path travels via tab env, not PTY input.
|
|
115
|
+
const fd = fs.openSync(launcher, "wx", 0o600);
|
|
116
|
+
launcherOwned = true;
|
|
117
|
+
try { fs.writeFileSync(fd, launchScriptContent(subArgs(spec)), "utf8"); }
|
|
118
|
+
finally { fs.closeSync(fd); }
|
|
119
|
+
await step(["pane", "run", paneId, LAUNCH_COMMAND], 10_000, "Failed to launch Sub Pi in Herdr tab.");
|
|
120
|
+
await this.waitForPi(handle, terminalId, signal);
|
|
121
|
+
// pane run only acknowledges admission; never unlink on that alone.
|
|
122
|
+
removeLauncher();
|
|
112
123
|
const name = agentName(spec.runId);
|
|
113
|
-
await step([
|
|
114
|
-
"agent", "start", name, "--kind", "pi", "--pane", paneId, "--timeout", "60000", "--", ...subArgs(spec),
|
|
115
|
-
], 70_000, "Failed to start Sub Pi in Herdr tab.");
|
|
124
|
+
await step(["agent", "rename", paneId, name], 10_000, "Failed to name the Sub Pi agent.");
|
|
116
125
|
await step([
|
|
117
126
|
"pane", "report-metadata", paneId, "--source", "subagent",
|
|
118
127
|
"--token", "subagent_role=sub", "--token", `subagent_main_session=${spec.mainSessionId}`,
|
|
@@ -121,16 +130,53 @@ export class HerdrTabAdapter implements SubSurfaceAdapter {
|
|
|
121
130
|
await step(["agent", "prompt", name, spec.task], 20_000, "Failed to submit the delegated task to the Herdr agent.");
|
|
122
131
|
return handle;
|
|
123
132
|
} catch (error) {
|
|
133
|
+
let rollback: unknown;
|
|
124
134
|
try {
|
|
125
135
|
// Rollback must not inherit the cancellation that caused the failure.
|
|
126
136
|
await this.close(handle);
|
|
127
|
-
} catch (cleanup) {
|
|
128
|
-
|
|
137
|
+
} catch (cleanup) { rollback = cleanup; }
|
|
138
|
+
try { removeLauncher(); }
|
|
139
|
+
catch (cleanup) {
|
|
140
|
+
error = new AggregateError([error, cleanup], `Sub launch failed: ${error instanceof Error ? error.message : String(error)}. Launch script cleanup also failed (${launcher}): ${cleanup instanceof Error ? cleanup.message : String(cleanup)}.`, { cause: error });
|
|
129
141
|
}
|
|
142
|
+
if (rollback) throw new HerdrLaunchCleanupError(handle, error, rollback);
|
|
130
143
|
throw error;
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
private async waitForPi(handle: SurfaceHandle, terminalId: string | undefined, signal?: AbortSignal): Promise<void> {
|
|
148
|
+
const deadline = Date.now() + 60_000;
|
|
149
|
+
while (true) {
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
const remaining = deadline - Date.now();
|
|
152
|
+
if (remaining <= 0) throw new Error("Sub Pi did not become ready within 60 seconds.");
|
|
153
|
+
const response = await this.pi.exec("herdr", ["agent", "get", handle.paneId!], { timeout: Math.min(3000, remaining), signal });
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
if (Date.now() >= deadline) throw new Error("Sub Pi did not become ready within 60 seconds.");
|
|
156
|
+
if (response.killed) throw new Error(response.stderr || "Herdr agent readiness check was interrupted.");
|
|
157
|
+
let payload: Record<string, unknown>;
|
|
158
|
+
try { payload = parseEnvelope(`${response.stdout}\n${response.stderr}`); }
|
|
159
|
+
catch (error) { throw new Error(response.stderr || "Invalid Herdr agent readiness response.", { cause: error }); }
|
|
160
|
+
if (response.code !== 0) {
|
|
161
|
+
if (nestedString(payload, "error", "code") !== "agent_not_found") {
|
|
162
|
+
throw new Error(response.stderr || "Failed to inspect the starting Sub Pi agent.");
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const agent = payload.agent as Record<string, unknown> | undefined;
|
|
166
|
+
if (!agent || typeof agent !== "object") throw new Error("Herdr readiness response did not contain an agent.");
|
|
167
|
+
if (agent.pane_id !== handle.paneId || agent.tab_id !== handle.tabId
|
|
168
|
+
|| (terminalId && agent.terminal_id !== terminalId)) throw new Error("The Sub Pi terminal changed during startup.");
|
|
169
|
+
if (agent.agent && agent.agent !== "pi") throw new Error(`Expected Sub Pi, but Herdr detected ${agent.agent}.`);
|
|
170
|
+
if (agent.agent_status === "blocked") throw new Error("Sub Pi is blocked during startup and is not ready for prompts.");
|
|
171
|
+
// interactive_ready is a managed-agent-start-only field in Herdr.
|
|
172
|
+
// Unmanaged Pi readiness comes from its lifecycle hook or UI detector.
|
|
173
|
+
if (agent.agent === "pi" && !agent.launch_pending && (agent.agent_status === "idle" || agent.agent_status === "done")) return;
|
|
174
|
+
}
|
|
175
|
+
try { await delay(Math.max(0, Math.min(100, deadline - Date.now())), undefined, { signal }); }
|
|
176
|
+
catch (error) { signal?.throwIfAborted(); throw error; }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
134
180
|
async status(handle: SurfaceHandle | undefined): Promise<SubAgentStatus> {
|
|
135
181
|
if (!handle?.paneId) return "unknown";
|
|
136
182
|
try {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { runtimeRoot } from "../channel.js";
|
|
5
|
+
|
|
6
|
+
// Only this fixed-size command crosses the PTY, never the path or Pi arguments.
|
|
7
|
+
export const LAUNCH_COMMAND = 'sh "$PI_SUBAGENT_LAUNCH"';
|
|
8
|
+
export const ABANDONED_LAUNCH_AGE_MS = 5 * 60_000;
|
|
9
|
+
const LAUNCH_FILE = /^launch-([1-9]\d*)-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.sh$/;
|
|
10
|
+
|
|
11
|
+
export function launchScriptPath(channelDir: string): string {
|
|
12
|
+
return path.join(channelDir, `launch-${process.pid}-${randomUUID()}.sh`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function launchScriptContent(args: readonly string[]): string {
|
|
16
|
+
const quoted = args.map((arg) => `'${arg.replaceAll("'", "'\\''")}'`);
|
|
17
|
+
// The shell has opened the file by this point and can keep reading its FD
|
|
18
|
+
// after unlinking it. Self-removal also works if Main has already crashed.
|
|
19
|
+
return `#!/bin/sh\nrm -f -- "$0"\nunset PI_SUBAGENT_LAUNCH\nexec pi ${quoted.join(" ")}\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function warnReapError(target: string, error: unknown): void {
|
|
23
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
24
|
+
console.warn(`Pi Subagent: abandoned launch script cleanup skipped (${target}): ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function entries(directory: string): fs.Dirent[] {
|
|
28
|
+
try { return fs.readdirSync(directory, { withFileTypes: true }); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
warnReapError(directory, error);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Best-effort sweep of expired launch files belonging to dead processes, never channels. */
|
|
36
|
+
export function reapAbandonedLaunchScripts(root = runtimeRoot(), now = Date.now()): void {
|
|
37
|
+
// Directory entries, rather than stat-followed paths, keep symlinked trees
|
|
38
|
+
// out of the sweep. Each run's normal channel cleanup remains independent.
|
|
39
|
+
try { if (!fs.lstatSync(root).isDirectory()) return; }
|
|
40
|
+
catch (error) {
|
|
41
|
+
warnReapError(root, error);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const owner of entries(root)) {
|
|
45
|
+
if (!owner.isDirectory()) continue;
|
|
46
|
+
const ownerDir = path.join(root, owner.name);
|
|
47
|
+
for (const run of entries(ownerDir)) {
|
|
48
|
+
if (!run.isDirectory()) continue;
|
|
49
|
+
const directory = path.join(ownerDir, run.name);
|
|
50
|
+
for (const entry of entries(directory)) {
|
|
51
|
+
const match = LAUNCH_FILE.exec(entry.name);
|
|
52
|
+
if (!entry.isFile() || !match) continue;
|
|
53
|
+
const file = path.join(directory, entry.name);
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.lstatSync(file);
|
|
56
|
+
if (!stat.isFile() || now - stat.mtimeMs < ABANDONED_LAUNCH_AGE_MS
|
|
57
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())) continue;
|
|
58
|
+
const pid = Number(match[1]);
|
|
59
|
+
if (!Number.isSafeInteger(pid) || pid > 0x7fffffff) continue;
|
|
60
|
+
try { process.kill(pid, 0); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
// EPERM and other ambiguous liveness failures must preserve it.
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") fs.unlinkSync(file);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
warnReapError(file, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -13,7 +13,9 @@ export function bindPromptSections(pi: ExtensionAPI, keys: readonly string[], ge
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const SUB_PROTOCOL = `## Pi Subagent Sub protocol
|
|
16
|
-
You are an isolated Sub Pi working with a Main Pi. You have not received the Main's conversation and must not read Pi session files.
|
|
16
|
+
You are an isolated Sub Pi working with a Main Pi. You have not received the Main's conversation and must not read Pi session files.
|
|
17
|
+
Use talk to report progress, ask for information, or deliver completed work. Make clear in the message whether it is a progress update, a blocking question, or completed work. By default, talk only sends the message. After a progress update, continue the assigned task without waiting for Main to acknowledge or say 'continue'.
|
|
18
|
+
Set endTurn=true only when the task is complete or you must wait for Main's reply before proceeding. Call terminating talk on its own, without other tools in the same batch. Ending a turn does not close the session; remain available for follow-ups. The Main alone decides whether to respond, request more work, or close this Sub session. Never close the session yourself.`;
|
|
17
19
|
|
|
18
20
|
export function applySubPromptSections(sections: Record<string, string>, profile: ResolvedProfile): void {
|
|
19
21
|
sections.subagent_sub_protocol = SUB_PROTOCOL;
|
package/src/tools/main.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function registerMainTools(pi: ExtensionAPI, manager: MainRunManager): vo
|
|
|
58
58
|
"Use delegate for work assigned by MAIN.md or a separable task that benefits from an independent context; select an explicitly configured profile and provide a short informative title.",
|
|
59
59
|
"After delegate launches, do not wait or repeatedly poll. Pi Subagent will wake the Main when the Sub uses talk.",
|
|
60
60
|
"For Subs labeled user-invoked specialists, follow up only on the user's task and results. Do not assign unrelated work or substitute them for general consulting profiles. New tasks are initiated by the user through a profile command.",
|
|
61
|
-
"Use talk to respond to an existing Sub, interrupt_sub to stop its current turn without closing it, and close_sub only after its
|
|
61
|
+
"Use talk to respond to an existing Sub, interrupt_sub to stop its current turn without closing it, and close_sub only after its completed work is accepted or the user asks to close it.",
|
|
62
62
|
],
|
|
63
63
|
parameters: Type.Object({
|
|
64
64
|
title: Type.String({ description: "Short Herdr tab title, up to 48 displayed characters." }),
|
|
@@ -131,6 +131,7 @@ export function registerMainTools(pi: ExtensionAPI, manager: MainRunManager): vo
|
|
|
131
131
|
label: "talk",
|
|
132
132
|
description: "Send one message to an existing Sub Pi. Messages enter the Sub's native follow-up queue and are processed after its current work. Format non-trivial messages as readable Markdown with paragraph breaks and lists.",
|
|
133
133
|
promptSnippet: "Send a message to an existing Sub Pi",
|
|
134
|
+
promptGuidelines: ["Sub talk messages may be progress updates while the Sub keeps working. Do not send routine 'continue' replies or close a Sub merely for reporting progress; respond with talk when input is needed, and use close_sub only after completed work is accepted or the user asks to close it."],
|
|
134
135
|
executionMode: "sequential",
|
|
135
136
|
parameters: Type.Object({
|
|
136
137
|
runId: Type.String({ description: "Full Sub run ID or a unique prefix." }),
|
package/src/tools/sub.ts
CHANGED
|
@@ -183,12 +183,16 @@ export function registerSub(pi: ExtensionAPI): void {
|
|
|
183
183
|
name: "talk",
|
|
184
184
|
label: "talk",
|
|
185
185
|
exposure: "model-only",
|
|
186
|
-
description: "Send one message to the Main Pi
|
|
187
|
-
promptSnippet: "Send a message to the Main Pi",
|
|
188
|
-
promptGuidelines: [
|
|
186
|
+
description: "Send one message to the Main Pi. Continue working by default; set endTurn=true only when the task is complete or you must wait for Main's reply. Format non-trivial messages as readable Markdown with paragraph breaks and lists.",
|
|
187
|
+
promptSnippet: "Send a message to the Main Pi, optionally ending the current turn",
|
|
188
|
+
promptGuidelines: [
|
|
189
|
+
"Use talk for progress updates, questions, and completed work. After progress updates, continue the task without waiting for an acknowledgement.",
|
|
190
|
+
"Set talk endTurn=true only when the task is complete or you must wait for Main's reply. Call terminating talk on its own, without other tools in the same batch. The Main decides when to close the Sub session.",
|
|
191
|
+
],
|
|
189
192
|
executionMode: "sequential",
|
|
190
193
|
parameters: Type.Object({
|
|
191
194
|
message: Type.String({ description: "Message to the other Agent. For non-trivial content, use readable Markdown with paragraph breaks and lists." }),
|
|
195
|
+
endTurn: Type.Optional(Type.Boolean({ description: "End the current turn after sending. Defaults to false: continue working. Set true only when the task is complete or you must wait for Main's reply." })),
|
|
192
196
|
}),
|
|
193
197
|
renderCall(args, theme, context) {
|
|
194
198
|
const message = typeof args.message === "string" ? args.message : "";
|
|
@@ -205,7 +209,7 @@ export function registerSub(pi: ExtensionAPI): void {
|
|
|
205
209
|
return {
|
|
206
210
|
content: [{ type: "text", text: "Message queued for the Main Pi." }],
|
|
207
211
|
details: { messageId: message.id },
|
|
208
|
-
terminate: true,
|
|
212
|
+
terminate: params.endTurn === true,
|
|
209
213
|
};
|
|
210
214
|
},
|
|
211
215
|
renderResult(result, _options, theme, context) {
|