@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.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/README.md +5 -11
- package/package.json +7 -10
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +23 -2
- package/src/host/pi-host.ts +57 -3
- package/src/index.ts +56 -110
- package/src/injectors/__tests__/engine-awareness.test.ts +2 -2
- package/src/injectors/__tests__/engine-section-stability.test.ts +6 -4
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
- package/src/injectors/engine-awareness.ts +0 -4
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/subagent-list-injector.ts +55 -113
- package/src/injectors/workflow-list-injector.ts +33 -66
- package/src/interface/__tests__/detectors.test.ts +45 -34
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +37 -1
- package/src/interface/bg-notify-render.ts +3 -13
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +1 -1
- package/src/interface/format.ts +15 -4
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +8 -129
- package/src/interface/list-component.ts +1 -1
- package/src/interface/list-shared.ts +1 -1
- package/src/interface/list-view.ts +1 -1
- package/src/interface/subagent-actions.ts +51 -676
- package/src/interface/subagent-tool-schema.ts +1 -4
- package/src/interface/subagent-tool.ts +1 -1
- package/src/interface/subagents.ts +1 -1
- package/src/interface/tool-render.ts +3 -13
- package/src/interface/tool-workflow-script.ts +33 -75
- package/src/interface/tool-workflow.ts +51 -88
- package/src/interface/views/WorkflowsView.ts +3 -11
- package/src/interface/views/format.ts +19 -58
- package/src/jsonl-run-store.ts +134 -222
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Subagent List Injector(迁移自 unified-hooks + P3/P4
|
|
2
|
+
* Subagent List Injector(迁移自 unified-hooks + P3/P4 改造;C5① 渲染改接 core)
|
|
3
3
|
*
|
|
4
4
|
* 发现所有可用 subagent(builtin + user + project 各源)并通过 before_agent_start
|
|
5
5
|
* 每 turn 注入 `<available_subagents>` 段(name + description),让模型能选对 agent
|
|
@@ -7,15 +7,23 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 改造点:
|
|
9
9
|
* - P4:发现路径由自实现扫 4 目录改为同包 ADR-031 统一发现 discoverResources
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* (P4 时点覆盖 7 源;C2 扩源后为统一发现全源 9 值 ResourceSource,含
|
|
11
|
+
* user-extension-paths/project-pi-tmp/project-host——序见 resource-discovery.ts
|
|
12
|
+
* 模块头注 + 优先级合并)。discoverResources 只返回 DiscoveredResource(path/source/
|
|
12
13
|
* available),不含 name/description——保留 parseAgentFrontmatter 解析每个 .md 的
|
|
13
14
|
* frontmatter 提取 name+description。
|
|
14
|
-
* - P3
|
|
15
|
-
*
|
|
15
|
+
* - P3:注入段引导语含正向触发引导(何时该 delegate)+ 名字约束(文案见
|
|
16
|
+
* SUBAGENT_LIST_GUIDE)。
|
|
17
|
+
* - C5①(convergence D-3):formatAgentList/sortByCodepoint 下沉 core,本文件改调
|
|
18
|
+
* core barrel(渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);
|
|
19
|
+
* guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
|
|
20
|
+
* - U11(sink 设计):装配循环(发现→解析→去重→排序 + warn/error 口径)整体改
|
|
21
|
+
* 消费 core discoverAgents(execution/agents-assembly,U2/A6)——壳侧收缩为
|
|
22
|
+
* 「宿主注入根现取 + 委托」,语义等值口径见 agents-assembly.ts 头注(⛔1 探针
|
|
23
|
+
* 对照注入 XML diff 为空验收)。
|
|
16
24
|
*
|
|
17
|
-
* 归位原因:injector 是 subagent-workflow
|
|
18
|
-
*
|
|
25
|
+
* 归位原因:injector 是 subagent-workflow 的内聚功能壳——事件接线与数据获取留
|
|
26
|
+
* 在插件层(before_agent_start / modelRegistry),解析/渲染算法消费 core。
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
|
|
@@ -31,18 +39,35 @@ import type {
|
|
|
31
39
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
32
40
|
|
|
33
41
|
import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
|
|
34
|
-
|
|
42
|
+
// U11(sink 设计):agent 装配循环单源 core discoverAgents(U2/A6 装配函数,
|
|
43
|
+
// 深路径消费);渲染(formatAgentList)与 workspace 根推导(findWorkspaceRoot)
|
|
44
|
+
// 走 barrel,parseResourceMeta 仍服务 parseAgentFrontmatter(严格注入投影语义锚)。
|
|
45
|
+
// 上方 getHostServices 深路径刻意保留:core 服务定位器(core 内模块统一经它取用
|
|
46
|
+
// 宿主端口),barrel 刻意不导出(D5 exports 面即 semver 契约、逐名列出,未列名
|
|
47
|
+
// 的内部件不经 barrel——见 subagent-core src/index.ts 头注),壳侧深路径消费经包
|
|
48
|
+
// `./*` -> src 通配豁免。
|
|
35
49
|
import {
|
|
36
|
-
|
|
50
|
+
type AgentEntry,
|
|
37
51
|
findWorkspaceRoot,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "@zhushanwen/subagent-core
|
|
41
|
-
import {
|
|
42
|
-
import { escapeXml, renderXmlSection } from "@zhushanwen/subagent-core/shared/xml-injection.ts";
|
|
52
|
+
formatAgentList,
|
|
53
|
+
parseResourceMeta,
|
|
54
|
+
} from "@zhushanwen/subagent-core";
|
|
55
|
+
import { discoverAgents } from "@zhushanwen/subagent-core/execution/agents-assembly.ts";
|
|
43
56
|
|
|
44
57
|
const logger = getLogger("injector");
|
|
45
58
|
|
|
59
|
+
/**
|
|
60
|
+
* pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入)。
|
|
61
|
+
*
|
|
62
|
+
* 2026-08 C5 重写依据(pi 现参数面,src/interface/subagent-tool-schema.ts):
|
|
63
|
+
* 旧句「pass systemPrompt alongside the agent name to create a dynamic agent」
|
|
64
|
+
* 指向的 systemPrompt 参数已不在 subagent tool schema——现 agent 参数 = .md 绝对
|
|
65
|
+
* 路径(<location>),缺省落 general-purpose(继承主 agent 模型与项目上下文),
|
|
66
|
+
* 动态指引经 task 文本 / appendSystemPrompt 参数承载。
|
|
67
|
+
*/
|
|
68
|
+
export const SUBAGENT_LIST_GUIDE =
|
|
69
|
+
"The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, ONLY use agents from this list — pass the <location> path (absolute .md path) as the agent param. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
|
|
70
|
+
|
|
46
71
|
/**
|
|
47
72
|
* Session 级 agent 列表缓存(per-process = per-session)。
|
|
48
73
|
*
|
|
@@ -64,17 +89,8 @@ let agentInjectionCache: string | null = null;
|
|
|
64
89
|
/** agentCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
65
90
|
function setAgentCache(entries: AgentEntry[] | null): void {
|
|
66
91
|
agentCache = entries;
|
|
67
|
-
agentInjectionCache =
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 从 .md frontmatter 提取的最小 agent 信息(m5:+ when/examples 路由样本;S1:+ path) */
|
|
71
|
-
export interface AgentEntry {
|
|
72
|
-
name: string;
|
|
73
|
-
description: string;
|
|
74
|
-
when?: string;
|
|
75
|
-
examples?: Array<{ match: string; action: string; positive: boolean }>;
|
|
76
|
-
/** agentRef:agent .md 文件的绝对路径(注入段 <location>,模型直接引用) */
|
|
77
|
-
path: string;
|
|
92
|
+
agentInjectionCache =
|
|
93
|
+
entries !== null ? formatAgentList(entries, { guide: SUBAGENT_LIST_GUIDE }) : null;
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
/**
|
|
@@ -97,103 +113,29 @@ export function parseAgentFrontmatter(content: string): AgentEntry | null {
|
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
/**
|
|
100
|
-
*
|
|
116
|
+
* 发现所有可用 agent(注入清单装配入口)。
|
|
101
117
|
*
|
|
102
|
-
* discoverResources
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
118
|
+
* U11(sink 设计):原手写装配循环(discoverResources 逐个 parseAgentFrontmatter
|
|
119
|
+
* 解析 → agent name 去重(高优先级靠后 Map 后写胜)→ name 码点序排序 + warn/error
|
|
120
|
+
* 口径)整体改消费 core `discoverAgents`(U2/A6 装配函数)——解析/去重/排序/warn
|
|
121
|
+
* 口径单源 core,第三宿主免复刻(G3/S5)。等值口径(IF1 严格层可见性、warn 仅限
|
|
122
|
+
* 「有 frontmatter 但解析失败」、KV-cache 码点序契约:注入段进每 turn system
|
|
123
|
+
* prompt,顺序与 readdir 枚举序解耦,目录内容不变时任意重建渲染逐字节一致)登记见
|
|
124
|
+
* core execution/agents-assembly.ts 头注。
|
|
107
125
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
126
|
+
* 壳侧保留职责:签名(workspaceRoot 单参)与宿主注入根现取(pi 壳 discoveryRoots
|
|
127
|
+
* 每次现取 getAgentDir,实例隔离;agentDir 形参已删——其唯一用途就是喂 ScanConfig,
|
|
128
|
+
* u0-data-discovery 偏差 #7)。
|
|
111
129
|
*
|
|
112
130
|
* 永不抛错——发现本身 fail-safe,单个文件读失败仅记日志。
|
|
113
131
|
*/
|
|
114
132
|
export async function discoverAllAgents(
|
|
115
133
|
workspaceRoot: string,
|
|
116
134
|
): Promise<AgentEntry[]> {
|
|
117
|
-
|
|
118
|
-
kind: "agents",
|
|
135
|
+
return discoverAgents(
|
|
119
136
|
workspaceRoot,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
hostRoots: getHostServices().discoveryRoots?.()?.agents ?? [],
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const agentMap = new Map<string, AgentEntry>();
|
|
126
|
-
for (const resource of resources) {
|
|
127
|
-
if (!resource.available) continue;
|
|
128
|
-
try {
|
|
129
|
-
const agent = getCachedParsed(resource.path, parseAgentFrontmatter);
|
|
130
|
-
if (agent) {
|
|
131
|
-
agentMap.set(agent.name, { ...agent, path: resource.path });
|
|
132
|
-
} else if (startsWithFrontmatter(resource.path)) {
|
|
133
|
-
// m5(评审 M3/F2 + minor-5):仅「有 frontmatter 但解析失败」才 warn
|
|
134
|
-
// (缺 name/description/examples 单条非法致整体 reject)——README 等
|
|
135
|
-
// 无 frontmatter 的 .md 不刷 warn(每 turn 扫描)。
|
|
136
|
-
logger.warn(
|
|
137
|
-
`[subagent-list-injector] ${resource.path}: agent frontmatter 解析失败(IF1 校验不通过)——agent 未注入`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
} catch (err) {
|
|
141
|
-
// 单个文件读失败不阻断整条 agent 列表注入
|
|
142
|
-
logger.error(
|
|
143
|
-
`[subagent-list-injector] skip unreadable agent file ${resource.path}`,
|
|
144
|
-
{ reason: err instanceof Error ? err.message : String(err) },
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return sortByCodepoint([...agentMap.values()], (a) => a.name);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** 码点序排序(显式契约,禁 localeCompare——宿主 locale 差异会破坏跨环境字节一致)。 */
|
|
152
|
-
function sortByCodepoint<T>(items: T[], key: (item: T) => string): T[] {
|
|
153
|
-
return items.sort((a, b) => {
|
|
154
|
-
const ka = key(a);
|
|
155
|
-
const kb = key(b);
|
|
156
|
-
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** 内容以 frontmatter 分隔符开头(解析失败才值得 warn 的判据)。 */
|
|
161
|
-
function startsWithFrontmatter(filePath: string): boolean {
|
|
162
|
-
return (getCachedFileContent(filePath) ?? "").trimStart().startsWith("---");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 将 agent 列表格式化为 XML 注入段。
|
|
167
|
-
*
|
|
168
|
-
* P3:引导语开头补正向触发条件(何时该 delegate),再保留原「ONLY use agent names
|
|
169
|
-
* from this list」名字约束。空列表返回空串(不注入)。
|
|
170
|
-
*/
|
|
171
|
-
export function formatAgentList(agents: AgentEntry[]): string {
|
|
172
|
-
if (agents.length === 0) return "";
|
|
173
|
-
|
|
174
|
-
const items = agents.map((agent) => {
|
|
175
|
-
let block = ` <agent><name>${escapeXml(agent.name)}</name><description>${escapeXml(agent.description)}</description>`;
|
|
176
|
-
// m5:路由样本(when + examples 正反原样渲染——negative 的 action 由作者写
|
|
177
|
-
// 「不调用(原因)」,渲染器不硬编码;全部内容 escapeXml 防 XML 注入段破坏)
|
|
178
|
-
if (agent.when) {
|
|
179
|
-
block += `<when>${escapeXml(agent.when)}</when>`;
|
|
180
|
-
}
|
|
181
|
-
if (agent.examples && agent.examples.length > 0) {
|
|
182
|
-
// 两极性原样渲染——negative 的 action 由作者写「不调用(原因)」,
|
|
183
|
-
// 渲染器不硬编码后缀(exec-review major-1:曾追加「(不调用)」致双后缀)
|
|
184
|
-
const exampleLines = agent.examples.map(
|
|
185
|
-
(e) => ` - "${escapeXml(e.match)}" → ${escapeXml(e.action)}`,
|
|
186
|
-
);
|
|
187
|
-
block += `\n <examples>\n${exampleLines.join("\n")}\n </examples>`;
|
|
188
|
-
}
|
|
189
|
-
block += `<location>${escapeXml(agent.path)}</location></agent>`;
|
|
190
|
-
return block;
|
|
191
|
-
});
|
|
192
|
-
return renderXmlSection({
|
|
193
|
-
tag: "available_subagents",
|
|
194
|
-
guide: "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, ONLY use agent names from this list. If no agent matches your task, pass systemPrompt alongside the agent name to create a dynamic agent.",
|
|
195
|
-
items,
|
|
196
|
-
});
|
|
137
|
+
getHostServices().discoveryRoots?.()?.agents ?? [],
|
|
138
|
+
);
|
|
197
139
|
}
|
|
198
140
|
|
|
199
141
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow List Injector(B2 / P1)
|
|
2
|
+
* Workflow List Injector(B2 / P1;C5① 渲染改接 core)
|
|
3
3
|
*
|
|
4
4
|
* 发现所有可用 workflow(.js/.mjs)并通过 before_agent_start 每 turn 注入
|
|
5
5
|
* `<available_workflows>` 段(name + description),与 subagent 注入段对称。
|
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
* 实现要点:
|
|
11
11
|
* - 发现走同包 ADR-031 统一发现 discoverResources({kind:"workflows", includeTmp:true})
|
|
12
12
|
* (includeTmp 覆盖 .pi/workflows/.tmp/,即 workflow-script generate 的产物)
|
|
13
|
-
* - 解析每个 workflow 的
|
|
13
|
+
* - 解析每个 workflow 的 meta 提取 name+description(IF1 parseResourceMeta)
|
|
14
14
|
* - description 截断为 prompt 友好的摘要(builtin 的 review-fix-loop 描述超长,全量
|
|
15
|
-
* 注入每 turn 会膨胀 prompt
|
|
15
|
+
* 注入每 turn 会膨胀 prompt)——summarizeDescription C5① 起改用 core barrel
|
|
16
|
+
* (算法逐字同本地旧实现)
|
|
17
|
+
* - C5①(convergence D-3):formatWorkflowList 下沉 core,本文件改调 core barrel
|
|
18
|
+
* (渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);guide 文案
|
|
19
|
+
* 是 pi 宿主注入(core 不内嵌平台文案)
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
|
|
@@ -28,14 +32,24 @@ import type {
|
|
|
28
32
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
29
33
|
|
|
30
34
|
import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
|
|
31
|
-
|
|
35
|
+
// C5①/C5⑦/C5b:发现与渲染统一走 core barrel——发现链辅助
|
|
36
|
+
// (resource-discovery/meta-parser 面:findWorkspaceRoot/getCachedParsed/
|
|
37
|
+
// parseResourceMeta)深路径 import 已归零。上方 getHostServices 深路径刻意
|
|
38
|
+
// 保留:core 服务定位器(core 内模块统一经它取用宿主端口),barrel 刻意不
|
|
39
|
+
// 导出(D5 exports 面即 semver 契约、逐名列出,未列名的内部件不经 barrel——
|
|
40
|
+
// 见 subagent-core src/index.ts 头注),壳侧深路径消费经包 `./*` -> src 通配豁免。
|
|
32
41
|
import {
|
|
33
42
|
discoverResources,
|
|
34
43
|
findWorkspaceRoot,
|
|
44
|
+
formatWorkflowList,
|
|
35
45
|
getCachedParsed,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
parseResourceMeta,
|
|
47
|
+
summarizeDescription,
|
|
48
|
+
type WorkflowEntry,
|
|
49
|
+
} from "@zhushanwen/subagent-core";
|
|
50
|
+
// U11(sink 设计):注入清单排序码点序单源——深路径消费 core sortByCodepoint
|
|
51
|
+
// (与 barrel 版同一实现,深路径遵循任务指定消费形态;`./*` -> src 通配豁免)。
|
|
52
|
+
import { sortByCodepoint } from "@zhushanwen/subagent-core/shared/injection-render.ts";
|
|
39
53
|
|
|
40
54
|
const logger = getLogger("injector");
|
|
41
55
|
|
|
@@ -54,44 +68,19 @@ let workflowInjectionCache: string | null = null;
|
|
|
54
68
|
/** workflowCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
55
69
|
function setWorkflowCache(entries: WorkflowEntry[] | null): void {
|
|
56
70
|
workflowCache = entries;
|
|
57
|
-
workflowInjectionCache =
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
|
|
61
|
-
const MAX_DESC_LEN = 160;
|
|
62
|
-
|
|
63
|
-
/** 断句阈值比例:句末标点位置须 >= maxLen 的 40% 才采用,否则硬截断保留更多信息 */
|
|
64
|
-
const DESC_BOUNDARY_MIN_RATIO = 0.4;
|
|
65
|
-
|
|
66
|
-
/** 解析后的 workflow 条目(name + 截断后的 description + agentRef 路径) */
|
|
67
|
-
export interface WorkflowEntry {
|
|
68
|
-
name: string;
|
|
69
|
-
description: string;
|
|
70
|
-
/** workflowRef:脚本 .js 文件的绝对路径(注入段 <location>,模型直接引用) */
|
|
71
|
-
path: string;
|
|
71
|
+
workflowInjectionCache =
|
|
72
|
+
entries !== null ? formatWorkflowList(entries, { guide: WORKFLOW_LIST_GUIDE }) : null;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入;
|
|
77
|
+
* 文案与改造前本地实现逐字一致——workflow 段 guide 无过期问题)。
|
|
78
|
+
*
|
|
79
|
+
* 引导语与具体 workflow 解耦:不写死内置名(列表本身已含全部 workflow,名字/描述
|
|
80
|
+
* 每 turn 由 @pi-meta 动态注入),只给通用路由指引 + read location 参数指针。
|
|
77
81
|
*/
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
maxLen = MAX_DESC_LEN,
|
|
81
|
-
): string {
|
|
82
|
-
const trimmed = desc.trim();
|
|
83
|
-
if (trimmed.length <= maxLen) return trimmed;
|
|
84
|
-
const slice = trimmed.slice(0, maxLen);
|
|
85
|
-
const boundary = Math.max(
|
|
86
|
-
slice.lastIndexOf("。"),
|
|
87
|
-
slice.lastIndexOf(";"),
|
|
88
|
-
slice.lastIndexOf(";"),
|
|
89
|
-
slice.lastIndexOf(". "),
|
|
90
|
-
);
|
|
91
|
-
// 断点过靠前(< 阈值比例)时不采用,改硬截断保留更多信息
|
|
92
|
-
if (boundary > maxLen * DESC_BOUNDARY_MIN_RATIO) return slice.slice(0, boundary + 1);
|
|
93
|
-
return `${slice}…`;
|
|
94
|
-
}
|
|
82
|
+
export const WORKFLOW_LIST_GUIDE =
|
|
83
|
+
"The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage).";
|
|
95
84
|
|
|
96
85
|
/**
|
|
97
86
|
* 解析 workflow .js 文件的 meta(name + description),经 IF1 parseResourceMeta。
|
|
@@ -138,31 +127,9 @@ export async function discoverAllWorkflows(
|
|
|
138
127
|
);
|
|
139
128
|
}
|
|
140
129
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 将 workflow 列表格式化为 XML 注入段。
|
|
148
|
-
*
|
|
149
|
-
* 引导语对齐 subagent injector:workflow 已在下方列出,模型应直接 run;
|
|
150
|
-
* list 仅用于查询运行态。builtin workflow 点名「run directly」。
|
|
151
|
-
* 空列表返回空串(不注入)。
|
|
152
|
-
*/
|
|
153
|
-
export function formatWorkflowList(workflows: WorkflowEntry[]): string {
|
|
154
|
-
if (workflows.length === 0) return "";
|
|
155
|
-
|
|
156
|
-
const items = workflows.map((wf) =>
|
|
157
|
-
` <workflow><name>${escapeXml(wf.name)}</name><description>${escapeXml(wf.description)}</description><location>${escapeXml(wf.path)}</location></workflow>`,
|
|
158
|
-
);
|
|
159
|
-
return renderXmlSection({
|
|
160
|
-
tag: "available_workflows",
|
|
161
|
-
// 引导语与具体 workflow 解耦:不写死内置名(列表本身已含全部 workflow,
|
|
162
|
-
// 名字/描述每 turn 由 @pi-meta 动态注入),只给通用路由指引 + read location 参数指针。
|
|
163
|
-
guide: "The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage).",
|
|
164
|
-
items,
|
|
165
|
-
});
|
|
130
|
+
// U11:内联码点序 sort 折叠为 core sortByCodepoint(实现逐字等价——同比较器
|
|
131
|
+
// 语义:ka < kb ? -1 : ka > kb ? 1 : 0,非变异副本排序)
|
|
132
|
+
return sortByCodepoint([...map.values()], (a) => a.name);
|
|
166
133
|
}
|
|
167
134
|
|
|
168
135
|
/**
|
|
@@ -17,63 +17,73 @@ import { describe, expect, it } from "vitest";
|
|
|
17
17
|
import { readFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// D9 下沉后权威实现迁至 core args-meta——宿主差异(TOOL_TOP_LEVEL 撞名保护)经
|
|
21
|
+
// reservedKeys 注入,与 tool-workflow 调用点保持同一契约。
|
|
22
|
+
import {
|
|
23
|
+
argKeysFromMeta,
|
|
24
|
+
findFlattenedArgKeys,
|
|
25
|
+
} from "@zhushanwen/subagent-core/orchestration/args-meta.ts";
|
|
26
|
+
import { TOOL_TOP_LEVEL } from "../tool-workflow";
|
|
21
27
|
import { parseResourceMeta } from "@zhushanwen/subagent-core/shared/meta-parser.ts";
|
|
22
28
|
|
|
23
|
-
//
|
|
24
|
-
|
|
29
|
+
// 宿主 reservedKeys 注入(与 tool-workflow ARGS_META_OPTIONS 等值契约)
|
|
30
|
+
const OPT = { reservedKeys: TOOL_TOP_LEVEL };
|
|
31
|
+
|
|
32
|
+
// 真实参数 meta(@pi-meta parameters——m6 动态数据源,与 TC2 同源防漂移)
|
|
33
|
+
function rflMeta() {
|
|
25
34
|
const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/review-fix-loop.js"), "utf-8");
|
|
26
35
|
const meta = parseResourceMeta(src, "workflow");
|
|
27
36
|
if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("rfl meta");
|
|
28
|
-
return
|
|
37
|
+
return meta.parameters;
|
|
29
38
|
}
|
|
30
|
-
function
|
|
39
|
+
function chainMeta() {
|
|
31
40
|
const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/chain.js"), "utf-8");
|
|
32
41
|
const meta = parseResourceMeta(src, "workflow");
|
|
33
42
|
if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("chain meta");
|
|
34
|
-
return
|
|
43
|
+
return meta.parameters;
|
|
35
44
|
}
|
|
36
|
-
const RFL =
|
|
37
|
-
const CHAIN =
|
|
45
|
+
const RFL = rflMeta();
|
|
46
|
+
const CHAIN = chainMeta();
|
|
38
47
|
|
|
39
48
|
describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
40
49
|
it("TC3a: trigger——chain 参数集平铺 task 被识别;跨 workflow 区分(items 非 chain 参数不触发)", () => {
|
|
41
50
|
expect(
|
|
42
|
-
findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN
|
|
51
|
+
findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN, OPT),
|
|
43
52
|
).toEqual(["task"]);
|
|
44
53
|
// 跨 workflow:items 是 map-reduce 参数非 chain 参数 → 不触发(m6 动态集语义)
|
|
45
54
|
expect(
|
|
46
|
-
findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN
|
|
55
|
+
findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN, OPT),
|
|
47
56
|
).toEqual([]);
|
|
48
57
|
});
|
|
49
58
|
|
|
50
59
|
it("TC3b: 嵌套 args 不触发", () => {
|
|
51
60
|
expect(
|
|
52
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN
|
|
61
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN, OPT),
|
|
53
62
|
).toEqual([]);
|
|
54
63
|
});
|
|
55
64
|
|
|
56
65
|
it("TC3c: 顶层 + args 内共存不触发(args-排除保留)", () => {
|
|
57
66
|
expect(
|
|
58
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN
|
|
67
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN, OPT),
|
|
59
68
|
).toEqual([]);
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
it("TC3d: 无已知键不触发;空集不触发", () => {
|
|
63
72
|
expect(
|
|
64
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN
|
|
73
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN, OPT),
|
|
65
74
|
).toEqual([]);
|
|
75
|
+
// meta undefined → 空键集(原「空集不触发」等价形态)
|
|
66
76
|
expect(
|
|
67
|
-
findFlattenedArgKeys({ action: "status" },
|
|
77
|
+
findFlattenedArgKeys({ action: "status" }, undefined),
|
|
68
78
|
).toEqual([]);
|
|
69
79
|
});
|
|
70
80
|
|
|
71
81
|
it("TC3e: review-fix-loop fixAgent 平铺被识别(动态集 exact)", () => {
|
|
72
82
|
expect(
|
|
73
|
-
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL
|
|
83
|
+
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL, OPT),
|
|
74
84
|
).toEqual(["fixAgent"]);
|
|
75
85
|
expect(
|
|
76
|
-
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL
|
|
86
|
+
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL, OPT),
|
|
77
87
|
).toEqual([]);
|
|
78
88
|
});
|
|
79
89
|
|
|
@@ -81,27 +91,27 @@ describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
|
81
91
|
expect(
|
|
82
92
|
findFlattenedArgKeys(
|
|
83
93
|
{ action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" },
|
|
84
|
-
RFL
|
|
85
|
-
|
|
94
|
+
RFL,
|
|
95
|
+
OPT,
|
|
86
96
|
),
|
|
87
97
|
).toEqual(["targetType", "target"]);
|
|
88
98
|
expect(
|
|
89
99
|
findFlattenedArgKeys(
|
|
90
100
|
{ action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true },
|
|
91
|
-
RFL
|
|
92
|
-
|
|
101
|
+
RFL,
|
|
102
|
+
OPT,
|
|
93
103
|
),
|
|
94
104
|
).toEqual(["batch1", "autoCommit"]);
|
|
95
105
|
expect(
|
|
96
106
|
findFlattenedArgKeys(
|
|
97
107
|
{ action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } },
|
|
98
|
-
RFL
|
|
99
|
-
|
|
108
|
+
RFL,
|
|
109
|
+
OPT,
|
|
100
110
|
),
|
|
101
111
|
).toEqual([]);
|
|
102
112
|
// batchl 拼错(非数字后缀)不触发(m6 评审 C-1:pattern 自带数字后缀校验)
|
|
103
113
|
expect(
|
|
104
|
-
findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL
|
|
114
|
+
findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL, OPT),
|
|
105
115
|
).toEqual([]);
|
|
106
116
|
});
|
|
107
117
|
|
|
@@ -109,42 +119,43 @@ describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
|
109
119
|
expect(
|
|
110
120
|
findFlattenedArgKeys(
|
|
111
121
|
{ action: "run", name: "review-fix-loop", model: "ds-flash", maxFixAttempts: 3, convergeNewIssues: 2, convergeRounds: 3 },
|
|
112
|
-
RFL
|
|
113
|
-
|
|
122
|
+
RFL,
|
|
123
|
+
OPT,
|
|
114
124
|
),
|
|
115
125
|
).toEqual(["maxFixAttempts", "convergeNewIssues", "convergeRounds"]);
|
|
116
126
|
expect(
|
|
117
127
|
findFlattenedArgKeys(
|
|
118
128
|
{ action: "run", name: "review-fix-loop", args: { model: "ds-flash", maxFixAttempts: 3 }, convergeRounds: 3 },
|
|
119
|
-
RFL
|
|
120
|
-
|
|
129
|
+
RFL,
|
|
130
|
+
OPT,
|
|
121
131
|
),
|
|
122
132
|
).toEqual(["convergeRounds"]);
|
|
123
133
|
});
|
|
124
134
|
|
|
125
135
|
it("TC3h: non-object 输入返回 []", () => {
|
|
126
|
-
expect(findFlattenedArgKeys(null
|
|
127
|
-
expect(findFlattenedArgKeys(undefined
|
|
136
|
+
expect(findFlattenedArgKeys(null)).toEqual([]);
|
|
137
|
+
expect(findFlattenedArgKeys(undefined)).toEqual([]);
|
|
128
138
|
});
|
|
129
139
|
|
|
130
140
|
it("TC3i: tool 顶层键不误报(M-3 回归——合成 parameters 直测 argKeysFromMeta 排除)", () => {
|
|
131
141
|
// 合成 parameters:workflow 声明参数 name(tool 键撞名)——argKeysFromMeta 必须排除
|
|
132
|
-
const
|
|
142
|
+
const synthetic = {
|
|
133
143
|
type: "object",
|
|
134
144
|
properties: { name: { type: "string" }, task: { type: "string" } },
|
|
135
145
|
required: ["name"],
|
|
136
|
-
}
|
|
146
|
+
};
|
|
147
|
+
const keys = argKeysFromMeta(synthetic, OPT);
|
|
137
148
|
expect(keys.exact.has("task")).toBe(true);
|
|
138
149
|
expect(keys.exact.has("name")).toBe(false); // TOOL_TOP_LEVEL 排除(M-3 真回归锁定)
|
|
139
150
|
// 合法调用不误报
|
|
140
151
|
expect(
|
|
141
152
|
findFlattenedArgKeys(
|
|
142
153
|
{ action: "run", name: "mywf", args: { name: "x" } },
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
synthetic,
|
|
155
|
+
OPT,
|
|
145
156
|
),
|
|
146
157
|
).toEqual([]);
|
|
147
158
|
// 真实 meta 不含 name(探针实测 16 键)——附加验证
|
|
148
|
-
expect(RFL.exact.has("name")).toBe(false);
|
|
159
|
+
expect(argKeysFromMeta(RFL, OPT).exact.has("name")).toBe(false);
|
|
149
160
|
});
|
|
150
161
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { dirname,join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
15
16
|
|
|
16
17
|
import { describe, expect, it } from "vitest";
|
|
17
18
|
|
|
@@ -153,12 +154,14 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
|
|
|
153
154
|
// 让弱模型撞错后第二次能直接照抄正确形态。
|
|
154
155
|
// 拍平后:startParam envelope 删除,平铺 task/slug 是合法形态;
|
|
155
156
|
// 平铺检测 guard(hasFlattenedStartFields)已删除,源码不应再含此表达式。
|
|
156
|
-
it("
|
|
157
|
+
it("startHandler throw 含 Correct 纠正正例(平铺形态)", () => {
|
|
158
|
+
// [D6②] startHandler 内核(含 Correct 文案)已下沉 core subagent-actions-core,
|
|
159
|
+
// 断言目标跟随文案权威源(resolve 方式同 prompt-quality-batch1 U4 先例)。
|
|
157
160
|
const actionsSrc = readFileSync(
|
|
158
|
-
|
|
161
|
+
createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/execution/subagent-actions-core.ts"),
|
|
159
162
|
"utf-8",
|
|
160
163
|
);
|
|
161
|
-
//
|
|
164
|
+
// 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
|
|
162
165
|
// 用 occurrences 计数——至少 3 处。
|
|
163
166
|
const occurrences = (actionsSrc.match(/Correct: \{"action":"start"/g) ?? []).length;
|
|
164
167
|
expect(occurrences).toBeGreaterThanOrEqual(3);
|