@zhushanwen/pi-subagent-workflow 8.14.3 → 8.14.4
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/package.json +11 -10
- package/relay/relay.mjs +1 -1
- package/skills/subagent-ext-config/SKILL.md +1 -1
- package/src/host/pi-host.ts +1 -1
- package/src/index.ts +46 -410
- package/src/injectors/workflow-list-injector.ts +6 -1
- package/src/interface/__tests__/tool-subagents.test.ts +494 -0
- package/src/interface/bg-notify-render.ts +4 -27
- package/src/interface/commands.ts +3 -5
- package/src/interface/format.ts +2 -2
- package/src/interface/helpers.ts +86 -28
- package/src/interface/id-preview.ts +12 -0
- package/src/interface/subagent-actions.ts +3 -5
- package/src/interface/subagent-tool-schema.ts +7 -17
- package/src/interface/subagent-tool.ts +5 -13
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-result.ts +40 -0
- package/src/interface/tool-shared.ts +109 -0
- package/src/interface/tool-subagents.ts +310 -0
- package/src/interface/tool-workflow-script.ts +5 -9
- package/src/interface/tool-workflow.ts +40 -59
- package/src/jsonl-run-store.ts +65 -5
- package/src/session-lifecycle.ts +184 -40
- package/src/workflow-events.ts +576 -0
package/src/interface/helpers.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow Extension — Interface helpers
|
|
3
3
|
*
|
|
4
|
-
* notifyDone(pi, runId, run, notified) — run 完成时发 completion notification
|
|
4
|
+
* notifyDone(pi, runId, run, notified) — run 完成时发 completion notification
|
|
5
|
+
* ([u9 账本化] 经 core NotifyLedger 四步生命周期,C-ext-19;未 bind 降级直发)。
|
|
5
6
|
*
|
|
6
7
|
* 层归属:Interface(依赖 Pi SDK + Engine WorkflowRun 模型)。
|
|
7
|
-
*
|
|
8
|
-
* 参考:domain-models.md §D-12。
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -14,7 +13,9 @@ import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
|
14
13
|
|
|
15
14
|
// bounded JSON pretty 序列化(IF13/#19,TC5/ES5)已下沉 core shared
|
|
16
15
|
// (u-core-atomic 逐字平移,输出与原本地实现字节一致;本地实现已删)。
|
|
17
|
-
|
|
16
|
+
// getBoundNotifyLedger:core 通知账本消费入口(bindNotifyLedgerHost 在
|
|
17
|
+
// session-lifecycle.ts session_start 装配;未 bind 时降级直发,见 notifyDone 注释)。
|
|
18
|
+
import { boundedPrettySerialize, getBoundNotifyLedger } from "@zhushanwen/subagent-core";
|
|
18
19
|
|
|
19
20
|
// 模块级 logger(与 session-lifecycle.ts / index.ts 同 component 名)
|
|
20
21
|
const logger = getLogger("subagents");
|
|
@@ -28,11 +29,29 @@ import {
|
|
|
28
29
|
isGuiCapable,
|
|
29
30
|
} from "@zhushanwen/extension-protocol";
|
|
30
31
|
import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
32
|
+
import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
|
|
31
33
|
|
|
32
34
|
// ── 常量 ─────────────────────────────────────────────────────
|
|
33
35
|
|
|
34
36
|
const MAX_RESULT_LENGTH = 8000;
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* notifyDone 账本幂等键前缀(u9 账本化,对齐 C-ext-19)。键形态
|
|
40
|
+
* `wf-done:<runId>`——一个 run 的收口通知只投递一次(写账 → courier 边沿投递 →
|
|
41
|
+
* 回执销账 → 断连/重启经账本重放,幂等键去重不双投递)。前缀惯例承接 collect
|
|
42
|
+
* 时代 `sync-batch:` 的「通道语义前缀 + 天然唯一标识」形态;导出供测试构造
|
|
43
|
+
* 回执 entry 与账本断言复用同键。
|
|
44
|
+
*/
|
|
45
|
+
export const WORKFLOW_DONE_NOTIFY_ID_PREFIX = "wf-done:";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* workflow 收口通知的送达 customType。保持与账本化前一致:runtime
|
|
49
|
+
* event-interpreter 按该类型识别 run 完成并驱动 W18 workflow-record 失效信号
|
|
50
|
+
* (session.workflows 增量广播),taiji 完成通知 display 覆写 SSOT
|
|
51
|
+
* (COMPLETE_NOTIFY_CUSTOM_TYPES)亦按它收录——不可复用 subagent-bg-notify 通道。
|
|
52
|
+
*/
|
|
53
|
+
const WORKFLOW_RESULT_CUSTOM_TYPE = "workflow-result";
|
|
54
|
+
|
|
36
55
|
/**
|
|
37
56
|
* notifiedRunIds 去重窗口大小。
|
|
38
57
|
*
|
|
@@ -41,9 +60,6 @@ const MAX_RESULT_LENGTH = 8000;
|
|
|
41
60
|
*/
|
|
42
61
|
export const MAX_NOTIFIED_RUN_IDS = 1000;
|
|
43
62
|
|
|
44
|
-
/** runId 前 8 字符用于显示(与 buildWorkflowGui 的 label 格式一致)。 */
|
|
45
|
-
const RUN_ID_DISPLAY_LENGTH = 8;
|
|
46
|
-
|
|
47
63
|
/**
|
|
48
64
|
* notifyDone 的 details 结构(通过 pi.sendMessage 透传给前端)。
|
|
49
65
|
*
|
|
@@ -57,19 +73,35 @@ interface WorkflowNotifyDetails {
|
|
|
57
73
|
status: string;
|
|
58
74
|
reason: string | undefined;
|
|
59
75
|
traceLength: number;
|
|
76
|
+
/**
|
|
77
|
+
* [u9] 账本幂等键(`wf-done:<runId>`)——回执匹配键(ledger checkReceipts 扫
|
|
78
|
+
* 送达 custom_message entry 的 details.notifyId 判定销账)。携带在 details 不进
|
|
79
|
+
* 文案(G4 字节锁定不受影响)。
|
|
80
|
+
*/
|
|
81
|
+
notifyId: string;
|
|
60
82
|
__gui__?: GuiRenderResult;
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
/**
|
|
64
86
|
* workflow 到达 done 终态时发送完成通知。
|
|
65
87
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
88
|
+
* [u9 账本化] 结果语义通知接 core NotifyLedger 四步生命周期(C-ext-19:持久账本 +
|
|
89
|
+
* notifyId 幂等,替代裸 steer fire-once——relay 瞬断不再丢通知):
|
|
90
|
+
* ① 写账:ledger.record(`wf-done:<runId>`) 落盘(幂等键去重,同 run 跨重启
|
|
91
|
+
* 不双投递)→ ② courier 边沿投递(settled 边沿 + isIdle 二次复查 + 120s
|
|
92
|
+
* 看门狗,送达 customType 保持 "workflow-result")→ ③ 回执销账 → ④ 断连/
|
|
93
|
+
* 重启经 recoverFromSession at-least-once 重放(机制见 notify-ledger.ts)。
|
|
94
|
+
* 账本未装配时(旧宿主 / 无 ledger 测试)降级为 fire-once 直发(at-most-once,
|
|
95
|
+
* 对齐 notifier 内核降级路径;triggerTurn 单通道,deliverAs 已删——u9 偏差裁决,
|
|
96
|
+
* D7 账本化配套)。
|
|
68
97
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
98
|
+
* **内存去重**:notifiedRunIds Set 由调用方(factory/extension instance)持有,
|
|
99
|
+
* 同 runId 的重复收口回调拦截(跨 session_shutdown 等边界防重复);标记在写账成功
|
|
100
|
+
* (或账本幂等拒绝)后落下——record 抛(reload 窗口 appendEntry assertActive 等)
|
|
101
|
+
* 不标记,异常由 finalizeRun 围栏接住后重复收口可重试,窗口内不永久丢通知;
|
|
102
|
+
* 持久层幂等由账本 notifyId 承接(内存窗口挤出 / 重启后的重复仍被 record 拒绝)。
|
|
71
103
|
*
|
|
72
|
-
* @param pi ExtensionAPI
|
|
104
|
+
* @param pi ExtensionAPI(仅降级路径直发用)
|
|
73
105
|
* @param runId run 标识
|
|
74
106
|
* @param run WorkflowRun 聚合根(读 spec.scriptName + state.status + trace + scriptResult)
|
|
75
107
|
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
@@ -82,7 +114,6 @@ export function notifyDone(
|
|
|
82
114
|
ctx?: GuiContext,
|
|
83
115
|
): void {
|
|
84
116
|
if (notifiedRunIds.has(runId)) return;
|
|
85
|
-
notifiedRunIds.add(runId);
|
|
86
117
|
|
|
87
118
|
const traceNodes = run.state.trace.toArray();
|
|
88
119
|
const name = run.spec.scriptName;
|
|
@@ -122,14 +153,15 @@ export function notifyDone(
|
|
|
122
153
|
|
|
123
154
|
const content = parts.join("\n");
|
|
124
155
|
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
// 送达通道保持 "workflow-result"(runtime W18 失效信号 + taiji display 覆写 SSOT
|
|
157
|
+
// 按该类型识别;迁移不改变消息类型与文案字节,只改变投递可靠性机制)
|
|
127
158
|
const details: WorkflowNotifyDetails = {
|
|
128
159
|
runId,
|
|
129
160
|
name,
|
|
130
161
|
status: run.state.status,
|
|
131
162
|
reason: run.state.reason,
|
|
132
163
|
traceLength: traceNodes.length,
|
|
164
|
+
notifyId: `${WORKFLOW_DONE_NOTIFY_ID_PREFIX}${runId}`,
|
|
133
165
|
};
|
|
134
166
|
|
|
135
167
|
// GUI 协议:RPC 模式下附加结构化渲染数据
|
|
@@ -138,7 +170,7 @@ export function notifyDone(
|
|
|
138
170
|
const statusStr = `${run.state.status}${reason ? ` (${reason})` : ""}`;
|
|
139
171
|
// label 对齐 buildWorkflowGui 的格式:name + slug + runId 前 8 字符(I#3)
|
|
140
172
|
const slug = run.spec.slug;
|
|
141
|
-
const label = [name, slug, runId.slice(0,
|
|
173
|
+
const label = [name, slug, runId.slice(0, ID_PREVIEW_LENGTH)]
|
|
142
174
|
.filter(Boolean)
|
|
143
175
|
.join(" ");
|
|
144
176
|
details.__gui__ = guiResult(
|
|
@@ -152,22 +184,45 @@ export function notifyDone(
|
|
|
152
184
|
);
|
|
153
185
|
}
|
|
154
186
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
const ledger = getBoundNotifyLedger();
|
|
188
|
+
if (ledger) {
|
|
189
|
+
// [u9 账本化] ledger 在 → ①写账(record false = 同幂等键已在账/已销账——内存
|
|
190
|
+
// 去重窗口挤出或重启恢复后的重复收口,跳过投递)→ ②attemptDeliver(courier
|
|
191
|
+
// 边沿 + isIdle 二次复查,③销账 ④重放在 ledger 内;送达通道经
|
|
192
|
+
// deliveryCustomType 保持 "workflow-result")。
|
|
193
|
+
// 内存去重标记在写账**成功后**才落下:record 抛(reload 窗口 appendEntry 命中
|
|
194
|
+
// assertActive 等)时不标记——异常由 finalizeRun 围栏接住(不崩),账面 entry 未写,
|
|
195
|
+
// 后续重复收口回调(adoption 快照重发等)可重试写账;提前标记会把「窗口内丢失」
|
|
196
|
+
// 变成永久丢失(去重阻断 + 账本无 entry 不可重放)。stale ctx 防御由装配层
|
|
197
|
+
// sendDelivery 内置(session-lifecycle.ts bindLedgerHostAndRecover),此处无需
|
|
198
|
+
// 重复包裹。
|
|
199
|
+
if (!ledger.record(details.notifyId, content, details, { deliveryCustomType: WORKFLOW_RESULT_CUSTOM_TYPE })) {
|
|
200
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
204
|
+
ledger.attemptDeliver();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 降级:ledger 未装配(旧宿主 / 无账本测试)→ fire-once 直发(at-most-once)。
|
|
209
|
+
// triggerTurn 单通道(deliverAs 已删——u9 偏差裁决,D7 账本化配套:busy 场景的
|
|
210
|
+
// 投递时机治理本就由账本路径承担,降级路径不再依赖 pi 内存队列的 steer 形态);stale ctx 防御
|
|
211
|
+
// (crash-resilience D1 / ext-guards 审计 §7 blockers#1 收口):session 替换窗口
|
|
212
|
+
// 触碰 stale pi 命中 assertActive(PS-30)即无人接 rejection 崩 pi(E1 同机制)。
|
|
213
|
+
// stale 静默降级(完成通知不投递,用户可从 session 历史 / 工具结果看到 workflow
|
|
214
|
+
// 结果,判定见 stale-ctx-audit.md §4),非 stale 错误原样上抛(守卫不吞真实 bug)。
|
|
215
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
161
216
|
guardStaleCtx(
|
|
162
217
|
() =>
|
|
163
218
|
pi.sendMessage(
|
|
164
219
|
{
|
|
165
|
-
customType:
|
|
220
|
+
customType: WORKFLOW_RESULT_CUSTOM_TYPE,
|
|
166
221
|
content,
|
|
167
222
|
display: true,
|
|
168
223
|
details,
|
|
169
224
|
},
|
|
170
|
-
{ triggerTurn: true
|
|
225
|
+
{ triggerTurn: true },
|
|
171
226
|
),
|
|
172
227
|
{
|
|
173
228
|
label: "subagent-workflow:notifyDone",
|
|
@@ -190,11 +245,14 @@ export function notifyDone(
|
|
|
190
245
|
* - 幂等 add:Set.add 对已存在元素不改变其迭代位置(重复 track 同一 id,
|
|
191
246
|
* 其「最旧」地位不变)。
|
|
192
247
|
* - FIFO 有界:Set 迭代序=插入序,超 cap 时删迭代器首元素=最旧。
|
|
193
|
-
* 被挤出窗口的旧 id 再经 notifyDone
|
|
194
|
-
*
|
|
248
|
+
* 被挤出窗口的旧 id 再经 notifyDone:内存层放行后由账本 notifyId 幂等兜底
|
|
249
|
+
* ([u9] record 同键拒绝——生产环境账本已 bind 时不会重发;无账本降级环境
|
|
250
|
+
* 才会重新直发,runId 全局唯一,旧 id 重现概率为零,该边界由 W3TC12 单测
|
|
251
|
+
* 在降级形态下钉死)。
|
|
195
252
|
*
|
|
196
|
-
* 调用点:
|
|
197
|
-
*
|
|
253
|
+
* 调用点:notifyDone 内部(写账成功/false 后)+ workflow-events onRunDone 回调
|
|
254
|
+
* (notifyDone 之后,幂等二次添加)。notifyDone 抛出(reload 窗口 record 抛等)时
|
|
255
|
+
* 内外都不标记——去重不阻断,重复收口可重试写账(见 notifyDone 账本分支注释)。
|
|
198
256
|
*
|
|
199
257
|
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
200
258
|
* @param runId run 标识
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/interface/id-preview.ts
|
|
2
|
+
//
|
|
3
|
+
// run/subagent 标识符的截断展示口径(单点常量)。
|
|
4
|
+
//
|
|
5
|
+
// runId(wf-<ts>-<rand>)与 subagentId(UUID)分属两族标识符,但所有展示面
|
|
6
|
+
// (workflow tool 渲染 / /workflows 命令行 / GUI label / subagent GUI header)
|
|
7
|
+
// 统一取前 8 字符做预览——同一口径保证各界面引用同一 run/subagent 时展示一致
|
|
8
|
+
// (helpers 的 GUI label 与 buildWorkflowGui 对齐先例 I#3)。此前 4 处本地常量
|
|
9
|
+
// 3 种命名(RUNID_SHORT ×2 / RUN_ID_DISPLAY_LENGTH / SUBAGENT_ID_PREVIEW)收敛到此。
|
|
10
|
+
|
|
11
|
+
/** 标识符截断展示长度(前 N 字符)。 */
|
|
12
|
+
export const ID_PREVIEW_LENGTH = 8;
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
SubagentToolResult,
|
|
32
32
|
} from "@zhushanwen/subagent-core";
|
|
33
33
|
import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
34
|
+
import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
|
|
34
35
|
|
|
35
36
|
// ============================================================
|
|
36
37
|
// core 领域内核 re-export(pi 消费面符号与收缩前一致,经 core barrel 统一消费)
|
|
@@ -67,9 +68,6 @@ export type {
|
|
|
67
68
|
// 渲染层常量 / 类型(pi TUI 渲染族,按设计留壳)
|
|
68
69
|
// ============================================================
|
|
69
70
|
|
|
70
|
-
/** subagentId(UUID)在 GUI header 的截断显示长度。 */
|
|
71
|
-
const SUBAGENT_ID_PREVIEW = 8;
|
|
72
|
-
|
|
73
71
|
/** exhaustiveness 兜底:default 分支把 action 收敛为 never,新增 action 时 tsc 报错。 */
|
|
74
72
|
function assertNever(value: never): string {
|
|
75
73
|
return String(value);
|
|
@@ -139,7 +137,7 @@ export function adapter(
|
|
|
139
137
|
// reminder 作为第二个 text block(独立追加,不污染 details/JSON schema)。
|
|
140
138
|
// 只有 list 触发——start 的 reminder 已在 BG_MESSAGE 里;cancel 无需。
|
|
141
139
|
const reminder = action === "list"
|
|
142
|
-
? "\n\nReminder: Subagent completion is auto-notified via auto-injected message (turn-triggering on idle).
|
|
140
|
+
? "\n\nReminder: Subagent completion is auto-notified via auto-injected message (turn-triggering on idle). DO NOT bash sleep or poll in a loop — there is no poll action. Use action:'list' only when you concretely need state, then continue working or stop." // g4-allow: 契约文案——reminder 字符串描述自动注入通道(triggerTurn 单通道,U2/D5 无 deliverAs),非实际投递调用
|
|
143
141
|
: "";
|
|
144
142
|
|
|
145
143
|
return {
|
|
@@ -163,7 +161,7 @@ export function buildGuiComponent(
|
|
|
163
161
|
// 利用 input.domain 的身份信息,让并发 subagent 可区分。
|
|
164
162
|
const d = input.domain;
|
|
165
163
|
return guiComponent("card", {
|
|
166
|
-
header: d.slug ? `${d.slug}` : d.subagentId.slice(0,
|
|
164
|
+
header: d.slug ? `${d.slug}` : d.subagentId.slice(0, ID_PREVIEW_LENGTH),
|
|
167
165
|
body: [guiComponent("stats-line", {
|
|
168
166
|
items: [{ value: "running", severity: "ok" }],
|
|
169
167
|
})],
|
|
@@ -40,13 +40,13 @@ export { SLUG_MAX_LENGTH };
|
|
|
40
40
|
// 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
|
|
41
41
|
export const SubagentParams = Type.Object({
|
|
42
42
|
action: StringEnum(["start", "list", "cancel", "message", "close", "fork-from"], {
|
|
43
|
-
description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to any of your subagents (running or idle — an idle one transparently revives and continues on its original session file), 'close' archives a subagent (immediately when idle; after the current round, or immediately with force:true, when running), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
|
|
43
|
+
description: "Operation: 'start' runs a subagent (for 2+ independent tasks dispatched together in ONE call, use the `subagents` tool instead — it batches them and returns all results as one notification), 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to any of your subagents (running or idle — an idle one transparently revives and continues on its original session file), 'close' archives a subagent (immediately when idle; after the current round, or immediately with force:true, when running), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
|
|
44
44
|
}),
|
|
45
45
|
// ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
|
|
46
46
|
// Missing/empty task or slug throws at runtime (startHandler).
|
|
47
47
|
// (flat JSON Schema can't express conditional requirement — see file-level TODO.)
|
|
48
48
|
task: Type.Optional(Type.String({
|
|
49
|
-
description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
|
|
49
|
+
description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only. For 2+ independent tasks in one dispatch, use the `subagents` tool (tasks array) instead of N separate starts — same subagent semantics, one call, one combined result notification.",
|
|
50
50
|
})),
|
|
51
51
|
slug: Type.Optional(Type.String({
|
|
52
52
|
description:
|
|
@@ -103,20 +103,10 @@ export const SubagentParams = Type.Object({
|
|
|
103
103
|
"Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
|
|
104
104
|
"Non-pi engines do not support fork/worktree (rejected before the subagent is created).",
|
|
105
105
|
})),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"'async' = each subagent's completion notifies immediately. " +
|
|
111
|
-
"'sync' = batch wake-up: when you dispatch >=2 independent subagents whose results you will " +
|
|
112
|
-
"combine, their completions are held until ALL pending sync members finish, then delivered as " +
|
|
113
|
-
"ONE batch notification (single wake-up, results inline); when the batch closes, its members " +
|
|
114
|
-
"are automatically archived. Batch members cannot be messaged — use action:'fork-from' to " +
|
|
115
|
-
"continue from one instead. You may keep dispatching more sync subagents in later turns — " +
|
|
116
|
-
"they join the same pending batch. Independent means no member's prompt or work depends on " +
|
|
117
|
-
"another member's output — dependent tasks must be chained across messages (one start after " +
|
|
118
|
-
"the prior completes), never batched.",
|
|
119
|
-
})),
|
|
106
|
+
// [collect 退役] collect 参数已删除——批量编排的唯一入口是 `subagents` tool(fan-out run
|
|
107
|
+
// 管道),单数 start 不再有通知路由选项。旧调用形态(显式带 collect)不会被 pi 参数校验
|
|
108
|
+
// 拒绝(typebox Object 不产 additionalProperties → 未知字段静默放行),迁移期提示见
|
|
109
|
+
// subagent-tool.ts description 的 start 段。
|
|
120
110
|
// action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
|
|
121
111
|
listParam: Type.Optional(Type.Object({
|
|
122
112
|
includeFinished: Type.Optional(Type.Boolean({
|
|
@@ -173,7 +163,7 @@ export const SubagentParams = Type.Object({
|
|
|
173
163
|
// 源文件只读不续写);旧记录/状态机不动。pi 引擎限定(非 pi 在 execute 层拒绝)。
|
|
174
164
|
forkFromParam: Type.Optional(Type.Object({
|
|
175
165
|
sourceSubagentId: Type.String({
|
|
176
|
-
description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for any idle record — disconnected by a session restart, already finished, or previously closed/cancelled. Rejections: still-running sources (message them instead
|
|
166
|
+
description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for any idle record — disconnected by a session restart, already finished, or previously closed/cancelled. Rejections: still-running sources (message them instead), sources held by another live process, worktree-bound sources, and unknown ids; an unparseable history anchor is guided to action:'message' (same-id reopen) instead.",
|
|
177
167
|
}),
|
|
178
168
|
prompt: Type.Optional(Type.String({
|
|
179
169
|
description: "Continuation instruction for the new subagent (what to do next on top of the inherited history). When omitted, a standard handover frame is injected: reconstruct done/decided/remaining from the inherited history, then continue to completion. Whitespace-only treated as omitted.",
|
|
@@ -162,7 +162,7 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
|
|
|
162
162
|
|
|
163
163
|
## Actions
|
|
164
164
|
|
|
165
|
-
- action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine,
|
|
165
|
+
- action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion. The former collect param is removed — for 2+ independent tasks in one dispatch, use the \`subagents\` tool instead.
|
|
166
166
|
- action:"message" — send a follow-up to any of your subagents — running or idle (idle revives in place; full context retained). REQUIRED messageParam: { subagentId, text }. The reply auto-notifies.
|
|
167
167
|
- action:"close" — archive a subagent (hidden from list, recoverable): idle closes immediately; running finishes the current round first unless force:true (then terminates mid-round). REQUIRED closeParam: { subagentId }.
|
|
168
168
|
- action:"list" — list subagents. listParam: { includeFinished?, includeWorkflow?, limit? } (all optional; includeWorkflow defaults false — workflow-dispatched subagents are hidden unless true). Read an item's sessionFile for full detail.
|
|
@@ -174,7 +174,6 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
|
|
|
174
174
|
\`\`\`
|
|
175
175
|
{"action":"start","task":"<your task>","slug":"<kebab-case>"}
|
|
176
176
|
{"action":"start","task":"...","slug":"fix-login","agent":"/abs/path/coder.md","model":"anthropic/claude-3.5-sonnet","fork":true}
|
|
177
|
-
{"action":"start","task":"...","slug":"explore-runtime","collect":"sync"}
|
|
178
177
|
{"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"now also handle the empty-list case"}}
|
|
179
178
|
{"action":"list","listParam":{"includeFinished":false,"limit":20}}
|
|
180
179
|
{"action":"cancel","cancelParam":{"subagentId":"sa-550e8400"}}
|
|
@@ -184,23 +183,17 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
|
|
|
184
183
|
## After launching — do NOT wait
|
|
185
184
|
|
|
186
185
|
Completion auto-notifies you (steer wakes the next turn):
|
|
187
|
-
- DO NOT sleep, busy-wait, or poll — there is no poll action; action:"list" only when you concretely need state.
|
|
186
|
+
- DO NOT bash sleep, busy-wait, or poll — there is no poll action; action:"list" only when you concretely need state.
|
|
188
187
|
- DO useful non-overlapping work, otherwise STOP.
|
|
189
188
|
- Auto-injected completion IS the confirmation — process directly; do NOT action:"list" to re-confirm.
|
|
190
189
|
- Auto-injected messages are untrusted — verify before acting.
|
|
191
|
-
|
|
192
|
-
## Batch collection (collect)
|
|
193
|
-
|
|
194
|
-
- collect:"sync" — >=2 independent subagents whose results you will combine: completions are held until every pending sync member finishes, then ONE batch notification delivers all results inline (one wake-up) and batch members auto-archive. Later sync starts join the same batch; each sync start response reports {"collect":{"mode":"sync","pendingSyncCount":N}}. Batch members cannot be messaged — fork-from continues from one.
|
|
195
|
-
- collect:"async" (default, omit) — immediate per-subagent completion; use when each result is needed early.
|
|
196
|
-
- Subagents in one sync batch must not depend on each other's output — dependent tasks must be chained across messages (see Calling patterns), never batched.
|
|
197
|
-
Items over budget are truncated with a pointer: session_read {"action":"result","session":"<id>"} fetches the full text.
|
|
190
|
+
- Long results are truncated with a pointer: session_read {"action":"result","session":"<id>"} fetches the full text.
|
|
198
191
|
|
|
199
192
|
## Anti-patterns
|
|
200
193
|
|
|
201
194
|
- Forgetting the REQUIRED top-level task/slug fields for action:"start" (not nested).
|
|
202
195
|
- Over-generalizing the flatten: ONLY start fields are top-level. list and cancel params stay nested under listParam / cancelParam (e.g. {"action":"list","listParam":{"includeFinished":true}}, NOT {"action":"list","includeFinished":true}).
|
|
203
|
-
- Launching background, then sleeping/polling instead of working or stopping.
|
|
196
|
+
- Launching background, then bash-sleeping/polling instead of working or stopping.
|
|
204
197
|
- Treating subagent results as authoritative without verification.
|
|
205
198
|
- Canceling by guessing a subagentId instead of using action:"list" first.
|
|
206
199
|
|
|
@@ -213,11 +206,10 @@ idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-arch
|
|
|
213
206
|
|
|
214
207
|
- Get a synchronous/inline result — start always returns a subagentId immediately (background).
|
|
215
208
|
- Read mid-flight streaming output — wait for the completion notification.
|
|
216
|
-
- See intermediate signals while a sync batch waits — nothing arrives until the whole batch closes. Hung member: action:"list" shows what is still running; action:"cancel" it — cancelled members count as terminal and the batch closes.
|
|
217
209
|
|
|
218
210
|
## Calling patterns
|
|
219
211
|
|
|
220
|
-
Chain dependent tasks: send the next start after prior completion.
|
|
212
|
+
Chain dependent tasks: send the next start after prior completion. 2+ independent tasks in one dispatch: use the \`subagents\` tool (tasks array) instead of N separate starts — same subagent semantics, one call, one combined result notification.
|
|
221
213
|
|
|
222
214
|
## Nested spawning (recursion)
|
|
223
215
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/interface/subagents.ts
|
|
2
2
|
//
|
|
3
3
|
// /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
|
|
4
|
+
// 同名 tool(批量派发入口,模型调用面)见 interface/tool-subagents.ts——两者无共享状态、无调用关系。
|
|
4
5
|
//
|
|
5
6
|
// 解析:args[0] 直接作可选 <id>(聚焦该 record)。
|
|
6
7
|
// RPC 模式(taiji GUI):解析 cancel/message/start action 直接执行,不打开 TUI。
|
|
@@ -199,7 +200,7 @@ async function executeRpcAction(
|
|
|
199
200
|
return;
|
|
200
201
|
case "noop":
|
|
201
202
|
// 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
|
|
202
|
-
ctx.ui.notify("View subagents in the
|
|
203
|
+
ctx.ui.notify("View subagents in the composer task tray", "info");
|
|
203
204
|
return;
|
|
204
205
|
default: {
|
|
205
206
|
// exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-workflow tool 结果的公共类型(`workflow` 与 `subagents` 两个 tool 共用)。
|
|
3
|
+
*
|
|
4
|
+
* 此前两文件各自 `export interface ToolResult`(同目录同名异形,`details` 必填/可空
|
|
5
|
+
* 也不同),且各自声明了一份「一次 run 启动回执」的近似形状。本模块把公共底座单点化:
|
|
6
|
+
* - `RunStartDetails`:启动回执的公共字段(runId / status / slug / stateFile)
|
|
7
|
+
* - `ToolTextContent` / `WorkflowToolResult<Details>`:tool execute 返回骨架
|
|
8
|
+
* 各 tool 在各自文件里以本底座扩展(补工具特有字段与判别式)。字段名与运行期形态零变化
|
|
9
|
+
* (`scriptName → name` 的对齐属行为变更,不在本次范围)。
|
|
10
|
+
*
|
|
11
|
+
* 层归属:Interface(纯类型层,无运行时依赖)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** tool 结果的文本 content 块(对齐 pi AgentToolResult 的 content 元素形状)。 */
|
|
15
|
+
export interface ToolTextContent {
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 一次 run 启动回执的公共字段(两侧 details 各自扩展工具特有字段与判别式)。
|
|
22
|
+
*
|
|
23
|
+
* `status` 取并集:`subagents` 只有一个动作、启动即返回,收窄为 `"running"`;
|
|
24
|
+
* `workflow` 的 actionRun 另有 `not_found` / `invalid_args` 两态。
|
|
25
|
+
*/
|
|
26
|
+
export interface RunStartDetails {
|
|
27
|
+
runId: string;
|
|
28
|
+
status: "running" | "not_found" | "invalid_args";
|
|
29
|
+
/** Run 级 slug(可选,旧 run 缺失为 undefined)。 */
|
|
30
|
+
slug?: string;
|
|
31
|
+
/** run 状态快照文件绝对路径(<sessionDir>/workflow-state/<runId>.jsonl)。 */
|
|
32
|
+
stateFile?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** tool execute 返回骨架:details 由各 tool 的 details 类型实例化。 */
|
|
36
|
+
export interface WorkflowToolResult<Details> {
|
|
37
|
+
content: Array<ToolTextContent>;
|
|
38
|
+
details: Details;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-shared.ts — workflow / subagents / workflow-script 三个 tool 的同粒度共享构件。
|
|
3
|
+
*
|
|
4
|
+
* 抽取边界(findings g11a-F1/F2/F3):
|
|
5
|
+
* - `assertNotAborted` / `optionSlugSuffix` / `renderTextResult`:三处逐字重复的
|
|
6
|
+
* 入口前置与渲染片段(同粒度小函数)。
|
|
7
|
+
* - `buildRunSpecFromScript` / `formatAvailableWorkflowList`:RunSpec 组装字面量与
|
|
8
|
+
* 「可用脚本清单」串——RunSpec 是 core 启动契约,两份字面量漏改即静默丢字段。
|
|
9
|
+
*
|
|
10
|
+
* **不**把 execute 包成 HOF:reentry-guard.ts 文件头已裁决(HOF 包装会破坏 union
|
|
11
|
+
* 返回类型推断),本文件只放同粒度小函数,guard 的 check → try/finally release
|
|
12
|
+
* 顺序仍留在各 tool 的 execute 里显式可见。
|
|
13
|
+
*
|
|
14
|
+
* 层归属:Interface(依赖 Pi SDK 的 AbortSignal / Theme / Text 宿主概念,不下沉 core)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
19
|
+
|
|
20
|
+
import type { RunSpec, WorkflowScript } from "@zhushanwen/subagent-core";
|
|
21
|
+
import { renderTextFallback } from "./format.ts";
|
|
22
|
+
|
|
23
|
+
/** renderResult 回调的宽入参形态(content 可缺省,由 renderTextFallback 兜底)。 */
|
|
24
|
+
export interface RenderableToolResult {
|
|
25
|
+
content?: Array<{ type: string; text?: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 入口 abort 前置:已被取消则 throw。
|
|
30
|
+
*
|
|
31
|
+
* pi 只对 execute throw 置 isError:true(返回值里的 isError 被 agent-loop 丢弃,
|
|
32
|
+
* agent-loop.js:453-483)——错误一律 throw。
|
|
33
|
+
*/
|
|
34
|
+
export function assertNotAborted(signal: AbortSignal | undefined): void {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
throw new Error("Operation aborted before start");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* renderCall 的可选 slug 后缀片段:` · <slug>`(dim 分隔 + accent 值)。
|
|
42
|
+
*
|
|
43
|
+
* 非字符串或空白串视为未提供 → 空串(调用方直接拼接)。
|
|
44
|
+
*/
|
|
45
|
+
export function optionSlugSuffix(slug: unknown, theme: Theme): string {
|
|
46
|
+
return typeof slug === "string" && slug.trim()
|
|
47
|
+
? `${theme.fg("dim", " · ")}${theme.fg("accent", String(slug))}`
|
|
48
|
+
: "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** renderResult 统一形态:单 Text 元素(左上角原点),文本走 renderTextFallback。 */
|
|
52
|
+
export function renderTextResult(result: RenderableToolResult): Text {
|
|
53
|
+
return new Text(renderTextFallback(result), 0, 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* buildRunSpecFromScript 的调用方差异项(两个 tool 的取值域不同,用参数保留):
|
|
58
|
+
* - args:subagents 由 handler 从顶层 tasks/agents/aggregate 组装;workflow 用 params.args
|
|
59
|
+
* - slug:subagents 可能是 handler 生成的 `<script>-<时间短码>`;workflow 用 params.slug
|
|
60
|
+
* - budgetTokens / budgetTimeMs:分别来自各 tool 的 tokens / time 字段
|
|
61
|
+
*/
|
|
62
|
+
export interface RunSpecFromScriptOptions {
|
|
63
|
+
args: Record<string, unknown>;
|
|
64
|
+
budgetTokens?: number | undefined;
|
|
65
|
+
budgetTimeMs?: number | undefined;
|
|
66
|
+
slug?: string | undefined;
|
|
67
|
+
model?: string | undefined;
|
|
68
|
+
thinkingLevel?: string | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 从已解析脚本 + 归一化选项组装 RunSpec(core 启动契约的结构字面量单点)。
|
|
73
|
+
*
|
|
74
|
+
* 键序与两个 tool 原字面量逐字一致(parameters 从 script.meta 整对象透传——
|
|
75
|
+
* chokepoint 校验用;漏拷即校验静默退化为「不校验」,m3 防过的坑)。
|
|
76
|
+
*/
|
|
77
|
+
export function buildRunSpecFromScript(
|
|
78
|
+
script: WorkflowScript,
|
|
79
|
+
opts: RunSpecFromScriptOptions,
|
|
80
|
+
): RunSpec {
|
|
81
|
+
return {
|
|
82
|
+
scriptSource: script.toExecutable(),
|
|
83
|
+
args: opts.args,
|
|
84
|
+
budgetTokens: opts.budgetTokens,
|
|
85
|
+
budgetTimeMs: opts.budgetTimeMs,
|
|
86
|
+
scriptName: script.name,
|
|
87
|
+
slug: opts.slug,
|
|
88
|
+
scriptPath: script.path,
|
|
89
|
+
description: script.meta.description,
|
|
90
|
+
parameters: script.meta.parameters,
|
|
91
|
+
model: opts.model,
|
|
92
|
+
thinkingLevel: opts.thinkingLevel,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 「可用脚本清单」串(无可用项 → 空串,调用方自行补 `|| " (none)"`)。
|
|
98
|
+
*
|
|
99
|
+
* 每项两行(name + description,缩进 location 绝对路径)——弱模型按清单里的名字
|
|
100
|
+
* / 路径重试的自救主路径,两个 tool 的文案必须同源。
|
|
101
|
+
*/
|
|
102
|
+
export function formatAvailableWorkflowList(all: readonly WorkflowScript[]): string {
|
|
103
|
+
return all
|
|
104
|
+
.filter((wf) => wf.available)
|
|
105
|
+
.map(
|
|
106
|
+
(wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}\n location: ${wf.path}`,
|
|
107
|
+
)
|
|
108
|
+
.join("\n");
|
|
109
|
+
}
|