@zhushanwen/pi-subagent-workflow 7.3.4 → 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 +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -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__/agent-registry.test.ts +13 -11
- 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/host-mode.ts +1 -1
- 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 +290 -75
- 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 +204 -22
- 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 +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- 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 +44 -17
- 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 +82 -17
- 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/__tests__/detail-content-session-file.test.ts +1 -1
- 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 +157 -0
- 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
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -49,7 +49,7 @@ describe("BgNotifier.flushPendingNotifications — deliverAs 契约", () => {
|
|
|
49
49
|
it("flush 时调 sendMessage 的 options.deliverAs === 'steer'(FR-3/AC-3)", () => {
|
|
50
50
|
notifier.notify({
|
|
51
51
|
id: "bg-test-1",
|
|
52
|
-
status: "
|
|
52
|
+
status: "closed",
|
|
53
53
|
agent: "explorer",
|
|
54
54
|
result: "done",
|
|
55
55
|
startedAt: Date.now() - 1000,
|
|
@@ -65,7 +65,7 @@ describe("BgNotifier.flushPendingNotifications — deliverAs 契约", () => {
|
|
|
65
65
|
it("flush 时 triggerTurn 也必须为 true(让父 agent 立即唤醒)", () => {
|
|
66
66
|
notifier.notify({
|
|
67
67
|
id: "bg-test-2",
|
|
68
|
-
status: "
|
|
68
|
+
status: "closed",
|
|
69
69
|
agent: "worker",
|
|
70
70
|
error: "boom",
|
|
71
71
|
startedAt: Date.now(),
|
|
@@ -100,7 +100,7 @@ describe("BgNotifier — isIdle gate 竞态修复", () => {
|
|
|
100
100
|
host.isIdle.mockReturnValue(false);
|
|
101
101
|
notifier.notify({
|
|
102
102
|
id: "bg-race-1",
|
|
103
|
-
status: "
|
|
103
|
+
status: "closed",
|
|
104
104
|
agent: "worker",
|
|
105
105
|
result: "ok",
|
|
106
106
|
startedAt: Date.now(),
|
|
@@ -131,7 +131,7 @@ describe("BgNotifier — isIdle gate 竞态修复", () => {
|
|
|
131
131
|
host.isIdle.mockReturnValue(false);
|
|
132
132
|
notifier.notify({
|
|
133
133
|
id: "bg-starve-1",
|
|
134
|
-
status: "
|
|
134
|
+
status: "closed",
|
|
135
135
|
agent: "worker",
|
|
136
136
|
result: "ok",
|
|
137
137
|
startedAt: Date.now(),
|
|
@@ -155,7 +155,7 @@ describe("BgNotifier — isIdle gate 竞态修复", () => {
|
|
|
155
155
|
const legacyNotifier = new BgNotifier(legacyHost);
|
|
156
156
|
legacyNotifier.notify({
|
|
157
157
|
id: "bg-legacy-1",
|
|
158
|
-
status: "
|
|
158
|
+
status: "closed",
|
|
159
159
|
agent: "worker",
|
|
160
160
|
result: "ok",
|
|
161
161
|
startedAt: Date.now(),
|
|
@@ -171,7 +171,7 @@ describe("BgNotifier — isIdle gate 竞态修复", () => {
|
|
|
171
171
|
host.isIdle.mockReturnValue(false);
|
|
172
172
|
notifier.notify({
|
|
173
173
|
id: "bg-dispose-1",
|
|
174
|
-
status: "
|
|
174
|
+
status: "closed",
|
|
175
175
|
agent: "worker",
|
|
176
176
|
result: "ok",
|
|
177
177
|
startedAt: Date.now(),
|
|
@@ -183,4 +183,206 @@ describe("BgNotifier — isIdle gate 竞态修复", () => {
|
|
|
183
183
|
vi.advanceTimersByTime(10_000);
|
|
184
184
|
expect(host.sendMessageCalls).toHaveLength(0);
|
|
185
185
|
});
|
|
186
|
-
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("BgNotifier dedup 按轮次(G1 决策 9:对话模式豁免 60s dedup)", () => {
|
|
189
|
+
let host: ReturnType<typeof makeMockHost>;
|
|
190
|
+
let notifier: BgNotifier;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
host = makeMockHost();
|
|
194
|
+
// hasRunningBackground=false → notify 立即 flush(不排队);dedup 仍在 push 前生效
|
|
195
|
+
notifier = new BgNotifier(host);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
notifier.dispose();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("对话模式:同 id 不同 round 的两次 notify 不互相吞(round 参与 dedup key)", () => {
|
|
203
|
+
notifier.notify({
|
|
204
|
+
id: "sa-chat", status: "idle", agent: "w", round: 1, result: "round1",
|
|
205
|
+
startedAt: 1, endedAt: 2,
|
|
206
|
+
});
|
|
207
|
+
notifier.notify({
|
|
208
|
+
id: "sa-chat", status: "idle", agent: "w", round: 2, result: "round2",
|
|
209
|
+
startedAt: 3, endedAt: 4,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// MF-1 修复:dedup key=id:round,不同 round 不互相吞
|
|
213
|
+
expect(host.sendMessageCalls).toHaveLength(2);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("同 id 同 round 60s 内第二次被 dedup 吞(防重复通知)", () => {
|
|
217
|
+
notifier.notify({
|
|
218
|
+
id: "sa-dup", status: "idle", agent: "w", round: 1, result: "r",
|
|
219
|
+
startedAt: 1, endedAt: 2,
|
|
220
|
+
});
|
|
221
|
+
notifier.notify({
|
|
222
|
+
id: "sa-dup", status: "idle", agent: "w", round: 1, result: "r",
|
|
223
|
+
startedAt: 3, endedAt: 4,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// 第二条被吞(dedup key=sa-dup:1 命中)
|
|
227
|
+
expect(host.sendMessageCalls).toHaveLength(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("非 chatMode(round undefined → key=id:0)行为同旧(向后兼容,60s 内同 id 吞)", () => {
|
|
231
|
+
// round undefined → dedup key="sa-once:0",与旧 record.id 单 key 行为一致
|
|
232
|
+
notifier.notify({
|
|
233
|
+
id: "sa-once", status: "closed", agent: "w", result: "done",
|
|
234
|
+
startedAt: 1, endedAt: 2,
|
|
235
|
+
});
|
|
236
|
+
notifier.notify({
|
|
237
|
+
id: "sa-once", status: "closed", agent: "w", result: "done",
|
|
238
|
+
startedAt: 3, endedAt: 4,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// 第二条被吞(旧 dedup 行为不变,一次性模式回归)
|
|
242
|
+
expect(host.sendMessageCalls).toHaveLength(1);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("BgNotifier buildLlmContent 指针行(wave2:chatMode sessionFile 透传)", () => {
|
|
247
|
+
let host: ReturnType<typeof makeMockHost>;
|
|
248
|
+
let notifier: BgNotifier;
|
|
249
|
+
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
host = makeMockHost();
|
|
252
|
+
// hasRunningBackground=false → notify 立即 flush,sendMessageCalls 恰 1 条
|
|
253
|
+
notifier = new BgNotifier(host);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(() => {
|
|
257
|
+
notifier.dispose();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/** 取唯一一条已发消息的 content(前置断言恰 1 条)。 */
|
|
261
|
+
function sentContent(): string {
|
|
262
|
+
expect(host.sendMessageCalls).toHaveLength(1);
|
|
263
|
+
return (host.sendMessageCalls[0]!.message as { content: string }).content;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
it("running(轮次通知)+ sessionFile → 末尾追加 \\n\\n 空行分隔的指针行", () => {
|
|
267
|
+
notifier.notify({
|
|
268
|
+
id: "sa-ptr-1", status: "running", agent: "w", round: 1, result: "round1 text",
|
|
269
|
+
sessionFile: "/tmp/sessions/child-1.jsonl",
|
|
270
|
+
startedAt: 1, endedAt: 2,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(sentContent()).toBe(
|
|
274
|
+
'Subagent "w" (sa-ptr-1) finished a round. Reply:\nround1 text' +
|
|
275
|
+
"\n\nFull transcript: /tmp/sessions/child-1.jsonl",
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("closed 成功文案 + sessionFile → 指针行追加在最终串末尾", () => {
|
|
280
|
+
notifier.notify({
|
|
281
|
+
id: "sa-ptr-2", status: "closed", agent: "w", result: "final result",
|
|
282
|
+
sessionFile: "/tmp/sessions/child-2.jsonl",
|
|
283
|
+
startedAt: 1, endedAt: 2,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(sentContent()).toBe(
|
|
287
|
+
'Subagent "w" (sa-ptr-2) completed. Result:\nfinal result' +
|
|
288
|
+
"\n\nFull transcript: /tmp/sessions/child-2.jsonl",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("closed + patchFile + sessionFile → 指针行追加在 patch 提示串末尾(最终串)", () => {
|
|
293
|
+
notifier.notify({
|
|
294
|
+
id: "sa-ptr-3", status: "closed", agent: "w", result: "did work",
|
|
295
|
+
patchFile: "/tmp/patches/sa-ptr-3.patch",
|
|
296
|
+
sessionFile: "/tmp/sessions/child-3.jsonl",
|
|
297
|
+
startedAt: 1, endedAt: 2,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const content = sentContent();
|
|
301
|
+
expect(content).toContain("git apply /tmp/patches/sa-ptr-3.patch");
|
|
302
|
+
expect(content.endsWith("\n\nFull transcript: /tmp/sessions/child-3.jsonl")).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("sessionFile 缺失 → 省略整行(running 通知正文与无指针形态逐字节一致)", () => {
|
|
306
|
+
notifier.notify({
|
|
307
|
+
id: "sa-ptr-4", status: "running", agent: "w", round: 2, result: "round2 text",
|
|
308
|
+
startedAt: 1, endedAt: 2,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(sentContent()).toBe('Subagent "w" (sa-ptr-4) finished a round. Reply:\nround2 text');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("cancelled → 不追加指针行(即使 sessionFile 有值)", () => {
|
|
315
|
+
notifier.notify({
|
|
316
|
+
id: "sa-ptr-5", status: "closed", closedReason: "cancelled", agent: "w",
|
|
317
|
+
sessionFile: "/tmp/sessions/child-5.jsonl",
|
|
318
|
+
startedAt: 1, endedAt: 2,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(sentContent()).toBe('Subagent "w" (sa-ptr-5) cancelled.');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("gc-failed(closed + closedReason=gc + error 有值)→ 不追加指针行", () => {
|
|
325
|
+
notifier.notify({
|
|
326
|
+
id: "sa-ptr-6", status: "closed", closedReason: "gc", agent: "w",
|
|
327
|
+
error: "spawn EPIPE",
|
|
328
|
+
sessionFile: "/tmp/sessions/child-6.jsonl",
|
|
329
|
+
startedAt: 1, endedAt: 2,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(sentContent()).toBe('Subagent "w" (sa-ptr-6) failed: spawn EPIPE');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("[review 修复] gc-failed + patchFile 并存 → failed 文案优先(失败轮也会写 patchFile,patch 提示不可达)", () => {
|
|
336
|
+
// 回归锚定:doFinalizeRecord Step 0 对 worktreeHandle 无条件 collectPatch,gc 失败 +
|
|
337
|
+
// worktree 并存时 patchFile 有值。判定顺序必须与 deriveClosedDisplay / renderRecordLines
|
|
338
|
+
// 同构(cancelled → gc+error → patch/result),否则 LLM 被告知 completed 掩盖失败。
|
|
339
|
+
notifier.notify({
|
|
340
|
+
id: "sa-ptr-8", status: "closed", closedReason: "gc", agent: "w",
|
|
341
|
+
error: "spawn EPIPE",
|
|
342
|
+
patchFile: "/tmp/patches/sa-ptr-8.patch",
|
|
343
|
+
sessionFile: "/tmp/sessions/child-8.jsonl",
|
|
344
|
+
startedAt: 1, endedAt: 2,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(sentContent()).toBe('Subagent "w" (sa-ptr-8) failed: spawn EPIPE');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("one-shot(sessionFile 未透传 → undefined)→ closed 通知与改造前逐字节一致(基线常量锚定)", () => {
|
|
351
|
+
notifier.notify({
|
|
352
|
+
id: "sa-ptr-7", status: "closed", agent: "worker", result: "done",
|
|
353
|
+
startedAt: 1, endedAt: 2,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// 改造前形态:sessionFile undefined 时指针为空串,追加不改变输出——逐字节锁定
|
|
357
|
+
expect(sentContent()).toBe('Subagent "worker" (sa-ptr-7) completed. Result:\ndone');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("[C-2] closed + totalRounds(chatMode close 终态通知)→ 文案附轮次统计 after N rounds", () => {
|
|
361
|
+
notifier.notify({
|
|
362
|
+
id: "sa-rounds-1", status: "closed", agent: "w", totalRounds: 3, result: "",
|
|
363
|
+
sessionFile: "/tmp/sessions/child-rounds.jsonl",
|
|
364
|
+
startedAt: 1, endedAt: 2,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// 设计 D2 路径①:closed 分支文案附轮次统计 + sessionFile 提示;
|
|
368
|
+
// 路径②正文空串(idle close)形态也走本分支(result 空串非 nullish,不触发 (empty))
|
|
369
|
+
expect(sentContent()).toBe(
|
|
370
|
+
'Subagent "w" (sa-rounds-1) completed after 3 rounds. Result:\n' +
|
|
371
|
+
"\n\nFull transcript: /tmp/sessions/child-rounds.jsonl",
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("[C-2] 对照:totalRounds 缺失(one-shot 完成通知)→ 无轮次统计,文案逐字节保持 completed. Result:", () => {
|
|
376
|
+
notifier.notify({
|
|
377
|
+
id: "sa-rounds-2", status: "closed", agent: "w", result: "done",
|
|
378
|
+
sessionFile: "/tmp/sessions/child-oneshot.jsonl",
|
|
379
|
+
startedAt: 1, endedAt: 2,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// one-shot record 无轮次语义(totalRounds 不设置)——G4:文案不含统计
|
|
383
|
+
expect(sentContent()).toBe(
|
|
384
|
+
'Subagent "w" (sa-rounds-2) completed. Result:\ndone' +
|
|
385
|
+
"\n\nFull transcript: /tmp/sessions/child-oneshot.jsonl",
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/execution/__tests__/one-shot-upgrade.test.ts
|
|
2
|
+
//
|
|
3
|
+
// SP-5 one-shot upgrade:非 chatMode active record 收到 message 时自动升级 chatMode。
|
|
4
|
+
//
|
|
5
|
+
// 场景:one-shot subagent(conversation=false)完成首轮后 record 仍在内存(running/idle),
|
|
6
|
+
// LLM 调 message 续聊 → messageHandler 检测非 chatMode + active → 置 chatMode=true(upgrade)→
|
|
7
|
+
// 走 deliverMessage 统一路径(热路径或冷路径 resume)。
|
|
8
|
+
//
|
|
9
|
+
// mock 策略:真实 SubagentService + mock runSpawn(受控),getChildByRecord/spawnedChildren
|
|
10
|
+
// 提供真实 Map 语义(deliverMessage 热路径需要 child 存在)。
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
|
|
18
|
+
// mock logger
|
|
19
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
20
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
|
|
23
|
+
|
|
24
|
+
// mock session-runner:runSpawn 受控 + killAllSpawnedChildren 空实现;
|
|
25
|
+
// getChildByRecord/spawnedChildren 提供真实 Map 语义。
|
|
26
|
+
vi.mock("../session-runner.ts", () => {
|
|
27
|
+
const spawnedChildren = new Map<string, unknown>();
|
|
28
|
+
return {
|
|
29
|
+
runSpawn: vi.fn(),
|
|
30
|
+
killAllSpawnedChildren: vi.fn(),
|
|
31
|
+
spawnedChildren,
|
|
32
|
+
getChildByRecord: (id: string): unknown => spawnedChildren.get(id),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
import { runSpawn, spawnedChildren } from "../session-runner.ts";
|
|
37
|
+
import type { SessionRunnerContext } from "../session-runner.ts";
|
|
38
|
+
import { createRecord } from "../execution-record.ts";
|
|
39
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
40
|
+
import type { ModelInfo } from "../model-resolver.ts";
|
|
41
|
+
import { RecordStore } from "../record-store.ts";
|
|
42
|
+
import { SubagentService } from "../subagent-service.ts";
|
|
43
|
+
import type { PiLike } from "../subagent-service.ts";
|
|
44
|
+
import type { AgentResult, ExecutionRecord } from "../types.ts";
|
|
45
|
+
import { messageHandler } from "../../interface/subagent-actions.ts";
|
|
46
|
+
|
|
47
|
+
const mockRunSpawn = vi.mocked(runSpawn);
|
|
48
|
+
|
|
49
|
+
const STUB_MODEL: ModelInfo = { id: "test-model", name: "Test", provider: "test", reasoning: false };
|
|
50
|
+
|
|
51
|
+
function makeTmpAgentDir(): string {
|
|
52
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "upgrade-test-"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makePi(): PiLike & {
|
|
56
|
+
appendEntry: ReturnType<typeof vi.fn>;
|
|
57
|
+
events: { emit: ReturnType<typeof vi.fn> };
|
|
58
|
+
sendMessage: ReturnType<typeof vi.fn>;
|
|
59
|
+
} {
|
|
60
|
+
return { appendEntry: vi.fn(), events: { emit: vi.fn() }, sendMessage: vi.fn() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeResult(success: boolean): AgentResult {
|
|
64
|
+
return {
|
|
65
|
+
text: success ? "done" : "err",
|
|
66
|
+
turns: 1,
|
|
67
|
+
durationMs: 100,
|
|
68
|
+
success,
|
|
69
|
+
error: success ? undefined : "boom",
|
|
70
|
+
sessionId: "sess-1",
|
|
71
|
+
toolCalls: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ServiceInternals {
|
|
76
|
+
store: RecordStore;
|
|
77
|
+
sessionRootId: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 非 chatMode background record(one-shot 模式)。 */
|
|
81
|
+
function makeOneShotRecord(
|
|
82
|
+
sessionRootId: string,
|
|
83
|
+
status: "running" = "running",
|
|
84
|
+
id = "sa-oneshot",
|
|
85
|
+
): ExecutionRecord {
|
|
86
|
+
const record = createRecord(id, {
|
|
87
|
+
agent: "general-purpose",
|
|
88
|
+
model: "test/test-model",
|
|
89
|
+
thinkingLevel: "low",
|
|
90
|
+
mode: "background",
|
|
91
|
+
task: "one-shot task",
|
|
92
|
+
slug: "oneshot",
|
|
93
|
+
startedAt: 1000,
|
|
94
|
+
rootSessionId: sessionRootId,
|
|
95
|
+
// chatMode 不传 = undefined = 非 chatMode(one-shot)
|
|
96
|
+
});
|
|
97
|
+
record.status = status;
|
|
98
|
+
record.controller = new AbortController();
|
|
99
|
+
// v4 B-1:one-shot 完成后 record 为 running(idle 折入 running)+ isResumable(无活进程)。
|
|
100
|
+
// sessionFile 总设(resumeRound 冷路径校验需要,热路径无害)。
|
|
101
|
+
record.sessionFile = "/tmp/fake-session.jsonl";
|
|
102
|
+
record.round = 1;
|
|
103
|
+
return record;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// SP-5 one-shot upgrade
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
describe("SP-5 one-shot upgrade(message → chatMode + 冷 resume)", () => {
|
|
111
|
+
let agentDir: string;
|
|
112
|
+
let service: SubagentService;
|
|
113
|
+
let store: RecordStore;
|
|
114
|
+
let sessionRootId: string;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
agentDir = makeTmpAgentDir();
|
|
118
|
+
const modelService = new ModelConfigService({ agentDir });
|
|
119
|
+
service = new SubagentService({ cwd: agentDir, modelService });
|
|
120
|
+
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
121
|
+
const internals = service as unknown as ServiceInternals;
|
|
122
|
+
store = internals.store;
|
|
123
|
+
sessionRootId = internals.sessionRootId!;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
service.dispose();
|
|
128
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// TC-1: one-shot done 后 message 触发 upgrade(chatMode=true)
|
|
132
|
+
// 场景:one-shot running record → message → chatMode 被置 true → deliverMessage 热路径(child 存在)
|
|
133
|
+
it("TC-1: one-shot running record 收到 message → chatMode 升级为 true", async () => {
|
|
134
|
+
const record = makeOneShotRecord(sessionRootId, "running");
|
|
135
|
+
store.register(record);
|
|
136
|
+
|
|
137
|
+
// 注册 mock child 让 deliverMessage 走热路径
|
|
138
|
+
const mockChild = { killed: false, pid: 12345, stdin: { write: vi.fn(() => true) } };
|
|
139
|
+
spawnedChildren.set(record.id, mockChild as unknown);
|
|
140
|
+
|
|
141
|
+
// 验证升级前 chatMode 为 falsy
|
|
142
|
+
expect(record.chatMode).toBeFalsy();
|
|
143
|
+
|
|
144
|
+
await messageHandler(service, { subagentId: record.id, text: "follow-up" });
|
|
145
|
+
|
|
146
|
+
// 验证 chatMode 被升级
|
|
147
|
+
expect(record.chatMode).toBe(true);
|
|
148
|
+
// 验证 record 仍为 running(deliverMessage 热路径设 running)
|
|
149
|
+
expect(record.status).toBe("running");
|
|
150
|
+
// 验证 record 在内存中(非终态,未被 archive)
|
|
151
|
+
expect(store.getMutable(record.id)).toBeDefined();
|
|
152
|
+
|
|
153
|
+
// 清理
|
|
154
|
+
spawnedChildren.delete(record.id);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// TC-2: upgrade 后走冷路径 resume
|
|
158
|
+
// 场景:one-shot idle record(进程已死)→ message → chatMode=true → deliverMessage 冷路径 → resumeRound
|
|
159
|
+
it("TC-2: one-shot idle record 收到 message → chatMode 升级 + 冷路径 resume", async () => {
|
|
160
|
+
// one-shot 完成态 record(running + isResumable)需要 sessionFile(resumeRound 冷路径校验)
|
|
161
|
+
fs.writeFileSync("/tmp/fake-session.jsonl", "");
|
|
162
|
+
const record = makeOneShotRecord(sessionRootId);
|
|
163
|
+
store.register(record);
|
|
164
|
+
|
|
165
|
+
// mock runSpawn 返回成功(resume 后的 spawn)
|
|
166
|
+
mockRunSpawn.mockImplementation(async (_ctx: SessionRunnerContext) => {
|
|
167
|
+
return makeResult(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(record.chatMode).toBeFalsy();
|
|
171
|
+
|
|
172
|
+
await messageHandler(service, { subagentId: record.id, text: "resume message" });
|
|
173
|
+
|
|
174
|
+
// 验证 chatMode 被升级
|
|
175
|
+
expect(record.chatMode).toBe(true);
|
|
176
|
+
// 验证走了 resumeRound → runSpawn(冷路径)
|
|
177
|
+
expect(mockRunSpawn).toHaveBeenCalled();
|
|
178
|
+
// record 被 resumeRound 设为 running,runAndFinalize 完成后保持 running(v4 B-1 idle 折入 running)
|
|
179
|
+
// 关键是 record 不在终态
|
|
180
|
+
expect(record.status).not.toBe("closed");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// TC-3: upgrade 后 record 可续聊(非终态)
|
|
184
|
+
// 场景:验证升级后的 record 不会被 finalize 为终态,仍可接受后续 message
|
|
185
|
+
it("TC-3: upgrade 后 record 可续聊(非终态,仍在内存)", async () => {
|
|
186
|
+
const record = makeOneShotRecord(sessionRootId);
|
|
187
|
+
store.register(record);
|
|
188
|
+
|
|
189
|
+
// runSpawn 返回成功
|
|
190
|
+
mockRunSpawn.mockImplementation(async () => makeResult(true));
|
|
191
|
+
|
|
192
|
+
await messageHandler(service, { subagentId: record.id, text: "first resume" });
|
|
193
|
+
|
|
194
|
+
// chatMode 已升级
|
|
195
|
+
expect(record.chatMode).toBe(true);
|
|
196
|
+
|
|
197
|
+
// 关键验证:record 仍在内存(getMutable 返回非 undefined)——未被 archive 到终态
|
|
198
|
+
const mutable = store.getMutable(record.id);
|
|
199
|
+
expect(mutable).toBeDefined();
|
|
200
|
+
expect(mutable!.chatMode).toBe(true);
|
|
201
|
+
|
|
202
|
+
// 验证 status 不是终态(closed 是终态;running 是非终态,v4 B-1 idle 折入 running)
|
|
203
|
+
expect(record.status).toBe("running");
|
|
204
|
+
});
|
|
205
|
+
});
|