@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 +6 -5
- package/package.json +14 -9
- package/relay/relay.mjs +45 -2
- package/skills/workflow-script-format/SKILL.md +2 -56
- package/src/host/__tests__/inflight-reporter.test.ts +26 -1
- package/src/host/__tests__/pi-host.test.ts +418 -4
- package/src/host/inflight-reporter.ts +13 -10
- package/src/host/pi-host.ts +205 -17
- package/src/index.ts +21 -72
- package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
- package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
- package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
- package/src/injectors/resource-list-injector.ts +103 -32
- package/src/injectors/subagent-list-injector.ts +4 -28
- package/src/injectors/workflow-list-injector.ts +8 -4
- package/src/interface/__tests__/capture-tool.ts +56 -0
- package/src/interface/__tests__/commands-resume.test.ts +103 -0
- package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
- package/src/interface/__tests__/tool-render.test.ts +105 -2
- package/src/interface/__tests__/tool-subagents.test.ts +24 -26
- package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
- package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
- package/src/interface/__tests__/tool-workflow.test.ts +884 -0
- package/src/interface/bg-notify-render.ts +18 -9
- package/src/interface/command-actions.ts +22 -17
- package/src/interface/commands.ts +58 -12
- package/src/interface/format.ts +50 -16
- package/src/interface/id-preview.ts +4 -4
- package/src/interface/subagent-actions.ts +18 -50
- package/src/interface/subagent-tool.ts +26 -18
- package/src/interface/subagents.ts +5 -11
- package/src/interface/tool-render.ts +14 -30
- package/src/interface/tool-shared.ts +55 -12
- package/src/interface/tool-subagents.ts +21 -11
- package/src/interface/tool-workflow-script.ts +52 -72
- package/src/interface/tool-workflow.ts +345 -139
- package/src/interface/views/WorkflowsView.ts +10 -7
- package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
- package/src/interface/views/detail-content.ts +3 -5
- package/src/jsonl-run-store.ts +656 -544
- package/src/model-events.ts +35 -0
- package/src/session-lifecycle.ts +349 -86
- package/src/subagents-events.ts +56 -0
- package/src/workflow-events.ts +285 -200
- package/src/workflow-notify.ts +456 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
- package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
- package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
- package/src/interface/__tests__/detectors.test.ts +0 -161
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
- package/src/interface/helpers.ts +0 -272
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
- package/src/interface/views/__tests__/record-progress.test.ts +0 -235
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* W4b:workflow tool 错误路径 throw 语义(代表路径)。
|
|
3
|
-
*
|
|
4
|
-
* pi 只对 execute throw 置 isError:true——返回值里的 isError 被 agent-loop 丢弃
|
|
5
|
-
* (agent-loop.js:453-483),故错误路径必须 throw。本文件抽 4 个代表路径
|
|
6
|
-
* (W4b 验收交付物 3):
|
|
7
|
-
* - not_found(abort 目标 runId 不存在)
|
|
8
|
-
* - reentry-busy(guard 占用,throw 前未 acquire)
|
|
9
|
-
* - 平铺检测(args 子字段平铺到顶层,LLM 常见错误)
|
|
10
|
-
* - slug 护栏(schema maxLength 之后的运行时第二道)
|
|
11
|
-
* generate 校验族(第 5 代表路径)见 tool-workflow-script-generate.test.ts。
|
|
12
|
-
*
|
|
13
|
-
* 经 registerWorkflowTool 注册层测(capture tool + 最小 deps stub),
|
|
14
|
-
* 与 W4 save/delete 测试同范式。框架:vitest。
|
|
15
|
-
*/
|
|
16
|
-
import { describe, expect, it, vi } from "vitest";
|
|
17
|
-
|
|
18
|
-
import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
|
|
19
|
-
import { REENTRY_BUSY_MESSAGE, type ReentryGuardRef } from "../reentry-guard.ts";
|
|
20
|
-
import { registerWorkflowTool } from "../tool-workflow.ts";
|
|
21
|
-
|
|
22
|
-
// ── capture helper ──
|
|
23
|
-
|
|
24
|
-
interface CapturedTool {
|
|
25
|
-
name: string;
|
|
26
|
-
execute: (
|
|
27
|
-
toolCallId: string,
|
|
28
|
-
params: Record<string, unknown>,
|
|
29
|
-
signal: AbortSignal | undefined,
|
|
30
|
-
onUpdate: unknown,
|
|
31
|
-
ctx: unknown,
|
|
32
|
-
) => Promise<unknown>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function captureTool(deps: unknown, reentryRef: ReentryGuardRef): CapturedTool {
|
|
36
|
-
const tools: CapturedTool[] = [];
|
|
37
|
-
const pi = { registerTool: (t: unknown) => tools.push(t as CapturedTool) };
|
|
38
|
-
registerWorkflowTool(pi as never, deps as never, reentryRef);
|
|
39
|
-
if (!tools[0] || tools[0].name !== "workflow") {
|
|
40
|
-
throw new Error("registerWorkflowTool did not register the workflow tool");
|
|
41
|
-
}
|
|
42
|
-
return tools[0];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 最小 LauncherDeps stub:被测路径只触 registry / runs(C5③ 起 actionRun 先查
|
|
46
|
-
* registry.get 再 registry.getPath——两查都须在 stub 面)。 */
|
|
47
|
-
function makeDeps(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
48
|
-
return {
|
|
49
|
-
runs: new Map(),
|
|
50
|
-
registry: {
|
|
51
|
-
get: vi.fn().mockResolvedValue(undefined),
|
|
52
|
-
getPath: vi.fn().mockResolvedValue(undefined),
|
|
53
|
-
loadAll: vi.fn().mockResolvedValue([]),
|
|
54
|
-
},
|
|
55
|
-
...overrides,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 平铺检测 / slug 护栏路径的 registry script stub(meta.parameters 驱动已知键集)。 */
|
|
60
|
-
function makeScript(parameters?: object): Record<string, unknown> {
|
|
61
|
-
return {
|
|
62
|
-
name: "demo-wf",
|
|
63
|
-
path: "/abs/demo-wf.js",
|
|
64
|
-
available: true,
|
|
65
|
-
sourceCode: "// demo",
|
|
66
|
-
meta: { description: "demo workflow", parameters },
|
|
67
|
-
toExecutable: () => "// demo",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── 代表路径 ──
|
|
72
|
-
|
|
73
|
-
describe("W4b: workflow tool 错误路径 throw 语义", () => {
|
|
74
|
-
it("not_found:abort 目标 runId 不存在 → throw(pi catch 后置 isError:true)", async () => {
|
|
75
|
-
const tool = captureTool(makeDeps(), { isProcessing: false });
|
|
76
|
-
await expect(
|
|
77
|
-
tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
|
|
78
|
-
).rejects.toThrow(
|
|
79
|
-
"Workflow 'no-such-run' not found. Use action:status to list active runs and their runIds.",
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("W4c not_found:run 目标路径不可读(getPath 返回 available:false stub 而非 undefined)→ throw,不假启动", async () => {
|
|
84
|
-
// config-loader.toCachedMeta 对不存在/不可读文件返回 available:false 的空壳实体
|
|
85
|
-
// (非 undefined)——旧判定仅 !script 绕过 not_found,空 sourceCode 的 run 假启动
|
|
86
|
-
// (W4b verifier 探针实测复现)。registry 层真行为见 config-loader.ts:143-151。
|
|
87
|
-
const deps = makeDeps({
|
|
88
|
-
registry: {
|
|
89
|
-
get: vi.fn().mockResolvedValue(undefined),
|
|
90
|
-
getPath: vi.fn().mockResolvedValue({
|
|
91
|
-
name: "ghost-wf",
|
|
92
|
-
path: "/tmp/no-such-workflow.js",
|
|
93
|
-
available: false,
|
|
94
|
-
sourceCode: "",
|
|
95
|
-
meta: { description: "", parameters: undefined },
|
|
96
|
-
toExecutable: () => "",
|
|
97
|
-
}),
|
|
98
|
-
loadAll: vi.fn().mockResolvedValue([]),
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
const tool = captureTool(deps, { isProcessing: false });
|
|
102
|
-
await expect(
|
|
103
|
-
tool.execute("id", { action: "run", name: "/tmp/no-such-workflow.js" }, undefined, undefined, {}),
|
|
104
|
-
).rejects.toThrow("Workflow '/tmp/no-such-workflow.js' not found.");
|
|
105
|
-
// 未假启动:runs 注册表不出现该 run
|
|
106
|
-
expect(deps.runs).toBeInstanceOf(Map);
|
|
107
|
-
expect((deps.runs as Map<string, unknown>).size).toBe(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("reentry-busy:guard 占用 → throw REENTRY_BUSY_MESSAGE,且 guard 状态不被污染", async () => {
|
|
111
|
-
const guard: ReentryGuardRef = { isProcessing: true };
|
|
112
|
-
const tool = captureTool(makeDeps(), guard);
|
|
113
|
-
await expect(
|
|
114
|
-
tool.execute("id", { action: "status" }, undefined, undefined, {}),
|
|
115
|
-
).rejects.toThrow(REENTRY_BUSY_MESSAGE);
|
|
116
|
-
// throw 发生在 acquire 之前——占用方语义保持,不产生双重 release
|
|
117
|
-
expect(guard.isProcessing).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("平铺检测:args 子字段提到顶层 → throw 'Detected ... at top level'(含 Correct 正例)", async () => {
|
|
121
|
-
const deps = makeDeps({
|
|
122
|
-
registry: {
|
|
123
|
-
get: vi.fn().mockResolvedValue(undefined),
|
|
124
|
-
getPath: vi.fn().mockResolvedValue(
|
|
125
|
-
makeScript({
|
|
126
|
-
type: "object",
|
|
127
|
-
properties: { task: { type: "string" }, items: { type: "array" } },
|
|
128
|
-
required: ["task"],
|
|
129
|
-
}),
|
|
130
|
-
),
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
const tool = captureTool(deps, { isProcessing: false });
|
|
134
|
-
await expect(
|
|
135
|
-
tool.execute(
|
|
136
|
-
"id",
|
|
137
|
-
{ action: "run", name: "/abs/demo-wf.js", task: "do work" },
|
|
138
|
-
undefined,
|
|
139
|
-
undefined,
|
|
140
|
-
{},
|
|
141
|
-
),
|
|
142
|
-
).rejects.toThrow(
|
|
143
|
-
/Detected task at top level — they belong inside 'args'\. Correct: \{"action":"run","name":"\/abs\/demo-wf\.js","args":\{"task": "<value>"\}\}/,
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("slug 护栏:slug 超 SLUG_MAX_LENGTH → throw 'slug exceeds ...'(运行时第二道)", async () => {
|
|
148
|
-
const deps = makeDeps({
|
|
149
|
-
registry: {
|
|
150
|
-
get: vi.fn().mockResolvedValue(undefined),
|
|
151
|
-
getPath: vi.fn().mockResolvedValue(
|
|
152
|
-
makeScript({
|
|
153
|
-
type: "object",
|
|
154
|
-
properties: { task: { type: "string" } },
|
|
155
|
-
required: ["task"],
|
|
156
|
-
}),
|
|
157
|
-
),
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
const tool = captureTool(deps, { isProcessing: false });
|
|
161
|
-
const longSlug = "a".repeat(SLUG_MAX_LENGTH + 1);
|
|
162
|
-
await expect(
|
|
163
|
-
tool.execute(
|
|
164
|
-
"id",
|
|
165
|
-
{ action: "run", name: "/abs/demo-wf.js", slug: longSlug, args: {} },
|
|
166
|
-
undefined,
|
|
167
|
-
undefined,
|
|
168
|
-
{},
|
|
169
|
-
),
|
|
170
|
-
).rejects.toThrow(
|
|
171
|
-
`slug exceeds ${SLUG_MAX_LENGTH} chars (got ${longSlug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`,
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("OR-1 入口校验:time 超 setTimeout 上限 → throw 含上限 2147483647 与实际值,run 未启动", async () => {
|
|
176
|
-
const deps = makeDeps({
|
|
177
|
-
registry: {
|
|
178
|
-
get: vi.fn().mockResolvedValue(undefined),
|
|
179
|
-
getPath: vi.fn().mockResolvedValue(
|
|
180
|
-
makeScript({
|
|
181
|
-
type: "object",
|
|
182
|
-
properties: { task: { type: "string" } },
|
|
183
|
-
required: ["task"],
|
|
184
|
-
}),
|
|
185
|
-
),
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
const tool = captureTool(deps, { isProcessing: false });
|
|
189
|
-
// LLM 对「跑久一点」完全可能生成 1e12——超 2^31-1 的典型形态(schema Type.Number
|
|
190
|
-
// 直通无上界,用户入口 fail-fast 不依赖 lifecycle 内层 assertSafeTimerDelay)
|
|
191
|
-
await expect(
|
|
192
|
-
tool.execute(
|
|
193
|
-
"id",
|
|
194
|
-
{ action: "run", name: "/abs/demo-wf.js", args: { task: "do work" }, time: 1_000_000_000_000 },
|
|
195
|
-
undefined,
|
|
196
|
-
undefined,
|
|
197
|
-
{},
|
|
198
|
-
),
|
|
199
|
-
).rejects.toThrow(
|
|
200
|
-
"time budget 1000000000000 ms exceeds the maximum of 2147483647 ms (~24.8 days). Retry with a smaller \"time\", or omit it for unlimited.",
|
|
201
|
-
);
|
|
202
|
-
// fail-fast 于 runWorkflow 之前:runs 无条目(abortRun not found 语义保持)
|
|
203
|
-
expect((deps.runs as Map<string, unknown>).size).toBe(0);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("throw 后 reentry guard 经 finally 正常释放(成功路径回归)", async () => {
|
|
207
|
-
// abort not_found throw 穿透 execute try/finally:guard 必须复位,否则后续命令全部 busy
|
|
208
|
-
const guard: ReentryGuardRef = { isProcessing: false };
|
|
209
|
-
const tool = captureTool(makeDeps(), guard);
|
|
210
|
-
await expect(
|
|
211
|
-
tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
|
|
212
|
-
).rejects.toThrow(/not found/);
|
|
213
|
-
expect(guard.isProcessing).toBe(false);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// src/interface/__tests__/workflow-state-file-exposure.test.ts
|
|
2
|
-
//
|
|
3
|
-
// W2: WorkflowToolDetails 携带 stateFile + toRunSummary 填充逻辑
|
|
4
|
-
//
|
|
5
|
-
// 防的 bug:workflow run 自身的状态文件路径(<sessionDir>/workflow-state/<runId>.jsonl)
|
|
6
|
-
// 从未暴露到 tool details——overlay/GUI 拿不到,无法定位 run 状态文件做后续查看。
|
|
7
|
-
// 本测试验证 toRunSummary 和 actionRun 的 details 包含 stateFile 字段。
|
|
8
|
-
//
|
|
9
|
-
// 使用源码断言(读 .ts 文件文本)避免 import 重 mock 链——与 workflow-tool-prompt.test.ts 同模式。
|
|
10
|
-
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
12
|
-
import { readFileSync } from "node:fs";
|
|
13
|
-
import { join, dirname } from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
|
|
16
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const TOOL_WORKFLOW_SRC = readFileSync(
|
|
18
|
-
join(__dirname, "../tool-workflow.ts"),
|
|
19
|
-
"utf-8",
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
describe("W2: workflow tool details 暴露 stateFile", () => {
|
|
23
|
-
it("RunSummary 类型声明含 stateFile 可选字段", () => {
|
|
24
|
-
// RunSummary 必须声明 stateFile,否则 toRunSummary 返回的对象类型不含此字段
|
|
25
|
-
expect(TOOL_WORKFLOW_SRC).toMatch(/stateFile\?\s*:\s*string/);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("toRunSummary 调用 deps.store.stateFilePath 填充 stateFile", () => {
|
|
29
|
-
// toRunSummary 必须从 store.stateFilePath(runId) 取值——这是数据来源
|
|
30
|
-
expect(TOOL_WORKFLOW_SRC).toMatch(/stateFilePath/);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("run action 的 details 含 stateFile 字段", () => {
|
|
34
|
-
// actionRun 返回的 details 必须携带 stateFile(run 启动时即暴露路径)
|
|
35
|
-
// 验证 actionRun 函数体中构造 details 时引用了 stateFilePath
|
|
36
|
-
expect(TOOL_WORKFLOW_SRC).toMatch(/action.*run[\s\S]*stateFilePath|stateFilePath[\s\S]*action.*run/i);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// U1: workflow tool 提示词包含内置 workflow 清单 + 交叉引用。
|
|
2
|
-
//
|
|
3
|
-
// agent(LLM)决策时唯一能看到的 tool 元信息就是 description + promptGuidelines。
|
|
4
|
-
// 若这些文本里不提及内置 workflow(chain/parallel/scatter-gather/map-reduce),
|
|
5
|
-
// LLM 无法知道有现成的编排工具可用,会倾向自己 generate 脚本或瞎猜 name。
|
|
6
|
-
//
|
|
7
|
-
// 本测试用源码断言(读 .ts 文件文本)验证提示词内容,避免 import 重 mock 链
|
|
8
|
-
// (tool-workflow.ts 依赖 pi-ai/typebox/pi-tui/lifecycle 等值导入)。
|
|
9
|
-
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
import { dirname,join } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
|
|
16
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const TOOL_WORKFLOW_SRC = readFileSync(
|
|
18
|
-
join(__dirname, "../tool-workflow.ts"),
|
|
19
|
-
"utf-8",
|
|
20
|
-
);
|
|
21
|
-
const TOOL_WORKFLOW_SCRIPT_SRC = readFileSync(
|
|
22
|
-
join(__dirname, "../tool-workflow-script.ts"),
|
|
23
|
-
"utf-8",
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
/** 截取 promptGuidelines 数组文本——防 KNOWN_ARG_KEYS 等注释/代码中的裸词污染断言。
|
|
27
|
-
* 数到数组闭合(跳过字符串字面量内的括号,exec-review F9:'],' 序列静默截断)。 */
|
|
28
|
-
function promptGuidelinesText(src: string): string {
|
|
29
|
-
const start = src.indexOf("promptGuidelines: [");
|
|
30
|
-
let depth = 0;
|
|
31
|
-
let inStr = false;
|
|
32
|
-
for (let i = start; i < src.length; i++) {
|
|
33
|
-
const ch = src[i];
|
|
34
|
-
if (inStr) {
|
|
35
|
-
if (ch === "\\") { i++; continue; }
|
|
36
|
-
if (ch === '"') inStr = false;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (ch === '"') inStr = true;
|
|
40
|
-
else if (ch === "[") depth++;
|
|
41
|
-
else if (ch === "]") {
|
|
42
|
-
depth--;
|
|
43
|
-
if (depth === 0) return src.slice(start, i + 1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return src.slice(start);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe("U1: workflow tool prompt mentions built-in workflows", () => {
|
|
50
|
-
it("TC4a: promptGuidelines 不含具体内置 args 枚举(m4 瘦身——参数知识在 read location)", () => {
|
|
51
|
-
// m4:BUILT-IN 枚举删除,发现职责转移给 <available_workflows> 注入段 + read location。
|
|
52
|
-
// 截取 promptGuidelines 段断言("batch1..batchN" 等在 KNOWN_ARG_KEYS 注释中出现)。
|
|
53
|
-
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
54
|
-
expect(guidelines).not.toContain("chain (sequential");
|
|
55
|
-
expect(guidelines).not.toContain("args: task");
|
|
56
|
-
expect(guidelines).not.toContain("batch1..batchN");
|
|
57
|
-
expect(guidelines).not.toContain("chain/parallel/scatter-gather/map-reduce");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("TC4b: promptGuidelines 引导 read location 获取参数细节(info 已砍,ADR-0003 D5)", () => {
|
|
61
|
-
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
62
|
-
expect(guidelines).toContain("read the <location>");
|
|
63
|
-
expect(guidelines).toContain("script file");
|
|
64
|
-
// info action 已砍,引导语不再含 workflow info
|
|
65
|
-
expect(guidelines).not.toContain("workflow info");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("tool-workflow.ts promptGuidelines 含 workflow-script list 交叉引用", () => {
|
|
69
|
-
// LLM 需要知道"先 list 再 run"的发现路径——两个 tool 之间必须有交叉引用。
|
|
70
|
-
// 文本可能跨字符串拼接行,分别断言两个关键词都存在。
|
|
71
|
-
expect(TOOL_WORKFLOW_SRC).toContain("workflow-script");
|
|
72
|
-
expect(TOOL_WORKFLOW_SRC).toMatch(/action.*list/i);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("tool-workflow.ts promptGuidelines 含 run action 的正例", () => {
|
|
76
|
-
// 给出 run 调用的 JSON 示例,LLM 才知道参数格式(action/name/args 嵌套)。
|
|
77
|
-
expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run","name":"');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("promptGuidelines 含 JSON 调用正例(run/status/lifecycle)", () => {
|
|
81
|
-
// 弱模型信任 schema 结构信号 > 文本信号,容易把 args 子字段平铺到顶层。
|
|
82
|
-
// promptGuidelines 必须有完整 JSON 调用正例,让模型能直接照抄 {"action":"run",...} 嵌套结构。
|
|
83
|
-
expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run"');
|
|
84
|
-
expect(TOOL_WORKFLOW_SRC).toContain("Call shapes (JSON)");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("promptGuidelines 含参数结构反例(args 平铺到顶层)", () => {
|
|
88
|
-
// 显式说明 args 子字段不能平铺到顶层,必须嵌在 args 里。
|
|
89
|
-
expect(TOOL_WORKFLOW_SRC).toContain("args");
|
|
90
|
-
expect(TOOL_WORKFLOW_SRC).toContain("Anti-patterns");
|
|
91
|
-
expect(TOOL_WORKFLOW_SRC).toContain("top level");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("runtime handler 错误文案含 Correct 纠正正例 + 平铺检测(m6:动态参数集)", () => {
|
|
95
|
-
// 读源码文本断言 actionRun/必填校验的错误文案含 Correct 正例,
|
|
96
|
-
// 让弱模型撞错后第二次能直接照抄正确形态。m6 后平铺检测数据源为动态
|
|
97
|
-
// argKeysFromMeta(schema 即 SSOT)——断言新机制存在(KNOWN_ARG_KEYS 已删)。
|
|
98
|
-
expect(TOOL_WORKFLOW_SRC).toContain("Correct:");
|
|
99
|
-
expect(TOOL_WORKFLOW_SRC).toContain("findFlattenedArgKeys");
|
|
100
|
-
expect(TOOL_WORKFLOW_SRC).toContain("argKeysFromMeta");
|
|
101
|
-
expect(TOOL_WORKFLOW_SRC).not.toMatch(/const\s+KNOWN_ARG_KEYS/);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow run 交叉引用", () => {
|
|
105
|
-
// 反向交叉引用:list 的指引里要提到用 workflow tool 的 run action 启动脚本。
|
|
106
|
-
expect(TOOL_WORKFLOW_SCRIPT_SRC).toMatch(/workflow.*tool.*run|run.*workflow.*tool/i);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("tool-workflow.ts promptGuidelines 强化 anti-generate(直接 run,不要 generate)", () => {
|
|
110
|
-
// session 证据:弱模型看到 workflow list 后倾向 workflow-script generate 而非直接 run。
|
|
111
|
-
// 提示词必须显式禁止对内置编排使用 generate。
|
|
112
|
-
expect(TOOL_WORKFLOW_SRC).toContain("NEVER use workflow-script action:generate");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("tool-workflow-script.ts promptGuidelines 强化 anti-generate(CRITICAL ANTI-PATTERN)", () => {
|
|
116
|
-
expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("CRITICAL ANTI-PATTERN");
|
|
117
|
-
expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("NEVER generate");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("promptGuidelines + parameter description 标注 budget 默认不限制(B/C:除非用户要求否则别设)", () => {
|
|
121
|
-
// budget 是 run 级参数,运行时 maxTokens===undefined → 不限制(budget.ts isExceeded 守卫)。
|
|
122
|
-
// 但 call shape 示例展示了 tokens/time 字段,LLM 会误以为每次都该填。
|
|
123
|
-
// promptGuidelines 必须明确 "Do NOT set ... unless user explicitly requests",
|
|
124
|
-
// parameter description 同步标 "omit = unlimited",双重约束压过示例的反引导。
|
|
125
|
-
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
126
|
-
expect(guidelines).toContain("Do NOT set tokens/time unless the user explicitly requests");
|
|
127
|
-
expect(TOOL_WORKFLOW_SRC).toContain("omit = unlimited (default)");
|
|
128
|
-
});
|
|
129
|
-
});
|
package/src/interface/helpers.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Extension — Interface helpers
|
|
3
|
-
*
|
|
4
|
-
* notifyDone(pi, runId, run, notified) — run 完成时发 completion notification
|
|
5
|
-
* ([u9 账本化] 经 core NotifyLedger 四步生命周期,C-ext-19;未 bind 降级直发)。
|
|
6
|
-
*
|
|
7
|
-
* 层归属:Interface(依赖 Pi SDK + Engine WorkflowRun 模型)。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { guardStaleCtx, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
12
|
-
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
13
|
-
|
|
14
|
-
// bounded JSON pretty 序列化(IF13/#19,TC5/ES5)已下沉 core shared
|
|
15
|
-
// (u-core-atomic 逐字平移,输出与原本地实现字节一致;本地实现已删)。
|
|
16
|
-
// getBoundNotifyLedger:core 通知账本消费入口(bindNotifyLedgerHost 在
|
|
17
|
-
// session-lifecycle.ts session_start 装配;未 bind 时降级直发,见 notifyDone 注释)。
|
|
18
|
-
import { boundedPrettySerialize, getBoundNotifyLedger } from "@zhushanwen/subagent-core";
|
|
19
|
-
|
|
20
|
-
// 模块级 logger(与 session-lifecycle.ts / index.ts 同 component 名)
|
|
21
|
-
const logger = getLogger("subagents");
|
|
22
|
-
|
|
23
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
24
|
-
import {
|
|
25
|
-
guiComponent,
|
|
26
|
-
type GuiContext,
|
|
27
|
-
type GuiRenderResult,
|
|
28
|
-
guiResult,
|
|
29
|
-
isGuiCapable,
|
|
30
|
-
} from "@zhushanwen/extension-protocol";
|
|
31
|
-
import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
32
|
-
import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
|
|
33
|
-
|
|
34
|
-
// ── 常量 ─────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const MAX_RESULT_LENGTH = 8000;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* notifyDone 账本幂等键前缀(u9 账本化,对齐 C-ext-19)。键形态
|
|
40
|
-
* `wf-done:<runId>`——一个 run 的收口通知只投递一次(写账 → courier 边沿投递 →
|
|
41
|
-
* 回执销账 → 断连/重启经账本重放,幂等键去重不双投递)。前缀惯例承接 collect
|
|
42
|
-
* 时代 `sync-batch:` 的「通道语义前缀 + 天然唯一标识」形态;导出供测试构造
|
|
43
|
-
* 回执 entry 与账本断言复用同键。
|
|
44
|
-
*/
|
|
45
|
-
export const WORKFLOW_DONE_NOTIFY_ID_PREFIX = "wf-done:";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* workflow 收口通知的送达 customType。保持与账本化前一致:runtime
|
|
49
|
-
* event-interpreter 按该类型识别 run 完成并驱动 W18 workflow-record 失效信号
|
|
50
|
-
* (session.workflows 增量广播),taiji 完成通知 display 覆写 SSOT
|
|
51
|
-
* (COMPLETE_NOTIFY_CUSTOM_TYPES)亦按它收录——不可复用 subagent-bg-notify 通道。
|
|
52
|
-
*/
|
|
53
|
-
const WORKFLOW_RESULT_CUSTOM_TYPE = "workflow-result";
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* notifiedRunIds 去重窗口大小。
|
|
57
|
-
*
|
|
58
|
-
* 最近该数量的已通知 runId 可去重,更旧挤出后不再去重——runId 全局唯一
|
|
59
|
-
* (wf-<Date.now>-<rand>),旧 id 重现概率为零,语义无损。
|
|
60
|
-
*/
|
|
61
|
-
export const MAX_NOTIFIED_RUN_IDS = 1000;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* notifyDone 的 details 结构(通过 pi.sendMessage 透传给前端)。
|
|
65
|
-
*
|
|
66
|
-
* 抽取为显式接口替代裸 Record<string, unknown>,明确 __gui__ 契约。
|
|
67
|
-
* 模块私有(原 export 已删):无跨文件消费者,测试经 notifyDone 公共入口
|
|
68
|
-
* 观察并内联类型。
|
|
69
|
-
*/
|
|
70
|
-
interface WorkflowNotifyDetails {
|
|
71
|
-
runId: string;
|
|
72
|
-
name: string;
|
|
73
|
-
status: string;
|
|
74
|
-
reason: string | undefined;
|
|
75
|
-
traceLength: number;
|
|
76
|
-
/**
|
|
77
|
-
* [u9] 账本幂等键(`wf-done:<runId>`)——回执匹配键(ledger checkReceipts 扫
|
|
78
|
-
* 送达 custom_message entry 的 details.notifyId 判定销账)。携带在 details 不进
|
|
79
|
-
* 文案(G4 字节锁定不受影响)。
|
|
80
|
-
*/
|
|
81
|
-
notifyId: string;
|
|
82
|
-
__gui__?: GuiRenderResult;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* workflow 到达 done 终态时发送完成通知。
|
|
87
|
-
*
|
|
88
|
-
* [u9 账本化] 结果语义通知接 core NotifyLedger 四步生命周期(C-ext-19:持久账本 +
|
|
89
|
-
* notifyId 幂等,替代裸 steer fire-once——relay 瞬断不再丢通知):
|
|
90
|
-
* ① 写账:ledger.record(`wf-done:<runId>`) 落盘(幂等键去重,同 run 跨重启
|
|
91
|
-
* 不双投递)→ ② courier 边沿投递(settled 边沿 + isIdle 二次复查 + 120s
|
|
92
|
-
* 看门狗,送达 customType 保持 "workflow-result")→ ③ 回执销账 → ④ 断连/
|
|
93
|
-
* 重启经 recoverFromSession at-least-once 重放(机制见 notify-ledger.ts)。
|
|
94
|
-
* 账本未装配时(旧宿主 / 无 ledger 测试)降级为 fire-once 直发(at-most-once,
|
|
95
|
-
* 对齐 notifier 内核降级路径;triggerTurn 单通道,deliverAs 已删——u9 偏差裁决,
|
|
96
|
-
* D7 账本化配套)。
|
|
97
|
-
*
|
|
98
|
-
* **内存去重**:notifiedRunIds Set 由调用方(factory/extension instance)持有,
|
|
99
|
-
* 同 runId 的重复收口回调拦截(跨 session_shutdown 等边界防重复);标记在写账成功
|
|
100
|
-
* (或账本幂等拒绝)后落下——record 抛(reload 窗口 appendEntry assertActive 等)
|
|
101
|
-
* 不标记,异常由 finalizeRun 围栏接住后重复收口可重试,窗口内不永久丢通知;
|
|
102
|
-
* 持久层幂等由账本 notifyId 承接(内存窗口挤出 / 重启后的重复仍被 record 拒绝)。
|
|
103
|
-
*
|
|
104
|
-
* @param pi ExtensionAPI(仅降级路径直发用)
|
|
105
|
-
* @param runId run 标识
|
|
106
|
-
* @param run WorkflowRun 聚合根(读 spec.scriptName + state.status + trace + scriptResult)
|
|
107
|
-
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
108
|
-
*/
|
|
109
|
-
export function notifyDone(
|
|
110
|
-
pi: ExtensionAPI,
|
|
111
|
-
runId: string,
|
|
112
|
-
run: WorkflowRun,
|
|
113
|
-
notifiedRunIds: Set<string>,
|
|
114
|
-
ctx?: GuiContext,
|
|
115
|
-
): void {
|
|
116
|
-
if (notifiedRunIds.has(runId)) return;
|
|
117
|
-
|
|
118
|
-
const traceNodes = run.state.trace.toArray();
|
|
119
|
-
const name = run.spec.scriptName;
|
|
120
|
-
const status = `${run.state.status}${run.state.reason ? ` (${run.state.reason})` : ""}`;
|
|
121
|
-
|
|
122
|
-
// 构建消息内容
|
|
123
|
-
const parts: string[] = [];
|
|
124
|
-
parts.push(`Workflow '${name}' done: ${status}`);
|
|
125
|
-
|
|
126
|
-
// 终止性原因(非正常完成)追加防偷懒收尾指令——budget/time 耗尽或 abort 不是任务完成,
|
|
127
|
-
// 模型可能把 "done" 当成功汇报(F3 偷懒完成)。收尾三步骤与 turn-limiter WRAP_UP_MESSAGE 对齐。
|
|
128
|
-
const TERMINAL_REASONS = new Set(["budget_limited", "time_limited", "aborted", "failed", "circular"]);
|
|
129
|
-
if (run.state.reason && TERMINAL_REASONS.has(run.state.reason)) {
|
|
130
|
-
parts.push("");
|
|
131
|
-
parts.push(
|
|
132
|
-
"This is NOT task completion. Summarize what was DONE and VERIFIED, list what remains " +
|
|
133
|
-
"NOT DONE, and give the user the single most important next step.",
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (run.state.scriptResult !== undefined && run.state.scriptResult !== null) {
|
|
138
|
-
// M10: scriptResult 来自 worker 脚本返回值(用户可控),可能含循环引用导致 JSON.stringify 抛 TypeError
|
|
139
|
-
// IF13(#19):bounded 序列化(只生成会被保留的前缀;BigInt/循环引用整体回退
|
|
140
|
-
// String(x) 与旧实现整串 catch 同款)——≤8000 与旧全量 pretty 逐字节一致,
|
|
141
|
-
// >8000 与 .slice(0,8000)+标记 逐字节一致(等价测试锚定)
|
|
142
|
-
const truncated = boundedPrettySerialize(run.state.scriptResult, MAX_RESULT_LENGTH);
|
|
143
|
-
parts.push("");
|
|
144
|
-
parts.push("--- Script Result ---");
|
|
145
|
-
parts.push(truncated);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
parts.push("");
|
|
149
|
-
parts.push("--- Agent Trace ---");
|
|
150
|
-
for (const node of traceNodes) {
|
|
151
|
-
parts.push(`[${node.stepIndex}] ${node.agent}: ${node.status}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const content = parts.join("\n");
|
|
155
|
-
|
|
156
|
-
// 送达通道保持 "workflow-result"(runtime W18 失效信号 + taiji display 覆写 SSOT
|
|
157
|
-
// 按该类型识别;迁移不改变消息类型与文案字节,只改变投递可靠性机制)
|
|
158
|
-
const details: WorkflowNotifyDetails = {
|
|
159
|
-
runId,
|
|
160
|
-
name,
|
|
161
|
-
status: run.state.status,
|
|
162
|
-
reason: run.state.reason,
|
|
163
|
-
traceLength: traceNodes.length,
|
|
164
|
-
notifyId: `${WORKFLOW_DONE_NOTIFY_ID_PREFIX}${runId}`,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// GUI 协议:RPC 模式下附加结构化渲染数据
|
|
168
|
-
if (ctx && isGuiCapable(ctx)) {
|
|
169
|
-
const reason = run.state.reason;
|
|
170
|
-
const statusStr = `${run.state.status}${reason ? ` (${reason})` : ""}`;
|
|
171
|
-
// label 对齐 buildWorkflowGui 的格式:name + slug + runId 前 8 字符(I#3)
|
|
172
|
-
const slug = run.spec.slug;
|
|
173
|
-
const label = [name, slug, runId.slice(0, ID_PREVIEW_LENGTH)]
|
|
174
|
-
.filter(Boolean)
|
|
175
|
-
.join(" ");
|
|
176
|
-
details.__gui__ = guiResult(
|
|
177
|
-
guiComponent("list-tree", {
|
|
178
|
-
items: [{
|
|
179
|
-
label,
|
|
180
|
-
status: mapRunStatus(statusStr),
|
|
181
|
-
icon: mapRunIcon(statusStr),
|
|
182
|
-
}],
|
|
183
|
-
}),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const ledger = getBoundNotifyLedger();
|
|
188
|
-
if (ledger) {
|
|
189
|
-
// [u9 账本化] ledger 在 → ①写账(record false = 同幂等键已在账/已销账——内存
|
|
190
|
-
// 去重窗口挤出或重启恢复后的重复收口,跳过投递)→ ②attemptDeliver(courier
|
|
191
|
-
// 边沿 + isIdle 二次复查,③销账 ④重放在 ledger 内;送达通道经
|
|
192
|
-
// deliveryCustomType 保持 "workflow-result")。
|
|
193
|
-
// 内存去重标记在写账**成功后**才落下:record 抛(reload 窗口 appendEntry 命中
|
|
194
|
-
// assertActive 等)时不标记——异常由 finalizeRun 围栏接住(不崩),账面 entry 未写,
|
|
195
|
-
// 后续重复收口回调(adoption 快照重发等)可重试写账;提前标记会把「窗口内丢失」
|
|
196
|
-
// 变成永久丢失(去重阻断 + 账本无 entry 不可重放)。stale ctx 防御由装配层
|
|
197
|
-
// sendDelivery 内置(session-lifecycle.ts bindLedgerHostAndRecover),此处无需
|
|
198
|
-
// 重复包裹。
|
|
199
|
-
if (!ledger.record(details.notifyId, content, details, { deliveryCustomType: WORKFLOW_RESULT_CUSTOM_TYPE })) {
|
|
200
|
-
trackNotifiedRunId(notifiedRunIds, runId);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
trackNotifiedRunId(notifiedRunIds, runId);
|
|
204
|
-
ledger.attemptDeliver();
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 降级:ledger 未装配(旧宿主 / 无账本测试)→ fire-once 直发(at-most-once)。
|
|
209
|
-
// triggerTurn 单通道(deliverAs 已删——u9 偏差裁决,D7 账本化配套:busy 场景的
|
|
210
|
-
// 投递时机治理本就由账本路径承担,降级路径不再依赖 pi 内存队列的 steer 形态);stale ctx 防御
|
|
211
|
-
// (crash-resilience D1 / ext-guards 审计 §7 blockers#1 收口):session 替换窗口
|
|
212
|
-
// 触碰 stale pi 命中 assertActive(PS-30)即无人接 rejection 崩 pi(E1 同机制)。
|
|
213
|
-
// stale 静默降级(完成通知不投递,用户可从 session 历史 / 工具结果看到 workflow
|
|
214
|
-
// 结果,判定见 stale-ctx-audit.md §4),非 stale 错误原样上抛(守卫不吞真实 bug)。
|
|
215
|
-
trackNotifiedRunId(notifiedRunIds, runId);
|
|
216
|
-
guardStaleCtx(
|
|
217
|
-
() =>
|
|
218
|
-
pi.sendMessage(
|
|
219
|
-
{
|
|
220
|
-
customType: WORKFLOW_RESULT_CUSTOM_TYPE,
|
|
221
|
-
content,
|
|
222
|
-
display: true,
|
|
223
|
-
details,
|
|
224
|
-
},
|
|
225
|
-
{ triggerTurn: true },
|
|
226
|
-
),
|
|
227
|
-
{
|
|
228
|
-
label: "subagent-workflow:notifyDone",
|
|
229
|
-
onStale: (error) =>
|
|
230
|
-
logger.warn("workflow completion notice delivery skipped (stale ctx)", {
|
|
231
|
-
runId,
|
|
232
|
-
error: toErrorMessage(error),
|
|
233
|
-
}),
|
|
234
|
-
},
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 把 runId 纳入 notifiedRunIds 去重窗口,超 cap 时删最旧(契约 W3C2)。
|
|
240
|
-
*
|
|
241
|
-
* 职责拆分:**去重判定**留在 notifyDone 的 has 读(本体零改动),
|
|
242
|
-
* 本函数只持**有界化**职责——返回 void,不引入双重去重判定语义。
|
|
243
|
-
*
|
|
244
|
-
* 语义:
|
|
245
|
-
* - 幂等 add:Set.add 对已存在元素不改变其迭代位置(重复 track 同一 id,
|
|
246
|
-
* 其「最旧」地位不变)。
|
|
247
|
-
* - FIFO 有界:Set 迭代序=插入序,超 cap 时删迭代器首元素=最旧。
|
|
248
|
-
* 被挤出窗口的旧 id 再经 notifyDone:内存层放行后由账本 notifyId 幂等兜底
|
|
249
|
-
* ([u9] record 同键拒绝——生产环境账本已 bind 时不会重发;无账本降级环境
|
|
250
|
-
* 才会重新直发,runId 全局唯一,旧 id 重现概率为零,该边界由 W3TC12 单测
|
|
251
|
-
* 在降级形态下钉死)。
|
|
252
|
-
*
|
|
253
|
-
* 调用点:notifyDone 内部(写账成功/false 后)+ workflow-events onRunDone 回调
|
|
254
|
-
* (notifyDone 之后,幂等二次添加)。notifyDone 抛出(reload 窗口 record 抛等)时
|
|
255
|
-
* 内外都不标记——去重不阻断,重复收口可重试写账(见 notifyDone 账本分支注释)。
|
|
256
|
-
*
|
|
257
|
-
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
258
|
-
* @param runId run 标识
|
|
259
|
-
* @param cap 窗口大小(默认 MAX_NOTIFIED_RUN_IDS)
|
|
260
|
-
*/
|
|
261
|
-
export function trackNotifiedRunId(
|
|
262
|
-
notifiedRunIds: Set<string>,
|
|
263
|
-
runId: string,
|
|
264
|
-
cap: number = MAX_NOTIFIED_RUN_IDS,
|
|
265
|
-
): void {
|
|
266
|
-
notifiedRunIds.add(runId);
|
|
267
|
-
while (notifiedRunIds.size > cap) {
|
|
268
|
-
const oldest = notifiedRunIds.values().next().value;
|
|
269
|
-
if (oldest === undefined) break;
|
|
270
|
-
notifiedRunIds.delete(oldest);
|
|
271
|
-
}
|
|
272
|
-
}
|