@oldflag2333333/pi-subagent 0.1.1 → 0.1.3
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 +22 -3
- package/README.zh-CN.md +22 -3
- package/docs/messaging.md +7 -18
- package/package.json +6 -6
- package/src/inbox-state.ts +0 -6
- package/src/profiles/loader.ts +21 -1
- package/src/profiles/tool-sources.ts +2 -1
- package/src/profiles/types.ts +2 -0
- package/src/talk-delivery.ts +2 -5
- package/docs/third-party-notices.md +0 -25
- package/src/parent-wake.ts +0 -86
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A **profile** defines a specialist's instructions, tools, and optional skills or
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
You need **Node.js 22.19+**, **[Pi](https://pi.dev) 1.0
|
|
11
|
+
You need **Node.js 22.19+**, **[Pi](https://pi.dev) 1.2.0+**, and **Herdr**. To launch specialists, run Pi inside a Herdr workspace with the `herdr` command available.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
pi install npm:@oldflag2333333/pi-subagent
|
|
@@ -117,9 +117,28 @@ Add these fields to `config.json` as needed:
|
|
|
117
117
|
| `"model": "provider/model-id"` | Choose a model available in your Pi configuration. |
|
|
118
118
|
| `"thinkingLevel": "high"` | Request a thinking level supported by the selected model. |
|
|
119
119
|
| `"skills": ["skill-name", "./path/to/SKILL.md"]` | Include installed skills or paths relative to the profile. |
|
|
120
|
+
| `"extensions": ["npm:pi-model-calibration", "./hooks.ts"]` | Explicitly load extra extensions in Subs, including prompt/event plugins that provide no tools. |
|
|
120
121
|
|
|
121
122
|
For a persistent specialist started with `/sub:reviewer`, invoking the command again from the same saved Main session resumes it after closure. A different Main session gets its own specialist. Manual-only profiles are not available for Pi to delegate on its own.
|
|
122
123
|
|
|
124
|
+
### Sub extension allowlist
|
|
125
|
+
|
|
126
|
+
Add this to the profile's `config.json`, for example:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
"extensions": ["npm:pi-model-calibration"]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Entries accept Pi's npm, `git:`, HTTP(S) repository URLs, `builtin:` identifiers, and local extension files or package directories. Relative local paths resolve from the **configuration file's directory**; absolute paths and `~/` work too. Missing local paths fail startup; repeated entries load once. Package specs are resolved by Pi in the Sub and may download or install packages, never while Main reads the profile.
|
|
133
|
+
|
|
134
|
+
`npm:` / `git:` entries use Pi's temporary package cache; a globally installed package may not be present there. For offline use, list the installed entry file directly, for example `"extensions": ["~/.pi/agent/npm/node_modules/pi-model-calibration/index.ts"]`, to avoid another download.
|
|
135
|
+
|
|
136
|
+
Omitting the field or using `[]` loads no extra plugins. Subs still use `--no-extensions`: only communication/Herdr integration, normal MCP support, selected tools' extension dependencies, and this allowlist load explicitly. Other Main plugins are not inherited. Project profiles still require project trust; same-named overrides replace rather than merge allowlists.
|
|
137
|
+
|
|
138
|
+
This field controls **Sub launches only**; it does not dynamically load extensions in the current process started with `pi --profile`. Use Pi's `-e` or normal extension configuration for that case. Loading an extension does not automatically add its tools to the profile's `tools`; custom non-MCP tools must still be available in Main.
|
|
139
|
+
|
|
140
|
+
Extensions can execute code, read context, and change prompts. **This is not a sandbox**; list only trusted plugins. After editing, reload Main and close/reopen affected Subs. For `model-calibration`, its rules must also match the Sub's selected model before calibration text is injected.
|
|
141
|
+
|
|
123
142
|
### Profile-specific skills
|
|
124
143
|
|
|
125
144
|
Put skills inside the profile directory to load them only when that profile is selected. No `skills` field is needed:
|
|
@@ -157,7 +176,7 @@ Close any orphaned Sub tabs before cleaning; the command only deletes files. Pro
|
|
|
157
176
|
|
|
158
177
|
## Things to know
|
|
159
178
|
|
|
160
|
-
Messages use custom inbox cards
|
|
179
|
+
Messages use custom inbox cards and Pi’s native run preparation, without extra user-role wake messages. See [messaging behavior](docs/messaging.md).
|
|
161
180
|
|
|
162
181
|
- **Herdr is required for Subs.** There is no headless fallback. Up to four Subs can be open at once, and Subs cannot delegate further.
|
|
163
182
|
- **Profiles are not a sandbox.** Subs run as your OS user. A read-only role is not a filesystem permission boundary.
|
|
@@ -165,7 +184,7 @@ Messages use custom inbox cards. An idle receiver also gets a short user-role wa
|
|
|
165
184
|
- **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.
|
|
166
185
|
- **Reloading Main does not close Subs.** Close tabs you no longer need. Hard crashes may require manual cleanup.
|
|
167
186
|
|
|
168
|
-
If a profile is missing or fails to start, run `/profiles` first. Check its directory/name match, project trust, and any referenced tools or
|
|
187
|
+
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.
|
|
169
188
|
|
|
170
189
|
To hide or show Pi Subagent Subs in Herdr's Agents panel, see the optional [Herdr companion plugin](herdr-plugin/README.md).
|
|
171
190
|
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
11
|
-
需要 **Node.js 22.19+**、**[Pi](https://pi.dev) 1.0
|
|
11
|
+
需要 **Node.js 22.19+**、**[Pi](https://pi.dev) 1.2.0+** 和 **Herdr**。启动助手时,Pi 必须运行在 Herdr 工作区中,并且能调用 `herdr` 命令。
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
pi install npm:@oldflag2333333/pi-subagent
|
|
@@ -117,9 +117,28 @@ pi --profile reviewer
|
|
|
117
117
|
| `"model": "provider/model-id"` | 指定 Pi 配置中可用的模型。 |
|
|
118
118
|
| `"thinkingLevel": "high"` | 请求所选模型支持的思考级别。 |
|
|
119
119
|
| `"skills": ["skill-name", "./path/to/SKILL.md"]` | 加载已安装的技能,或使用相对于角色配置的路径。 |
|
|
120
|
+
| `"extensions": ["npm:pi-model-calibration", "./hooks.ts"]` | 为子助手显式加载额外扩展,包括不提供工具的提示词或事件插件。 |
|
|
120
121
|
|
|
121
122
|
通过 `/sub:reviewer` 启动的持久化助手,在关闭后,可以从同一个已保存的主会话中再次执行该命令来恢复。不同的主会话各自拥有独立的助手。设置为手动调用的角色不能由 Pi 自行委派。
|
|
122
123
|
|
|
124
|
+
### 子助手扩展白名单
|
|
125
|
+
|
|
126
|
+
在角色的 `config.json` 中添加,例如:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
"extensions": ["npm:pi-model-calibration"]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
支持 Pi 的 npm、`git:`、HTTP(S) 仓库地址、`builtin:` 标识,以及本地扩展文件或包目录。本地相对路径以 **配置文件所在目录** 为基准,也支持绝对路径和 `~/`。本地路径不存在时,启动会报错;重复条目只加载一次。包来源交给子助手中的 Pi 解析,可能触发下载或安装,不会在主 Pi 读取角色配置时执行。
|
|
133
|
+
|
|
134
|
+
`npm:` / `git:` 条目使用 Pi 的临时包缓存,已全局安装的包不一定在该缓存中;离线使用时,可直接列出已安装的入口文件,例如 `"extensions": ["~/.pi/agent/npm/node_modules/pi-model-calibration/index.ts"]`,避免重新下载。
|
|
135
|
+
|
|
136
|
+
不配置或设置 `[]` 时,不额外加载插件。子助手仍使用 `--no-extensions`:只显式加载通信与 Herdr 集成、常规 MCP 支持、角色工具依赖的扩展,以及此白名单,不会自动继承主 Pi 的其他插件。项目角色仍须经过项目信任,同名角色覆盖时不会合并白名单。
|
|
137
|
+
|
|
138
|
+
这个字段只控制 **Sub 启动**,不会给使用 `pi --profile` 的当前进程动态加载扩展;该场景仍需使用 Pi 的 `-e` 或常规扩展配置。扩展的加载不会自动将其工具加入角色的 `tools`;自定义非 MCP 工具仍须在主 Pi 中可用。
|
|
139
|
+
|
|
140
|
+
扩展可以执行代码、读取上下文并修改提示词,**白名单不是沙箱**,只添加你信任的插件。修改后重载主 Pi,并关闭、重新打开相关子助手。对于 `model-calibration`,加载后还需其规则匹配子助手所选模型才会注入校准提示词。
|
|
141
|
+
|
|
123
142
|
### 角色专属技能
|
|
124
143
|
|
|
125
144
|
将技能放在角色目录中,就只会在选中该角色时加载,无需填写 `skills` 字段:
|
|
@@ -157,7 +176,7 @@ reviewer/
|
|
|
157
176
|
|
|
158
177
|
## 使用须知
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
助手间消息使用自定义收件卡片,由 Pi 原生处理启动准备,不再添加用户角色唤醒消息。详见[消息机制](docs/messaging.md)。
|
|
161
180
|
|
|
162
181
|
- **子助手依赖 Herdr。** 不支持无界面后台运行;最多同时打开四个子助手,子助手不能继续委派。
|
|
163
182
|
- **角色配置不是沙箱。** 子助手以你的系统用户身份运行,配置“只读角色”不等于限制文件系统权限。
|
|
@@ -165,7 +184,7 @@ reviewer/
|
|
|
165
184
|
- **子助手拥有独立对话。** 它会收到任务、角色配置和常规项目上下文,但不会收到主对话的聊天记录。请在任务中提供必要信息。
|
|
166
185
|
- **重载主 Pi 不会关闭子助手。** 不再需要时请关闭标签页;异常崩溃后可能需要手动清理。
|
|
167
186
|
|
|
168
|
-
如果找不到角色或启动失败,先运行 `/profiles`,检查目录名与 `name`
|
|
187
|
+
如果找不到角色或启动失败,先运行 `/profiles`,检查目录名与 `name` 是否一致、项目是否受信任,以及引用的工具、技能和扩展是否可用。如果提示 Herdr 不可用,请确认 Pi 正在 Herdr 工作区内运行。
|
|
169
188
|
|
|
170
189
|
如需在 Herdr 的 Agents 面板中隐藏或显示 Pi Subagent 子助手,可安装可选的 [Herdr 配套插件](herdr-plugin/README.md)(英文说明)。
|
|
171
190
|
|
package/docs/messaging.md
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
# Message delivery and rendering
|
|
2
2
|
|
|
3
|
-
Main ↔ Sub messages
|
|
3
|
+
Pi Subagent requires **Pi 1.2.0 or newer**. Main ↔ Sub messages use `pi.sendMessage()` with `deliverAs: "followUp"` and `triggerTurn: true` in both directions:
|
|
4
4
|
|
|
5
|
-
- **Busy receiver:**
|
|
6
|
-
- **Idle receiver:**
|
|
5
|
+
- **Busy receiver:** Pi queues the custom message behind its current work.
|
|
6
|
+
- **Idle receiver:** Pi prepares and starts the run, including `before_agent_start`, the tool loadout, and prompt sections. The event has `messageType: "custom"`, with the starting `message` rather than a user `prompt`.
|
|
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
10
|
## Delivery guarantees
|
|
11
11
|
|
|
12
12
|
- `content` carries the peer message and routing instructions; `details` carries rendering and receipt metadata and is not sent to the model.
|
|
13
13
|
- Custom cards retain their title, collapsed/expanded rendering, and saved-session rendering.
|
|
14
|
-
- A message file is removed only after its receipt appears in the receiving session.
|
|
15
|
-
-
|
|
16
|
-
- If a wake never starts, a later idle delivery can request another wake after ten seconds. The deadline does not create a timer or promise autonomous retry after an input hook handles the wake.
|
|
17
|
-
- A synchronous wake failure keeps the message file retryable; retrying does not append the custom card again. Pi's fire-and-forget extension API does not expose asynchronous prompt failures as delivery receipts.
|
|
14
|
+
- A message file is removed only after its receipt appears in the receiving session. Existing user-message receipts remain recognized to avoid redelivery of saved messages; this is data compatibility, not support for older Pi runtimes.
|
|
15
|
+
- In-flight message IDs prevent duplicate submission. Synchronous submission failures leave the file retryable. Pi owns run preparation and queueing; the plugin does not keep a separate wake reservation or retry timer.
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Pi 1.0.4 and 1.1.0 have an idle custom-message wake path that bypasses `before_agent_start` and can drop profile prompt sections:
|
|
22
|
-
|
|
23
|
-
- [#5581: custom-message starts bypass run preparation](https://github.com/earendil-works/pi/issues/5581)
|
|
24
|
-
- [#10267: contributed prompt sections are dropped](https://github.com/earendil-works/pi/issues/10267)
|
|
25
|
-
|
|
26
|
-
The implementation adapts [nicobailon/pi-subagents' parentWake](https://github.com/nicobailon/pi-subagents/blob/0c33ec7cb26ed1db270d72e746c3c975be880aeb/src/shared/parent-wake.ts), with retries for synchronous wake failures. Attribution and the MIT license are in [third-party notices](third-party-notices.md).
|
|
27
|
-
|
|
28
|
-
Real Pi SDK tests cover the first idle wake, subsequent turns, busy follow-ups, and reload. The previous upstream TODO is now a required passing regression test. This workaround does not patch Pi internals or claim to fix unrelated retry/resume paths in Pi itself.
|
|
17
|
+
Pi 1.2.0 includes the native custom-message preparation fix in [#10739](https://github.com/earendil-works/pi/pull/10739), resolving [#5581](https://github.com/earendil-works/pi/issues/5581) and [#10267](https://github.com/earendil-works/pi/issues/10267). Real SDK regression tests require profile sections to remain present on the first idle delivery, subsequent turns, busy follow-ups, and after reload. No upstream TODO or workaround is retained.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oldflag2333333/pi-subagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Reusable specialist profiles and independent sub-agents for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-agent-core": "*",
|
|
47
47
|
"@earendil-works/pi-ai": "*",
|
|
48
|
-
"@earendil-works/pi-coding-agent": ">=1.0
|
|
48
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
49
49
|
"@earendil-works/pi-tui": "*",
|
|
50
50
|
"typebox": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^1.0
|
|
54
|
-
"@earendil-works/pi-ai": "^1.0
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^1.0
|
|
56
|
-
"@earendil-works/pi-tui": "^1.0
|
|
53
|
+
"@earendil-works/pi-agent-core": "^1.2.0",
|
|
54
|
+
"@earendil-works/pi-ai": "^1.2.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "^1.2.0",
|
|
56
|
+
"@earendil-works/pi-tui": "^1.2.0",
|
|
57
57
|
"@types/node": "^22.15.0",
|
|
58
58
|
"@types/semver": "^7.8.0",
|
|
59
59
|
"semver": "^7.8.5",
|
package/src/inbox-state.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { MESSAGE_TYPE, type TalkDirection } from "./channel.js";
|
|
3
3
|
import type { DelegateManifest, TalkMessage } from "./types.js";
|
|
4
4
|
import { readTalkReceipt } from "./talk-message.js";
|
|
5
|
-
import { parentWakeFor } from "./parent-wake.js";
|
|
6
5
|
|
|
7
6
|
type ReadonlySessionManager = ExtensionContext["sessionManager"];
|
|
8
7
|
interface PendingTalk {
|
|
@@ -69,7 +68,6 @@ export function bindInboxEvents(pi: ExtensionAPI, getContext: () => ExtensionCon
|
|
|
69
68
|
pi.on("agent_start", () => {
|
|
70
69
|
const ctx = getContext();
|
|
71
70
|
if (!ctx) return;
|
|
72
|
-
parentWakeFor(pi, ctx).agentStarted();
|
|
73
71
|
const value = state(ctx);
|
|
74
72
|
value.runSerial++;
|
|
75
73
|
value.aborted = ctx.signal?.aborted === true;
|
|
@@ -78,10 +76,6 @@ export function bindInboxEvents(pi: ExtensionAPI, getContext: () => ExtensionCon
|
|
|
78
76
|
ctx.signal?.addEventListener("abort", () => { if (value.runSerial === runSerial) value.aborted = true; }, { once: true });
|
|
79
77
|
wakePending();
|
|
80
78
|
});
|
|
81
|
-
pi.on("session_shutdown", (event, ctx) => {
|
|
82
|
-
// Use the event context: manager shutdown may already have cleared its active context.
|
|
83
|
-
parentWakeFor(pi, ctx).sessionShutdown(event.reason);
|
|
84
|
-
});
|
|
85
79
|
pi.on("agent_end", (event) => {
|
|
86
80
|
const ctx = getContext();
|
|
87
81
|
if (!ctx) return;
|
package/src/profiles/loader.ts
CHANGED
|
@@ -26,6 +26,7 @@ const ALLOWED_KEYS = new Set([
|
|
|
26
26
|
"invocation",
|
|
27
27
|
"tools",
|
|
28
28
|
"skills",
|
|
29
|
+
"extensions",
|
|
29
30
|
"instructions",
|
|
30
31
|
]);
|
|
31
32
|
|
|
@@ -48,6 +49,7 @@ function parseProfile(value: unknown, expectedName: string): ProfileDefinition {
|
|
|
48
49
|
}
|
|
49
50
|
if (!stringArray(value.tools, 128) || value.tools.length === 0) throw new Error("tools must be a non-empty string array with at most 128 entries");
|
|
50
51
|
if (value.skills !== undefined && !stringArray(value.skills, 128)) throw new Error("skills must be a string array with at most 128 entries");
|
|
52
|
+
if (value.extensions !== undefined && !stringArray(value.extensions, 128)) throw new Error("extensions must be a string array with at most 128 entries");
|
|
51
53
|
if (value.sessionPersistence !== undefined && value.sessionPersistence !== "ephemeral" && value.sessionPersistence !== "persistent") {
|
|
52
54
|
throw new Error("sessionPersistence must be 'ephemeral' or 'persistent'");
|
|
53
55
|
}
|
|
@@ -76,6 +78,7 @@ function parseProfile(value: unknown, expectedName: string): ProfileDefinition {
|
|
|
76
78
|
? { sessionPersistence: value.sessionPersistence }
|
|
77
79
|
: {}),
|
|
78
80
|
...(Array.isArray(value.skills) ? { skills: [...new Set(value.skills as string[])] } : {}),
|
|
81
|
+
...(Array.isArray(value.extensions) ? { extensions: [...new Set((value.extensions as string[]).map((entry) => entry.trim()))] } : {}),
|
|
79
82
|
...(typeof value.instructions === "string" ? { instructions: value.instructions } : {}),
|
|
80
83
|
};
|
|
81
84
|
}
|
|
@@ -294,6 +297,23 @@ function discoverProfileSkills(profile: LoadedProfile): string[] {
|
|
|
294
297
|
return skills;
|
|
295
298
|
}
|
|
296
299
|
|
|
300
|
+
function resolveExtension(reference: string, profile: LoadedProfile): string {
|
|
301
|
+
// Pi resolves package and built-in specs in the Sub process. Do not install
|
|
302
|
+
// packages or execute extension code while reading profiles in Main.
|
|
303
|
+
if (/^(?:npm:|git:|https?:\/\/|builtin:)/.test(reference)) return reference;
|
|
304
|
+
const resolved = path.resolve(path.dirname(profile.sourcePath), expandHome(reference));
|
|
305
|
+
let stat: fs.Stats;
|
|
306
|
+
try {
|
|
307
|
+
stat = fs.statSync(resolved);
|
|
308
|
+
} catch {
|
|
309
|
+
throw new Error(`Profile '${profile.name}' extension is missing or unreadable: ${resolved}.`);
|
|
310
|
+
}
|
|
311
|
+
if (!stat.isFile() && !stat.isDirectory()) {
|
|
312
|
+
throw new Error(`Profile '${profile.name}' has an unsupported extension path: ${resolved}.`);
|
|
313
|
+
}
|
|
314
|
+
return resolved;
|
|
315
|
+
}
|
|
316
|
+
|
|
297
317
|
export function resolveProfile(name: string, cwd: string, includeProject: boolean, options: { agentOnly?: boolean } = {}): ResolvedProfile {
|
|
298
318
|
const catalog = loadProfiles(cwd, includeProject);
|
|
299
319
|
const invalid = catalog.diagnostics.map((item) => `${item.path}: ${item.message}`);
|
|
@@ -320,6 +340,6 @@ export function resolveProfile(name: string, cwd: string, includeProject: boolea
|
|
|
320
340
|
return {
|
|
321
341
|
...profile,
|
|
322
342
|
resolvedSkills,
|
|
323
|
-
resolvedExtensions: [],
|
|
343
|
+
resolvedExtensions: [...new Set((profile.extensions ?? []).map((extension) => resolveExtension(extension, profile)))],
|
|
324
344
|
};
|
|
325
345
|
}
|
|
@@ -16,7 +16,8 @@ interface ToolSource {
|
|
|
16
16
|
|
|
17
17
|
export function resolveToolExtensions(profile: ResolvedProfile, tools: ToolSource[]): ResolvedProfile {
|
|
18
18
|
const available = new Map(tools.map((tool) => [tool.name, tool]));
|
|
19
|
-
|
|
19
|
+
// Keep explicitly allowlisted event-only extensions alongside tool providers.
|
|
20
|
+
const extensionPaths = new Set<string>(profile.resolvedExtensions);
|
|
20
21
|
for (const name of profile.tools) {
|
|
21
22
|
// Native MCP connects asynchronously; do not require its tools in Main yet.
|
|
22
23
|
if (isMcpTool(name)) continue;
|
package/src/profiles/types.ts
CHANGED
package/src/talk-delivery.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { MESSAGE_TYPE, removeTalk, type TalkDirection } from "./channel.js";
|
|
3
3
|
import type { DelegateManifest, TalkMessage } from "./types.js";
|
|
4
4
|
import { readTalkReceipt } from "./talk-message.js";
|
|
5
|
-
import { parentWakeFor } from "./parent-wake.js";
|
|
6
5
|
import { canUseNativeQueue, forgetTalk, markTalkQueued, rememberTalk } from "./inbox-state.js";
|
|
7
6
|
|
|
8
7
|
function hasReceipt(ctx: ExtensionContext, direction: TalkDirection, runId: string, messageId: string): boolean {
|
|
@@ -27,7 +26,6 @@ export function deliverTalk(
|
|
|
27
26
|
content: string,
|
|
28
27
|
): "acknowledged" | "waiting" | "sent" {
|
|
29
28
|
if (hasReceipt(ctx, direction, manifest.runId, message.id)) {
|
|
30
|
-
if (!parentWakeFor(pi, ctx).retryFailedWake()) return "waiting";
|
|
31
29
|
forgetTalk(ctx, direction, manifest.runId, message.id);
|
|
32
30
|
removeTalk(channelDir, direction, message.id);
|
|
33
31
|
return "acknowledged";
|
|
@@ -39,9 +37,8 @@ export function deliverTalk(
|
|
|
39
37
|
try {
|
|
40
38
|
// Pi owns waiting behind current work, not Pi Subagent. In-flight IDs prevent
|
|
41
39
|
// repeated file notifications from submitting the same follow-up twice.
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
parentWakeFor(pi, ctx).sendMessage({
|
|
40
|
+
// Pi 1.2+ prepares idle custom-message runs and preserves their prompt sections.
|
|
41
|
+
pi.sendMessage({
|
|
45
42
|
customType: MESSAGE_TYPE,
|
|
46
43
|
content,
|
|
47
44
|
display: true,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Third-party notices
|
|
2
|
-
|
|
3
|
-
`src/parent-wake.ts` adapts the parent wake workaround from [nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents/blob/0c33ec7cb26ed1db270d72e746c3c975be880aeb/src/shared/parent-wake.ts).
|
|
4
|
-
|
|
5
|
-
## pi-subagents — MIT License
|
|
6
|
-
|
|
7
|
-
Copyright (c) 2026 Nico Bailon
|
|
8
|
-
|
|
9
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
-
in the Software without restriction, including without limitation the rights
|
|
12
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
-
furnished to do so, subject to the following conditions:
|
|
15
|
-
|
|
16
|
-
The above copyright notice and this permission notice shall be included in all
|
|
17
|
-
copies or substantial portions of the Software.
|
|
18
|
-
|
|
19
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
package/src/parent-wake.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// Adapted from nicobailon/pi-subagents (MIT), src/shared/parent-wake.ts.
|
|
2
|
-
// Copyright (c) 2026 Nico Bailon. See docs/third-party-notices.md.
|
|
3
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
|
|
5
|
-
// Shared by Main and Sub. Peer content stays in the custom message, never in this prompt.
|
|
6
|
-
export const PARENT_WAKE_TEXT = "Process the new Pi Subagent inbox messages above and continue the task already authorized for this session. Follow your profile and communication protocol. Do not infer new user authorization.";
|
|
7
|
-
export const WAKE_PENDING_MS = 10_000;
|
|
8
|
-
|
|
9
|
-
type WakeContext = Pick<ExtensionContext, "isIdle" | "sessionManager">;
|
|
10
|
-
type WakeReservation = { sessionId: string; sentAt?: number; failed?: boolean };
|
|
11
|
-
const reservationsSymbol = Symbol.for("pi-subagent.parent-wake-reservations.v1");
|
|
12
|
-
const wakeGlobal = globalThis as typeof globalThis & { [reservationsSymbol]?: WeakMap<object, WakeReservation> };
|
|
13
|
-
const reservations = wakeGlobal[reservationsSymbol] ?? (wakeGlobal[reservationsSymbol] = new WeakMap<object, WakeReservation>());
|
|
14
|
-
|
|
15
|
-
export interface ParentWake {
|
|
16
|
-
sendMessage(...args: Parameters<ExtensionAPI["sendMessage"]>): void;
|
|
17
|
-
/** A custom receipt alone must not acknowledge a synchronously failed wake. */
|
|
18
|
-
retryFailedWake(): boolean;
|
|
19
|
-
bindSession(ctx: WakeContext): void;
|
|
20
|
-
agentStarted(): void;
|
|
21
|
-
sessionShutdown(reason: string | undefined): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createParentWake(pi: Pick<ExtensionAPI, "sendMessage" | "sendUserMessage">, now: () => number = Date.now): ParentWake {
|
|
25
|
-
let ctx: WakeContext;
|
|
26
|
-
let reservation: WakeReservation = { sessionId: "" };
|
|
27
|
-
const wake = () => {
|
|
28
|
-
if (reservation.sentAt !== undefined && now() - reservation.sentAt < WAKE_PENDING_MS) return;
|
|
29
|
-
reservation.sentAt = now();
|
|
30
|
-
try {
|
|
31
|
-
// Steer safely queues the wake if another prompt starts a run first.
|
|
32
|
-
pi.sendUserMessage(PARENT_WAKE_TEXT, { deliverAs: "steer" });
|
|
33
|
-
reservation.failed = false;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
reservation.sentAt = undefined;
|
|
36
|
-
reservation.failed = true;
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
return {
|
|
41
|
-
sendMessage(message, options) {
|
|
42
|
-
if (options?.triggerTurn !== true || !ctx.isIdle()) {
|
|
43
|
-
pi.sendMessage(message, options);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// Pi's idle custom-message wake bypasses before_agent_start (#5581/#10267).
|
|
47
|
-
// Retain custom rendering but use native user-input preparation to start the run.
|
|
48
|
-
pi.sendMessage(message, { triggerTurn: false });
|
|
49
|
-
wake();
|
|
50
|
-
},
|
|
51
|
-
retryFailedWake() {
|
|
52
|
-
if (!reservation.failed) return true;
|
|
53
|
-
if (!ctx.isIdle()) return false;
|
|
54
|
-
wake();
|
|
55
|
-
return true;
|
|
56
|
-
},
|
|
57
|
-
bindSession(context) {
|
|
58
|
-
ctx = context;
|
|
59
|
-
const sessionId = context.sessionManager.getSessionId();
|
|
60
|
-
const retained = reservations.get(context.sessionManager);
|
|
61
|
-
reservation = retained?.sessionId === sessionId ? retained : { sessionId };
|
|
62
|
-
reservations.set(context.sessionManager, reservation);
|
|
63
|
-
},
|
|
64
|
-
agentStarted() {
|
|
65
|
-
reservation.sentAt = undefined;
|
|
66
|
-
reservation.failed = false;
|
|
67
|
-
},
|
|
68
|
-
sessionShutdown(reason) {
|
|
69
|
-
if (reason !== "reload") {
|
|
70
|
-
reservation.sentAt = undefined;
|
|
71
|
-
reservation.failed = false;
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const runtimes = new WeakMap<ExtensionAPI, ParentWake>();
|
|
78
|
-
export function parentWakeFor(pi: ExtensionAPI, ctx: WakeContext): ParentWake {
|
|
79
|
-
let wake = runtimes.get(pi);
|
|
80
|
-
if (!wake) {
|
|
81
|
-
wake = createParentWake(pi);
|
|
82
|
-
runtimes.set(pi, wake);
|
|
83
|
-
}
|
|
84
|
-
wake.bindSession(ctx);
|
|
85
|
-
return wake;
|
|
86
|
-
}
|