@zhushanwen/pi-subagent-workflow 8.12.0 → 8.14.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/package.json +10 -10
- package/src/host/__tests__/inflight-reporter.test.ts +18 -12
- package/src/host/inflight-reporter.ts +90 -40
- package/src/host/pi-host.ts +1 -1
- package/src/index.ts +6 -3
- package/src/injectors/__tests__/engine-awareness.test.ts +1 -1
- package/src/injectors/__tests__/engine-section-stability.test.ts +1 -1
- package/src/injectors/engine-awareness.ts +1 -1
- package/src/injectors/model-list-injector.ts +2 -1
- package/src/injectors/subagent-list-injector.ts +1 -1
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +1 -1
- package/src/interface/format.ts +18 -7
- package/src/interface/gui-mappers.ts +6 -2
- package/src/interface/list-component.ts +9 -3
- package/src/interface/list-view.ts +1 -1
- package/src/interface/subagent-actions.ts +1 -1
- package/src/interface/subagent-tool-schema.ts +33 -33
- package/src/interface/subagent-tool.ts +10 -11
- package/src/interface/tool-workflow.ts +1 -1
- package/src/interface/views/WorkflowsView.ts +1 -1
- package/src/interface/views/__tests__/record-progress.test.ts +2 -1
- package/src/jsonl-run-store.ts +1 -11
- package/src/session-lifecycle.ts +2 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -54,21 +54,21 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@xyz-agent/extension-protocol": "0.
|
|
58
|
-
"@xyz-agent/session-delivery": "0.
|
|
59
|
-
"@zhushanwen/pi-ext-guards": "0.
|
|
60
|
-
"@zhushanwen/
|
|
61
|
-
"@zhushanwen/subagent-
|
|
62
|
-
"@zhushanwen/
|
|
63
|
-
"@zhushanwen/
|
|
57
|
+
"@xyz-agent/extension-protocol": "0.11.0",
|
|
58
|
+
"@xyz-agent/session-delivery": "0.4.1",
|
|
59
|
+
"@zhushanwen/pi-ext-guards": "0.4.0",
|
|
60
|
+
"@zhushanwen/subagent-core": "0.10.1",
|
|
61
|
+
"@zhushanwen/pi-subagent-cli": "0.4.0",
|
|
62
|
+
"@zhushanwen/zcode-subagent-cli": "0.3.1",
|
|
63
|
+
"@zhushanwen/pi-extension-logger": "0.6.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
67
67
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
68
68
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
69
69
|
"typebox": "*",
|
|
70
|
-
"@zhushanwen/pi-pending-notifications": "0.7.
|
|
71
|
-
"@zhushanwen/pi-structured-output": "5.1.
|
|
70
|
+
"@zhushanwen/pi-pending-notifications": "0.7.2",
|
|
71
|
+
"@zhushanwen/pi-structured-output": "5.1.7"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 三视角:
|
|
4
4
|
// ①使用者(runtime event-adapter 视角)——帧形状:title=SUBAGENT_INFLIGHT_MARKER、
|
|
5
|
-
// options=[JSON 帧](
|
|
6
|
-
// ②构建者——初始上报(attachSession
|
|
7
|
-
//
|
|
8
|
-
// 多次迁移合并为单帧且携带最新绝对计数;
|
|
5
|
+
// options=[JSON 帧](inFlight/sessionId/emittedAt)、控制面级 timeout 在场;
|
|
6
|
+
// ②构建者——初始上报(attachSession 触发,无需任何 subagent 调用)→ ack 清失败
|
|
7
|
+
// 计数;失败折叠 + 延迟重试;推送在途期间多次迁移合并为单帧且携带最新绝对计数;
|
|
9
8
|
// ③观察者——onInFlightChanged 同步返回(不 await select,不进生命周期链);
|
|
10
9
|
// detachSession 停重试,session 死后通道静默。
|
|
10
|
+
//
|
|
11
|
+
// [HISTORICAL] 行为于 9f914b749 变更:kind='initial'|'delta' 帧字段与 initialAcked
|
|
12
|
+
// 状态机作为死面删除(消费方 event-adapter 从不读 kind,绝对计数语义下帧间等价;
|
|
13
|
+
// 该 commit 漏改本文件致断言失效)。断言已对齐现行协议形状——SubagentInFlightReport
|
|
14
|
+
// 无 kind 字段,kind 断言改为 undefined 锁形(防字段悄悄回流)。
|
|
11
15
|
|
|
12
16
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
17
|
|
|
@@ -87,7 +91,7 @@ async function advance(ms: number): Promise<void> {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
describe("初始上报(D5:触发时点 = extension 加载完成 / session 就绪)", () => {
|
|
90
|
-
it("attachSession
|
|
94
|
+
it("attachSession 即发初始帧(count=当下快照,无需任何 subagent 调用)", async () => {
|
|
91
95
|
const channel = makeSelectChannel();
|
|
92
96
|
const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
|
|
93
97
|
|
|
@@ -98,7 +102,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
|
|
|
98
102
|
expect(channel.calls[0].title).toBe(SUBAGENT_INFLIGHT_MARKER);
|
|
99
103
|
expect(channel.calls[0].timeout).toBe(SELECT_TIMEOUT_MS);
|
|
100
104
|
const frame = parseFrame(channel.calls[0]);
|
|
101
|
-
expect(frame.kind).
|
|
105
|
+
expect(frame.kind).toBeUndefined(); // kind 已删(9f914b749):锁现行协议形状,防字段回流
|
|
102
106
|
expect(frame.inFlight).toBe(0);
|
|
103
107
|
expect(frame.sessionId).toBe("sess-u7a");
|
|
104
108
|
expect(typeof frame.emittedAt).toBe("number");
|
|
@@ -107,7 +111,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
|
|
|
107
111
|
await advance(0);
|
|
108
112
|
});
|
|
109
113
|
|
|
110
|
-
it("
|
|
114
|
+
it("初始未送达前发生的迁移不产生第二帧(合并为单帧,ack 后补推最新快照)", async () => {
|
|
111
115
|
const channel = makeSelectChannel();
|
|
112
116
|
const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS });
|
|
113
117
|
reporter.attachSession(makeCtx(channel));
|
|
@@ -123,7 +127,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
|
|
|
123
127
|
await advance(0);
|
|
124
128
|
expect(channel.calls).toHaveLength(2);
|
|
125
129
|
const delta = parseFrame(channel.calls[1]);
|
|
126
|
-
expect(delta.kind).
|
|
130
|
+
expect(delta.kind).toBeUndefined(); // kind 已删(9f914b749):帧间等价,无 initial/delta 之分
|
|
127
131
|
expect(delta.inFlight).toBe(2); // 补推帧携带最新绝对计数
|
|
128
132
|
});
|
|
129
133
|
});
|
|
@@ -155,7 +159,7 @@ describe("绝对计数语义(每帧携带当下值,非增量)", () => {
|
|
|
155
159
|
});
|
|
156
160
|
|
|
157
161
|
describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)", () => {
|
|
158
|
-
it("select resolve undefined(超时/旧版 runtime)→
|
|
162
|
+
it("select resolve undefined(超时/旧版 runtime)→ 折叠重试;重试帧携带完整快照;ack 后停", async () => {
|
|
159
163
|
const channel = makeSelectChannel();
|
|
160
164
|
const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
|
|
161
165
|
reporter.attachSession(makeCtx(channel));
|
|
@@ -168,9 +172,11 @@ describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)"
|
|
|
168
172
|
expect(channel.calls).toHaveLength(1); // 未到退避点不重试
|
|
169
173
|
await advance(1);
|
|
170
174
|
expect(channel.calls).toHaveLength(2);
|
|
171
|
-
|
|
175
|
+
const retryFrame = parseFrame(channel.calls[1]);
|
|
176
|
+
expect(retryFrame.kind).toBeUndefined(); // kind 已删(9f914b749):重试帧与普通帧等价
|
|
177
|
+
expect(retryFrame.inFlight).toBe(0); // 重试帧携带完整绝对计数快照
|
|
172
178
|
|
|
173
|
-
// 重试帧得到 ack →
|
|
179
|
+
// 重试帧得到 ack → 重试停止;随后迁移照常送达
|
|
174
180
|
channel.settle(1, INFLIGHT_REPORT_ACK);
|
|
175
181
|
await advance(RETRY_MS * 10);
|
|
176
182
|
expect(channel.calls).toHaveLength(2);
|
|
@@ -178,7 +184,7 @@ describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)"
|
|
|
178
184
|
reporter.onInFlightChanged();
|
|
179
185
|
await advance(0);
|
|
180
186
|
expect(channel.calls).toHaveLength(3);
|
|
181
|
-
expect(parseFrame(channel.calls[2]).
|
|
187
|
+
expect(parseFrame(channel.calls[2]).inFlight).toBe(0);
|
|
182
188
|
channel.settleAll(INFLIGHT_REPORT_ACK);
|
|
183
189
|
});
|
|
184
190
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/host/inflight-reporter.ts
|
|
2
2
|
//
|
|
3
|
-
// 壳层在途上报出口(u7a,设计权威源:docs/
|
|
3
|
+
// 壳层在途上报出口(u7a,设计权威源:docs/architecture/crash-forensics-and-watchdog.md
|
|
4
4
|
// §3.3 D5「extension 聚合上报」)。本文件属壳侧(shell),对 pi SDK(ExtensionContext
|
|
5
5
|
// 的 ctx.ui.select 通道)的消费收敛在 host/ 层——core 闭包红线只约束 core(出口回调
|
|
6
6
|
// 由 core 的 inflight-snapshot 注入,见组合根 index.ts 的 setInFlightListener 接线)。
|
|
@@ -15,26 +15,32 @@
|
|
|
15
15
|
// ② 事件产生点在 core、上报出口在壳层——本文件即出口(core 零 pi SDK);
|
|
16
16
|
// ③ marker 路由不广播前端是 u7b(runtime 侧)的约束,本文件不涉及。
|
|
17
17
|
//
|
|
18
|
+
// 环境门控(2026-09-13 oe-audit + 裸 TUI 闪框事故):上报的消费方是 xyz-agent
|
|
19
|
+
// runtime 的 event-adapter marker 路由,而 runtime spawn pi 恒为 --mode rpc——
|
|
20
|
+
// ctx.mode !== 'rpc' 即无拦截方(裸 pi TUI 下 marker select 会弹真框超时,2026-09-12
|
|
21
|
+
// 实测无限闪框),此时不启动上报(ask-user channel-handler 的 mode 二值判定同款
|
|
22
|
+
// 先例)。非 rpc 会话的 inFlight 判定归 runtime 侧镜像(spawn 预置 0),无信息损失。
|
|
23
|
+
//
|
|
18
24
|
// 语义(D5):每帧携带**绝对计数**(getInFlightSnapshot,谓词与 core 内
|
|
19
|
-
// hasRunningBackground
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// createExtensionAPI :209 函数体无 ui 字段);per-session ctx 由
|
|
23
|
-
// dist/core/extensions/runner.js createContext() :503(get ui :508)在事件派发时点
|
|
24
|
-
// 构造,emit :624 每次现场建 ctx)——session_start 是 pi 启动序列里最早带 ctx 的钩子
|
|
25
|
-
// (dist/core/agent-session.js:1919 _extensionRunner.emit(_sessionStartEvent),默认
|
|
26
|
-
// 事件 :152)= 「session 就绪」,即设计所指加载完成时点;不挂任何懒触发(无 subagent
|
|
27
|
-
// 的 session 也必上报)。
|
|
25
|
+
// hasRunningBackground 同源),非增量;初始上报触发时点 = extension 加载完成
|
|
26
|
+
// (session_start 是 pi 启动序列里最早带 ctx 的钩子 = 「session 就绪」,即设计所指
|
|
27
|
+
// 加载完成时点;不挂任何懒触发(无 subagent 的 session 也必上报)。
|
|
28
28
|
//
|
|
29
|
-
// 失败语义(D5
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// 失败语义(D5 缺席语义②,2026-09-13 oe-audit 修订):select 失败(超时/通道异常/
|
|
30
|
+
// 非确认回包)折叠后延迟重试,**累计 MAX_REPORT_ATTEMPTS 次放弃**(原「重试直至
|
|
31
|
+
// 成功一次」的无界语义为「旧版 runtime」版本错配场景设计——该场景已被同 bundle
|
|
32
|
+
// 发布 + pi 随 runtime 退出销毁两条事实证伪;有界化的防挂死兜底对齐 plugin-bridge
|
|
33
|
+
// MAX_SYNC_ATTEMPTS 形态。session_start 首帧早于 runtime adapter attach 的竞态
|
|
34
|
+
// (R2 实证)在 30×2s=60s 窗口内必然自愈)。送达判据 = runtime resolve 的确认回包
|
|
35
|
+
// (INFLIGHT_REPORT_ACK)——fire-and-forget 下 resolve(undefined) 与超时不可区分,
|
|
36
|
+
// 靠显式 ack 区分「已送达」与「无路由」;放弃后镜像按 absent-report 走 errs 推迟
|
|
37
|
+
// (30min 有界),不丢 errs-safe 兜底。
|
|
33
38
|
|
|
34
39
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
35
|
-
import { SUBAGENT_INFLIGHT_MARKER, isInFlightReportAck } from "@xyz-agent/extension-protocol";
|
|
40
|
+
import { SUBAGENT_INFLIGHT_MARKER, callMarkerRpc, isInFlightReportAck } from "@xyz-agent/extension-protocol";
|
|
36
41
|
import { getInFlightSnapshot } from "@zhushanwen/subagent-core";
|
|
37
42
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
43
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
38
44
|
|
|
39
45
|
/** select 通道级超时(控制面单请求,秒级校准——超时默认原则规则 19)。取值对齐
|
|
40
46
|
* plugin-bridge 启动 sync 的 2s 自愈闸:session_start 首帧可能早于 runtime adapter
|
|
@@ -44,11 +50,16 @@ const SELECT_TIMEOUT_MS = 2_000;
|
|
|
44
50
|
/** 失败重试退避(对齐 plugin-bridge SYNC_RETRY_MS 控制面节奏)。 */
|
|
45
51
|
const RETRY_DELAY_MS = 2_000;
|
|
46
52
|
|
|
53
|
+
/** 累计失败放弃上限(对齐 plugin-bridge MAX_SYNC_ATTEMPTS:60s 窗口覆盖 attach 竞态,
|
|
54
|
+
* 有界防 rpc-but-非-xyz orchestrator 场景的永久空转)。 */
|
|
55
|
+
const MAX_REPORT_ATTEMPTS = 30;
|
|
56
|
+
|
|
47
57
|
/** 在途上报器(组合根 index.ts 持有;per-factory 实例,session_start/shutdown 驱动)。 */
|
|
48
58
|
export interface InFlightReporter {
|
|
49
59
|
/**
|
|
50
60
|
* session_start 注入当前 ctx 并发起初始上报(fire-and-forget——本方法同步返回,
|
|
51
|
-
* 不阻塞 session_start 装配链;初始帧
|
|
61
|
+
* 不阻塞 session_start 装配链;初始帧 count 为当下绝对计数)。非 rpc 模式
|
|
62
|
+
* (ctx.mode !== 'rpc')下为 no-op:无 runtime 拦截方,上报通道不该启动。
|
|
52
63
|
*/
|
|
53
64
|
attachSession(ctx: ExtensionContext): void;
|
|
54
65
|
/** session_shutdown 摘除 ctx 并停止重试(session 已死,上报通道随之终结)。 */
|
|
@@ -62,6 +73,8 @@ export interface InFlightReporterOpts {
|
|
|
62
73
|
selectTimeoutMs?: number;
|
|
63
74
|
/** 测试注入:重试退避(ms)。缺省 RETRY_DELAY_MS。 */
|
|
64
75
|
retryDelayMs?: number;
|
|
76
|
+
/** 测试注入:累计失败放弃上限。缺省 MAX_REPORT_ATTEMPTS。 */
|
|
77
|
+
maxAttempts?: number;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/**
|
|
@@ -80,19 +93,22 @@ function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
|
80
93
|
export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFlightReporter {
|
|
81
94
|
const selectTimeoutMs = opts.selectTimeoutMs ?? SELECT_TIMEOUT_MS;
|
|
82
95
|
const retryDelayMs = opts.retryDelayMs ?? RETRY_DELAY_MS;
|
|
96
|
+
const maxAttempts = opts.maxAttempts ?? MAX_REPORT_ATTEMPTS;
|
|
83
97
|
const logger = getLogger("subagents");
|
|
84
98
|
|
|
85
99
|
// 闭包状态(per-factory 实例;禁模块级 let——同进程多 factory 实例会串台)。
|
|
86
100
|
let ctx: ExtensionContext | null = null;
|
|
87
|
-
/** 初始上报是否已送达(送达后所有帧 kind='delta')。 */
|
|
88
|
-
let initialAcked = false;
|
|
89
101
|
/** 一次推送尝试在途(串行化——绝对计数语义下中间值可安全合并丢弃)。 */
|
|
90
102
|
let attemptInFlight = false;
|
|
91
103
|
/** 有待推帧(onInFlightChanged 在推送在途期间置位,成功后立即补推最新值)。 */
|
|
92
104
|
let dirty = false;
|
|
93
105
|
/** 重试定时器句柄(单飞;成功/dispose 即清)。 */
|
|
94
106
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
-
/**
|
|
107
|
+
/** 累计失败次数(成功清零;达 maxAttempts 放弃——本 session 不再重试)。 */
|
|
108
|
+
let failureCount = 0;
|
|
109
|
+
/** 已放弃(累计到顶;session 内终态,成功路径永不触及)。 */
|
|
110
|
+
let givenUp = false;
|
|
111
|
+
/** 首次失败已 warn 留痕(重试循环不刷屏)。 */
|
|
96
112
|
let firstFailureLogged = false;
|
|
97
113
|
|
|
98
114
|
function clearRetryTimer(): void {
|
|
@@ -102,9 +118,9 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
/**
|
|
121
|
+
/** 推送在途、已放弃或无 ctx 时仅置脏;否则发起一次尝试(void,不阻塞调用方)。 */
|
|
106
122
|
function kick(): void {
|
|
107
|
-
if (attemptInFlight || ctx === null) {
|
|
123
|
+
if (attemptInFlight || givenUp || ctx === null) {
|
|
108
124
|
dirty = true;
|
|
109
125
|
return;
|
|
110
126
|
}
|
|
@@ -120,34 +136,53 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
|
|
|
120
136
|
return;
|
|
121
137
|
}
|
|
122
138
|
// 帧内容在发送时刻现取:绝对计数 = 此刻快照(「当前非 idle 句柄数」)、
|
|
123
|
-
// 产生时点 =
|
|
124
|
-
// initial 语义送达——对 errs 判别等价:runtime 收到任何帧即「在场」)。
|
|
139
|
+
// 产生时点 = 此刻(runtime 收到任何帧即「在场」,errs 判别按帧到达置位)。
|
|
125
140
|
const snapshot = getInFlightSnapshot();
|
|
126
141
|
const payload = JSON.stringify({
|
|
127
|
-
kind: initialAcked ? "delta" : "initial",
|
|
128
142
|
inFlight: snapshot.inFlight,
|
|
129
143
|
sessionId: getSessionId(active),
|
|
130
144
|
emittedAt: Date.now(),
|
|
131
145
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
// 发送+折叠半边走 protocol 的 callMarkerRpc 原语(D8,fire-and-forget:void 发起
|
|
147
|
+
// 不变):ok:false 四态(cancelled/timeout/channel-error/non-json)统一折叠进下方
|
|
148
|
+
// 延迟重试路径;送达判据 = ack 全等匹配(不是 JSON 消费),留在本侧。原语的失败
|
|
149
|
+
// 留痕经注入的 log 承载本侧「首败 warn / 后续 debug」防刷屏策略。
|
|
150
|
+
// guiCtx = ExtensionContext 的 GuiContext 最小子集(ask-user runRpcInteraction 同款
|
|
151
|
+
// 先例:ui.custom 泛型签名静态不兼容,callMarkerRpc 只读 ui.select)。
|
|
152
|
+
const guiCtx = {
|
|
153
|
+
mode: active.mode,
|
|
154
|
+
hasUI: active.hasUI,
|
|
155
|
+
ui: { select: active.ui.select.bind(active.ui) },
|
|
156
|
+
};
|
|
157
|
+
const result = await callMarkerRpc(guiCtx, SUBAGENT_INFLIGHT_MARKER, payload, {
|
|
158
|
+
timeout: selectTimeoutMs,
|
|
159
|
+
log: primitiveLog,
|
|
160
|
+
});
|
|
140
161
|
attemptInFlight = false;
|
|
141
|
-
if (
|
|
142
|
-
//
|
|
143
|
-
|
|
162
|
+
if (result.ok && isInFlightReportAck(result.value)) {
|
|
163
|
+
// 送达确认:清重试与失败计数,补推积压脏帧。
|
|
164
|
+
failureCount = 0;
|
|
144
165
|
clearRetryTimer();
|
|
145
166
|
if (dirty && ctx !== null) kick();
|
|
146
167
|
return;
|
|
147
168
|
}
|
|
148
|
-
// 失败折叠(resolve undefined =
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
// 失败折叠(resolve undefined = 超时/取消/无路由 / 回包非 ack / 通道异常)→ 延迟
|
|
170
|
+
// 重试,累计到顶放弃(放弃后镜像按 absent-report 走 errs 推迟,30min 有界,
|
|
171
|
+
// errs-safe 兜底不丢)。
|
|
172
|
+
logFailure(
|
|
173
|
+
result.ok ? "no ack (non-ack response)" : `no ack (${result.reason})`,
|
|
174
|
+
result.ok ? result.value : undefined,
|
|
175
|
+
);
|
|
176
|
+
failureCount += 1;
|
|
177
|
+
if (failureCount >= maxAttempts) {
|
|
178
|
+
givenUp = true;
|
|
179
|
+
clearRetryTimer();
|
|
180
|
+
logger.warn(
|
|
181
|
+
`[subagent-inflight] in-flight report gave up after ${failureCount} attempts; ` +
|
|
182
|
+
`mirror will treat this session as absent-report (errs-deferred, bounded)`,
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
151
186
|
if (ctx !== null && retryTimer === null) {
|
|
152
187
|
retryTimer = setTimeout(() => {
|
|
153
188
|
retryTimer = null;
|
|
@@ -158,11 +193,17 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
|
|
|
158
193
|
}
|
|
159
194
|
}
|
|
160
195
|
|
|
196
|
+
/** 原语留痕注入(D8):msg/detail 由 callMarkerRpc 产出;防刷屏策略(首败 warn /
|
|
197
|
+
* 后续 debug)留本侧 logFailure。detail 是原语侧小对象,序列化保信息。 */
|
|
198
|
+
function primitiveLog(msg: string, detail?: object): void {
|
|
199
|
+
logFailure(msg, detail === undefined ? undefined : JSON.stringify(detail));
|
|
200
|
+
}
|
|
201
|
+
|
|
161
202
|
function logFailure(reason: string, detail: unknown): void {
|
|
162
203
|
if (!firstFailureLogged) {
|
|
163
204
|
firstFailureLogged = true;
|
|
164
|
-
logger.warn(`[subagent-inflight] in-flight report failed (${reason}); retrying every ${retryDelayMs}ms
|
|
165
|
-
detail:
|
|
205
|
+
logger.warn(`[subagent-inflight] in-flight report failed (${reason}); retrying every ${retryDelayMs}ms (bounded at ${maxAttempts} attempts)`, {
|
|
206
|
+
detail: toErrorMessage(detail),
|
|
166
207
|
});
|
|
167
208
|
return;
|
|
168
209
|
}
|
|
@@ -171,6 +212,15 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
|
|
|
171
212
|
|
|
172
213
|
return {
|
|
173
214
|
attachSession(target: ExtensionContext): void {
|
|
215
|
+
// 环境门控:非 rpc 模式(裸 pi TUI / json / print)无 runtime 拦截方,marker
|
|
216
|
+
// select 会弹真框(2026-09-12 裸 TUI 无限闪框事故根因)——不设 ctx,本 session
|
|
217
|
+
// 全程 no-op。ask-user 的 ctx.mode === "rpc" 二值判定同款先例。
|
|
218
|
+
if (target.mode !== "rpc") return;
|
|
219
|
+
// attach = 新 reporting epoch(新 session / respawn 后重载):放弃态与失败计数
|
|
220
|
+
// 随旧 session 终结,重置重试资格(同一 session 内放弃不恢复——absent-report 兜底)。
|
|
221
|
+
givenUp = false;
|
|
222
|
+
failureCount = 0;
|
|
223
|
+
firstFailureLogged = false;
|
|
174
224
|
ctx = target;
|
|
175
225
|
clearRetryTimer();
|
|
176
226
|
// 初始上报(count=当下绝对计数;session 就绪时点恒为 0——子进程只会在后续
|
package/src/host/pi-host.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/host/pi-host.ts
|
|
2
2
|
//
|
|
3
3
|
// pi 壳宿主实现(subagent-core 包抽离 u0-wire)。设计权威源:
|
|
4
|
-
// docs/
|
|
4
|
+
// docs/architecture/subagent-core-package-extraction.md §3.3 D2(含计划期契约细化 3 条)。
|
|
5
5
|
//
|
|
6
6
|
// 本文件属壳侧(shell),不进 core 切面——对 pi SDK 与 pi 宿主协作件
|
|
7
7
|
// (@earendil-works/pi-coding-agent / @zhushanwen/pi-extension-logger /
|
package/src/index.ts
CHANGED
|
@@ -99,8 +99,11 @@ const logger = getLogger("subagents");
|
|
|
99
99
|
//
|
|
100
100
|
// session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
|
|
101
101
|
// 触发;sync 子进程(controller 为 undefined,abortRunningControllers 跳过它们)会
|
|
102
|
-
// 泄漏为孤儿。process.on
|
|
103
|
-
//
|
|
102
|
+
// 泄漏为孤儿。process.on 兜底调 killAllSpawnedChildren——[如实口径] core 侧该入口
|
|
103
|
+
// 为镜像置死 no-op(仅清空 core spawnedChildren 镜像记账,不发任何进程信号),真实
|
|
104
|
+
// 回收链 = 子进程 stdin-EOF 自灭(宿主退出 / EngineClient 销毁)+ dispose 链;本调用
|
|
105
|
+
// 保留兜底占位(镜像一致性),不构成真实收割。guard 防多信号叠加(如 SIGINT 后又
|
|
106
|
+
// beforeExit)重复触发。
|
|
104
107
|
let processShutdownHookFired = false;
|
|
105
108
|
|
|
106
109
|
function reapSpawnedChildrenOnShutdown(): void {
|
|
@@ -670,7 +673,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
|
|
|
670
673
|
// session_shutdown),Symbol key 单定义点随迁,避免双份定义漂移。
|
|
671
674
|
|
|
672
675
|
// 跨扩展 channel handler 注册入口已收口到 core 深路径
|
|
673
|
-
// `@zhushanwen/subagent-core/execution/channel-registry-access.ts`
|
|
676
|
+
// `@zhushanwen/subagent-core/execution/assembly/channel-registry-access.ts`
|
|
674
677
|
// (getOrCreateChannelRegistry / UiChannelRegistry / ChannelHandler)。
|
|
675
678
|
// 历史上的包根 re-export 已删:ask-user 等跨扩展消费者经 globalThis 握手
|
|
676
679
|
// (DIALOG_QUEUE_KEY 同款进程级单例),不再经包根 import 消费本模块。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine-awareness 单测([engine-awareness U3])
|
|
2
2
|
//
|
|
3
|
-
// 覆盖(设计 docs/design/subagent-engine-awareness-injection.md
|
|
3
|
+
// 覆盖(设计 docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)验收挂钩 D1/D1b/D2/D3/D5):
|
|
4
4
|
// 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,直连导入)
|
|
5
5
|
// 2. buildEngineChangeNotice:§3.1 文案骨架、pi/非 pi 指路段分界、不含任何模型清单(D4)
|
|
6
6
|
// 3. runEngineAwarenessTurn 编排:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine-section-stability.test.ts —— [engine-awareness U4 stability-guard]
|
|
2
2
|
//
|
|
3
|
-
// 字节稳定守护(设计 docs/design/subagent-engine-awareness-injection.md
|
|
3
|
+
// 字节稳定守护(设计 docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)§3.3 D7 /
|
|
4
4
|
// §4 A8 的代码级前置):engine 段延续注入纪律——确定性渲染、链尾位置,引擎切换只断
|
|
5
5
|
// system prompt 尾部 cache 前缀(与 provider models 段变更同判,不新增 cache 破坏面,
|
|
6
6
|
// cache-probe 前缀指纹归因兼容)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/injectors/engine-awareness.ts
|
|
2
2
|
//
|
|
3
3
|
// [engine-awareness U3] 引擎感知检测编排。设计权威源:
|
|
4
|
-
// docs/design/subagent-engine-awareness-injection.md
|
|
4
|
+
// docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)§2.3 物理数据流 /
|
|
5
5
|
// §3.3 D1(per-turn poll)、D1b(lastEngine 初始化与静默基线化)、D2(reload 先行)、
|
|
6
6
|
// D3(通知通道)、D5(三态读取语义)。
|
|
7
7
|
//
|
|
@@ -42,6 +42,7 @@ import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
|
42
42
|
|
|
43
43
|
// C5①/C5⑦:渲染统一走 core barrel(formatModelList + ModelEntry 类型为 barrel 导出面)
|
|
44
44
|
import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
|
|
45
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
45
46
|
|
|
46
47
|
const logger = getLogger("injector");
|
|
47
48
|
|
|
@@ -87,7 +88,7 @@ export function setupModelListInjector(pi: ExtensionAPI): void {
|
|
|
87
88
|
return { systemPrompt: event.systemPrompt + injection };
|
|
88
89
|
} catch (err) {
|
|
89
90
|
logger.error("[model-list-injector] before_agent_start failed", {
|
|
90
|
-
reason:
|
|
91
|
+
reason: toErrorMessage(err),
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* core barrel(渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);
|
|
15
15
|
* guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
|
|
16
16
|
* - U11(sink 设计):装配循环(发现→解析→去重→排序 + warn/error 口径)整体改
|
|
17
|
-
* 消费 core discoverAgents(execution/agents-assembly,U2/A6)——经工厂 assemble
|
|
17
|
+
* 消费 core discoverAgents(execution/assembly/agents-assembly,U2/A6)——经工厂 assemble
|
|
18
18
|
* 覆写槽委托,壳侧收缩为「宿主注入根现取 + 委托」,语义等值口径见
|
|
19
19
|
* agents-assembly.ts 头注(⛔1 探针对照注入 XML diff 为空验收)。
|
|
20
20
|
*
|
|
@@ -167,7 +167,7 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
|
|
|
167
167
|
"..",
|
|
168
168
|
);
|
|
169
169
|
const actionsSrc = readFileSync(
|
|
170
|
-
join(coreRoot, "src/execution/subagent-actions-core.ts"),
|
|
170
|
+
join(coreRoot, "src/execution/assembly/subagent-actions-core.ts"),
|
|
171
171
|
"utf-8",
|
|
172
172
|
);
|
|
173
173
|
// 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
|
package/src/interface/format.ts
CHANGED
|
@@ -13,6 +13,8 @@ import os from "node:os";
|
|
|
13
13
|
|
|
14
14
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
16
|
+
import { firstContentText } from "@xyz-agent/extension-protocol";
|
|
17
|
+
|
|
16
18
|
import type { AgentEventLogEntry, DisplayItem, ExecutionStatus } from "@zhushanwen/subagent-core";
|
|
17
19
|
import { DEFAULT_AGENT_NAME } from "@zhushanwen/subagent-core";
|
|
18
20
|
import type {
|
|
@@ -138,16 +140,24 @@ export function shortId(id: string): string {
|
|
|
138
140
|
* running → { icon: undefined, color: "accent" }
|
|
139
141
|
* icon 留空是因为 running 的 spinner 需 seed 驱动,
|
|
140
142
|
* 调用方用 detailsSeed(details) 算 seed 后调 spinnerGlyph(seed).
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
143
|
+
* idle → { "✓", "success" }
|
|
144
|
+
*
|
|
145
|
+
* [U2 两态迁移] 永久会话模型(subagent-permanent-session-model §3.2.2)状态机收敛为
|
|
146
|
+
* running | idle 两态,终态概念删除——旧 closed 分支(v4 B-1 统一终态 ✓ success 色)
|
|
147
|
+
* 迁移为 idle(已收口/等续聊,语义上「上一轮有产出」保留 ✓ success 色;stopReason
|
|
148
|
+
* 派生词的渲染细化归 U8 投影面)。签名兼容 BgNotifyRecord 的通知词汇 "closed"
|
|
149
|
+
* (通知文案载体的本地类型不受 ExecutionStatus 两态约束,桥接映射同 idle)。
|
|
144
150
|
*/
|
|
145
|
-
export function statusGlyph(
|
|
151
|
+
export function statusGlyph(
|
|
152
|
+
status: ExecutionStatus | "closed",
|
|
153
|
+
): { icon: string | undefined; color: string } {
|
|
146
154
|
switch (status) {
|
|
147
155
|
case "running":
|
|
148
156
|
return { icon: undefined, color: "accent" };
|
|
157
|
+
case "idle":
|
|
149
158
|
case "closed":
|
|
150
|
-
//
|
|
159
|
+
// 已收口/等续聊(closed = BgNotifyRecord 通知词汇桥接)。默认 ✓ success 色
|
|
160
|
+
// (承接旧 closed 分支的视觉基线)。
|
|
151
161
|
return { icon: "✓", color: "success" };
|
|
152
162
|
}
|
|
153
163
|
}
|
|
@@ -554,12 +564,13 @@ export function formatTokenStat(
|
|
|
554
564
|
/**
|
|
555
565
|
* renderResult 的文本兜底:从 result.content[0] 提取纯文本。
|
|
556
566
|
* 多处 tool 的 renderResult 曾各自内联此逻辑,提取后统一调用。
|
|
567
|
+
* 内核收敛至 extension-protocol firstContentText(ext-simplify-17 D9);
|
|
568
|
+
* 本包装保留 content 可选的宽入参形态(tool renderResult 回调契约 content 可缺省)。
|
|
557
569
|
*/
|
|
558
570
|
export function renderTextFallback(
|
|
559
571
|
result: { content?: Array<{ type: string; text?: string }> },
|
|
560
572
|
): string {
|
|
561
|
-
|
|
562
|
-
return first?.type === "text" ? (first.text ?? "") : "";
|
|
573
|
+
return firstContentText({ content: result.content ?? [] });
|
|
563
574
|
}
|
|
564
575
|
|
|
565
576
|
/** Format a single activity line: ToolName(argsPreview). */
|
|
@@ -48,12 +48,16 @@ function isFailedStatus(s: string): boolean {
|
|
|
48
48
|
* 把 workflow/subagent 状态字符串映射到 list-tree 的三态 status。
|
|
49
49
|
*
|
|
50
50
|
* 输入可能是纯 RunStatus(running/done)、RunStatus+reason 组合
|
|
51
|
-
* (如 "done (failed)"),或 subagent status(running/done/failed/
|
|
51
|
+
* (如 "done (failed)"),或 subagent status(running/idle/legacy done/failed/
|
|
52
|
+
* cancelled/crashed)。
|
|
52
53
|
*
|
|
53
54
|
* 映射规则:
|
|
54
55
|
* - running → running
|
|
55
56
|
* - failed / aborted / error / crashed / cancelled / budget_limited / time_limited → failed
|
|
56
|
-
* - 其他(done / completed / success / pending)→ done
|
|
57
|
+
* - 其他(done / completed / success / pending / idle)→ done
|
|
58
|
+
*
|
|
59
|
+
* [U8 两态] subagent 的 idle(无任务在飞可续聊)在协议三态里落 done(空闲 = 无
|
|
60
|
+
* 进行中工作,check 图标);「为什么停」不进树形状态(协议无该维度)。
|
|
57
61
|
*/
|
|
58
62
|
export function mapRunStatus(status: string): TreeStatus {
|
|
59
63
|
const s = status.toLowerCase();
|
|
@@ -602,12 +602,18 @@ export class SubagentsListComponent implements Component {
|
|
|
602
602
|
}
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
/** 元数据段:id/mode/status/turns/tokens 行 + model/thinking 行 + parent/children 层级行。 */
|
|
605
|
+
/** 元数据段:id/mode/status/stopReason/turns/tokens 行 + model/thinking 行 + parent/children 层级行。 */
|
|
606
606
|
private pushMetaSection(content: string[], record: SubagentRecord, width: number): void {
|
|
607
607
|
const t = this.theme;
|
|
608
|
-
// 元数据:第 1 行 id + 状态 + turns + tokens
|
|
608
|
+
// 元数据:第 1 行 id + 状态 + turns + tokens。stopReason(上一轮为什么停)只在
|
|
609
|
+
// 详情面板出现(§3.2.1 展示层词汇收敛:列表主展示用状态词,停因是排障信息);
|
|
610
|
+
// 有值即追加 `stopped: <reason>` 段(与 SubagentTab 同判据——A-lite 轮终
|
|
611
|
+
// running-resumable 也携带合法停因),缺省省略(在飞首轮 / 存量无字段)。
|
|
612
|
+
const stopPart = record.stopReason
|
|
613
|
+
? ` · stopped: ${record.stopReason}`
|
|
614
|
+
: "";
|
|
609
615
|
content.push(truncLine(
|
|
610
|
-
t.fg("dim", `${record.id} · ${record.mode} · ${record.status} · ${record.turns} turns · ${formatTokens(record.totalTokens)}`),
|
|
616
|
+
t.fg("dim", `${record.id} · ${record.mode} · ${record.status}${stopPart} · ${record.turns} turns · ${formatTokens(record.totalTokens)}`),
|
|
611
617
|
width,
|
|
612
618
|
));
|
|
613
619
|
// 元数据:第 2 行 model + thinking(括号分组)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// └────────────────────────────┘ ← overlay 底空白行
|
|
18
18
|
// (外层框线仅为示意,实际是空白行/空格列)
|
|
19
19
|
//
|
|
20
|
-
// 契约(ctx.ui.custom overlay,对照
|
|
20
|
+
// 契约(ctx.ui.custom overlay,对照 tui-rendering-pitfalls.md §3.2):
|
|
21
21
|
// custom<void>((tui, theme, kb, done) => Component, {overlay:true, overlayOptions})
|
|
22
22
|
// Component: render(width):string[] + invalidate() + handleInput?(data)
|
|
23
23
|
//
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// subagent tool 六 action 的 pi adapter 壳(sink 设计 D6② 消费收缩)。
|
|
4
4
|
//
|
|
5
5
|
// 领域内核(入参校验 / message 拒绝文案分流 / fork-from 拒绝链 / 终态映射 /
|
|
6
|
-
// list 投影)已下沉 core @zhushanwen/subagent-core/execution/subagent-actions-core——
|
|
6
|
+
// list 投影)已下沉 core @zhushanwen/subagent-core/execution/assembly/subagent-actions-core——
|
|
7
7
|
// ⛔4 行为快照等值锚定见 core __tests__/subagent-actions-core.test.ts(期望值硬编码自
|
|
8
8
|
// 本文件迁移前实测输出,含错误文案逐字锚)。本壳按设计仅保留三类职责:
|
|
9
9
|
// 1. core 调用面 re-export:六 handler + 领域类型(签名不变,消费方零改动)
|
|
@@ -28,7 +28,7 @@ export { SLUG_MAX_LENGTH };
|
|
|
28
28
|
|
|
29
29
|
// Params schema(跨包契约测试的真实 typebox 校验入口)。
|
|
30
30
|
//
|
|
31
|
-
// action:"start" 的
|
|
31
|
+
// action:"start" 的 16 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
|
|
32
32
|
// 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
|
|
33
33
|
// startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
|
|
34
34
|
// 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
|
|
@@ -40,7 +40,7 @@ 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
|
|
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).",
|
|
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).
|
|
@@ -88,40 +88,34 @@ export const SubagentParams = Type.Object({
|
|
|
88
88
|
description: 'Override the working directory for the subagent execution. Must be an absolute path (no "~" shorthand, no relative paths); ".." segments are rejected. Defaults to the parent session\'s cwd.',
|
|
89
89
|
pattern: "^/",
|
|
90
90
|
})),
|
|
91
|
-
conversation
|
|
92
|
-
|
|
93
|
-
"Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
|
|
94
|
-
"\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
|
|
95
|
-
"\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
|
|
96
|
-
"\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
|
|
97
|
-
"Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
|
|
98
|
-
})),
|
|
91
|
+
// [modeless 波5] conversation 参数已删除:chatMode 字段消亡后「模式」不存在——
|
|
92
|
+
// 一切 record 永续可续聊(idle 后 message 即续、fork-from 可继承),无模式开关可表达。
|
|
99
93
|
idleTimeoutMs: Type.Optional(Type.Number({
|
|
100
94
|
description:
|
|
101
|
-
"Idle
|
|
102
|
-
"Default: 300000 (5min).
|
|
103
|
-
"Pass 0 or a negative value to DISABLE idle
|
|
104
|
-
"
|
|
95
|
+
"Idle-recycle cadence for ALL subagents (modeless: every subagent stays continuable — this is NOT a mode switch). Controls how long an idle subagent (between rounds, no activity) stays before being automatically archived. " +
|
|
96
|
+
"Default: 300000 (5min). Raise it for long-interval collaboration where your next message may arrive more than 5min after a round ends. " +
|
|
97
|
+
"Pass 0 or a negative value to DISABLE idle recycling entirely (subagent stays available until you close it). " +
|
|
98
|
+
"Priority: this param > env XYZ_SUBAGENT_IDLE_TIMEOUT_MS > default.",
|
|
105
99
|
})),
|
|
106
100
|
engine: Type.Optional(StringEnum(["pi", "zcode"], {
|
|
107
101
|
description:
|
|
108
102
|
"Execution engine for this subagent. Omit to inherit the global config. " +
|
|
109
103
|
"Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
|
|
110
|
-
"Non-pi engines do not support
|
|
104
|
+
"Non-pi engines do not support fork/worktree (rejected before the subagent is created).",
|
|
111
105
|
})),
|
|
112
106
|
collect: Type.Optional(StringEnum(["async", "sync"], {
|
|
113
107
|
description:
|
|
114
|
-
"Completion-notification
|
|
108
|
+
"Completion-notification routing (NOT a record mode — batch membership is routing bookkeeping only). " +
|
|
115
109
|
"Omit to use the config default (currently async). " +
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
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.",
|
|
125
119
|
})),
|
|
126
120
|
// action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
|
|
127
121
|
listParam: Type.Optional(Type.Object({
|
|
@@ -145,24 +139,30 @@ export const SubagentParams = Type.Object({
|
|
|
145
139
|
description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
|
|
146
140
|
}),
|
|
147
141
|
})),
|
|
148
|
-
// action:"message" → messageParam.subagentId + text REQUIRED. Any
|
|
149
|
-
//
|
|
142
|
+
// action:"message" → messageParam.subagentId + text REQUIRED. Any reachable subagent works —
|
|
143
|
+
// running joins the in-flight round (D2 打断入队);idle transparently revives on the same
|
|
144
|
+
// session file([U4 §3.2.3] 万物可续——形态枚举 gate 与 SP-5 升级路径均消亡,message 直接
|
|
145
|
+
// 续聊任何 idle record)。引擎续聊能力轴 gate 保留(core messageHandler 入口)。
|
|
146
|
+
// description 与实现锚点见 messageHandler。
|
|
150
147
|
messageParam: Type.Optional(Type.Object({
|
|
151
148
|
subagentId: Type.String({
|
|
152
|
-
description: "REQUIRED for action:'message'. The subagentId to message
|
|
149
|
+
description: "REQUIRED for action:'message'. The subagentId to message. Any subagent reachable in this session tree works, running or idle: an idle subagent transparently revives on the same id and continues writing its original session file; a running subagent has your message interrupt-and-join its in-flight round. Rejections: unknown id, session file held by another live process, a record from a different session tree, workflow-origin records (their results belong to the workflow run), and records on engines that do not support continuation (fork-from or re-dispatch instead).",
|
|
153
150
|
}),
|
|
154
151
|
text: Type.String({
|
|
155
152
|
description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
|
|
156
153
|
}),
|
|
157
154
|
interrupt: Type.Optional(Type.Boolean({
|
|
158
|
-
|
|
155
|
+
// [H1 U6 / D2] 参数已退役(messageHandler 零消费):在途轮存在即打断入队,
|
|
156
|
+
// 不区分抢占/排队——字段保留防存量调用 schema 报错,description 据实声明 no-op。
|
|
157
|
+
description: "Deprecated, no effect: a message to a running subagent always interrupts its in-flight round (the round aborts and your message is processed next) regardless of this flag; an idle subagent always starts a new round.",
|
|
159
158
|
})),
|
|
160
159
|
})),
|
|
161
|
-
// action:"close" → closeParam.subagentId REQUIRED.
|
|
162
|
-
//
|
|
160
|
+
// action:"close" → closeParam.subagentId REQUIRED. 归档(archived):列表隐藏、可寻回、
|
|
161
|
+
// 非终态化([U5 §3.2.5])。idle 立即归档收口;running 默认等当前轮收口后归档,
|
|
162
|
+
// force:true 立即终止随即归档(closeHandler 头注行为分流)。
|
|
163
163
|
closeParam: Type.Optional(Type.Object({
|
|
164
164
|
subagentId: Type.String({
|
|
165
|
-
description: "REQUIRED for action:'close'. The subagentId to close (any
|
|
165
|
+
description: "REQUIRED for action:'close'. The subagentId to close (any reachable subagent — running or idle). Close archives the record: hidden from list, recoverable, not a terminal state. Idle subagents close immediately; running ones finish the current round first, or terminate immediately when force:true.",
|
|
166
166
|
}),
|
|
167
167
|
force: Type.Optional(Type.Boolean({
|
|
168
168
|
description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
|
|
@@ -173,7 +173,7 @@ export const SubagentParams = Type.Object({
|
|
|
173
173
|
// 源文件只读不续写);旧记录/状态机不动。pi 引擎限定(非 pi 在 execute 层拒绝)。
|
|
174
174
|
forkFromParam: Type.Optional(Type.Object({
|
|
175
175
|
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
|
|
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), 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
177
|
}),
|
|
178
178
|
prompt: Type.Optional(Type.String({
|
|
179
179
|
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,12 +162,12 @@ 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, collect, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd,
|
|
166
|
-
- action:"message" — send a follow-up to
|
|
167
|
-
- action:"close" —
|
|
165
|
+
- action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine, collect, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion.
|
|
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
|
+
- 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.
|
|
169
|
-
- action:"cancel" — stop a background subagent (
|
|
170
|
-
- action:"fork-from" — restart-disconnect recovery: spawn a NEW subagent inheriting the old one's history via --fork. REQUIRED forkFromParam: { sourceSubagentId }. Optional: prompt (continuation; default handover frame). Returns { newSubagentId, sourceSessionFile }. Rejects
|
|
169
|
+
- action:"cancel" — stop a background subagent (to archive it instead, use close). REQUIRED cancelParam: { subagentId }.
|
|
170
|
+
- action:"fork-from" — restart-disconnect recovery: spawn a NEW subagent inheriting the old one's history via --fork. REQUIRED forkFromParam: { sourceSubagentId }. Optional: prompt (continuation; default handover frame). Returns { newSubagentId, sourceSessionFile }. Rejects still-running / foreign-live / worktree-bound sources; unparseable history anchors are guided to action:"message" (same-id reopen).
|
|
171
171
|
|
|
172
172
|
## Examples
|
|
173
173
|
|
|
@@ -191,8 +191,8 @@ Completion auto-notifies you (steer wakes the next turn):
|
|
|
191
191
|
|
|
192
192
|
## Batch collection (collect)
|
|
193
193
|
|
|
194
|
-
- collect:"sync" — >=2 independent
|
|
195
|
-
- collect:"async" (default, omit) — immediate per-subagent completion;
|
|
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
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
197
|
Items over budget are truncated with a pointer: session_read {"action":"result","session":"<id>"} fetches the full text.
|
|
198
198
|
|
|
@@ -204,16 +204,15 @@ Items over budget are truncated with a pointer: session_read {"action":"result",
|
|
|
204
204
|
- Treating subagent results as authoritative without verification.
|
|
205
205
|
- Canceling by guessing a subagentId instead of using action:"list" first.
|
|
206
206
|
|
|
207
|
-
##
|
|
207
|
+
## Continuing a subagent (modeless)
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
idleTimeoutMs: idle
|
|
209
|
+
Every subagent stays continuable — no mode switch: action:"message" revives an idle record in place (or joins a running one's round), action:"fork-from" branches a new subagent from old history, action:"close" archives it.
|
|
210
|
+
idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-archive on expiry (default 300000 / 5min; 0/negative disables; env XYZ_SUBAGENT_IDLE_TIMEOUT_MS: global default, per-call wins).
|
|
211
211
|
|
|
212
212
|
## You cannot
|
|
213
213
|
|
|
214
214
|
- Get a synchronous/inline result — start always returns a subagentId immediately (background).
|
|
215
215
|
- Read mid-flight streaming output — wait for the completion notification.
|
|
216
|
-
- Combine collect:"sync" with conversation:true — rejected before start; sync is one-shot only (remove one).
|
|
217
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.
|
|
218
217
|
|
|
219
218
|
## Calling patterns
|
|
@@ -431,7 +431,7 @@ export async function actionRun(
|
|
|
431
431
|
const args = params.args ?? {};
|
|
432
432
|
const tokens = params.tokens;
|
|
433
433
|
const time = params.time;
|
|
434
|
-
// OR-1 入口 fail-fast(unbounded-wait-audit §7.2 T3
|
|
434
|
+
// OR-1 入口 fail-fast(crash-forensics-and-watchdog.md 附录 E(原 unbounded-wait-audit §7.2 T3①)):schema 的 time 是
|
|
435
435
|
// Type.Number 直通(无上界)——超 setTimeout 安全域的值会穿透到 lifecycle 内层
|
|
436
436
|
// 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链。错误带合法上限
|
|
437
437
|
// 与实际传入值,LLM 可据消息自纠(clamp 或省略走 unlimited 语义)。
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* SDK 集成:ctx.ui.custom factory 返回 Component{render(width), handleInput(data),
|
|
13
13
|
* invalidate},第二参数 `{overlay:true, overlayOptions}`(全屏 overlay,对齐 main +
|
|
14
|
-
* subagents 扩展 + docs/
|
|
14
|
+
* subagents 扩展 + docs/extensions/tui-rendering-pitfalls.md §3.2)。按键经
|
|
15
15
|
* matchesKey(data, KeyId) 解析(兼容 xterm/iTerm/kitty 转义序列差异)。
|
|
16
16
|
* escape/ctrl+c 在 keybindings 同映射到 exit。
|
|
17
17
|
*
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { describe, it, expect } from "vitest";
|
|
21
21
|
|
|
22
|
-
import { computeElapsedSeconds, getAllToolCalls
|
|
22
|
+
import { computeElapsedSeconds, getAllToolCalls } from "@zhushanwen/subagent-core";
|
|
23
|
+
import { projectLiveProgress } from "@zhushanwen/subagent-core/execution/persistence/execution-record.ts";
|
|
23
24
|
import type { ExecutionRecord, SubagentRecord, WorkflowRun } from "@zhushanwen/subagent-core";
|
|
24
25
|
import { collectNodeLiveProgress } from "../WorkflowsView.ts";
|
|
25
26
|
import { projectRecordProgress } from "../detail-content.ts";
|
package/src/jsonl-run-store.ts
CHANGED
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
pruneStateFilesBeyondCap,
|
|
91
91
|
type RunSnapshot,
|
|
92
92
|
} from "@zhushanwen/subagent-core";
|
|
93
|
-
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
93
|
+
import { isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
94
94
|
|
|
95
95
|
// ── Workflow-record self-describing entry (W17, D4) ─────────
|
|
96
96
|
|
|
@@ -226,16 +226,6 @@ async function loadRunFromStateFile(filePath: string): Promise<WorkflowRun | nul
|
|
|
226
226
|
|
|
227
227
|
// ── JsonlRunStore ────────────────────────────────────────────
|
|
228
228
|
|
|
229
|
-
/** Node fs 错误 code 判定(ENOENT = 路径不存在,并发删除场景)。 */
|
|
230
|
-
function isEnoentError(err: unknown): boolean {
|
|
231
|
-
return (
|
|
232
|
-
typeof err === "object" &&
|
|
233
|
-
err !== null &&
|
|
234
|
-
"code" in err &&
|
|
235
|
-
(err as { code: unknown }).code === "ENOENT"
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
229
|
const logger = getLogger("subagents");
|
|
240
230
|
|
|
241
231
|
/**
|
package/src/session-lifecycle.ts
CHANGED
|
@@ -300,7 +300,6 @@ function appendSubagentIdentityEntry(pi: ExtensionAPI): void {
|
|
|
300
300
|
process.env.PI_SUBAGENT_FORK_DEPTH !== undefined
|
|
301
301
|
? Number(process.env.PI_SUBAGENT_FORK_DEPTH)
|
|
302
302
|
: undefined,
|
|
303
|
-
chatMode: process.env.PI_SUBAGENT_CHAT_MODE === "true",
|
|
304
303
|
// [review round2] worktree 隔离标志(session-runner 注入):跨重启重建路径据此
|
|
305
304
|
// 拒绝续聊(handle 不可序列化,reattach 不可行)。
|
|
306
305
|
worktree: process.env.PI_SUBAGENT_WORKTREE === "true",
|
|
@@ -373,7 +372,8 @@ export function bindLedgerHostAndRecover(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
373
372
|
/**
|
|
374
373
|
* 随迁块 4 的进程级维护三连(各 try-catch「失败记日志不阻断」,设计 §3.4):
|
|
375
374
|
* 过期 session 文件清理 / ADR-035 manifest tmp 恢复 / ADR-035 worktree reaper 扫描。
|
|
376
|
-
*
|
|
375
|
+
* ([modeless 波5] 原 [E1] sync 批崩溃恢复接线已摘除——collectMode 记录态消亡后
|
|
376
|
+
* core 侧 recoverSyncCollectBatch 已是 accepted-no-op,调用点随之退役。)
|
|
377
377
|
*/
|
|
378
378
|
async function runProcessLevelMaintenance(
|
|
379
379
|
agentDir: string,
|
|
@@ -381,22 +381,6 @@ async function runProcessLevelMaintenance(
|
|
|
381
381
|
service: SubagentService,
|
|
382
382
|
deps: SessionLifecycleDeps,
|
|
383
383
|
): Promise<void> {
|
|
384
|
-
// [E1] sync 批崩溃恢复(subagent-sync-collect 设计 §3.1.5 E1,U5 接线):扫描主
|
|
385
|
-
// session 末条 entry 重建批缓冲;全员终态未投递 → notifyBatch 补发 + 统一补
|
|
386
|
-
// batchFinalized 标记(账本同 hash 幂等拒绝也算已投递)。须晚于 initSession(孤儿
|
|
387
|
-
// 终态恢复先行收敛 running 成员,createOrReuseServices 内部同步完成)与 ledger
|
|
388
|
-
// bind(补发走 notifyBatch 写账链,见上方 bindNotifyLedgerHost)。per-session 域
|
|
389
|
-
// (主 session 文件),不用 oncePerProcess;best-effort 不阻断 session_start。
|
|
390
|
-
// 时序约束在调用点 setupSessionLifecycle 已满足(bindLedgerHostAndRecover 与
|
|
391
|
-
// createOrReuseServices 均先于本 helper 调用)。
|
|
392
|
-
try {
|
|
393
|
-
service.recoverSyncCollectBatch();
|
|
394
|
-
} catch (err) {
|
|
395
|
-
logger.warn("[subagents] sync collect batch recovery failed", {
|
|
396
|
-
reason: toErrorMessage(err),
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
384
|
try {
|
|
401
385
|
// 递归扫描 <agentDir>/subagents + unlink 超 TTL 跨 session 文件属进程级维护
|
|
402
386
|
// ——oncePerProcess 守卫防双跑(u-audit-fix)。
|