@zhushanwen/pi-subagent-workflow 8.3.0 → 8.4.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.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.",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"ajv": "^8.20.0",
|
|
49
49
|
"yaml": "^2.9.0",
|
|
50
|
-
"@xyz-agent/extension-protocol": "0.6.0",
|
|
51
50
|
"@xyz-agent/session-delivery": "0.2.0",
|
|
52
51
|
"@zhushanwen/pi-extension-logger": "0.3.0",
|
|
52
|
+
"@xyz-agent/extension-protocol": "0.6.0",
|
|
53
53
|
"@zhushanwen/pi-file-lock": "0.1.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
* 设计为纯函数(无 ctx / service 依赖),便于独立单测,handler 只做薄分发。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** /subagents RPC action 判别联合。
|
|
11
|
+
/** /subagents RPC action 判别联合。message/start 为 GUI 定向消息通道(设计 §3.3.3,
|
|
12
|
+
* 仅 RPC 分支消费;missing-args 携带 missing 字段供 handler 输出指明缺什么的 usage)。 */
|
|
12
13
|
export type SubagentRpcAction =
|
|
13
14
|
| { action: "cancel"; recordId: string }
|
|
14
15
|
| { action: "cancel-missing-id" }
|
|
16
|
+
| { action: "message"; recordId: string; text: string }
|
|
17
|
+
| { action: "message-missing-args"; missing: "recordId" | "text" }
|
|
18
|
+
| { action: "start"; slug: string; task: string }
|
|
19
|
+
| { action: "start-missing-args"; missing: "slug" | "task" }
|
|
15
20
|
| { action: "noop" };
|
|
16
21
|
|
|
17
22
|
/** /workflows RPC action 判别联合。 */
|
|
@@ -46,25 +51,84 @@ function isRemovedLifecycleVerb(verb: string): verb is "pause" | "resume" {
|
|
|
46
51
|
return REMOVED_LIFECYCLE_VERBS.has(verb as "pause" | "resume");
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 还原转义协议(设计 §3.3.3 / 探针 P3):字面 `\n`(反斜杠 + n 两字符)→ 真实换行、
|
|
56
|
+
* 字面 `\\`(两反斜杠)→ 单反斜杠。
|
|
57
|
+
*
|
|
58
|
+
* 与 runtime encodeDirectiveText(session-service.ts)互逆:composer 多行输入在
|
|
59
|
+
* client.prompt 传输前把真实换行编码为字面 \n、原生反斜杠编码为 \\(命令保持单行),
|
|
60
|
+
* extension 解析侧在此还原。反斜杠转义必须与换行转义在**单次遍历**里成对处理
|
|
61
|
+
* (交替分支 `\\\\|\\n`,两反斜杠优先匹配)——若只处理 \n,原文里的字面反斜杠+n
|
|
62
|
+
* (如路径 `C:\new`)会被误解码为换行,往返歧义。
|
|
63
|
+
*/
|
|
64
|
+
function decodeNewlineEscapes(s: string): string {
|
|
65
|
+
return s.replace(/\\\\|\\n/g, (m) => (m === "\\\\" ? "\\" : "\n"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 提取首个非空白 token 与其后剩余原文。
|
|
70
|
+
*
|
|
71
|
+
* 与 split(/\s+/) 不同:rest 保留 token 之后的全部原文(含空格/引号/换行转义),
|
|
72
|
+
* 供 message text / start task 的「剩余全量到字符串末尾」语义使用(设计 §3.3.3——
|
|
73
|
+
* pi 以首个空格拆命令名后 args 为其后全文,文本内的空格/引号必须原样保留)。
|
|
74
|
+
* rest 跳过 token 后的分隔空白(分隔符不属文本),但保留其后全部内容原样。
|
|
75
|
+
*/
|
|
76
|
+
function splitFirstToken(s: string): { token: string; rest: string } | null {
|
|
77
|
+
const head = s.trimStart();
|
|
78
|
+
if (!head) return null;
|
|
79
|
+
const idx = head.search(/\s/);
|
|
80
|
+
if (idx === -1) return { token: head, rest: "" };
|
|
81
|
+
return { token: head.slice(0, idx), rest: head.slice(idx + 1).trimStart() };
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
/**
|
|
50
85
|
* 解析 /subagents RPC 命令字符串。
|
|
51
86
|
*
|
|
52
87
|
* 支持格式:
|
|
53
88
|
* - `cancel <id>` → { action: "cancel", recordId }
|
|
54
89
|
* - `cancel`(无 id)→ { action: "cancel-missing-id" }
|
|
90
|
+
* - `message <recordId> <text...>` → { action: "message", recordId, text }
|
|
91
|
+
* text 为第二 token 后的剩余全量(含空格/引号原样;字面 \n 还原为换行、字面 \\ 还原为
|
|
92
|
+
* 反斜杠——composer 定向消息经此协议编码,与 runtime encodeDirectiveText 互逆,设计 §3.3.3)
|
|
93
|
+
* - `message`(缺 recordId 或 text 为空白)→ { action: "message-missing-args", missing }
|
|
94
|
+
* - `start <slug> <task...>` → { action: "start", slug, task }(task 同 text 转义协议)
|
|
95
|
+
* - `start`(缺 slug 或 task 为空白)→ { action: "start-missing-args", missing }
|
|
55
96
|
* - 其他(空 / 未知 action / 无参)→ { action: "noop" }
|
|
56
97
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
98
|
+
* missing-args 携带 missing 字段(缺哪个参数),handler 据此输出可操作的 usage
|
|
99
|
+
* 错误(全局规则:错误信息指向恢复动作)。noop 表示 GUI 端无对应程序化操作(GUI
|
|
100
|
+
* 已在 CommandPopover 屏蔽 /subagents 入口,此分支仅兜底手动 prompt)。
|
|
59
101
|
*/
|
|
60
102
|
export function parseSubagentRpcCommand(argsStr: string): SubagentRpcAction {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
103
|
+
const first = splitFirstToken(argsStr);
|
|
104
|
+
if (!first) return { action: "noop" };
|
|
63
105
|
|
|
64
|
-
const
|
|
106
|
+
const { token: verb, rest } = first;
|
|
65
107
|
if (verb === "cancel") {
|
|
66
|
-
|
|
67
|
-
return { action: "cancel"
|
|
108
|
+
const idToken = splitFirstToken(rest);
|
|
109
|
+
if (!idToken) return { action: "cancel-missing-id" };
|
|
110
|
+
return { action: "cancel", recordId: idToken.token };
|
|
111
|
+
}
|
|
112
|
+
if (verb === "message" || verb === "start") {
|
|
113
|
+
// message 与 start 共用解析骨架,仅结果字段名不同(recordId/text vs slug/task)
|
|
114
|
+
const isMessage = verb === "message";
|
|
115
|
+
// 第二 token:message→recordId / start→slug;其后剩余全量(还原换行转义)为 text/task
|
|
116
|
+
const second = splitFirstToken(rest);
|
|
117
|
+
if (!second) {
|
|
118
|
+
return isMessage
|
|
119
|
+
? { action: "message-missing-args", missing: "recordId" }
|
|
120
|
+
: { action: "start-missing-args", missing: "slug" };
|
|
121
|
+
}
|
|
122
|
+
// 先还原再判空:纯字面 \n 还原后是真实换行(whitespace),应在解析层拦截为缺参
|
|
123
|
+
const payload = decodeNewlineEscapes(second.rest);
|
|
124
|
+
if (!payload.trim()) {
|
|
125
|
+
return isMessage
|
|
126
|
+
? { action: "message-missing-args", missing: "text" }
|
|
127
|
+
: { action: "start-missing-args", missing: "task" };
|
|
128
|
+
}
|
|
129
|
+
return isMessage
|
|
130
|
+
? { action: "message", recordId: second.token, text: payload }
|
|
131
|
+
: { action: "start", slug: second.token, task: payload };
|
|
68
132
|
}
|
|
69
133
|
return { action: "noop" };
|
|
70
134
|
}
|
|
@@ -329,10 +329,13 @@ export interface MessageHandlerInput {
|
|
|
329
329
|
interrupt?: boolean;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
/** message 领域对象(adapter 包成 messageResponse)。
|
|
332
|
+
/** message 领域对象(adapter 包成 messageResponse)。
|
|
333
|
+
* slug 来自 record(GUI /subagents message 通道的留痕 details 需要,设计 §3.3.3),
|
|
334
|
+
* 避免调用方二次 getRecordForAction 查询。 */
|
|
333
335
|
export type MessageHandlerResult = {
|
|
334
336
|
kind: "message";
|
|
335
337
|
subagentId: string;
|
|
338
|
+
slug: string;
|
|
336
339
|
response: MessageResponse;
|
|
337
340
|
};
|
|
338
341
|
|
|
@@ -393,7 +396,7 @@ export async function messageHandler(
|
|
|
393
396
|
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
394
397
|
);
|
|
395
398
|
}
|
|
396
|
-
return { kind: "message", subagentId: id, response: { delivered: true } };
|
|
399
|
+
return { kind: "message", subagentId: id, slug: record.slug, response: { delivered: true } };
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
// ============================================================
|
|
@@ -3,16 +3,211 @@
|
|
|
3
3
|
// /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
|
|
4
4
|
//
|
|
5
5
|
// 解析:args[0] 直接作可选 <id>(聚焦该 record)。
|
|
6
|
-
// RPC 模式(xyz-agent GUI):解析 cancel action 直接执行,不打开 TUI。
|
|
6
|
+
// RPC 模式(xyz-agent GUI):解析 cancel/message/start action 直接执行,不打开 TUI。
|
|
7
|
+
// message/start 为 GUI 定向消息通道(设计 §3.3.3):GUI 经 client.prompt 短路
|
|
8
|
+
// extension 命令(不经主 agent LLM),TUI 分支不消费这两个 verb(行为零变化)。
|
|
7
9
|
|
|
8
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
11
|
|
|
10
12
|
import { getSubagentService } from "../execution/subagent-service.ts";
|
|
13
|
+
import type { SubagentService } from "../execution/subagent-service.ts";
|
|
11
14
|
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
15
|
+
import { messageHandler, startHandler } from "./subagent-actions.ts";
|
|
12
16
|
import { parseSubagentRpcCommand } from "./command-actions.ts";
|
|
17
|
+
import type { SubagentRpcAction } from "./command-actions.ts";
|
|
13
18
|
import { LIST_LIMIT } from "./list-shared.ts";
|
|
14
19
|
import { createSubagentsView } from "./list-view.ts";
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* subagent-directive custom_message 的 customType。
|
|
23
|
+
*
|
|
24
|
+
* 定向消息留痕载体(设计 §3.3.3):message/start 成功派发后落主 session 的
|
|
25
|
+
* custom_message entry,一 entry 双消费——
|
|
26
|
+
* 1. 主 agent 上下文(custom_message 进 context,主 agent 下次 turn 可见定向对话)
|
|
27
|
+
* 2. renderer 定向气泡渲染源(§3.3.3a live/reload 双链路,后续 wave 消费)
|
|
28
|
+
* 字段形状是 GUI 契约,改动需与 renderer 侧同步。
|
|
29
|
+
*/
|
|
30
|
+
export const SUBAGENT_DIRECTIVE_CUSTOM_TYPE = "subagent-directive";
|
|
31
|
+
|
|
32
|
+
/** subagent-directive entry 的 details 形状(GUI 定向气泡渲染契约)。 */
|
|
33
|
+
export interface SubagentDirectiveDetails {
|
|
34
|
+
subagentId: string;
|
|
35
|
+
slug: string;
|
|
36
|
+
/** 消息方向:'user' = 用户 → subagent 定向(当前唯一方向,命名预留双向扩展)。 */
|
|
37
|
+
direction: "user";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 定向消息留痕:向主 session 落 subagent-directive custom_message entry。
|
|
42
|
+
*
|
|
43
|
+
* 按主 agent streaming 状态分流 sendMessage options。pi 0.84.1 sendCustomMessage
|
|
44
|
+
* 实装(agent-session.js):isStreaming 且无 deliverAs 时默认 agent.steer()——会把
|
|
45
|
+
* 定向消息注入正在运行的主 agent LLM turn,违反「不经主 agent LLM 直达 subagent」。
|
|
46
|
+
* 故按调用时刻的权威 streaming 状态(ctx.isIdle(),与 sendCustomMessage 内部
|
|
47
|
+
* isStreaming 判据精确互补,含 agent_end 后 retry/continuation 窗口)分流:
|
|
48
|
+
* - streaming(isMainAgentIdle=false):传 { deliverAs: "nextTurn" }——消息入
|
|
49
|
+
* pi 内存 _pendingNextTurnMessages 队列,下个 turn 注入主 agent 上下文;不打断、
|
|
50
|
+
* 不 steer 当前 turn。注意:该队列不落 entry,留痕延迟到下个 turn
|
|
51
|
+
* - 非 streaming(isMainAgentIdle=true):不传 options——立即 append entry 留痕
|
|
52
|
+
* + message_start/end 双发(renderer live 链路即时可见,现状行为)
|
|
53
|
+
* 两者都不传 triggerTurn——不产生新 turn(§3.3.8「留痕 ≠ 处理」的结构性保证);
|
|
54
|
+
* display:false 使 pi TUI 不渲染该 entry(GUI 侧由 §3.3.3a 定向气泡通路渲染)。
|
|
55
|
+
*/
|
|
56
|
+
function emitSubagentDirective(
|
|
57
|
+
pi: Pick<ExtensionAPI, "sendMessage">,
|
|
58
|
+
details: SubagentDirectiveDetails,
|
|
59
|
+
text: string,
|
|
60
|
+
isMainAgentIdle: boolean,
|
|
61
|
+
): void {
|
|
62
|
+
pi.sendMessage(
|
|
63
|
+
{
|
|
64
|
+
customType: SUBAGENT_DIRECTIVE_CUSTOM_TYPE,
|
|
65
|
+
content: text,
|
|
66
|
+
display: false,
|
|
67
|
+
details,
|
|
68
|
+
},
|
|
69
|
+
isMainAgentIdle ? undefined : { deliverAs: "nextTurn" },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** RPC cancel 执行体(行为等价拆分自 handler,复杂度治理)。 */
|
|
74
|
+
async function rpcCancel(
|
|
75
|
+
service: SubagentService,
|
|
76
|
+
recordId: string,
|
|
77
|
+
ctx: ExtensionCommandContext,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
const ok = service.cancel(recordId);
|
|
81
|
+
ctx.ui.notify(
|
|
82
|
+
ok ? `Cancelled subagent ${recordId}` : `Subagent ${recordId} not found or already finished`,
|
|
83
|
+
ok ? "info" : "warning",
|
|
84
|
+
);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// service.cancel 内部 assertReady 在 session_shutdown 并发 dispose 时会抛
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
ctx.ui.notify(`Failed to cancel subagent ${recordId}: ${msg}`, "warning");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** RPC message 执行体(行为等价拆分自 handler,复杂度治理)。 */
|
|
93
|
+
async function rpcMessage(
|
|
94
|
+
pi: ExtensionAPI,
|
|
95
|
+
service: SubagentService,
|
|
96
|
+
recordId: string,
|
|
97
|
+
text: string,
|
|
98
|
+
ctx: ExtensionCommandContext,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
// GUI 定向消息(设计 §3.3.3):不经主 agent LLM 直达 subagent。
|
|
101
|
+
// one-shot 首条 message 自动升级 chatMode 的机制在 messageHandler 内(勿在此重复)。
|
|
102
|
+
try {
|
|
103
|
+
const result = await messageHandler(service, {
|
|
104
|
+
subagentId: recordId,
|
|
105
|
+
text,
|
|
106
|
+
});
|
|
107
|
+
// 留痕(§3.3.3):成功派发后才留痕——失败时不留痕,GUI 按 toast 错误重发。
|
|
108
|
+
// ctx.isIdle() 按调用时刻分流(streaming → nextTurn 队列延迟留痕,见
|
|
109
|
+
// emitSubagentDirective JSDoc),保证任何时刻都不 steer 主 agent 当前 turn
|
|
110
|
+
emitSubagentDirective(
|
|
111
|
+
pi,
|
|
112
|
+
{ subagentId: result.subagentId, slug: result.slug, direction: "user" },
|
|
113
|
+
text,
|
|
114
|
+
ctx.isIdle(),
|
|
115
|
+
);
|
|
116
|
+
ctx.ui.notify(`Message delivered to subagent ${result.slug} (${result.subagentId})`, "info");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
ctx.ui.notify(`Failed to message subagent ${recordId}: ${msg}`, "warning");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** RPC start 执行体(行为等价拆分自 handler,复杂度治理)。 */
|
|
124
|
+
async function rpcStart(
|
|
125
|
+
pi: ExtensionAPI,
|
|
126
|
+
service: SubagentService,
|
|
127
|
+
slug: string,
|
|
128
|
+
task: string,
|
|
129
|
+
ctx: ExtensionCommandContext,
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
// GUI 定向新建(设计 §3.3.3):conversation 固定 true(GUI 定向对话场景需要可续聊)
|
|
132
|
+
try {
|
|
133
|
+
const result = await startHandler(
|
|
134
|
+
service,
|
|
135
|
+
{
|
|
136
|
+
slug,
|
|
137
|
+
task,
|
|
138
|
+
conversation: true,
|
|
139
|
+
},
|
|
140
|
+
// RPC 命令无外层 AbortSignal(GUI 请求生命周期不映射到 subagent 取消——
|
|
141
|
+
// start 是 detached 后台语义,取消走 /subagents cancel)
|
|
142
|
+
undefined,
|
|
143
|
+
);
|
|
144
|
+
emitSubagentDirective(
|
|
145
|
+
pi,
|
|
146
|
+
{ subagentId: result.subagentId, slug: result.slug, direction: "user" },
|
|
147
|
+
task,
|
|
148
|
+
ctx.isIdle(),
|
|
149
|
+
);
|
|
150
|
+
ctx.ui.notify(`Started subagent ${result.slug} (${result.subagentId})`, "info");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
ctx.ui.notify(`Failed to start subagent ${slug}: ${msg}`, "warning");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* RPC 模式(xyz-agent GUI):解析后的 action 分发执行,不打开 TUI。
|
|
159
|
+
* 行为等价拆分自 handler(fallow 圈复杂度 21 > 15):三个执行体
|
|
160
|
+
* (cancel/message/start)各自成函数,本函数只做 switch 分发 +
|
|
161
|
+
* usage notify + exhaustiveness 断言。
|
|
162
|
+
*/
|
|
163
|
+
async function executeRpcAction(
|
|
164
|
+
pi: ExtensionAPI,
|
|
165
|
+
service: SubagentService,
|
|
166
|
+
parsed: SubagentRpcAction,
|
|
167
|
+
ctx: ExtensionCommandContext,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
switch (parsed.action) {
|
|
170
|
+
case "cancel":
|
|
171
|
+
await rpcCancel(service, parsed.recordId, ctx);
|
|
172
|
+
return;
|
|
173
|
+
case "cancel-missing-id":
|
|
174
|
+
ctx.ui.notify("Usage: /subagents cancel <id>", "warning");
|
|
175
|
+
return;
|
|
176
|
+
case "message":
|
|
177
|
+
await rpcMessage(pi, service, parsed.recordId, parsed.text, ctx);
|
|
178
|
+
return;
|
|
179
|
+
case "message-missing-args":
|
|
180
|
+
// 错误可操作:指明缺什么 + 完整 usage(全局规则 16)
|
|
181
|
+
ctx.ui.notify(
|
|
182
|
+
parsed.missing === "recordId"
|
|
183
|
+
? "Usage: /subagents message <recordId> <text> — recordId is missing"
|
|
184
|
+
: "Usage: /subagents message <recordId> <text> — text is missing",
|
|
185
|
+
"warning",
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
case "start":
|
|
189
|
+
await rpcStart(pi, service, parsed.slug, parsed.task, ctx);
|
|
190
|
+
return;
|
|
191
|
+
case "start-missing-args":
|
|
192
|
+
ctx.ui.notify(
|
|
193
|
+
parsed.missing === "slug"
|
|
194
|
+
? "Usage: /subagents start <slug> <task> — slug is missing"
|
|
195
|
+
: "Usage: /subagents start <slug> <task> — task is missing",
|
|
196
|
+
"warning",
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
case "noop":
|
|
200
|
+
// 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
|
|
201
|
+
ctx.ui.notify("View subagents in the sidebar Agents tab", "info");
|
|
202
|
+
return;
|
|
203
|
+
default: {
|
|
204
|
+
// exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
|
|
205
|
+
const _exhaustive: never = parsed;
|
|
206
|
+
throw new Error(`Unhandled subagent RPC action: ${String(_exhaustive)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
16
211
|
/** 注册 /subagents 命令(= list overlay)。 */
|
|
17
212
|
export function registerSubagentsCommand(pi: ExtensionAPI): void {
|
|
18
213
|
pi.registerCommand("subagents", {
|
|
@@ -59,35 +254,8 @@ export function registerSubagentsCommand(pi: ExtensionAPI): void {
|
|
|
59
254
|
// ── RPC 模式(xyz-agent GUI):解析 action 直接执行,不打开 TUI ──
|
|
60
255
|
// hasUI 在 TUI 和 RPC 都为 true,不能用于区分;用 ctx.mode === "rpc" 判定 GUI 通道。
|
|
61
256
|
if (ctx.mode === "rpc") {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
case "cancel": {
|
|
65
|
-
try {
|
|
66
|
-
const ok = service.cancel(parsed.recordId);
|
|
67
|
-
ctx.ui.notify(
|
|
68
|
-
ok ? `Cancelled subagent ${parsed.recordId}` : `Subagent ${parsed.recordId} not found or already finished`,
|
|
69
|
-
ok ? "info" : "warning",
|
|
70
|
-
);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
// service.cancel 内部 assertReady 在 session_shutdown 并发 dispose 时会抛
|
|
73
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
-
ctx.ui.notify(`Failed to cancel subagent ${parsed.recordId}: ${msg}`, "warning");
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
case "cancel-missing-id":
|
|
79
|
-
ctx.ui.notify("Usage: /subagents cancel <id>", "warning");
|
|
80
|
-
return;
|
|
81
|
-
case "noop":
|
|
82
|
-
// 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
|
|
83
|
-
ctx.ui.notify("View subagents in the sidebar Agents tab", "info");
|
|
84
|
-
return;
|
|
85
|
-
default: {
|
|
86
|
-
// exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
|
|
87
|
-
const _exhaustive: never = parsed;
|
|
88
|
-
throw new Error(`Unhandled subagent RPC action: ${String(_exhaustive)}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
257
|
+
await executeRpcAction(pi, service, parseSubagentRpcCommand(argsStr), ctx);
|
|
258
|
+
return;
|
|
91
259
|
}
|
|
92
260
|
|
|
93
261
|
// ── print/json 模式(headless):不可交互 ──
|