@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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// 反向 import 壳侧 interface,常量随之内化)——此处 re-export 保持既有 import 路径。
|
|
18
18
|
|
|
19
19
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
20
|
-
import {
|
|
20
|
+
import { Type } from "typebox";
|
|
21
21
|
|
|
22
22
|
import { THINKING_ORDER } from "@zhushanwen/subagent-core/shared/model-ref.ts";
|
|
23
23
|
|
|
@@ -158,6 +158,3 @@ export const SubagentParams = Type.Object({
|
|
|
158
158
|
})),
|
|
159
159
|
})),
|
|
160
160
|
});
|
|
161
|
-
|
|
162
|
-
/** Params schema 的 Static 投影(消费方经 `Static<typeof SubagentParams>` 使用,见 subagent-tool.ts)。 */
|
|
163
|
-
export type SubagentParamsStatic = Static<typeof SubagentParams>;
|
|
@@ -63,7 +63,7 @@ type SubagentRenderResultCb = (
|
|
|
63
63
|
// schema 常量经真实 typebox 编译校验(SW 自身 vitest 把 typebox alias 到 mock,
|
|
64
64
|
// 丢 options——required/description 断言必须以真实构造为基准)。
|
|
65
65
|
|
|
66
|
-
// extractAgentName 已上移到
|
|
66
|
+
// extractAgentName 已上移到 ./format.ts 共享(tool-render / subagent-tool 复用)。
|
|
67
67
|
|
|
68
68
|
/** exhaustiveness 承重 helper:default 分支把 action 收敛为 never,新增 action 时 tsc 报错。 */
|
|
69
69
|
function assertNever(value: never): string {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/interface/tool-render.ts
|
|
2
2
|
//
|
|
3
3
|
// 对话流 tool block 渲染。renderCall(标题行)+ renderResult(背景色 block)。
|
|
4
4
|
//
|
|
@@ -28,9 +28,8 @@ import type {
|
|
|
28
28
|
import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
|
|
29
29
|
import {
|
|
30
30
|
extractAgentName,
|
|
31
|
-
|
|
31
|
+
firstLineSanitized,
|
|
32
32
|
formatElapsedSeconds,
|
|
33
|
-
sanitizeLabel,
|
|
34
33
|
statusGlyph,
|
|
35
34
|
type ThemeLike,
|
|
36
35
|
truncLine,
|
|
@@ -304,7 +303,7 @@ function shortSessionLabel(file: string): string {
|
|
|
304
303
|
* execute 抛错(如 hub disposed / task 缺失)时,subagents handler 不 catch,
|
|
305
304
|
* Pi 框架会把 error.message 塞进 result.content[0].text。renderResult 的 fallback
|
|
306
305
|
* 分支用它把真实原因显示出来,避免只显「no details available」让 AI 盲猜。
|
|
307
|
-
* content
|
|
306
|
+
* content 可能多行,只取首行(共享 firstLineSanitized 裁断 + sanitize)。
|
|
308
307
|
*/
|
|
309
308
|
function extractResultError(content: AgentToolResult<SubagentToolResult>["content"]): string | undefined {
|
|
310
309
|
if (!Array.isArray(content)) return undefined;
|
|
@@ -322,15 +321,6 @@ function getStringText(item: unknown): string | undefined {
|
|
|
322
321
|
return typeof val === "string" && val.trim().length > 0 ? val : undefined;
|
|
323
322
|
}
|
|
324
323
|
|
|
325
|
-
/**
|
|
326
|
-
* 取文本首个非空行(多行压成首行展示),并 sanitize。
|
|
327
|
-
* 用于 done/failed 的交付物预览。
|
|
328
|
-
* 共享 firstLine(./format.ts)取首行,本 wrapper 叠加 sanitizeLabel。
|
|
329
|
-
*/
|
|
330
|
-
function firstLineSanitized(text?: string): string {
|
|
331
|
-
return sanitizeLabel(firstLine(text));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
324
|
// ============================================================
|
|
335
325
|
// list 渲染 helper(action:"list" 分支)
|
|
336
326
|
// ============================================================
|
|
@@ -10,32 +10,40 @@
|
|
|
10
10
|
* - delete: 删除脚本(前查 isRunning 防删运行中脚本)
|
|
11
11
|
* - list: 列出可用脚本(调 registry.loadAll)
|
|
12
12
|
*
|
|
13
|
+
* C5②(convergence D-6):generate 五道闸校验管线 + tmp 写盘下沉 core
|
|
14
|
+
* generateWorkflowScript(报错文案逐字平移,CA2 前提);save/delete 改调 core
|
|
15
|
+
* barrel(第三可选目录参数缺省即 pi 布局 .pi/workflows——pi 现两参调用形态零变化)。
|
|
16
|
+
* 结构化 {ok}|{error} → pi 的 execute-throw 契约转换在本层(pi 只对 execute throw
|
|
17
|
+
* 置 isError:true);AbortSignal 的 aborted 检查留宿主层(C4 偏差 #4 已声明)。
|
|
18
|
+
*
|
|
13
19
|
* 层归属:Interface。依赖 Pi SDK + engine script-lint + infra workflow-files。
|
|
14
20
|
*
|
|
15
21
|
* 参考:domain-models.md §FR-5(tool 收口 4→2)。
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { resolve as pathResolve } from "node:path";
|
|
20
|
-
|
|
21
24
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
22
25
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
23
26
|
import { Text } from "@earendil-works/pi-tui";
|
|
24
27
|
import { type Static, Type } from "typebox";
|
|
25
28
|
|
|
26
29
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
guiComponent,
|
|
31
|
+
type GuiContext,
|
|
32
|
+
type GuiRenderResult,
|
|
33
|
+
guiResult,
|
|
34
|
+
isGuiCapable,
|
|
32
35
|
} from "@xyz-agent/extension-protocol";
|
|
36
|
+
// C5②/C5⑦:创作闭环统一走 core barrel(generateWorkflowScript/saveWorkflow/
|
|
37
|
+
// deleteWorkflow/lintScript 均为 barrel 导出面;深路径在 npm/vendored 形态不可达)
|
|
38
|
+
import {
|
|
39
|
+
deleteWorkflow,
|
|
40
|
+
generateWorkflowScript,
|
|
41
|
+
lintScript,
|
|
42
|
+
saveWorkflow,
|
|
43
|
+
} from "@zhushanwen/subagent-core";
|
|
33
44
|
import type { WorkflowScriptRegistry } from "@zhushanwen/subagent-core/orchestration/models/workflow-script-registry.ts";
|
|
34
|
-
import { lintScript } from "@zhushanwen/subagent-core/orchestration/script-lint.ts";
|
|
35
|
-
import { deleteWorkflow, saveWorkflow } from "@zhushanwen/subagent-core/orchestration/workflow-files.ts";
|
|
36
45
|
import { toGuiCtx } from "./gui-mappers.ts";
|
|
37
46
|
import { renderTextFallback } from "./views/format.ts";
|
|
38
|
-
import { parseResourceMetaDetailed } from "@zhushanwen/subagent-core/shared/meta-parser.ts";
|
|
39
47
|
|
|
40
48
|
// ── Parameter schema ─────────────────────────────────────────
|
|
41
49
|
|
|
@@ -242,73 +250,23 @@ export function registerWorkflowScriptTool(
|
|
|
242
250
|
|
|
243
251
|
export function actionGenerate(params: ScriptParams, signal: AbortSignal | undefined): TextContent {
|
|
244
252
|
if (signal?.aborted) {
|
|
245
|
-
// throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError
|
|
253
|
+
// throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)。
|
|
254
|
+
// AbortSignal 是 pi tool 契约层关注——core 管线不含 signal 检查,宿主自留(C4 偏差 #4)
|
|
246
255
|
throw new Error("Operation aborted before start");
|
|
247
256
|
}
|
|
248
|
-
const name = params.name;
|
|
249
|
-
const script = params.script;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
throw new Error(
|
|
258
|
-
"Script uses ESM 'import' syntax. Workflow scripts run in a CJS Worker — use require() instead.",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
const hasExportMeta = /\bexport\s+const\s+meta\s*=/.test(stripped);
|
|
262
|
-
const otherExports = stripped.match(/\bexport\s+(?:const|let|var|function|default|\{)/g);
|
|
263
|
-
if (otherExports && !hasExportMeta) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
"Script uses ESM 'export' (non-meta). Use 'const meta = {...}' at top level instead.",
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// 2. Validate meta declaration (/* @pi-meta */ new format preferred; legacy const meta accepted during transition — m0)
|
|
270
|
-
const hasPiMeta = /\/\*\s*@pi-meta\s*\n/.test(script);
|
|
271
|
-
const hasLegacyMeta = script.includes("const meta") || script.includes("export const meta");
|
|
272
|
-
if (!hasPiMeta && !hasLegacyMeta) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
"Script must contain a meta declaration: a /* @pi-meta */ YAML block comment (preferred) or legacy const meta = { ... }. The block has the form: a block comment starting with /* @pi-meta followed by YAML (name/description/phases/parameters?/usage?), closed by */ on its own line.",
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 3. Check agent usage
|
|
279
|
-
if (!/\bagent\s*\(/.test(stripped)) {
|
|
280
|
-
throw new Error(
|
|
281
|
-
"Script does not contain any agent() calls. A workflow must call agent() at least once.",
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 4. Syntax check (wrap in async IIFE like runtime)
|
|
286
|
-
const cjsScript = script.replace(/\bexport\s+const\s+meta\b/, "const meta");
|
|
287
|
-
try {
|
|
288
|
-
new Function(`(async () => { ${cjsScript} })();`);
|
|
289
|
-
} catch (err: unknown) {
|
|
290
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
291
|
-
throw new Error(`Syntax error in script: ${msg}`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 4b. Round-trip: validate /* @pi-meta */ YAML before writing (v5 §4.7 / ERR4 — report linePos, don't write bad files)
|
|
295
|
-
if (hasPiMeta) {
|
|
296
|
-
const detailed = parseResourceMetaDetailed(script, "workflow");
|
|
297
|
-
if (!detailed.ok) {
|
|
298
|
-
const loc = "linePos" in detailed && detailed.linePos
|
|
299
|
-
? ` (line ${detailed.linePos.line}, col ${detailed.linePos.col})`
|
|
300
|
-
: "";
|
|
301
|
-
throw new Error(
|
|
302
|
-
`Generated /* @pi-meta */ YAML cannot be parsed${loc}: ${detailed.error}. Common causes: YAML indent errors, patternProperties regex must use double backslash (\\d not \d), or a stray star-slash inside the YAML body. Fix the meta block and retry.`,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
257
|
+
const name = params.name ?? "";
|
|
258
|
+
const script = params.script ?? "";
|
|
259
|
+
|
|
260
|
+
// C5②:五道闸(ESM 拒/meta 必需/agent() 必需/语法/@pi-meta round-trip)+ tmp 写盘
|
|
261
|
+
// 全部在 core generateWorkflowScript(缺省 tmpDir 即 pi 布局 .pi/workflows/.tmp,
|
|
262
|
+
// 相对 cwd resolve——与旧本地实现一致)。结构化 {ok}|{error} → execute-throw 契约
|
|
263
|
+
// 转换在此(报错文案逐字平移自 pi 旧版,含 round-trip 行列信息)。
|
|
264
|
+
const result = generateWorkflowScript(name, script);
|
|
265
|
+
if (!result.ok) {
|
|
266
|
+
throw new Error(result.error);
|
|
305
267
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const tmpDir = pathResolve(".pi/workflows/.tmp");
|
|
309
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
310
|
-
const filePath = pathResolve(tmpDir, `${name}.js`);
|
|
311
|
-
writeFileSync(filePath, script, "utf-8");
|
|
268
|
+
// result.ok ⇒ name/script 非空(core 对空入参返回 error)
|
|
269
|
+
const filePath = result.path;
|
|
312
270
|
|
|
313
271
|
return {
|
|
314
272
|
content: [
|
|
@@ -33,10 +33,16 @@ import { type Static, Type } from "typebox";
|
|
|
33
33
|
|
|
34
34
|
import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core/execution/execute-options-mapper.ts";
|
|
35
35
|
import { THINKING_ORDER } from "@zhushanwen/subagent-core/execution/model-resolver.ts";
|
|
36
|
+
import { MAX_TIMER_DELAY_MS } from "@zhushanwen/subagent-core/shared/timer-delay.ts";
|
|
37
|
+
import {
|
|
38
|
+
argKeysFromMeta,
|
|
39
|
+
findFlattenedArgKeys,
|
|
40
|
+
} from "@zhushanwen/subagent-core/orchestration/args-meta.ts";
|
|
36
41
|
import type { LauncherDeps } from "@zhushanwen/subagent-core/orchestration/launcher.ts";
|
|
37
42
|
import { abortRun, runWorkflow } from "@zhushanwen/subagent-core/orchestration/lifecycle.ts";
|
|
38
43
|
import type { RunStore } from "@zhushanwen/subagent-core/orchestration/models/ports.ts";
|
|
39
44
|
import type { WorkflowRun } from "@zhushanwen/subagent-core/orchestration/models/workflow-run.ts";
|
|
45
|
+
import { runSummary } from "@zhushanwen/subagent-core/orchestration/workflow-run-summary.ts";
|
|
40
46
|
import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
|
|
41
47
|
import {
|
|
42
48
|
acquireReentryGuard,
|
|
@@ -63,7 +69,7 @@ const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
|
|
|
63
69
|
const WorkflowParams = Type.Object({
|
|
64
70
|
action: StringEnum(WORKFLOW_ACTIONS, { description: "Workflow action to execute" }),
|
|
65
71
|
name: Type.Optional(
|
|
66
|
-
Type.String({ description: "Workflow ref: absolute path to the .js script (use <location
|
|
72
|
+
Type.String({ description: "Workflow ref: builtin/saved workflow name from <available_workflows> (C5: run accepts names), or absolute path to the .js script (use <location>; run action)" }),
|
|
67
73
|
),
|
|
68
74
|
slug: Type.Optional(
|
|
69
75
|
Type.String({
|
|
@@ -82,7 +88,7 @@ const WorkflowParams = Type.Object({
|
|
|
82
88
|
}),
|
|
83
89
|
),
|
|
84
90
|
tokens: Type.Optional(Type.Number({ description: "Max token budget — ONLY set when user explicitly requests a limit; omit = unlimited (default)" })),
|
|
85
|
-
time: Type.Optional(Type.Number({ description:
|
|
91
|
+
time: Type.Optional(Type.Number({ description: `Max time budget in ms — ONLY set when user explicitly requests a limit; omit = unlimited (default; hard ceiling ${MAX_TIMER_DELAY_MS} ms — larger values fail fast at entry)` })),
|
|
86
92
|
error: Type.Optional(
|
|
87
93
|
Type.String({ description: "Error/reason message (optional, used with abort)" }),
|
|
88
94
|
),
|
|
@@ -105,8 +111,12 @@ const RUNID_SHORT = 8;
|
|
|
105
111
|
* tool 自身顶层键(workflow params schema 键)——workflow 参数名与 tool 键撞名时
|
|
106
112
|
* (如 workflow 声明参数 name),顶层同名键是 tool 参数而非平铺(m6 评审 M-3)。
|
|
107
113
|
* 未来新增 tool 顶层键需同步此集合。
|
|
114
|
+
*
|
|
115
|
+
* D9 下沉后作为 core args-meta 的 reservedKeys 注入(ArgMetaOptions)——core 缺省
|
|
116
|
+
* 空集,不注入则撞名保护失效(run 调用顶层必有 action/name,会被误判平铺)。
|
|
117
|
+
* export 供 detectors.test 复用同一集合防漂移。
|
|
108
118
|
*/
|
|
109
|
-
const TOOL_TOP_LEVEL = new Set([
|
|
119
|
+
export const TOOL_TOP_LEVEL = new Set([
|
|
110
120
|
"action",
|
|
111
121
|
"name",
|
|
112
122
|
"slug",
|
|
@@ -124,76 +134,11 @@ const TOOL_TOP_LEVEL = new Set([
|
|
|
124
134
|
]);
|
|
125
135
|
|
|
126
136
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* - exact:properties keys(精确匹配)
|
|
131
|
-
* - patterns:patternProperties 原样转正则数组(如 /^batch\\d+$/——与旧
|
|
132
|
-
* KNOWN_ARG_KEY_PREFIXES 语义一致,自动兼容 \\d{2} 等变体;schema pattern 已是
|
|
133
|
-
* 正则源码,直接 new RegExp 即可)
|
|
134
|
-
* - 构建时排除 TOOL_TOP_LEVEL(撞名保护)
|
|
137
|
+
* core args-meta 的宿主差异注入项(D9 下沉):reservedKeys = TOOL_TOP_LEVEL。
|
|
138
|
+
* core 版 argKeysFromMeta / findFlattenedArgKeys 缺省 reservedKeys 为空集(core
|
|
139
|
+
* 中性形态),不传此项则失去撞名保护、行为不等值——调用点必须携带(等值契约)。
|
|
135
140
|
*/
|
|
136
|
-
|
|
137
|
-
parameters: Record<string, unknown> | undefined,
|
|
138
|
-
): { exact: ReadonlySet<string>; patterns: readonly RegExp[] } {
|
|
139
|
-
const exact = new Set<string>();
|
|
140
|
-
const patterns: RegExp[] = [];
|
|
141
|
-
if (parameters === undefined || parameters === null || typeof parameters !== "object") {
|
|
142
|
-
return { exact, patterns };
|
|
143
|
-
}
|
|
144
|
-
const props = parameters.properties;
|
|
145
|
-
if (props !== null && typeof props === "object") {
|
|
146
|
-
for (const k of Object.keys(props as Record<string, unknown>)) {
|
|
147
|
-
if (!TOOL_TOP_LEVEL.has(k)) exact.add(k);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const pp = parameters.patternProperties;
|
|
151
|
-
if (pp !== null && typeof pp === "object") {
|
|
152
|
-
for (const p of Object.keys(pp as Record<string, unknown>)) {
|
|
153
|
-
try {
|
|
154
|
-
const re = new RegExp(p); // schema pattern 已是正则源码
|
|
155
|
-
// S1(m6 exec-review):跳过能命中 tool 顶层键的 pattern——否则
|
|
156
|
-
// ^run.*$ 类 pattern 会匹配 runId/name 等 tool 键,合法调用恒误报
|
|
157
|
-
if ([...TOOL_TOP_LEVEL].some((tk) => re.test(tk))) continue;
|
|
158
|
-
patterns.push(re);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
// 非法 pattern(schema 校验 m3 已保证合法,双保险)——跳过并记录
|
|
161
|
-
logger.warn(`[tool-workflow] patternProperties 非法正则跳过: ${p}`, {
|
|
162
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return { exact, patterns };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* 检测弱模型把 args 子字段平铺到 workflow params 顶层(P0 静默失败防护)。
|
|
172
|
-
* 返回被平铺的键名列表(空 = 未平铺)。export 供 behavioral 测试(trigger/no-trigger/edge)。
|
|
173
|
-
* 参数取 unknown 以便测试构造任意对象、并解耦 WorkflowToolParams 的 index-signature 限制。
|
|
174
|
-
*
|
|
175
|
-
* knownKeys/knownPatterns 由 argKeysFromMeta 动态构建(m6)——匹配谓词:
|
|
176
|
-
* knownKeys.has(k) || knownPatterns.some(re => re.test(k))(pattern 自带数字后缀
|
|
177
|
-
* 语义——loose startsWith 会误报 batchl/target1);保留 args-排除(顶层 + args
|
|
178
|
-
* 内共存不算平铺)。
|
|
179
|
-
*/
|
|
180
|
-
export function findFlattenedArgKeys(
|
|
181
|
-
params: unknown,
|
|
182
|
-
knownKeys: ReadonlySet<string>,
|
|
183
|
-
knownPatterns: readonly RegExp[],
|
|
184
|
-
): string[] {
|
|
185
|
-
if (typeof params !== "object" || params === null) return [];
|
|
186
|
-
const p = params as Record<string, unknown>;
|
|
187
|
-
const args = typeof p.args === "object" && p.args !== null ? p.args : undefined;
|
|
188
|
-
const isKnownKey = (k: string) =>
|
|
189
|
-
knownKeys.has(k) || knownPatterns.some((re) => re.test(k));
|
|
190
|
-
// hasOwnProperty.call 而非 in(原型链——constructor/toString 类参数名不被继承键掩盖)
|
|
191
|
-
return Object.keys(p).filter(
|
|
192
|
-
(k) =>
|
|
193
|
-
isKnownKey(k) &&
|
|
194
|
-
!(args !== undefined && Object.prototype.hasOwnProperty.call(args, k)),
|
|
195
|
-
);
|
|
196
|
-
}
|
|
141
|
+
const ARGS_META_OPTIONS = { reservedKeys: TOOL_TOP_LEVEL };
|
|
197
142
|
|
|
198
143
|
// ── Types ────────────────────────────────────────────────────
|
|
199
144
|
|
|
@@ -321,7 +266,7 @@ export function registerWorkflowTool(
|
|
|
321
266
|
"<available_workflows> (injected each turn). For parameter details, read the <location> " +
|
|
322
267
|
"script file (script header has @pi-meta parameters + usage + phases). Do NOT use " +
|
|
323
268
|
"workflow-script generate for patterns already covered by available workflows.",
|
|
324
|
-
"run: pass the absolute .js path from <available_workflows>
|
|
269
|
+
"run: pass the workflow ref as name — the listed <name> (builtin/saved workflow) or its <location> absolute .js path from <available_workflows> — then start in background (no user confirmation needed).",
|
|
325
270
|
"Do NOT poll status after starting — results appear automatically via notifyDone.",
|
|
326
271
|
"Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.",
|
|
327
272
|
"Call shapes (JSON): " +
|
|
@@ -425,7 +370,17 @@ export async function actionRun(
|
|
|
425
370
|
// 顶层(缺 args 嵌套)。args ?? {} 会静默 args={},启动缺参 run 不报错——比 subagent
|
|
426
371
|
// 平铺事故更严重。m6:先 registry.getPath(动态参数集来源——schema 即 SSOT),
|
|
427
372
|
// not_found 优先返回;平铺检测报错带 Correct 正例纠正。
|
|
428
|
-
|
|
373
|
+
//
|
|
374
|
+
// C5③(convergence D-4 pi 半边):name 解析先查内置/已保存 workflow 名
|
|
375
|
+
// (registry.get——内置 chain/parallel/map-reduce/scatter-gather/review-fix-loop 或
|
|
376
|
+
// 用户 project/user 级脚本名,按 tmp>project>user>npm 优先级合并)——内置名命中
|
|
377
|
+
// 直接用内置脚本;未命中再走 getPath(绝对路径,~/ 展开——normalizeRef 既有)。
|
|
378
|
+
// 严格超集:现有路径用法零变化(路径串不会撞 workflow 名——名字是简单标识符),
|
|
379
|
+
// 两者都未命中仍走原 not_found 报错(建议清单不变)。
|
|
380
|
+
let script = await deps.registry.get(name);
|
|
381
|
+
if (!script) {
|
|
382
|
+
script = await deps.registry.getPath(name);
|
|
383
|
+
}
|
|
429
384
|
// W4c:config-loader 的 toCachedMeta 对不可读/不存在文件返回 available:false 的
|
|
430
385
|
// stub(非 undefined),仅判 !script 会绕过 not_found → 空 sourceCode 假启动
|
|
431
386
|
//(W4b verifier 探针实测复现)。与下方 suggestions 分支的 wf.available 过滤口径对齐。
|
|
@@ -444,7 +399,10 @@ export async function actionRun(
|
|
|
444
399
|
|
|
445
400
|
// m6:动态参数集(schema 即 SSOT)→ 平铺检测;无 parameters → 单次 warn + 跳过
|
|
446
401
|
// (legacy const-meta 类永久无检测——D1 无 adapter 声明)
|
|
447
|
-
const { exact: knownKeys, patterns: knownPatterns } = argKeysFromMeta(
|
|
402
|
+
const { exact: knownKeys, patterns: knownPatterns } = argKeysFromMeta(
|
|
403
|
+
script.meta.parameters,
|
|
404
|
+
ARGS_META_OPTIONS,
|
|
405
|
+
);
|
|
448
406
|
if (knownKeys.size === 0 && knownPatterns.length === 0) {
|
|
449
407
|
// M-2 显式信号:无参数契约(未声明/解析空)→ 单次 warn——静默退化变显式
|
|
450
408
|
// (m6 exec-review M1:原实现排除 undefined 与设计相反)
|
|
@@ -452,7 +410,9 @@ export async function actionRun(
|
|
|
452
410
|
`[tool-workflow] ${script.name}: 未声明参数契约(或解析为空)——平铺检测跳过,args 不校验`,
|
|
453
411
|
);
|
|
454
412
|
}
|
|
455
|
-
|
|
413
|
+
// core 版接收 meta 内联构建键集(签名与本地版键集三参不同,判定谓词逐字同源)——
|
|
414
|
+
// 键集构建两次(此处 + core 内部)仅为 M-2 空契约信号,成本每 run 一次可忽略
|
|
415
|
+
const flattened = findFlattenedArgKeys(params, script.meta.parameters, ARGS_META_OPTIONS);
|
|
456
416
|
if (flattened.length > 0) {
|
|
457
417
|
throw new Error(
|
|
458
418
|
`Detected ${flattened.join(", ")} at top level — they belong inside 'args'. ` +
|
|
@@ -468,6 +428,16 @@ export async function actionRun(
|
|
|
468
428
|
const args = params.args ?? {};
|
|
469
429
|
const tokens = params.tokens;
|
|
470
430
|
const time = params.time;
|
|
431
|
+
// OR-1 入口 fail-fast(unbounded-wait-audit §7.2 T3①):schema 的 time 是
|
|
432
|
+
// Type.Number 直通(无上界)——超 setTimeout 安全域的值会穿透到 lifecycle 内层
|
|
433
|
+
// 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链。错误带合法上限
|
|
434
|
+
// 与实际传入值,LLM 可据消息自纠(clamp 或省略走 unlimited 语义)。
|
|
435
|
+
if (time !== undefined && time > MAX_TIMER_DELAY_MS) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`time budget ${time} ms exceeds the maximum of ${MAX_TIMER_DELAY_MS} ms (~24.8 days). ` +
|
|
438
|
+
`Retry with a smaller "time", or omit it for unlimited.`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
471
441
|
|
|
472
442
|
// 构建 RunSpec + 启动(m3:parameters 从 script.meta 拷贝——chokepoint 校验用;
|
|
473
443
|
// 校验失败 → ArgsValidationError 直接 throw 给 pi(W4:err.message 含 §5.3 指引,
|
|
@@ -568,19 +538,12 @@ async function actionLifecycle(
|
|
|
568
538
|
|
|
569
539
|
// ── helpers ──────────────────────────────────────────────────
|
|
570
540
|
|
|
571
|
-
/** WorkflowRun → 摘要(status action
|
|
541
|
+
/** WorkflowRun → 摘要(status action 用):core runSummary 单源投影 + 宿主扩展 stateFile。
|
|
542
|
+
*
|
|
543
|
+
* 字段集不再本地维护(runSummary 双投影分叉的收口点,D8/B5);stateFile 依赖
|
|
544
|
+
* RunStore 实例,属宿主扩展——core 投影刻意不依赖具体 store。 */
|
|
572
545
|
function toRunSummary(run: WorkflowRun, store: RunStore): RunSummary {
|
|
573
|
-
return {
|
|
574
|
-
runId: run.runId,
|
|
575
|
-
name: run.spec.scriptName,
|
|
576
|
-
slug: run.spec.slug,
|
|
577
|
-
status: run.state.status,
|
|
578
|
-
reason: run.state.reason,
|
|
579
|
-
startedAt: run.meta.startedAt,
|
|
580
|
-
completedAt: run.meta.completedAt,
|
|
581
|
-
error: run.state.error,
|
|
582
|
-
stateFile: store.stateFilePath(run.runId),
|
|
583
|
-
};
|
|
546
|
+
return { ...runSummary(run), stateFile: store.stateFilePath(run.runId) };
|
|
584
547
|
}
|
|
585
548
|
|
|
586
549
|
// W4b:原 textResult(text, isError) helper 已删除——23 处错误路径全部改 throw
|
|
@@ -57,16 +57,8 @@ import {
|
|
|
57
57
|
visibleLen,
|
|
58
58
|
} from "./format.ts";
|
|
59
59
|
|
|
60
|
-
// L2 详情内容构建 + 滚动按键(纯函数)抽到 detail-content.ts
|
|
61
|
-
|
|
62
|
-
export {
|
|
63
|
-
buildDetailContent,
|
|
64
|
-
detailContentLength,
|
|
65
|
-
type DetailKeyResult,
|
|
66
|
-
type DetailScrollContext,
|
|
67
|
-
processDetailKey,
|
|
68
|
-
} from "./detail-content.ts";
|
|
69
|
-
import { buildDetailContent, detailContentLength, type DetailScrollContext,processDetailKey } from "./detail-content.ts";
|
|
60
|
+
// L2 详情内容构建 + 滚动按键(纯函数)抽到 detail-content.ts(view 与其测试直接 import)。
|
|
61
|
+
import { buildDetailContent, detailContentLength, type DetailScrollContext, processDetailKey } from "./detail-content.ts";
|
|
70
62
|
|
|
71
63
|
// ── TUI layout constants ──────────────────────────────────────
|
|
72
64
|
|
|
@@ -93,7 +85,7 @@ const PRINTABLE_CHAR_MIN = 32;
|
|
|
93
85
|
function b(theme: ThemeLike, s: string): string {
|
|
94
86
|
return theme.fg("borderMuted", s);
|
|
95
87
|
}
|
|
96
|
-
/** 着色单字符填充用的
|
|
88
|
+
/** 着色单字符填充用的 ─(本文件 dashes() 满宽填充复用)。 */
|
|
97
89
|
function dash(theme: ThemeLike): string {
|
|
98
90
|
return theme.fg("borderMuted", "─");
|
|
99
91
|
}
|
|
@@ -12,6 +12,8 @@ import type { ExecutionTraceNode, ToolCallEntry } from "@zhushanwen/subagent-cor
|
|
|
12
12
|
import type { DoneReason, RunStatus } from "@zhushanwen/subagent-core/orchestration/models/types.ts";
|
|
13
13
|
import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
|
|
14
14
|
|
|
15
|
+
import type { ThemeLike as BaseThemeLike } from "../format.ts";
|
|
16
|
+
|
|
15
17
|
// ── Constants ─────────────────────────────────────────────────
|
|
16
18
|
|
|
17
19
|
export const SIDEBAR_WIDTH = 24;
|
|
@@ -53,10 +55,12 @@ type StatusText =
|
|
|
53
55
|
|
|
54
56
|
// ── Theme interface (avoids importing Pi runtime) ─────────────
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* views 侧主题形状:仅 fg/bold——本文件所有函数只消费这两个方法。
|
|
60
|
+
* 单源派生自 src/interface/format.ts 的 ThemeLike(Pick 收窄),不再独立声明;
|
|
61
|
+
* 保留窄形状是为了让只有 fg/bold 的 duck-typed theme(含测试 stub)可直接传入。
|
|
62
|
+
*/
|
|
63
|
+
export type ThemeLike = Pick<BaseThemeLike, "fg" | "bold">;
|
|
60
64
|
|
|
61
65
|
// ── Status helpers ────────────────────────────────────────────
|
|
62
66
|
|
|
@@ -128,17 +132,7 @@ export function formatElapsed(startedAt?: string, now: number = Date.now()): str
|
|
|
128
132
|
return `${mins}m${remSecs}s`;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
* Format elapsed time from integer seconds(live 路径用)。
|
|
133
|
-
* 与 formatElapsed 输出格式一致,但输入是 computeElapsedSeconds 的秒数(非时间戳)。
|
|
134
|
-
*/
|
|
135
|
-
export function formatElapsedSeconds(seconds: number): string {
|
|
136
|
-
if (seconds < 1) return "0s";
|
|
137
|
-
if (seconds < SECS_PER_MIN) return `${seconds}s`;
|
|
138
|
-
const mins = Math.floor(seconds / SECS_PER_MIN);
|
|
139
|
-
const remSecs = seconds % SECS_PER_MIN;
|
|
140
|
-
return `${mins}m${remSecs}s`;
|
|
141
|
-
}
|
|
135
|
+
// formatElapsedSeconds 单源自 src/interface/format.ts(见文末「单源折叠垫片」)。
|
|
142
136
|
|
|
143
137
|
/**
|
|
144
138
|
* Format a live eventLog entry(live 路径 Activity 区用)。
|
|
@@ -204,48 +198,15 @@ export function formatActivityLine(entry: ToolCallEntry, maxWidth: number): stri
|
|
|
204
198
|
return `${entry.name}(${truncated})`;
|
|
205
199
|
}
|
|
206
200
|
|
|
207
|
-
// ──
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Pad an ANSI-escaped string to a target *visible* width.
|
|
217
|
-
* 只 pad 不截断:超宽时原样返回(调用方负责先截断)。
|
|
218
|
-
* 对齐 subagents 的 padToVisible 语义。
|
|
219
|
-
*/
|
|
220
|
-
export function padVisible(s: string, width: number): string {
|
|
221
|
-
const vl = visibleLen(s);
|
|
222
|
-
if (vl >= width) return s;
|
|
223
|
-
return s + " ".repeat(width - vl);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 分段着色版填充:title 和 fill 都已着色(含 ANSI),拼接时各自 ANSI 延续。
|
|
228
|
-
* 解决 ANSI 嵌套失色:若用 fg("c1", fill(title, "─", n)),
|
|
229
|
-
* title 内的 \x1b[0m 会重置外层 c1,导致 title 之后的 ─ 失去 c1。
|
|
230
|
-
* 改成 title + fill.repeat(后),fill 整段保持自己的 ANSI,不依赖外层包裹。
|
|
231
|
-
*
|
|
232
|
-
* 对齐 subagents format.ts segFillColored(同源移植)。
|
|
233
|
-
*/
|
|
234
|
-
export function segFillColored(
|
|
235
|
-
titleStyled: string | undefined,
|
|
236
|
-
fillStyled: string,
|
|
237
|
-
width: number,
|
|
238
|
-
): string {
|
|
239
|
-
if (width <= 0) return "";
|
|
240
|
-
const fillW = visibleLen(fillStyled);
|
|
241
|
-
if (!titleStyled || fillW === 0) {
|
|
242
|
-
return fillStyled.repeat(width);
|
|
243
|
-
}
|
|
244
|
-
const tw = visibleLen(titleStyled);
|
|
245
|
-
if (tw >= width) return truncateToWidth(titleStyled, width);
|
|
246
|
-
const fillCount = width - tw;
|
|
247
|
-
return titleStyled + fillStyled.repeat(fillCount);
|
|
248
|
-
}
|
|
201
|
+
// ── 单源折叠垫片(设计 U11 宿主内合并)─────────────────────────
|
|
202
|
+
// 通用文本 helper 已折叠单源到 src/interface/format.ts——truncLine 为自研
|
|
203
|
+
// ANSI 安全实现(pi-tui truncateToWidth 会在省略号处插 \x1b[0m 断裂背景色,
|
|
204
|
+
// 见 interface/format.ts truncLine 注释),统一取 interface 侧实现。
|
|
205
|
+
// 此处按原名 re-export:WorkflowsView / detail-content(本 wave 只读)的
|
|
206
|
+
// import 面不变即完成单源切换。segFillColored 无 views 侧消费方,直接删除。
|
|
207
|
+
export { formatElapsedSeconds } from "../format.ts";
|
|
208
|
+
export { padToVisible as padVisible } from "../format.ts";
|
|
209
|
+
export { visibleWidth as visibleLen } from "@earendil-works/pi-tui";
|
|
249
210
|
|
|
250
211
|
// ── Phase group (filters empty phases) ────────────────────────
|
|
251
212
|
|
|
@@ -290,7 +251,7 @@ export function formatPhaseLine(
|
|
|
290
251
|
// pointer(2) + dot(1) + space(1)
|
|
291
252
|
const PHASE_PREFIX_WIDTH = 4;
|
|
292
253
|
const budget = maxWidth - PHASE_PREFIX_WIDTH;
|
|
293
|
-
const truncated =
|
|
254
|
+
const truncated = visibleWidth(label) > budget
|
|
294
255
|
? truncateToWidth(label, budget - 1) + ELLIPSIS
|
|
295
256
|
: label;
|
|
296
257
|
return `${pointer}${dot} ${truncated}`;
|