@zhushanwen/pi-subagent-workflow 7.4.0 → 8.0.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 +15 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +270 -68
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +200 -19
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +43 -16
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// 测试框架:vitest
|
|
2
|
+
// 运行命令:npx vitest run src/orchestration/models/__tests__/trace.test.ts
|
|
3
|
+
//
|
|
4
|
+
// Trace 首个直接单测(W1TC1-W1TC12,.cw/swf-perf-impl/rt-w1-design.json):
|
|
5
|
+
// - W1TC1-3/9:byIndex 倒排索引一致性与 no-op 防御语义
|
|
6
|
+
// - W1TC4-8:result.content 裁剪(append/update 入口、8000 边界、patch 缺省、fromArray 原样保留)
|
|
7
|
+
// - W1TC10-11:集成——executeAgentCall 真链路 / jsonl save-load round-trip
|
|
8
|
+
// - W1TC12:重复 stepIndex 违规语义锚定(last-wins + remove 后 desync 孤儿)
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { TRACE_RESULT_MAX_CHARS, Trace } from "../trace.js";
|
|
17
|
+
import { AgentCall } from "../agent-call.js";
|
|
18
|
+
import { Budget } from "../budget.js";
|
|
19
|
+
import { WorkflowRun } from "../workflow-run.js";
|
|
20
|
+
import type { ExecutionTraceNode, AgentResult, RunSpec } from "../types.js";
|
|
21
|
+
import type { AgentRunner } from "../ports.js";
|
|
22
|
+
import { executeAgentCall } from "../../execute-agent-call.js";
|
|
23
|
+
import { JsonlRunStore } from "../../jsonl-run-store.js";
|
|
24
|
+
|
|
25
|
+
// ── 测试辅助 ─────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** 构造最小 ExecutionTraceNode(status 默认 pending,无 result)。 */
|
|
28
|
+
function makeTraceNode(stepIndex: number): ExecutionTraceNode {
|
|
29
|
+
return {
|
|
30
|
+
stepIndex,
|
|
31
|
+
agent: "test-agent",
|
|
32
|
+
task: "test task",
|
|
33
|
+
model: "default",
|
|
34
|
+
status: "pending",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 构造指定长度 content 的 AgentResult(sessionId 等可选字段透传)。 */
|
|
39
|
+
function makeResult(content: string, extras: Partial<AgentResult> = {}): AgentResult {
|
|
40
|
+
return { content, ...extras };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── byIndex 索引一致性 ────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("Trace byIndex 索引一致性", () => {
|
|
46
|
+
it("W1TC1: append/update/remove 后 find 命中且引用共享", () => {
|
|
47
|
+
const trace = new Trace();
|
|
48
|
+
const node0 = makeTraceNode(0);
|
|
49
|
+
const node1 = makeTraceNode(1);
|
|
50
|
+
const node2 = makeTraceNode(2);
|
|
51
|
+
trace.append(node0);
|
|
52
|
+
trace.append(node1);
|
|
53
|
+
trace.append(node2);
|
|
54
|
+
|
|
55
|
+
trace.update(1, { status: "completed" });
|
|
56
|
+
|
|
57
|
+
// byIndex 与 nodes 引用共享非拷贝
|
|
58
|
+
expect(trace.find(0)).toBe(node0);
|
|
59
|
+
expect(trace.find(1)).toBe(node1);
|
|
60
|
+
expect(trace.find(2)).toBe(node2);
|
|
61
|
+
// Map 值是节点引用,update 字段 mutate 可见
|
|
62
|
+
expect(trace.find(1)!.status).toBe("completed");
|
|
63
|
+
|
|
64
|
+
trace.removeByStepIndex(1);
|
|
65
|
+
expect(trace.find(1)).toBeUndefined();
|
|
66
|
+
expect(trace.length).toBe(2);
|
|
67
|
+
// 其余节点不受影响
|
|
68
|
+
expect(trace.find(0)).toBe(node0);
|
|
69
|
+
expect(trace.find(2)).toBe(node2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── remove 后 re-append 同 stepIndex 覆盖 ─────────────────────
|
|
74
|
+
|
|
75
|
+
describe("Trace remove 后 re-append 同 stepIndex", () => {
|
|
76
|
+
it("W1TC2: rebuild discard 清理后重跑重发同 callId——旧节点不再可达", () => {
|
|
77
|
+
const trace = new Trace();
|
|
78
|
+
const nodeA = makeTraceNode(0);
|
|
79
|
+
trace.append(nodeA);
|
|
80
|
+
trace.removeByStepIndex(0);
|
|
81
|
+
|
|
82
|
+
const nodeB = makeTraceNode(0);
|
|
83
|
+
trace.append(nodeB);
|
|
84
|
+
|
|
85
|
+
expect(trace.find(0)).toBe(nodeB);
|
|
86
|
+
expect(trace.toArray()).toHaveLength(1);
|
|
87
|
+
expect(trace.toArray()[0]).toBe(nodeB);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── 重复 stepIndex 违规语义锚定(W1TC12)──────────────────────
|
|
92
|
+
|
|
93
|
+
describe("Trace 重复 stepIndex 违规语义锚定(W1TC12)", () => {
|
|
94
|
+
it("W1TC12: 重复 append find last-wins;重复 append 后 remove 呈 desync 孤儿", () => {
|
|
95
|
+
// ① 重复 append 同 stepIndex 且未 remove:find 返回第二个节点(Map
|
|
96
|
+
// last-wins;旧线性扫 first-match 会返回第一个)
|
|
97
|
+
const t1 = new Trace();
|
|
98
|
+
const first = makeTraceNode(0);
|
|
99
|
+
const second = makeTraceNode(0);
|
|
100
|
+
t1.append(first);
|
|
101
|
+
t1.append(second);
|
|
102
|
+
expect(t1.find(0)).toBe(second);
|
|
103
|
+
expect(t1.length).toBe(2);
|
|
104
|
+
|
|
105
|
+
// ② 重复 append 后 removeByStepIndex:findIndex 命中首个旧节点 splice、
|
|
106
|
+
// byIndex.delete 删掉整个键——第二个节点残留为孤儿(find 不可达但
|
|
107
|
+
// nodes.length=1)。desync 行为锚定:防未来改回线性扫时静默漂移
|
|
108
|
+
// (线性扫实现下 find(0) 会命中残留节点 second,本断言即失败)
|
|
109
|
+
const t2 = new Trace();
|
|
110
|
+
const a = makeTraceNode(0);
|
|
111
|
+
const b = makeTraceNode(0);
|
|
112
|
+
t2.append(a);
|
|
113
|
+
t2.append(b);
|
|
114
|
+
t2.removeByStepIndex(0);
|
|
115
|
+
expect(t2.find(0)).toBeUndefined();
|
|
116
|
+
expect(t2.length).toBe(1);
|
|
117
|
+
expect(t2.toArray()[0]).toBe(b);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── fromArray 重建 ───────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("Trace.fromArray 重建", () => {
|
|
124
|
+
it("W1TC3: 索引全命中 + 防御性拷贝语义保持", () => {
|
|
125
|
+
const node0 = makeTraceNode(0);
|
|
126
|
+
const node1 = makeTraceNode(1);
|
|
127
|
+
const node2 = makeTraceNode(2);
|
|
128
|
+
const src = [node0, node1, node2];
|
|
129
|
+
|
|
130
|
+
const trace = Trace.fromArray(src);
|
|
131
|
+
|
|
132
|
+
expect(trace.length).toBe(3);
|
|
133
|
+
// fromArray push {...node} 副本——find(i) 与 toArray()[i] 同引用(byIndex 命中副本)
|
|
134
|
+
for (let i = 0; i < 3; i++) {
|
|
135
|
+
expect(trace.find(i)).toBe(trace.toArray()[i]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 传入数组后续 mutate 不影响 trace(浅拷贝语义保持)
|
|
139
|
+
src.push(makeTraceNode(3));
|
|
140
|
+
expect(trace.length).toBe(3);
|
|
141
|
+
src[0]!.status = "failed";
|
|
142
|
+
expect(trace.find(0)!.status).toBe("pending");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── append 入口裁剪 ──────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
describe("Trace append 入口超长 result 裁剪", () => {
|
|
149
|
+
it("W1TC4: 标记含原始长度,节点引用不变,其余字段浅拷贝保留", () => {
|
|
150
|
+
const content = "x".repeat(10000);
|
|
151
|
+
const node = {
|
|
152
|
+
...makeTraceNode(0),
|
|
153
|
+
result: makeResult(content, { sessionId: "s1", parsedOutput: { a: 1 } }),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const trace = new Trace();
|
|
157
|
+
trace.append(node);
|
|
158
|
+
|
|
159
|
+
const stored = trace.toArray()[0]!;
|
|
160
|
+
// 节点对象引用不变(mutate result 字段,不 push 副本——D-10 traceNode 引用共享)
|
|
161
|
+
expect(stored).toBe(node);
|
|
162
|
+
|
|
163
|
+
const marker = `\n…[trace result truncated, original 10000 chars]…\n`;
|
|
164
|
+
expect(stored.result!.content).toBe(content.slice(0, 4000) + marker + content.slice(-4000));
|
|
165
|
+
// 头尾与标记子串
|
|
166
|
+
expect(stored.result!.content.startsWith(content.slice(0, 4000))).toBe(true);
|
|
167
|
+
expect(stored.result!.content.endsWith(content.slice(-4000))).toBe(true);
|
|
168
|
+
expect(stored.result!.content).toContain("[trace result truncated, original 10000 chars]");
|
|
169
|
+
// result 其余字段浅拷贝保留
|
|
170
|
+
expect(stored.result!.sessionId).toBe("s1");
|
|
171
|
+
expect(stored.result!.parsedOutput).toEqual({ a: 1 });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ── update 入口裁剪 ──────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe("Trace update patch.result 超长裁剪", () => {
|
|
178
|
+
it("W1TC5: 节点持裁剪副本,patch 原对象不被污染(call.result 保真)", () => {
|
|
179
|
+
const trace = new Trace();
|
|
180
|
+
trace.append(makeTraceNode(0));
|
|
181
|
+
|
|
182
|
+
const full = makeResult("y".repeat(9000), { sessionId: "s1" });
|
|
183
|
+
trace.update(0, { result: full });
|
|
184
|
+
|
|
185
|
+
const stored = trace.find(0)!.result!;
|
|
186
|
+
const marker = `\n…[trace result truncated, original 9000 chars]…\n`;
|
|
187
|
+
expect(stored.content.length).toBe(4000 + marker.length + 4000);
|
|
188
|
+
expect(stored.content).toContain("original 9000 chars");
|
|
189
|
+
// 节点持裁剪副本新对象,与 patch.result 脱钩
|
|
190
|
+
expect(stored).not.toBe(full);
|
|
191
|
+
// 浅拷贝保留其他字段
|
|
192
|
+
expect(stored.sessionId).toBe("s1");
|
|
193
|
+
// AgentCall.result 保真——原对象不被污染(replay 数据源)
|
|
194
|
+
expect(full.content).toHaveLength(9000);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── 边界:恰好 8000 不裁 / 8001 裁 ────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe("Trace 裁剪边界(严格大于 TRACE_RESULT_MAX_CHARS)", () => {
|
|
201
|
+
it("W1TC6: 恰好 8000 引用透传零拷贝;8001 触发裁剪", () => {
|
|
202
|
+
const trace = new Trace();
|
|
203
|
+
trace.append(makeTraceNode(0));
|
|
204
|
+
trace.append(makeTraceNode(1));
|
|
205
|
+
|
|
206
|
+
// 恰好 8000:不裁
|
|
207
|
+
const exact = makeResult("a".repeat(TRACE_RESULT_MAX_CHARS));
|
|
208
|
+
trace.update(0, { result: exact });
|
|
209
|
+
expect(trace.find(0)!.result).toBe(exact);
|
|
210
|
+
expect(trace.find(0)!.result!.content).toHaveLength(8000);
|
|
211
|
+
expect(trace.find(0)!.result!.content).not.toContain("truncated");
|
|
212
|
+
|
|
213
|
+
// 8001:裁剪(head/tail 固定 4000 比例,重叠段属预期)
|
|
214
|
+
const over = makeResult("b".repeat(TRACE_RESULT_MAX_CHARS + 1));
|
|
215
|
+
trace.update(1, { result: over });
|
|
216
|
+
expect(trace.find(1)!.result!.content).toContain("original 8001 chars");
|
|
217
|
+
expect(trace.find(1)!.result!.content).not.toBe(over.content);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── patch.result 未提供不触发裁剪 ────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe("Trace update patch.result 缺省", () => {
|
|
224
|
+
it("W1TC7: 只改 status/completedAt 等字段——已持 result 不被触碰", () => {
|
|
225
|
+
const origResult = makeResult("keep");
|
|
226
|
+
const node = { ...makeTraceNode(0), result: origResult };
|
|
227
|
+
const trace = new Trace();
|
|
228
|
+
trace.append(node);
|
|
229
|
+
|
|
230
|
+
trace.update(0, { status: "completed", completedAt: "2026-08-15T00:00:00Z" });
|
|
231
|
+
expect(trace.find(0)!.result!.content).toBe("keep");
|
|
232
|
+
expect(trace.find(0)!.result).toBe(origResult);
|
|
233
|
+
|
|
234
|
+
// 其他字段路径同样不影响 result
|
|
235
|
+
trace.update(0, { sessionId: "s9" });
|
|
236
|
+
expect(trace.find(0)!.result).toBe(origResult);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── fromArray 原样保留(read 路径不二次裁剪)──────────────────
|
|
241
|
+
|
|
242
|
+
describe("Trace.fromArray 原样保留", () => {
|
|
243
|
+
it("W1TC8: 超长 content 不被二次裁剪;已含标记的形态逐字节不变", () => {
|
|
244
|
+
// 旧版本未裁剪的快照重水合:20000 字符全长保留
|
|
245
|
+
const long = {
|
|
246
|
+
...makeTraceNode(0),
|
|
247
|
+
result: makeResult("z".repeat(20000)),
|
|
248
|
+
};
|
|
249
|
+
const trace = Trace.fromArray([long]);
|
|
250
|
+
expect(trace.find(0)!.result!.content).toHaveLength(20000);
|
|
251
|
+
expect(trace.find(0)!.result!.content).not.toContain("truncated");
|
|
252
|
+
|
|
253
|
+
// 新快照 round-trip:已裁剪含标记的 content 经 fromArray 后逐字节不变(无标记嵌套)
|
|
254
|
+
const marker = `\n…[trace result truncated, original 10000 chars]…\n`;
|
|
255
|
+
const trimmed = "x".repeat(4000) + marker + "x".repeat(4000);
|
|
256
|
+
const trace2 = Trace.fromArray([{ ...makeTraceNode(0), result: makeResult(trimmed) }]);
|
|
257
|
+
expect(trace2.find(0)!.result!.content).toBe(trimmed);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── no-op 防御语义回归 ───────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
describe("Trace no-op 防御语义", () => {
|
|
264
|
+
it("W1TC9: update/remove 不存在的 stepIndex 不抛错、状态不变", () => {
|
|
265
|
+
const trace = new Trace();
|
|
266
|
+
trace.append(makeTraceNode(0));
|
|
267
|
+
|
|
268
|
+
expect(() => trace.update(999, { status: "completed" })).not.toThrow();
|
|
269
|
+
expect(() => trace.removeByStepIndex(999)).not.toThrow();
|
|
270
|
+
expect(trace.length).toBe(1);
|
|
271
|
+
expect(trace.find(999)).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── 集成:executeAgentCall 真链路(W1TC10)────────────────────
|
|
276
|
+
|
|
277
|
+
describe("W1TC10: executeAgentCall 真链路——call.result 全量、trace 节点持裁剪副本", () => {
|
|
278
|
+
it("W1TC10: runner.run 返回 12000 字符 → 两形态并存(AgentCall.result 不裁)", async () => {
|
|
279
|
+
const content = "w".repeat(12000);
|
|
280
|
+
const runner: AgentRunner = {
|
|
281
|
+
run: vi.fn().mockResolvedValue(makeResult(content, { sessionId: "s1" })),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const trace = new Trace();
|
|
285
|
+
const traceNode = makeTraceNode(0);
|
|
286
|
+
trace.append(traceNode);
|
|
287
|
+
const call = new AgentCall(0, { prompt: "test task", agent: "worker" }, traceNode);
|
|
288
|
+
|
|
289
|
+
await executeAgentCall(call, runner, new Budget(), new AbortController().signal, trace);
|
|
290
|
+
|
|
291
|
+
// AgentCall.result 全量(worker cached replay 数据源保真)
|
|
292
|
+
expect(call.result!.content).toHaveLength(12000);
|
|
293
|
+
// trace 节点持裁剪副本
|
|
294
|
+
const stored = trace.find(0)!.result!;
|
|
295
|
+
const marker = `\n…[trace result truncated, original 12000 chars]…\n`;
|
|
296
|
+
expect(stored.content).toBe(content.slice(0, 4000) + marker + content.slice(-4000));
|
|
297
|
+
expect(stored.content.length).toBe(4000 + marker.length + 4000);
|
|
298
|
+
expect(stored.sessionId).toBe("s1");
|
|
299
|
+
expect(trace.find(0)!.status).toBe("completed");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ── 集成:jsonl save/load round-trip(W1TC11)─────────────────
|
|
304
|
+
|
|
305
|
+
describe("W1TC11: jsonl save/load round-trip——落盘裁剪形态 + 重水合不二次裁", () => {
|
|
306
|
+
let tmpDir: string;
|
|
307
|
+
|
|
308
|
+
beforeEach(() => {
|
|
309
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-trace-trim-test-"));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
afterEach(() => {
|
|
313
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
function makeSpec(): RunSpec {
|
|
317
|
+
return {
|
|
318
|
+
scriptSource: "module.exports = async () => {};",
|
|
319
|
+
args: {},
|
|
320
|
+
scriptName: "test-script",
|
|
321
|
+
scriptPath: "/tmp/test.js",
|
|
322
|
+
description: "test",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 构造含 10000 字符 result 已完成 call 的 WorkflowRun(模式对齐 jsonl-run-store-session-file.test.ts)。 */
|
|
327
|
+
function makeRunWithDoneCall(): WorkflowRun {
|
|
328
|
+
const trace = new Trace();
|
|
329
|
+
const node = makeTraceNode(0);
|
|
330
|
+
trace.append(node);
|
|
331
|
+
const call = new AgentCall(0, { prompt: "task", agent: "worker" }, node);
|
|
332
|
+
call.markRunning();
|
|
333
|
+
// markDone 存全量 result(AgentCall.result 不裁)
|
|
334
|
+
call.markDone(makeResult("q".repeat(10000), { sessionId: "session-abc" }));
|
|
335
|
+
call.setSessionId("session-abc");
|
|
336
|
+
// trace.update 经入口裁剪——节点持裁剪副本
|
|
337
|
+
trace.update(0, {
|
|
338
|
+
status: "completed",
|
|
339
|
+
result: call.result,
|
|
340
|
+
completedAt: new Date().toISOString(),
|
|
341
|
+
sessionId: "session-abc",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return new WorkflowRun(
|
|
345
|
+
"run-trim-001",
|
|
346
|
+
makeSpec(),
|
|
347
|
+
{
|
|
348
|
+
status: "done",
|
|
349
|
+
reason: "completed",
|
|
350
|
+
budget: new Budget(),
|
|
351
|
+
calls: new Map([[0, call]]),
|
|
352
|
+
trace,
|
|
353
|
+
errorLogs: [],
|
|
354
|
+
},
|
|
355
|
+
{ startedAt: new Date().toISOString(), completedAt: new Date().toISOString() },
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
it("W1TC11: save 落盘 trace 裁剪 + calls[].result 全量;loadAll 回读逐字节一致", async () => {
|
|
360
|
+
const entries: Array<{ type: string; customType?: string; data?: unknown }> = [];
|
|
361
|
+
const mockPi = {
|
|
362
|
+
appendEntry: vi.fn((type: string, data: unknown) => {
|
|
363
|
+
entries.push({ type: "custom", customType: type, data });
|
|
364
|
+
}),
|
|
365
|
+
};
|
|
366
|
+
const mockCtx = {
|
|
367
|
+
sessionManager: { getEntries: () => entries },
|
|
368
|
+
};
|
|
369
|
+
const store = new JsonlRunStore({
|
|
370
|
+
sessionDir: tmpDir,
|
|
371
|
+
pi: mockPi as never,
|
|
372
|
+
ctx: mockCtx as never,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const run = makeRunWithDoneCall();
|
|
376
|
+
await store.save(run);
|
|
377
|
+
|
|
378
|
+
// 读磁盘快照验证落盘形态
|
|
379
|
+
const raw = fs.readFileSync(
|
|
380
|
+
path.join(tmpDir, "workflow-state", "run-trim-001.jsonl"),
|
|
381
|
+
"utf8",
|
|
382
|
+
);
|
|
383
|
+
const snapshot = JSON.parse(raw.trim()) as {
|
|
384
|
+
state: {
|
|
385
|
+
trace: Array<{ result?: { content: string } }>;
|
|
386
|
+
calls: Array<{ result?: { content: string }; traceNode: { result?: { content: string } } }>;
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// trace 投影瘦身生效:trace[0] 含标记
|
|
391
|
+
expect(snapshot.state.trace[0]!.result!.content).toContain(
|
|
392
|
+
"[trace result truncated, original 10000 chars]",
|
|
393
|
+
);
|
|
394
|
+
// calls[].traceNode 是同一裁剪后节点引用(剥 live 后的 rest),亦含标记
|
|
395
|
+
expect(snapshot.state.calls[0]!.traceNode.result!.content).toContain(
|
|
396
|
+
"[trace result truncated, original 10000 chars]",
|
|
397
|
+
);
|
|
398
|
+
// 顶层 calls[].result 字段不裁——全量 10000
|
|
399
|
+
expect(snapshot.state.calls[0]!.result!.content).toHaveLength(10000);
|
|
400
|
+
|
|
401
|
+
// loadAll 回读:fromArray 不再裁,与落盘形态逐字节一致(无标记嵌套)
|
|
402
|
+
const loaded = await store.loadAll();
|
|
403
|
+
expect(loaded).toHaveLength(1);
|
|
404
|
+
const restoredContent = loaded[0]!.state.trace.toArray()[0]!.result!.content;
|
|
405
|
+
expect(restoredContent).toBe(snapshot.state.trace[0]!.result!.content);
|
|
406
|
+
expect(restoredContent.match(/truncated/g)).toHaveLength(1);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
@@ -100,7 +100,7 @@ export class Budget {
|
|
|
100
100
|
* maxTokens===0 或 undefined 视为不限制(守卫);
|
|
101
101
|
* maxCost===0 或 undefined 视为不限制。
|
|
102
102
|
* 时间预算(maxTimeMs)不由本方法判断——它是 wall-clock 约束,需参照 startedAt,
|
|
103
|
-
* 由 lifecycle 层的 scheduleTimeBudget(runWorkflow
|
|
103
|
+
* 由 lifecycle 层的 scheduleTimeBudget(runWorkflow 内 setTimeout)
|
|
104
104
|
* 独立调度,到期 abortRun(doneReason="time_limited")。
|
|
105
105
|
*/
|
|
106
106
|
isExceeded(): boolean {
|
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
* 职责:封装一次 running-segment 的所有技术资源(worker 线程 + 并发信号量 +
|
|
8
8
|
* abort controller),统一 release 入口(AC-2:单 release 替代多 boolean flag)。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* gate 队列也在 worker 重跑脚本 + callCache replay 时清空无影响——所以整个
|
|
15
|
-
* RunRuntime 重建最简单、语义最清晰。
|
|
10
|
+
* 一次性生命周期(G3-001):runtime 释放后不再复用——AbortController 一次性
|
|
11
|
+
* 语义决定 controller 无法跨释放复用,gate 队列也在 worker 重跑脚本 +
|
|
12
|
+
* callCache replay 时清空无影响,所以整个 RunRuntime 重建。唯一注入路径:
|
|
13
|
+
* assignRuntime(runWorkflow 创建)与 replaceRuntime(error-recovery 崩溃重试)。
|
|
16
14
|
*
|
|
17
15
|
* 参考:domain-models.md §10、clarification.md G3-001。
|
|
18
16
|
*/
|
|
@@ -23,11 +21,11 @@ import { WorkerHandle } from "../worker-handle.ts";
|
|
|
23
21
|
/**
|
|
24
22
|
* release mode 枚举——调用方表达意图。
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* 一次性生命周期后唯一语义:terminal(终局释放,worker + controller 全释放,
|
|
25
|
+
* runtime 即被调用方丢弃)。原 "pause" 值已随 pause/resume 生命周期删除(F8)
|
|
26
|
+
* ——release 后不存在「保留待恢复」的中间形态。
|
|
29
27
|
*/
|
|
30
|
-
export type ReleaseMode = "
|
|
28
|
+
export type ReleaseMode = "terminal";
|
|
31
29
|
|
|
32
30
|
// ── RunRuntime ───────────────────────────────────────────────
|
|
33
31
|
|
|
@@ -39,8 +37,8 @@ export class RunRuntime {
|
|
|
39
37
|
/** per-running-segment AbortController(一次性,无法复用——G3-001)。 */
|
|
40
38
|
readonly controller: AbortController;
|
|
41
39
|
/** Run 级墙钟时间预算计时器(spec.budgetTimeMs > 0 时由 lifecycle 调度,
|
|
42
|
-
* 到期 abortRun time_limited)。release 时清理,避免
|
|
43
|
-
*
|
|
40
|
+
* 到期 abortRun time_limited)。release 时清理,避免 abort/replaceRuntime
|
|
41
|
+
* 后孤儿计时器仍触发(rebuildRuntime 会重排一个全新的计时器,旧的不应残留)。 */
|
|
44
42
|
readonly timeBudgetTimer?: ReturnType<typeof setTimeout>;
|
|
45
43
|
/** 防止 release 重复执行(幂等)。 */
|
|
46
44
|
private released = false;
|
|
@@ -62,24 +60,24 @@ export class RunRuntime {
|
|
|
62
60
|
*
|
|
63
61
|
* 幂等——重复调用安全(第二次起 no-op,released flag 守卫)。
|
|
64
62
|
* 调用后此 RunRuntime 应被调用方丢弃(WorkflowRun.runtime = undefined),
|
|
65
|
-
*
|
|
63
|
+
* 崩溃重试时由 replaceRuntime 注入新实例(G3-001)。
|
|
66
64
|
*
|
|
67
65
|
* worker.terminate 本身幂等,controller.abort 本身幂等
|
|
68
66
|
* (重复 abort 无副作用),但 released flag 让本方法语义更明确:
|
|
69
67
|
* 「释放过一次的 runtime 不再释放第二次」。
|
|
70
68
|
*
|
|
71
|
-
* @param mode
|
|
69
|
+
* @param mode terminal —— 终局释放(唯一值,保留参数为调用方语义显式化)
|
|
72
70
|
*/
|
|
73
71
|
release(_mode: ReleaseMode): void {
|
|
74
72
|
if (this.released) return;
|
|
75
73
|
this.released = true;
|
|
76
|
-
// 清理 run 级时间预算计时器——
|
|
77
|
-
// (
|
|
74
|
+
// 清理 run 级时间预算计时器——abort/terminate/replaceRuntime 后它不应再触发
|
|
75
|
+
// (rebuildRuntime 会重排全新计时器;孤儿触发会把已终态的 run 误转 done)。
|
|
78
76
|
if (this.timeBudgetTimer) clearTimeout(this.timeBudgetTimer);
|
|
79
77
|
// worker.terminate 异步但幂等——不 await(release 是同步签名,调用方
|
|
80
78
|
// 不应被底层线程关闭阻塞;worker 收到 terminate 后自行清理)。
|
|
81
79
|
void this.worker.terminate();
|
|
82
|
-
// controller.abort 触发 listener(kill agent subprocess
|
|
80
|
+
// controller.abort 触发 listener(kill agent subprocess、中止在飞调用)。
|
|
83
81
|
// 一次性语义——已 aborted 的 controller 重复 abort 无副作用。
|
|
84
82
|
this.controller.abort();
|
|
85
83
|
}
|
|
@@ -18,7 +18,7 @@ import type { Budget } from "./budget.ts";
|
|
|
18
18
|
/**
|
|
19
19
|
* RunSpec——一次 workflow run 的不可变输入规格。
|
|
20
20
|
*
|
|
21
|
-
* 作为 RunStore 持久化的一部分(WorkflowRun.spec
|
|
21
|
+
* 作为 RunStore 持久化的一部分(WorkflowRun.spec),崩溃恢复重水合后
|
|
22
22
|
* 需要 scriptSource/args 重建 worker(G3-001)。
|
|
23
23
|
*/
|
|
24
24
|
export interface RunSpec {
|
|
@@ -30,7 +30,7 @@ export interface RunSpec {
|
|
|
30
30
|
* undefined = 不校验(安全退化——漏拷 parameters 退化是「不校验」非「校验错」)。
|
|
31
31
|
* 由调用方(actionRun/runAndWait/executeNestedWorkflow)从 script.meta.parameters 拷贝。
|
|
32
32
|
* lifecycle.runWorkflow 首行经 validateRunArgs 校验 spec.args(coerceTypes 原地规范化
|
|
33
|
-
* args 对象内容,字段引用不变;worker
|
|
33
|
+
* args 对象内容,字段引用不变;worker 启动与崩溃重建共用同一对象)。
|
|
34
34
|
*/
|
|
35
35
|
readonly parameters?: Record<string, unknown>;
|
|
36
36
|
/** 调用方传入的参数(worker 内通过 $ARGS 访问)。 */
|
|
@@ -22,16 +22,16 @@ import type { DoneReason, RunStatus, WorkerLogEntry } from "./types.ts";
|
|
|
22
22
|
* RunState——一次 run 的可持久化执行状态。
|
|
23
23
|
*
|
|
24
24
|
* 持久化由 RunStore.save(WorkflowRun) 触发(WorkflowRun 持 RunState)。
|
|
25
|
-
*
|
|
25
|
+
* 跨进程重启时 RunState 从 JSONL 重水合(callCache 保留,worker 由崩溃恢复重建)。
|
|
26
26
|
*/
|
|
27
27
|
export interface RunState {
|
|
28
|
-
/** 当前状态(running/
|
|
28
|
+
/** 当前状态(running/done)。 */
|
|
29
29
|
status: RunStatus;
|
|
30
30
|
/** 终态原因(done 时必有)。 */
|
|
31
31
|
reason?: DoneReason;
|
|
32
32
|
/** Token/cost 预算(含 usedTokens/usedCost 累积)。 */
|
|
33
33
|
budget: Budget;
|
|
34
|
-
/** 按 callId 索引的 agent 调用集合(含 result,跨
|
|
34
|
+
/** 按 callId 索引的 agent 调用集合(含 result,跨 runtime 重建存活——callCache replay)。 */
|
|
35
35
|
calls: Map<number, AgentCall>;
|
|
36
36
|
/** 执行追踪事件流(唯一来源 D-10)。 */
|
|
37
37
|
trace: Trace;
|