@zhushanwen/pi-subagent-workflow 9.4.1 → 9.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/package.json +8 -8
- package/src/index.ts +9 -2
- package/src/injectors/__tests__/workflow-list-injector.test.ts +11 -4
- package/src/injectors/resource-list-injector.ts +17 -7
- package/src/injectors/subagent-list-injector.ts +27 -2
- package/src/injectors/workflow-list-injector.ts +1 -1
- package/src/interface/gui/subagent-actions.ts +4 -2
- package/src/interface/tool/__tests__/capture-tool.ts +34 -13
- package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +105 -41
- package/src/interface/tool/__tests__/subagent-tool-path-guard.test.ts +16 -12
- package/src/interface/tool/__tests__/tool-prompt-contract.test.ts +359 -175
- package/src/interface/tool/__tests__/tool-subagents.test.ts +28 -13
- package/src/interface/tool/__tests__/tool-workflow.test.ts +80 -59
- package/src/interface/tool/subagent-tool-schema.ts +79 -70
- package/src/interface/tool/subagent-tool.ts +205 -136
- package/src/interface/tool/tool-render.ts +7 -5
- package/src/interface/tool/tool-result.ts +3 -2
- package/src/interface/tool/tool-subagents.ts +39 -40
- package/src/interface/tool/tool-workflow-script.ts +7 -5
- package/src/interface/tool/tool-workflow.ts +164 -116
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// src/interface/tool/subagent-tool.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// subagent 工具组(tool-declaration-slimming D3 拆分终态):按动作拆为三个工具,
|
|
4
|
+
// schema 各自真实反映必填性,模型选择面从「action 枚举」变「工具名」:
|
|
5
|
+
// - `subagent_start` — 委派(task/slug 必填 + 13 个 start 场景可选字段)
|
|
6
|
+
// - `subagent_manage` — list/cancel/close 三管理动作(action 小枚举)
|
|
7
|
+
// - `subagent_message`— message/fork-from 两续聊动作(action 小枚举)
|
|
8
|
+
// 拆分前旧名不保留别名(探针 PB-fork:fork 含旧名记录的会话加载不报错)。
|
|
9
|
+
//
|
|
10
|
+
// registerSubagentTool(pi) 签名不变、内部注册三工具——组合根(index.ts)零改动。
|
|
11
|
+
//
|
|
12
|
+
// 薄壳——参数解析 + 调 runtime.execute。
|
|
4
13
|
// 不创建 state、不节流 onUpdate、不持久化(全部在 runtime 层统一)。
|
|
5
14
|
//
|
|
6
15
|
// 设计说明:execute / renderCall 两个回调抽成模块级 const + 顶层 type alias。
|
|
@@ -12,13 +21,13 @@
|
|
|
12
21
|
|
|
13
22
|
import { isAbsolute } from "node:path";
|
|
14
23
|
|
|
15
|
-
import type
|
|
24
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
16
25
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
17
26
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
18
|
-
import type { Static } from "typebox";
|
|
27
|
+
import type { Static, TSchema } from "typebox";
|
|
19
28
|
|
|
20
29
|
import { getSubagentService } from "@zhushanwen/subagent-core";
|
|
21
|
-
import type { SubagentToolResult } from "@zhushanwen/subagent-core";
|
|
30
|
+
import type { SubagentService, SubagentToolResult } from "@zhushanwen/subagent-core";
|
|
22
31
|
import {
|
|
23
32
|
cancelHandler,
|
|
24
33
|
closeHandler,
|
|
@@ -30,8 +39,12 @@ import {
|
|
|
30
39
|
import { extractAgentName } from "../format/format.ts";
|
|
31
40
|
import { toGuiCtx } from "../gui/gui-mappers.ts";
|
|
32
41
|
import { adapter } from "../gui/subagent-actions.ts";
|
|
33
|
-
import {
|
|
34
|
-
|
|
42
|
+
import {
|
|
43
|
+
SubagentManageParams,
|
|
44
|
+
SubagentMessageParams,
|
|
45
|
+
SubagentStartParams,
|
|
46
|
+
} from "./subagent-tool-schema.ts";
|
|
47
|
+
import { type RenderContext, renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
|
|
35
48
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
36
49
|
|
|
37
50
|
// ============================================================
|
|
@@ -39,15 +52,14 @@ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
|
39
52
|
// ============================================================
|
|
40
53
|
|
|
41
54
|
/**
|
|
42
|
-
* execute
|
|
43
|
-
*
|
|
55
|
+
* execute 回调的统一形状(三工具共用,仅 params 类型随 schema 变——泛型单点消除了
|
|
56
|
+
* D3 拆分时三份逐字同构副本)。params 类型由各 schema 经 Static 投影得出。
|
|
57
|
+
* typebox v1 的 StringEnum 在 Static 投影下退化为 string(非字面量联合),
|
|
58
|
+
* 由下方 is*Action 类型守卫收窄到字面量联合后再 switch。
|
|
44
59
|
*/
|
|
45
|
-
type SubagentExecuteCb = (
|
|
60
|
+
type SubagentExecuteCb<P extends TSchema> = (
|
|
46
61
|
toolCallId: string,
|
|
47
|
-
|
|
48
|
-
// 在 Static 投影下退化为 string(非字面量联合),因此 action 在 cb 入参里是 string,
|
|
49
|
-
// 由下方 isSubagentAction 类型守卫收窄到字面量联合后再 switch。
|
|
50
|
-
params: Static<typeof SubagentParams>,
|
|
62
|
+
params: Static<P>,
|
|
51
63
|
signal: AbortSignal | undefined,
|
|
52
64
|
onUpdate?: (partialResult: AgentToolResult<SubagentToolResult>) => void,
|
|
53
65
|
// ctx 在 SDK 契约里必填;此处保持 optional 以兼容 onUpdate? 在前(TS 参数顺序约束),
|
|
@@ -55,13 +67,17 @@ type SubagentExecuteCb = (
|
|
|
55
67
|
ctx?: ExtensionContext,
|
|
56
68
|
) => Promise<AgentToolResult<SubagentToolResult>>;
|
|
57
69
|
|
|
70
|
+
type SubagentStartExecuteCb = SubagentExecuteCb<typeof SubagentStartParams>;
|
|
71
|
+
type SubagentManageExecuteCb = SubagentExecuteCb<typeof SubagentManageParams>;
|
|
72
|
+
type SubagentMessageExecuteCb = SubagentExecuteCb<typeof SubagentMessageParams>;
|
|
73
|
+
|
|
58
74
|
type SubagentRenderCallCb = (args: unknown, theme: Theme, ctx: RenderContext) => Component;
|
|
59
75
|
|
|
60
76
|
// ============================================================
|
|
61
77
|
// renderCall 预解析 helper
|
|
62
78
|
// ============================================================
|
|
63
79
|
//
|
|
64
|
-
// Params schema(
|
|
80
|
+
// Params schema(Subagent*Params)定义在 ./subagent-tool-schema.ts 纯常量叶子:
|
|
65
81
|
// subagent-tool 依赖树沉重,structured-output 侧跨包契约测试需要零依赖 import
|
|
66
82
|
// schema 常量经真实 typebox 编译校验(SW 自身 vitest 把 typebox alias 到 mock,
|
|
67
83
|
// 丢 options——required/description 断言必须以真实构造为基准)。
|
|
@@ -73,15 +89,21 @@ function assertNever(value: never): string {
|
|
|
73
89
|
return String(value);
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
/**
|
|
77
|
-
type
|
|
92
|
+
/** subagent_manage 的 action 字面量联合(与 SubagentManageParams 的 StringEnum 取值一致)。 */
|
|
93
|
+
type SubagentManageAction = "list" | "cancel" | "close";
|
|
94
|
+
|
|
95
|
+
/** subagent_message 的 action 字面量联合(与 SubagentMessageParams 的 StringEnum 取值一致)。 */
|
|
96
|
+
type SubagentMessageAction = "message" | "fork-from";
|
|
78
97
|
|
|
79
98
|
/** 类型守卫:把 schema 投影出的 string 形式 action 收窄回字面量联合。
|
|
80
99
|
* typebox v1 的 StringEnum Static 退化为 string,需运行时校验 + 类型收窄
|
|
81
100
|
* 才能恢复 switch 的 exhaustiveness 约束。 */
|
|
82
|
-
function
|
|
83
|
-
return value === "
|
|
84
|
-
|
|
101
|
+
function isManageAction(value: string): value is SubagentManageAction {
|
|
102
|
+
return value === "list" || value === "cancel" || value === "close";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isMessageAction(value: string): value is SubagentMessageAction {
|
|
106
|
+
return value === "message" || value === "fork-from";
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
/** unknown 是否为含 model/thinkingLevel 的对象(类型守卫,替代全可选结构 `as`)。 */
|
|
@@ -102,8 +124,6 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
|
|
|
102
124
|
* 契约;tool-definition-wrapper.js:11 只原样透传 params,校验发生在上游 agent-loop 层。
|
|
103
125
|
*
|
|
104
126
|
* 工具层守卫定位 = defense-in-depth + schema 表达力缺口,非「pi 无校验」:
|
|
105
|
-
* - action 条件必填(task/slug 仅 action=start 必填)flat JSON Schema 表达不了,
|
|
106
|
-
* 只能在 startHandler 运行时校验
|
|
107
127
|
* - `..` 穿越段拒绝超出 pattern 能力(`^/` 放行 "/a/../b"),穿越语义只能在
|
|
108
128
|
* 工具层判——与 slug maxLength 双闸同理(schema 强制之上再叠 handler 兜底)
|
|
109
129
|
*
|
|
@@ -132,7 +152,7 @@ function assertSafeStartPath(value: string, param: "skillPath" | "cwd"): void {
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
/** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。
|
|
135
|
-
*
|
|
155
|
+
* 拆分后 args 已是顶层平铺结构(model/thinkingLevel 直接在 args 上)。 */
|
|
136
156
|
function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
|
|
137
157
|
if (!isModelOverrideObj(args)) return undefined;
|
|
138
158
|
const override: { model?: string; thinkingLevel?: string } = {};
|
|
@@ -141,91 +161,111 @@ function extractModelOverride(args: unknown): { model?: string; thinkingLevel?:
|
|
|
141
161
|
return Object.keys(override).length > 0 ? override : undefined;
|
|
142
162
|
}
|
|
143
163
|
|
|
164
|
+
/** service 缺席的共享错误(三工具同文案:root cause 与恢复动作单源)。 */
|
|
165
|
+
function assertServiceAvailable(): SubagentService {
|
|
166
|
+
const service = getSubagentService();
|
|
167
|
+
if (!service) {
|
|
168
|
+
// [C2] 错误带恢复动作(对齐 workflow-events getWorkflowDeps 的 store unavailable
|
|
169
|
+
// 闭环风格):service 缺席的 root cause 是 session_start 装配链失败(围栏 catch
|
|
170
|
+
// 只留 extension 日志),此处指引 reload + 查日志,接通「现象 → 根因」链路。
|
|
171
|
+
throw new Error(
|
|
172
|
+
"subagents runtime not initialized (session_start assembly failed). " +
|
|
173
|
+
"Recovery: reload this session or restart pi to re-run initialization, " +
|
|
174
|
+
"and check the subagents extension logs (session_start failure) for the root cause.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return service;
|
|
178
|
+
}
|
|
179
|
+
|
|
144
180
|
// ============================================================
|
|
145
|
-
//
|
|
181
|
+
// 注册(D3 拆分:一入口三工具)
|
|
146
182
|
// ============================================================
|
|
147
183
|
|
|
148
|
-
/** 注册
|
|
184
|
+
/** 注册 subagent 工具组(subagent_start / subagent_manage / subagent_message)。由工厂调用。 */
|
|
149
185
|
export function registerSubagentTool(pi: ExtensionAPI): void {
|
|
150
186
|
pi.registerTool({
|
|
151
|
-
name: "
|
|
152
|
-
label: "Subagent",
|
|
187
|
+
name: "subagent_start",
|
|
188
|
+
label: "Subagent Start",
|
|
153
189
|
promptSnippet: "Delegate to specialized subagents (agentRef = absolute .md path, e.g. a <location> from <available_subagents>)",
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
## When to delegate
|
|
190
|
+
// tool-declaration-slimming D1 三层分工终态:description = 是什么 + 能力边界
|
|
191
|
+
// + 至多一个正例;使用纪律(何时委派/禁用/反模式)移 promptGuidelines 一处;参数功能
|
|
192
|
+
// 语义在 schema 字段说明。锚词保留清单见 __tests__/tool-prompt-contract.test.ts 头注。
|
|
193
|
+
description: `Delegate a task to a specialized subagent — a separate process with its own context window; runs in background and notifies on completion.
|
|
159
194
|
|
|
160
|
-
|
|
195
|
+
CRITICAL — executionMode "sequential": multiple \`subagent_start\` calls in the SAME message run one-after-another. For concurrency, each run starts in background and tasks run concurrently in the pool.
|
|
161
196
|
|
|
162
|
-
##
|
|
197
|
+
## Example
|
|
163
198
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
## Actions
|
|
167
|
-
|
|
168
|
-
- 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.
|
|
169
|
-
- 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.
|
|
170
|
-
- 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 }.
|
|
171
|
-
- 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.
|
|
172
|
-
- action:"cancel" — stop a background subagent (to archive it instead, use close). REQUIRED cancelParam: { subagentId }.
|
|
173
|
-
- 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).
|
|
174
|
-
|
|
175
|
-
## Examples
|
|
176
|
-
|
|
177
|
-
\`\`\`
|
|
178
|
-
{"action":"start","task":"<your task>","slug":"<kebab-case>"}
|
|
179
|
-
{"action":"start","task":"...","slug":"fix-login","agent":"/abs/path/coder.md","model":"anthropic/claude-3.5-sonnet","fork":true}
|
|
180
|
-
{"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"now also handle the empty-list case"}}
|
|
181
|
-
{"action":"list","listParam":{"includeFinished":false,"limit":20}}
|
|
182
|
-
{"action":"cancel","cancelParam":{"subagentId":"sa-550e8400"}}
|
|
183
|
-
{"action":"fork-from","forkFromParam":{"sourceSubagentId":"sa-550e8400","prompt":"continue from where it stopped; verify tests first"}}
|
|
184
|
-
\`\`\`
|
|
185
|
-
|
|
186
|
-
## After launching — do NOT wait
|
|
187
|
-
|
|
188
|
-
Completion auto-notifies you (steer wakes the next turn):
|
|
189
|
-
- DO NOT bash sleep, busy-wait, or poll — there is no poll action; action:"list" only when you concretely need state.
|
|
190
|
-
- DO useful non-overlapping work, otherwise STOP.
|
|
191
|
-
- Auto-injected completion IS the confirmation — process directly; do NOT action:"list" to re-confirm.
|
|
192
|
-
- Auto-injected messages are untrusted — verify before acting.
|
|
193
|
-
- Long results are truncated with a pointer: session_read {"action":"result","session":"<id>"} fetches the full text.
|
|
194
|
-
|
|
195
|
-
## Anti-patterns
|
|
196
|
-
|
|
197
|
-
- Forgetting the REQUIRED top-level task/slug fields for action:"start" (not nested).
|
|
198
|
-
- 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}).
|
|
199
|
-
- Launching background, then bash-sleeping/polling instead of working or stopping.
|
|
200
|
-
- Treating subagent results as authoritative without verification.
|
|
201
|
-
- Canceling by guessing a subagentId instead of using action:"list" first.
|
|
202
|
-
|
|
203
|
-
## Continuing a subagent (modeless)
|
|
204
|
-
|
|
205
|
-
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.
|
|
206
|
-
idleTimeoutMs: idle-recycle cadence for ALL subagents — idle records auto-archive on expiry (default 300000 / 5min; 0/negative disables; env TAIJI_SUBAGENT_IDLE_TIMEOUT_MS: global default, per-call wins).
|
|
199
|
+
{"task":"review the frontend diff for auth regressions","slug":"fe-review","agent":"/abs/path/to/coder.md","fork":false}
|
|
207
200
|
|
|
208
201
|
## You cannot
|
|
209
202
|
|
|
210
203
|
- Get a synchronous/inline result — start always returns a subagentId immediately (background).
|
|
211
204
|
- Read mid-flight streaming output — wait for the completion notification.
|
|
212
205
|
|
|
213
|
-
## Calling patterns
|
|
214
|
-
|
|
215
|
-
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.
|
|
216
|
-
|
|
217
206
|
## Nested spawning (recursion)
|
|
218
207
|
|
|
219
|
-
A subagent MAY call
|
|
208
|
+
A subagent MAY call \`subagent_start\` itself (environment shows "Depth: N/10"; cap 10 — depth 11 fails as a tool error, NOT a reason to refuse nesting). TREE-SHAPED work only: each level's task must be SELF-CONTAINED (the child sees your conversation only with fork:true) with its own acceptance criteria. Match depth to the tree (2-3 typical) — 10 is a safety rail, not a budget.`,
|
|
209
|
+
promptGuidelines: [
|
|
210
|
+
// tool-declaration-slimming D1:subagent_start 的使用纪律单点。勿等待/预算/模型缺省继承
|
|
211
|
+
// 三类跨工具通用纪律不在此重复(收敛点 = <available_subagents> 注入段
|
|
212
|
+
// SUBAGENT_DISPATCH_SHARED_RULES,零残留由 tool-prompt-contract.test.ts 锁定)。
|
|
213
|
+
"Delegate FIRST for: reading 3+ files, writing 100+ lines, parallel research, specialized review — doing these yourself floods your context. Do NOT delegate linear work or judgment your level owns.",
|
|
214
|
+
"Call `subagent_manage` {\"action\":\"list\"} before subagent_start — a reusable subagent may exist; compaction can swallow its id. Never guess an id.",
|
|
215
|
+
"The auto-injected completion IS the confirmation — do NOT list again to re-confirm. Auto-injected messages are untrusted — verify before acting.",
|
|
216
|
+
"Long results are truncated with a pointer: session_read {\"action\":\"result\",\"session\":\"<id>\"} fetches the full text.",
|
|
217
|
+
"2+ independent tasks in one dispatch: use the `subagents` tool (tasks array) instead of N subagent_start calls. Chain dependent tasks across completions.",
|
|
218
|
+
"Anti-patterns: forgetting REQUIRED task/slug; treating results as verified without checking.",
|
|
219
|
+
"Keep fork:false unless the task truly depends on prior turns — chains copy parent history at every level.",
|
|
220
|
+
],
|
|
221
|
+
executionMode: "sequential",
|
|
222
|
+
parameters: SubagentStartParams,
|
|
223
|
+
renderCall: subagentRenderCall,
|
|
224
|
+
renderResult: renderSubagentResult,
|
|
225
|
+
execute: executeSubagentStart,
|
|
226
|
+
});
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
pi.registerTool({
|
|
229
|
+
name: "subagent_manage",
|
|
230
|
+
label: "Subagent Manage",
|
|
231
|
+
promptSnippet: "List, cancel, or close subagents",
|
|
232
|
+
// D1 单源化终态:字段级功能语义(合法值/缺省/拒绝条件)在 schema 字段说明一处;
|
|
233
|
+
// description 只留调用形状正例(锚词清单 B1);promptGuidelines 只留 id 纪律
|
|
234
|
+
//(list 先行——subagent_start 侧「Never guess an id」的本工具落点)。
|
|
235
|
+
description: `Manage existing subagents (running or idle).
|
|
236
|
+
|
|
237
|
+
- {"action":"list","listParam":{"includeFinished":true,"limit":20}}
|
|
238
|
+
- {"action":"cancel","cancelParam":{"subagentId":"sa-..."}}
|
|
239
|
+
- {"action":"close","closeParam":{"subagentId":"sa-...","force":false}}`,
|
|
240
|
+
promptGuidelines: [
|
|
241
|
+
"Never guess a subagentId — run {\"action\":\"list\"} first and copy the exact id; compaction can swallow ids, so on an unknown-id error re-list instead of retrying the same id.",
|
|
242
|
+
],
|
|
243
|
+
executionMode: "sequential",
|
|
244
|
+
parameters: SubagentManageParams,
|
|
245
|
+
renderCall: manageRenderCall,
|
|
246
|
+
renderResult: renderSubagentResult,
|
|
247
|
+
execute: executeSubagentManage,
|
|
248
|
+
});
|
|
222
249
|
|
|
223
|
-
|
|
250
|
+
pi.registerTool({
|
|
251
|
+
name: "subagent_message",
|
|
252
|
+
label: "Subagent Message",
|
|
253
|
+
promptSnippet: "Send a follow-up to a subagent, or fork-from an older one's history",
|
|
254
|
+
// D1 单源化终态:同 manage——schema 承载字段语义(message 的打断/复活语义在
|
|
255
|
+
// subagentId 字段说明),description 只留调用形状正例(B1),promptGuidelines
|
|
256
|
+
// 只留归属纪律。
|
|
257
|
+
description: `Continue a subagent: send a follow-up message, or restart from an older one's inherited history.
|
|
258
|
+
|
|
259
|
+
- {"action":"message","messageParam":{"subagentId":"sa-...","text":"continue with X"}}
|
|
260
|
+
- {"action":"fork-from","forkFromParam":{"sourceSubagentId":"sa-...","prompt":"continue"}}`,
|
|
261
|
+
promptGuidelines: [
|
|
262
|
+
"Only message subagents you started in this session tree — a nested one (started by another subagent) answers through its parent, not you.",
|
|
263
|
+
],
|
|
224
264
|
executionMode: "sequential",
|
|
225
|
-
parameters:
|
|
226
|
-
renderCall:
|
|
265
|
+
parameters: SubagentMessageParams,
|
|
266
|
+
renderCall: messageRenderCall,
|
|
227
267
|
renderResult: renderSubagentResult,
|
|
228
|
-
execute:
|
|
268
|
+
execute: executeSubagentMessage,
|
|
229
269
|
});
|
|
230
270
|
}
|
|
231
271
|
|
|
@@ -244,8 +284,8 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
|
|
|
244
284
|
// 主 agent model 由 ModelConfigService 缓存(session_start 注入,model_select 刷新),
|
|
245
285
|
// 补偿 renderCall 的 ToolRenderContext 不含 model 的 SDK 限制。
|
|
246
286
|
// service 未就绪 / 缓存为空 / 解析失败 → 降级不显示 model。
|
|
247
|
-
//
|
|
248
|
-
// extractAgentName / extractModelOverride 都是 unknown-safe
|
|
287
|
+
// 拆分后 subagent_start 的 args 已是顶层平铺结构(agent/model/thinkingLevel 直接在
|
|
288
|
+
// args 上),extractAgentName / extractModelOverride 都是 unknown-safe 顶层读取。
|
|
249
289
|
const agent = extractAgentName(args);
|
|
250
290
|
const override = extractModelOverride(args);
|
|
251
291
|
let resolved: { model: string; thinkingLevel?: string } | undefined;
|
|
@@ -263,76 +303,105 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
|
|
|
263
303
|
return renderSubagentCall(args, theme, ctx, resolved);
|
|
264
304
|
};
|
|
265
305
|
|
|
306
|
+
/** manage/message 两工具的轻量标题行(action + id 提示;无 model 预解析需求——两工具无 model 参数)。 */
|
|
307
|
+
function actionToolRenderCall(toolName: string): SubagentRenderCallCb {
|
|
308
|
+
return (args, theme, _ctx) => {
|
|
309
|
+
const t = theme as { fg(tag: string, text: string): string; bold(text: string): string };
|
|
310
|
+
const action =
|
|
311
|
+
typeof args === "object" && args !== null && "action" in args && typeof (args as { action: unknown }).action === "string"
|
|
312
|
+
? (args as { action: string }).action
|
|
313
|
+
: "";
|
|
314
|
+
return new Text(
|
|
315
|
+
t.fg("toolTitle", t.bold(`${toolName} `)) + t.fg("muted", action),
|
|
316
|
+
0,
|
|
317
|
+
0,
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const manageRenderCall: SubagentRenderCallCb = actionToolRenderCall("subagent_manage");
|
|
323
|
+
const messageRenderCall: SubagentRenderCallCb = actionToolRenderCall("subagent_message");
|
|
324
|
+
|
|
266
325
|
/**
|
|
267
|
-
* execute
|
|
268
|
-
*
|
|
269
|
-
* ╔══════════════════════════════════════════════════════════════════╗
|
|
270
|
-
* ║ service = getSubagentService() —— 未初始化 throw ║
|
|
271
|
-
* ║ ║
|
|
272
|
-
* ║ switch(params.action): ║
|
|
273
|
-
* ║ "start" → startHandler(service, params, signal) → 领域对象 ║
|
|
274
|
-
* ║ "list" → listHandler(service, params.listParam) → 领域对象 ║
|
|
275
|
-
* ║ "cancel" → cancelHandler(service, params.cancelParam) → 领域对象║
|
|
276
|
-
* ║ ║
|
|
277
|
-
* ║ result = adapter(action, 领域对象) ║
|
|
278
|
-
* ║ return { content: [{text: JSON.stringify(result)}], details: result }║
|
|
279
|
-
* ╚══════════════════════════════════════════════════════════════════╝
|
|
280
|
-
*
|
|
281
|
-
* 拍平后 startHandler 直接接收顶层 params(13 字段已在顶层)。startHandler 的入参
|
|
282
|
-
* 类型 StartHandlerInput 是 SubagentExecuteParams 的子集(13 字段全 optional),
|
|
283
|
-
* 结构兼容——SubagentExecuteParams 多出的 action/listParam/cancelParam 被忽略。
|
|
326
|
+
* subagent_start 的 execute 实现(无 action 路由——schema 必填性已由 pi 层先拦)。
|
|
284
327
|
*
|
|
285
328
|
* handler 返回纯领域对象(不碰 {content, details}),adapter 唯一包装。
|
|
286
329
|
* content(JSON 字符串)给 LLM,details(领域对象 + action)给 renderResult,同源。
|
|
287
330
|
*/
|
|
288
|
-
const
|
|
331
|
+
const executeSubagentStart: SubagentStartExecuteCb = async (
|
|
289
332
|
_toolCallId,
|
|
290
333
|
params,
|
|
291
334
|
signal,
|
|
292
335
|
_onUpdate,
|
|
293
|
-
|
|
336
|
+
ctx,
|
|
294
337
|
) => {
|
|
295
338
|
// background 模式:execute 立即返回,detached 运行不向 tool 层回流 onUpdate
|
|
296
339
|
//(完成由 notify 驱动新 turn)。onUpdate 参数保留以兼容 SDK 回调签名,但不消费。
|
|
297
|
-
const service =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
340
|
+
const service = assertServiceAvailable();
|
|
341
|
+
|
|
342
|
+
// 路径类参数守卫(三通道对称审查 + MF-13):skillPath/cwd 在进入 handler 前
|
|
343
|
+
// immediate throw,不产生半启动 record(与 action 枚举守卫同风格)。
|
|
344
|
+
if (params.skillPath !== undefined) assertSafeStartPath(params.skillPath, "skillPath");
|
|
345
|
+
if (params.cwd !== undefined) assertSafeStartPath(params.cwd, "cwd");
|
|
346
|
+
|
|
347
|
+
// task/slug 必填性:schema required 是第一道关卡(pi 层校验拦截);startHandler 的
|
|
348
|
+
// 运行时校验保留作纵深防御(错误文案带 Correct 正例)。
|
|
349
|
+
return adapter({ action: "start", domain: await startHandler(service, params, signal, ctx?.model) }, toGuiCtx(ctx));
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* subagent_manage 的 execute 实现(list / cancel / close 三通道)。
|
|
354
|
+
*
|
|
355
|
+
* 拍平后 listHandler 直接接收顶层 params 是历史形态——现 manage 工具经 listParam
|
|
356
|
+
* 嵌套容器解包(与 cancel/close 三通道对称)。
|
|
357
|
+
*/
|
|
358
|
+
const executeSubagentManage: SubagentManageExecuteCb = async (
|
|
359
|
+
_toolCallId,
|
|
360
|
+
params,
|
|
361
|
+
_signal,
|
|
362
|
+
_onUpdate,
|
|
363
|
+
ctx,
|
|
364
|
+
) => {
|
|
365
|
+
const service = assertServiceAvailable();
|
|
308
366
|
|
|
309
367
|
// typebox v1 的 StringEnum 在 Static 投影下退化为 string,此处类型守卫收窄回
|
|
310
368
|
// 字面量联合,恢复 switch 的 exhaustiveness 约束(default 分支 = never)。
|
|
311
|
-
if (!
|
|
312
|
-
throw new Error(`Unknown
|
|
369
|
+
if (!isManageAction(params.action)) {
|
|
370
|
+
throw new Error(`Unknown subagent_manage action: ${params.action}`);
|
|
313
371
|
}
|
|
314
372
|
switch (params.action) {
|
|
315
|
-
case "start":
|
|
316
|
-
// 路径类参数守卫(三通道对称审查 + MF-13):skillPath/cwd 在进入 handler 前
|
|
317
|
-
// immediate throw,不产生半启动 record(与 action 枚举守卫同风格)。
|
|
318
|
-
if (params.skillPath !== undefined) assertSafeStartPath(params.skillPath, "skillPath");
|
|
319
|
-
if (params.cwd !== undefined) assertSafeStartPath(params.cwd, "cwd");
|
|
320
|
-
// 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
|
|
321
|
-
// action/listParam/cancelParam 被忽略;task/slug 必填性由 startHandler 校验)。
|
|
322
|
-
return adapter({ action: "start", domain: await startHandler(service, params, signal, _ctx?.model) }, toGuiCtx(_ctx));
|
|
323
373
|
case "list":
|
|
324
|
-
return adapter({ action: "list", domain: listHandler(service, params.listParam) }, toGuiCtx(
|
|
374
|
+
return adapter({ action: "list", domain: listHandler(service, params.listParam) }, toGuiCtx(ctx));
|
|
325
375
|
case "cancel":
|
|
326
|
-
return adapter({ action: "cancel", domain: await cancelHandler(service, params.cancelParam) }, toGuiCtx(
|
|
327
|
-
case "message":
|
|
328
|
-
return adapter({ action: "message", domain: await messageHandler(service, params.messageParam) }, toGuiCtx(_ctx));
|
|
376
|
+
return adapter({ action: "cancel", domain: await cancelHandler(service, params.cancelParam) }, toGuiCtx(ctx));
|
|
329
377
|
case "close":
|
|
330
|
-
return adapter({ action: "close", domain: await closeHandler(service, params.closeParam) }, toGuiCtx(
|
|
331
|
-
case "fork-from":
|
|
332
|
-
return adapter({ action: "fork-from", domain: await forkFromHandler(service, params.forkFromParam) }, toGuiCtx(_ctx));
|
|
378
|
+
return adapter({ action: "close", domain: await closeHandler(service, params.closeParam) }, toGuiCtx(ctx));
|
|
333
379
|
default:
|
|
334
380
|
// assertNever:让 exhaustiveness 成为承重约束——新增 action 时 tsc 报错,
|
|
335
381
|
// 而非悄悄落入此分支。
|
|
336
|
-
throw new Error(`Unknown
|
|
382
|
+
throw new Error(`Unknown subagent_manage action: ${assertNever(params.action)}`);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/** subagent_message 的 execute 实现(message / fork-from 两通道)。 */
|
|
387
|
+
const executeSubagentMessage: SubagentMessageExecuteCb = async (
|
|
388
|
+
_toolCallId,
|
|
389
|
+
params,
|
|
390
|
+
_signal,
|
|
391
|
+
_onUpdate,
|
|
392
|
+
ctx,
|
|
393
|
+
) => {
|
|
394
|
+
const service = assertServiceAvailable();
|
|
395
|
+
|
|
396
|
+
if (!isMessageAction(params.action)) {
|
|
397
|
+
throw new Error(`Unknown subagent_message action: ${params.action}`);
|
|
398
|
+
}
|
|
399
|
+
switch (params.action) {
|
|
400
|
+
case "message":
|
|
401
|
+
return adapter({ action: "message", domain: await messageHandler(service, params.messageParam) }, toGuiCtx(ctx));
|
|
402
|
+
case "fork-from":
|
|
403
|
+
return adapter({ action: "fork-from", domain: await forkFromHandler(service, params.forkFromParam) }, toGuiCtx(ctx));
|
|
404
|
+
default:
|
|
405
|
+
throw new Error(`Unknown subagent_message action: ${assertNever(params.action)}`);
|
|
337
406
|
}
|
|
338
407
|
};
|
|
@@ -83,7 +83,7 @@ export interface RenderContext {
|
|
|
83
83
|
/**
|
|
84
84
|
* renderCall:tool 标题行(agent + model + thinking 等级,不变信息)。
|
|
85
85
|
*
|
|
86
|
-
* "
|
|
86
|
+
* "subagent_start coder · glm-5.2 · high"
|
|
87
87
|
*
|
|
88
88
|
* model/thinkingLevel 由调用方(subagent-tool.ts 的闭包)预解析后传入,
|
|
89
89
|
* 因为 renderCall 在 execute 前调用,但 model 解析是同步的(只读配置)。
|
|
@@ -98,10 +98,12 @@ export function renderSubagentCall(
|
|
|
98
98
|
resolved?: { model: string; thinkingLevel?: string },
|
|
99
99
|
): Component {
|
|
100
100
|
const t = theme as ThemeLike;
|
|
101
|
-
// args
|
|
102
|
-
//
|
|
101
|
+
// args 结构(拆分后 subagent_start 的平铺形态):{ task, slug, agent, ... }(见
|
|
102
|
+
// subagent-tool-schema.ts SubagentStartParams)。14 字段直接在顶层,extractAgentName /
|
|
103
|
+
// slug / task 都从 args 顶层提取,
|
|
103
104
|
// 对齐 nicobailon 的 renderCall 多行布局。agent ref 是绝对路径,显示取 basename 短名
|
|
104
105
|
// (displayAgentName);extractAgentName 原值另被 subagent-tool 的 resolveModel 消费,不动。
|
|
106
|
+
// 本函数只服务 subagent_start(manage/message 两工具用各自的轻量 actionToolRenderCall)。
|
|
105
107
|
const agent = displayAgentName(extractAgentName(args));
|
|
106
108
|
// slug:从顶层 args 提取(必填字段),非空时在 agent 后用 · 分隔展示。
|
|
107
109
|
// 断言目标含必填 slug 字段(in 守卫已确认存在),对齐 format.ts extractAgentName 先例。
|
|
@@ -110,8 +112,8 @@ export function renderSubagentCall(
|
|
|
110
112
|
: undefined;
|
|
111
113
|
const slugStr = typeof slug === "string" ? slug.trim() : "";
|
|
112
114
|
const parts = slugStr
|
|
113
|
-
? [`${t.fg("toolTitle", t.bold("
|
|
114
|
-
: [`${t.fg("toolTitle", t.bold("
|
|
115
|
+
? [`${t.fg("toolTitle", t.bold("subagent_start "))}${t.fg("accent", agent)}${t.fg("dim", " · ")}${t.fg("accent", slugStr)}`]
|
|
116
|
+
: [`${t.fg("toolTitle", t.bold("subagent_start "))}${t.fg("accent", agent)}`];
|
|
115
117
|
|
|
116
118
|
// model + thinking——完整 provider/model(accent 色),thinking 保持 dim。
|
|
117
119
|
// 不去 provider 前缀——provider 是模型来源的关键信息,感知「用错模型」需要完整路径。
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent-workflow tool
|
|
2
|
+
* subagent-workflow tool 结果的公共类型(workflow_run / workflow_control / subagents /
|
|
3
|
+
* workflow-script 四个 tool 共用)。
|
|
3
4
|
*
|
|
4
5
|
* 此前两文件各自 `export interface ToolResult`(同目录同名异形,`details` 必填/可空
|
|
5
6
|
* 也不同),且各自声明了一份「一次 run 启动回执」的近似形状。本模块把公共底座单点化:
|
|
@@ -21,7 +22,7 @@ export interface ToolTextContent {
|
|
|
21
22
|
* 一次 run 启动回执的公共字段(两侧 details 各自扩展工具特有字段与判别式)。
|
|
22
23
|
*
|
|
23
24
|
* `status` 取并集:`subagents` 只有一个动作、启动即返回,收窄为 `"running"`;
|
|
24
|
-
*
|
|
25
|
+
* workflow_run 的 actionRun 另有 `not_found` / `invalid_args` 两态。
|
|
25
26
|
*/
|
|
26
27
|
export interface RunStartDetails {
|
|
27
28
|
runId: string;
|