@zhushanwen/pi-subagent-workflow 0.1.0 → 0.2.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.
Files changed (76) hide show
  1. package/agents/context-builder.md +1 -3
  2. package/agents/oracle.md +2 -2
  3. package/agents/planner.md +1 -3
  4. package/agents/researcher.md +0 -2
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +13 -3
  7. package/agents/worker.md +0 -2
  8. package/package.json +5 -3
  9. package/skills/workflow-script-format/SKILL.md +6 -6
  10. package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
  11. package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
  12. package/src/execution/__tests__/execute-options-mapper.test.ts +40 -8
  13. package/src/execution/__tests__/gui-mode-dispatch.test.ts +60 -0
  14. package/src/execution/__tests__/sdk-contract.test.ts +5 -2
  15. package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
  16. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  17. package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
  18. package/src/execution/__tests__/tool-action.test.ts +26 -4
  19. package/src/execution/agent-result-mapper.ts +4 -1
  20. package/src/execution/concurrency-pool.ts +38 -6
  21. package/src/execution/execute-options-mapper.ts +21 -4
  22. package/src/execution/execution-record.ts +5 -0
  23. package/src/execution/record-store.ts +2 -0
  24. package/src/execution/session-reconstructor.ts +11 -0
  25. package/src/execution/session-runner.ts +12 -0
  26. package/src/execution/stream-sink.ts +83 -0
  27. package/src/execution/subagent-service.ts +68 -43
  28. package/src/execution/subprocess-agent-runner.ts +16 -4
  29. package/src/execution/types.ts +23 -3
  30. package/src/index.ts +15 -2
  31. package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
  32. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
  33. package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
  34. package/src/interface/command-actions.ts +77 -0
  35. package/src/interface/commands.ts +40 -4
  36. package/src/interface/gui-mappers.ts +83 -0
  37. package/src/interface/helpers.ts +52 -9
  38. package/src/interface/list-component.ts +3 -1
  39. package/src/interface/subagent-actions.ts +35 -22
  40. package/src/interface/subagent-tool.ts +54 -23
  41. package/src/interface/subagents.ts +45 -5
  42. package/src/interface/tool-render.ts +16 -5
  43. package/src/interface/tool-workflow-script.ts +113 -15
  44. package/src/interface/tool-workflow.ts +92 -34
  45. package/src/interface/views/WorkflowsView.ts +13 -4
  46. package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
  47. package/src/interface/views/detail-content.ts +20 -0
  48. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
  49. package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
  50. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
  51. package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
  52. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
  53. package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
  54. package/src/orchestration/agent-opts-resolver.ts +11 -2
  55. package/src/orchestration/error-recovery.ts +131 -23
  56. package/src/orchestration/execute-agent-call.ts +12 -3
  57. package/src/orchestration/jsonl-run-store.ts +10 -0
  58. package/src/orchestration/lifecycle.ts +1 -1
  59. package/src/orchestration/models/agent-call.ts +7 -0
  60. package/src/orchestration/models/ports.ts +15 -2
  61. package/src/orchestration/models/run-spec.ts +6 -0
  62. package/src/orchestration/models/trace.ts +1 -0
  63. package/src/orchestration/models/types.ts +19 -0
  64. package/src/orchestration/node-ops.ts +2 -0
  65. package/src/orchestration/worker-script-builder.ts +1 -0
  66. package/workflows/README.md +58 -0
  67. package/workflows/chain.js +107 -0
  68. package/workflows/map-reduce.js +142 -0
  69. package/workflows/parallel.js +131 -0
  70. package/workflows/scatter-gather.js +146 -0
  71. package/examples/README.md +0 -43
  72. package/examples/chain.example.js +0 -92
  73. package/examples/map-reduce.example.js +0 -99
  74. package/examples/parallel.example.js +0 -82
  75. package/examples/scatter-gather.example.js +0 -106
  76. package/src/interface/gui-adapter.ts +0 -136
@@ -2,8 +2,6 @@
2
2
  name: context-builder
3
3
  description: 需求分析与元提示生成
4
4
  tools: read
5
- extensions: false
6
- category: planning
7
5
  ---
8
6
 
9
7
  You are a context builder. Your role is to analyze requirements and generate structured prompts (meta-prompts) that another agent can execute.
@@ -14,4 +12,4 @@ Do not implement the task yourself. Your job is to produce a meta-prompt that ca
14
12
 
15
13
  Use absolute file paths only.
16
14
 
17
- **Output:** Produce a structured meta-prompt: objective, requirements (numbered), constraints, success criteria, and relevant file paths. Do not write implementation code write the prompt that describes the work.
15
+ **Output:** Produce a structured meta-prompt — a task description for another agent to execute. Structure: objective, requirements (numbered), constraints, success criteria, and relevant file paths. Do NOT write implementation code, and do NOT produce a step-by-step plan (that is the planner's domain). Write what needs to be done, not how to do it step by step.
package/agents/oracle.md CHANGED
@@ -2,8 +2,6 @@
2
2
  name: oracle
3
3
  description: 高上下文决策一致性守护
4
4
  tools: read
5
- extensions: false
6
- category: planning
7
5
  ---
8
6
 
9
7
  You are a decision oracle. Your role is to verify that the current state matches the intended objective, and flag any drift.
@@ -12,6 +10,8 @@ Complete the verification fully — check every requirement in the objective aga
12
10
 
13
11
  Do not implement fixes yourself. Your job is to detect and report drift, not correct it.
14
12
 
13
+ Scope: requirements alignment only — verifying the current state matches the objective. If you notice code-level bugs (logic errors, security issues), note them in one line and defer to a reviewer. Do not analyze the bug itself.
14
+
15
15
  Use absolute file paths only.
16
16
 
17
17
  **Output:** For each requirement: state whether it is DONE (with evidence), PARTIALLY DONE (what's missing), or NOT DONE. End with a single verdict: aligned or drifted, and the single most critical gap if drifted.
package/agents/planner.md CHANGED
@@ -2,8 +2,6 @@
2
2
  name: planner
3
3
  description: 实施计划 agent
4
4
  tools: read
5
- extensions: false
6
- category: planning
7
5
  ---
8
6
 
9
7
  You are a planning agent. Your role is to break down tasks and create implementation plans.
@@ -14,4 +12,4 @@ Do not implement the plan yourself. Your job is to produce the plan, not execute
14
12
 
15
13
  Use absolute file paths only.
16
14
 
17
- **Output:** Provide a numbered, ordered implementation plan. Each step: what to do, which files it touches (absolute paths), and dependencies on prior steps. Do not write code describe steps.
15
+ **Output:** Provide a numbered, ordered implementation plan — an execution guide for a worker. Each step: what to do, which files it touches (absolute paths), and dependencies on prior steps. Do NOT write code, and do NOT produce a meta-prompt or requirements analysis (that is the context-builder's domain). Describe ordered steps, not objectives or constraints.
@@ -2,8 +2,6 @@
2
2
  name: researcher
3
3
  description: 网络调研 agent
4
4
  tools: read, web_search
5
- extensions: false
6
- category: research
7
5
  ---
8
6
 
9
7
  You are a web researcher. Your role is to search, evaluate, and synthesize findings.
@@ -2,8 +2,6 @@
2
2
  name: reviewer
3
3
  description: 代码审查 agent(diff 分析、问题发现)
4
4
  tools: read
5
- extensions: false
6
- category: coding
7
5
  ---
8
6
 
9
7
  You are a code reviewer. Your role is to find bugs, logic errors, and security issues.
@@ -12,6 +10,8 @@ Complete the review fully — cover all files you were asked to review. Don't sk
12
10
 
13
11
  Do not fix issues yourself. Your job is to report them, not implement fixes.
14
12
 
13
+ Scope: code-level issues only — bugs, logic errors, security vulnerabilities, performance problems. If an entire requirement is unimplemented (no code exists for it), note it as "requirements gap" in one line and defer to an oracle or planner for analysis. Do not analyze the gap itself.
14
+
15
15
  Use absolute file paths only.
16
16
 
17
17
  **Output:** For each issue found, report: severity (critical/major/minor), file path + line number, what the problem is, and why it matters. Do not narrate your review process.
package/agents/scout.md CHANGED
@@ -2,15 +2,25 @@
2
2
  name: scout
3
3
  description: 快速代码库侦查
4
4
  tools: read, bash, grep
5
- extensions: false
6
- category: research
7
5
  ---
8
6
 
9
7
  You are a codebase recon agent. Your role is to explore structure and return compressed context.
10
8
 
11
9
  Complete the recon fully — cover the areas you were asked to explore. Don't stop after listing the top-level directory if the task asks for deeper structure.
12
10
 
13
- You are read-only. Do not modify, create, or delete files. Your bash access is for exploration only (`ls`, `cat`, `grep`, `find`, `wc`). Do not run commands that change state. If you need a command not listed here, say so — do not run unlisted commands.
11
+ You are read-only inspect, never mutate. Bash commands are restricted below:
12
+
13
+ NEVER run (state-changing):
14
+ - File writes/deletes: rm, mv, cp, touch, mkdir, chmod, chown
15
+ - Git mutations: git add, git commit, git push, git reset, git checkout, git switch, git rebase, git merge, git stash, git clean
16
+ - Package installs: npm install, npm ci, pnpm install, yarn install, pip install
17
+ - Shell redirection to files: any command with `>` or `>>`
18
+ - Network mutations: curl, wget (downloads create/modify files)
19
+ - Process control: kill, pkill
20
+
21
+ Free to run (read-only): ls, cat, head, tail, wc, tree, file, stat, grep, find, rg, git log, git diff, git show, git status, git branch (without -D), and pipes combining these.
22
+
23
+ If unsure whether a command changes state, do NOT run it — report that you need it instead.
14
24
 
15
25
  Use absolute file paths only.
16
26
 
package/agents/worker.md CHANGED
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  name: worker
3
3
  description: 通用执行 agent(编码、修复、文件操作)
4
- extensions: true
5
- category: coding
6
4
  ---
7
5
 
8
6
  You are a coding agent. Your role is to implement, fix, and modify code precisely.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -21,7 +21,7 @@
21
21
  "index.ts",
22
22
  "agents/",
23
23
  "skills/",
24
- "examples/",
24
+ "workflows/",
25
25
  "src/index.ts",
26
26
  "src/execution/",
27
27
  "src/orchestration/",
@@ -39,7 +39,9 @@
39
39
  "./agents"
40
40
  ]
41
41
  },
42
- "dependencies": {},
42
+ "dependencies": {
43
+ "@xyz-agent/extension-protocol": "^0.2.0"
44
+ },
43
45
  "peerDependencies": {
44
46
  "@mariozechner/pi-coding-agent": "*",
45
47
  "@mariozechner/pi-ai": "*",
@@ -112,8 +112,8 @@ const final = await pipeline([
112
112
  // 对每个 file 依次跑 review → fix
113
113
  await pipeline(
114
114
  files, // items
115
- (file) => agent({ prompt: `Review ${file}`, description: `review-${file}`, schema: {...} }),
116
- (review, file) => agent({ prompt: `Fix ${file}: ${JSON.stringify(review)}`, description: `fix-${file}` }),
115
+ (file) => agent({ prompt: `Review ${file}`, description: 'review', schema: {...} }),
116
+ (review, file) => agent({ prompt: `Fix ${file}: ${JSON.stringify(review)}`, description: 'fix' }),
117
117
  );
118
118
  // stage 函数签名:(prevResult, currentItem) => result;第一个 stage 只收 currentItem
119
119
  ```
@@ -158,7 +158,7 @@ const results = await parallel(
158
158
  );
159
159
  ```
160
160
 
161
- > 完整模式模板(chain / parallel / scatter-gather / map-reduce,含 `meta` + `$ARGS` + try-catch 错误处理)见 `extensions/subagent-workflow/examples/`。本段教 API,examples 教模式。
161
+ > 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
162
162
 
163
163
  ### Other globals
164
164
 
@@ -224,7 +224,7 @@ while (round < MAX_ROUNDS) {
224
224
  },
225
225
  required: ['mustFix'],
226
226
  },
227
- description: `review-${round}`,
227
+ description: 'review',
228
228
  });
229
229
 
230
230
  // result is already a parsed object thanks to schema
@@ -286,7 +286,7 @@ const review = await agent({
286
286
  },
287
287
  required: ['findings'],
288
288
  },
289
- description: `review-${file}`,
289
+ description: 'review',
290
290
  });
291
291
 
292
292
  const verify = await agent({
@@ -301,7 +301,7 @@ Did the review cover: (1) all functions in the file, (2) at least 3 potential is
301
301
  },
302
302
  required: ['valid'],
303
303
  },
304
- description: `verify-review-${file}`,
304
+ description: 'verify-review',
305
305
  });
306
306
 
307
307
  if (!verify.valid) {
@@ -33,6 +33,23 @@ describe("mapToWorkflowAgentResult (D-A10)", () => {
33
33
  expect(result.toolCalls).toEqual([]);
34
34
  });
35
35
 
36
+ // ── sessionFile 透传(方案 A:让 workflow 继承 subagent 的 session jsonl 路径)──
37
+
38
+ it("映射 sessionFile: subagents AgentResult.sessionFile → workflow AgentResult.sessionFile 透传", () => {
39
+ const r: SubagentsAgentResult = {
40
+ ...minimalResult,
41
+ sessionFile: "/abs/path/.pi/agent/subagents/enc/sessions/2026-07-15T10-00-00-000Z_session-123.jsonl",
42
+ };
43
+ const result = mapToWorkflowAgentResult(r);
44
+ expect(result.sessionFile).toBe("/abs/path/.pi/agent/subagents/enc/sessions/2026-07-15T10-00-00-000Z_session-123.jsonl");
45
+ });
46
+
47
+ it("映射 sessionFile: 无 sessionFile → workflow AgentResult.sessionFile undefined(窗口期/未回填)", () => {
48
+ const r: SubagentsAgentResult = { ...minimalResult, sessionFile: undefined };
49
+ const result = mapToWorkflowAgentResult(r);
50
+ expect(result.sessionFile).toBeUndefined();
51
+ });
52
+
36
53
  it("映射 parsedOutput 透传(structured-output 契约 BC-8)", () => {
37
54
  const parsedData = { score: 0.95, label: "positive" };
38
55
  const r: SubagentsAgentResult = { ...minimalResult, parsedOutput: parsedData };
@@ -49,10 +66,15 @@ describe("mapToWorkflowAgentResult (D-A10)", () => {
49
66
  expect(result.content).toBe("Hello"); // content 仍保留(可能有部分输出)
50
67
  });
51
68
 
52
- it("映射失败: success=false 但无 error error=undefined", () => {
69
+ it("映射失败: success=false 但无 error(abort 路径)→ error 填入 fallback 而非 undefined", () => {
70
+ // H4: session-runner.ts abort 时 success=false, error=undefined。
71
+ // 旧代码 error=undefined → executeAgentCall 误判 completed。
72
+ // 修复后应 synthesize fallback error。
53
73
  const r: SubagentsAgentResult = { ...minimalResult, success: false };
54
74
  const result = mapToWorkflowAgentResult(r);
55
- expect(result.error).toBeUndefined();
75
+ expect(result.error).toBeDefined();
76
+ expect(typeof result.error).toBe("string");
77
+ expect(result.error.length).toBeGreaterThan(0);
56
78
  });
57
79
 
58
80
  it("映射成功: success=true 且 error 存在 → error=undefined(不误填)", () => {
@@ -214,4 +214,37 @@ describe("DefaultConcurrencyPool", () => {
214
214
  expect(negative.maxConcurrent).toBe(1);
215
215
  });
216
216
  });
217
+
218
+ // ============================================================
219
+ // H2: acquire 支持 AbortSignal — abort 时排队条目 reject
220
+ // ============================================================
221
+ describe("H2 abort support for acquire", () => {
222
+ it("abort signal rejects queued acquire with AbortError", async () => {
223
+ const pool = new DefaultConcurrencyPool(1);
224
+ await pool.acquire(0); // 占满
225
+
226
+ const controller = new AbortController();
227
+ const queued = pool.acquire(0, undefined, controller.signal);
228
+
229
+ await new Promise((r) => setTimeout(r, 5));
230
+ expect(pool.active).toBe(1); // 仍在排队
231
+
232
+ controller.abort();
233
+
234
+ await expect(queued).rejects.toThrow("aborted");
235
+ });
236
+
237
+ it("non-aborted acquire still resolves normally after release", async () => {
238
+ const pool = new DefaultConcurrencyPool(1);
239
+ await pool.acquire(0); // 占满
240
+
241
+ const controller = new AbortController();
242
+ const queued = pool.acquire(0, undefined, controller.signal);
243
+
244
+ pool.release(); // 释放 → queued 应被 resolve
245
+ await queued; // 不应 reject
246
+ expect(pool.active).toBe(1);
247
+ pool.release();
248
+ });
249
+ });
217
250
  });
@@ -45,22 +45,39 @@ describe("mapToExecuteOptions (D-A2)", () => {
45
45
  expect((result as unknown as { schemaEnv?: string }).schemaEnv).toBeUndefined();
46
46
  });
47
47
 
48
- it("T3.5 model 填底: opts.model 优先", () => {
48
+ it("T3.5 model: opts.model 优先(显式 override 透传,不与 ctxModel 混合)", () => {
49
49
  const opts: AgentCallOpts = { ...baseOpts, model: "explicit-model" };
50
50
  const ctxModel: ModelInfo = { id: "ctx-model", provider: "test", input: [] } as ModelInfo;
51
51
  const result = mapToExecuteOptions(opts, ctxModel);
52
52
  expect(result.model).toBe("explicit-model");
53
53
  });
54
54
 
55
- it("T3.5 model 填底: opts.model 空 → ctxModel (D-008)", () => {
55
+ it("T3.5 model: opts.model 空 → model undefined(不再从 ctxModel.id 填底)", () => {
56
56
  const ctxModel: ModelInfo = { id: "ctx-model", provider: "test", input: [] } as ModelInfo;
57
57
  const result = mapToExecuteOptions(baseOpts, ctxModel);
58
- expect(result.model).toBe("ctx-model");
58
+ expect(result.model).toBeUndefined();
59
+ });
60
+
61
+ it("T3.5 ctxModel 透传: opts.model 空时 ctxModel 作为完整 ModelInfo 对象传入 ExecuteOptions.ctxModel", () => {
62
+ const ctxModel: ModelInfo = { id: "mimo-v2.5-pro", provider: "router-openai", input: [] } as ModelInfo;
63
+ const result = mapToExecuteOptions(baseOpts, ctxModel);
64
+ expect(result.ctxModel).toBe(ctxModel);
65
+ expect(result.ctxModel?.id).toBe("mimo-v2.5-pro");
66
+ expect(result.ctxModel?.provider).toBe("router-openai");
67
+ });
68
+
69
+ it("T3.5 ctxModel 透传: opts.model 有值时 ctxModel 仍透传(双层可用)", () => {
70
+ const opts: AgentCallOpts = { ...baseOpts, model: "explicit-model" };
71
+ const ctxModel: ModelInfo = { id: "ctx-model", provider: "test", input: [] } as ModelInfo;
72
+ const result = mapToExecuteOptions(opts, ctxModel);
73
+ expect(result.model).toBe("explicit-model");
74
+ expect(result.ctxModel).toBe(ctxModel);
59
75
  });
60
76
 
61
- it("T3.5 model 填底: opts.model 空且 ctxModel → model undefined", () => {
77
+ it("T3.5 ctxModel: 两层均空 → model/ctxModel undefined", () => {
62
78
  const result = mapToExecuteOptions(baseOpts);
63
79
  expect(result.model).toBeUndefined();
80
+ expect(result.ctxModel).toBeUndefined();
64
81
  });
65
82
 
66
83
  it("skillPath 透传", () => {
@@ -68,11 +85,26 @@ describe("mapToExecuteOptions (D-A2)", () => {
68
85
  expect(result.skillPath).toBe("/path/to/skill.md");
69
86
  });
70
87
 
71
- it("忽略 systemPromptFiles(不映射到 ExecuteOptions)", () => {
72
- const opts: AgentCallOpts = { ...baseOpts, systemPromptFiles: ["/tmp/a.txt"] };
88
+ it("thinkingLevel 透传 (M1)", () => {
89
+ const opts: AgentCallOpts = { ...baseOpts, thinkingLevel: "high" };
73
90
  const result = mapToExecuteOptions(opts);
74
- // systemPromptFiles 不应在 ExecuteOptions 中出现
75
- expect((result as Record<string, unknown>).appendSystemPrompt).toBeUndefined();
91
+ expect(result.thinkingLevel).toBe("high");
92
+ });
93
+
94
+ it("thinkingLevel 不传 → thinkingLevel undefined", () => {
95
+ const result = mapToExecuteOptions(baseOpts);
96
+ expect(result.thinkingLevel).toBeUndefined();
97
+ });
98
+
99
+ it("systemPromptFiles → appendSystemPrompt 映射 (M2)", () => {
100
+ const opts: AgentCallOpts = { ...baseOpts, systemPromptFiles: ["/tmp/a.md"] };
101
+ const result = mapToExecuteOptions(opts);
102
+ expect(result.appendSystemPrompt).toEqual(["/tmp/a.md"]);
103
+ });
104
+
105
+ it("systemPromptFiles 不传 → appendSystemPrompt undefined", () => {
106
+ const result = mapToExecuteOptions(baseOpts);
107
+ expect(result.appendSystemPrompt).toBeUndefined();
76
108
  });
77
109
  });
78
110
 
@@ -0,0 +1,60 @@
1
+ // src/__tests__/gui-mode-dispatch.test.ts
2
+ //
3
+ // S6: ctx.mode RPC 分发契约测试。
4
+ //
5
+ // 审查发现:sdk-contract.test.ts 只测 ctx.model 透传,未覆盖 ctx.mode === "rpc"
6
+ // 时 __gui__ 被填充、ctx.mode === "tui" 时不填充的 GUI 通道分发。
7
+ //
8
+ // adapter() 是纯函数,ctx.mode === "rpc" → details.__gui__ 被附加,
9
+ // ctx.mode === "tui"/"json"/"print" → details.__gui__ 为 undefined。
10
+
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ import type { GuiContext } from "@xyz-agent/extension-protocol";
14
+
15
+ import { adapter } from "../../interface/subagent-actions.ts";
16
+ import type { AdapterInput } from "../../interface/subagent-actions.ts";
17
+
18
+ function makeStartInput(): AdapterInput {
19
+ return {
20
+ action: "start",
21
+ domain: {
22
+ subagentId: "test-id",
23
+ sessionFile: "/test/session.jsonl",
24
+ slug: "test-slug",
25
+ response: { status: "started" },
26
+ },
27
+ } as unknown as AdapterInput;
28
+ }
29
+
30
+ describe("S6: ctx.mode dispatches __gui__ output correctly", () => {
31
+ it("ctx.mode=rpc → details.__gui__ is populated", () => {
32
+ const ctx = { mode: "rpc", hasUI: true } as GuiContext;
33
+ const result = adapter(makeStartInput(), ctx);
34
+ expect(result.details).toHaveProperty("__gui__");
35
+ expect(result.details.__gui__).toBeDefined();
36
+ });
37
+
38
+ it("ctx.mode=tui → details.__gui__ is undefined (TUI renders differently)", () => {
39
+ const ctx = { mode: "tui", hasUI: true } as GuiContext;
40
+ const result = adapter(makeStartInput(), ctx);
41
+ expect(result.details.__gui__).toBeUndefined();
42
+ });
43
+
44
+ it("ctx.mode=json → details.__gui__ is undefined (headless)", () => {
45
+ const ctx = { mode: "json", hasUI: false } as GuiContext;
46
+ const result = adapter(makeStartInput(), ctx);
47
+ expect(result.details.__gui__).toBeUndefined();
48
+ });
49
+
50
+ it("ctx.mode=print → details.__gui__ is undefined (headless)", () => {
51
+ const ctx = { mode: "print", hasUI: false } as GuiContext;
52
+ const result = adapter(makeStartInput(), ctx);
53
+ expect(result.details.__gui__).toBeUndefined();
54
+ });
55
+
56
+ it("ctx=undefined → details.__gui__ is undefined (backward compat)", () => {
57
+ const result = adapter(makeStartInput(), undefined);
58
+ expect(result.details.__gui__).toBeUndefined();
59
+ });
60
+ });
@@ -174,13 +174,14 @@ describe("subagent tool contract [MANDATORY]", () => {
174
174
  mode: "background",
175
175
  subagentId: "bg-test",
176
176
  sessionFile: "/test/session.jsonl",
177
+ details: { slug: "test-slug" },
177
178
  });
178
179
  const ctxModel = { id: "test-model", name: "Test", provider: "test", reasoning: false };
179
180
  const ctx = { model: ctxModel } as object;
180
181
 
181
182
  await capturedExecute!(
182
183
  "call-1",
183
- { action: "start", startParam: { task: "test task" } },
184
+ { action: "start", startParam: { task: "test task", slug: "test-slug" } },
184
185
  undefined,
185
186
  undefined,
186
187
  ctx,
@@ -188,7 +189,7 @@ describe("subagent tool contract [MANDATORY]", () => {
188
189
 
189
190
  expect(mockServiceExecute).toHaveBeenCalledTimes(1);
190
191
  expect(mockServiceExecute).toHaveBeenCalledWith(
191
- expect.objectContaining({ ctxModel }),
192
+ expect.objectContaining({ ctxModel, slug: "test-slug" }),
192
193
  );
193
194
  });
194
195
 
@@ -212,6 +213,7 @@ describe("subagent tool contract [MANDATORY]", () => {
212
213
  mode: "background",
213
214
  subagentId: "bg-fork-wt",
214
215
  sessionFile: "/test/session.jsonl",
216
+ details: { slug: "iso-work" },
215
217
  });
216
218
 
217
219
  await capturedExecute!(
@@ -220,6 +222,7 @@ describe("subagent tool contract [MANDATORY]", () => {
220
222
  action: "start",
221
223
  startParam: {
222
224
  task: "isolated work",
225
+ slug: "iso-work",
223
226
  fork: true,
224
227
  worktree: true,
225
228
  cwd: "/x",
@@ -167,6 +167,26 @@ describe("reconstructFromFile", () => {
167
167
  expect(rec!.rootSessionId).toBeUndefined();
168
168
  });
169
169
 
170
+ it("读出 identity 里的 slug", () => {
171
+ writeJsonl([
172
+ headerLine(),
173
+ identityEntry({ id: "bg-1", agent: "w", mode: "background", task: "t", slug: "extract-urls", startedAt: 100 }),
174
+ assistantEntry([{ type: "text", text: "ok" }]),
175
+ ]);
176
+ const rec = reconstructFromFile(filePath);
177
+ expect(rec!.slug).toBe("extract-urls");
178
+ });
179
+
180
+ it("旧文件 identity 无 slug → 兜底空串(向后兼容)", () => {
181
+ writeJsonl([
182
+ headerLine(),
183
+ identityEntry({ id: "bg-1", agent: "w", mode: "background", task: "t", startedAt: 100 }),
184
+ assistantEntry([{ type: "text", text: "ok" }]),
185
+ ]);
186
+ const rec = reconstructFromFile(filePath);
187
+ expect(rec!.slug).toBe("");
188
+ });
189
+
170
190
  it("读出 identity 里的 parentRecordId/depth(递归层级)", () => {
171
191
  writeJsonl([
172
192
  headerLine(),
@@ -0,0 +1,60 @@
1
+ // src/__tests__/subagent-service-abort.test.ts
2
+ //
3
+ // S4: subagent-service abort 终态一致性测试。
4
+ //
5
+ // 审查发现:排队中被 abort(pool.acquire reject)走 finalizeFailed(status=failed),
6
+ // 而已运行中被 abort 走 cancelled。同一用户 abort 因时机不同产生两种终态。
7
+ //
8
+ // 修复后:pool reject 的 error 带 name="AbortError",runAndFinalize catch 判断
9
+ // signal?.aborted 走 cancelled 路径。
10
+ //
11
+ // 本文件覆盖两层:
12
+ // 1. pool 层:acquire reject 的 error.name 是 "AbortError"(S1 前半)
13
+ // 2. pool 层:pre-aborted signal 也带 AbortError name(S1 另一处)
14
+
15
+ import { describe, expect, it } from "vitest";
16
+
17
+ import { DefaultConcurrencyPool } from "../concurrency-pool.ts";
18
+
19
+ describe("S1: ConcurrencyPool abort reject produces AbortError name", () => {
20
+ it("queued acquire aborted via signal rejects with error.name=AbortError", async () => {
21
+ const pool = new DefaultConcurrencyPool(1);
22
+ await pool.acquire(0); // 占满
23
+
24
+ const controller = new AbortController();
25
+ const queued = pool.acquire(0, undefined, controller.signal);
26
+
27
+ await new Promise((r) => setTimeout(r, 5));
28
+ expect(pool.active).toBe(1); // 仍在排队
29
+
30
+ controller.abort();
31
+
32
+ try {
33
+ await queued;
34
+ expect.unreachable("should have rejected");
35
+ } catch (err) {
36
+ expect(err).toBeInstanceOf(Error);
37
+ expect((err as Error).name).toBe("AbortError");
38
+ expect((err as Error).message).toContain("abort");
39
+ }
40
+ pool.release();
41
+ });
42
+
43
+ it("pre-aborted signal rejects immediately with error.name=AbortError", async () => {
44
+ const pool = new DefaultConcurrencyPool(1);
45
+ await pool.acquire(0); // 占满
46
+
47
+ const controller = new AbortController();
48
+ controller.abort(); // 先 abort 再 acquire
49
+
50
+ try {
51
+ await pool.acquire(0, undefined, controller.signal);
52
+ expect.unreachable("should have rejected");
53
+ } catch (err) {
54
+ expect(err).toBeInstanceOf(Error);
55
+ expect((err as Error).name).toBe("AbortError");
56
+ expect((err as Error).message).toContain("abort");
57
+ }
58
+ pool.release();
59
+ });
60
+ });
@@ -15,9 +15,9 @@
15
15
 
16
16
  import { describe, expect, it, vi } from "vitest";
17
17
 
18
- import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
19
- import type { SubprocessAgentRunnerDeps } from "../subprocess-agent-runner.ts";
20
18
  import type { AgentCallOpts, AgentResult } from "../../orchestration/models/types.ts";
19
+ import type { SubprocessAgentRunnerDeps } from "../subprocess-agent-runner.ts";
20
+ import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
21
21
 
22
22
  // ── 测试辅助 ──
23
23
 
@@ -42,6 +42,7 @@ function createMockService(impl?: typeof vi.fn) {
42
42
  opts: Record<string, unknown>,
43
43
  signal?: AbortSignal,
44
44
  onEvent?: (e: Record<string, unknown>) => void,
45
+ stream?: unknown,
45
46
  ) => Promise<AgentResult>;
46
47
  };
47
48
  }
@@ -175,7 +176,37 @@ describe("SubprocessAgentRunner (wave-4 delegate)", () => {
175
176
  const opts = { ...makeBaseOpts(), model: undefined };
176
177
  await sar.run(opts, new AbortController().signal);
177
178
 
178
- expect(capturedOpts!.model).toBe("ctx-model");
179
+ // 修复后:opts.model 不再从 ctxModel.id 填底,ctxModel 作为完整对象透传
180
+ expect(capturedOpts!.model).toBeUndefined();
181
+ expect(capturedOpts!.ctxModel).toBe(ctxModel);
182
+ });
183
+ });
184
+
185
+ // ────────────────────────────────────────────────
186
+ // H1: SAR ctxModel 刷新(model_select 后不再 stale)
187
+ // ────────────────────────────────────────────────
188
+ describe("H1 ctxModel refresh via updateCtxModel", () => {
189
+ it("updateCtxModel 后 run() 传入新的 ctxModel 而非旧值", async () => {
190
+ let capturedOpts: Record<string, unknown> | undefined;
191
+ const mockService = createMockService(
192
+ vi.fn().mockImplementation((opts: Record<string, unknown>) => {
193
+ capturedOpts = opts;
194
+ return Promise.resolve(makeMockResult());
195
+ }),
196
+ );
197
+ const oldModel = { id: "old-model", provider: "test", input: [] };
198
+ const newModel = { id: "new-model", provider: "test", input: [] };
199
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel: oldModel };
200
+ const sar = new SubprocessAgentRunner(deps);
201
+
202
+ // 模拟 model_select:刷新 ctxModel
203
+ sar.updateCtxModel(newModel);
204
+
205
+ const opts = { ...makeBaseOpts(), model: undefined };
206
+ await sar.run(opts, new AbortController().signal);
207
+
208
+ expect(capturedOpts!.ctxModel).toBe(newModel);
209
+ expect(capturedOpts!.ctxModel).not.toBe(oldModel);
179
210
  });
180
211
  });
181
212
 
@@ -386,4 +417,42 @@ describe("SubprocessAgentRunner (wave-4 delegate)", () => {
386
417
  expect(result.error).toBe("raw string error");
387
418
  });
388
419
  });
420
+
421
+ // ────────────────────────────────────────────────
422
+ // U1: stream 透传给 executeAndAwait
423
+ // ────────────────────────────────────────────────
424
+ describe("U1 stream 透传", () => {
425
+ it("SAR.run 传 stream → executeAndAwait 第 4 参收到同一 stream 对象", async () => {
426
+ let capturedStream: unknown;
427
+ const mockService = createMockService(
428
+ vi.fn().mockImplementation((_opts, _sig, _onEvt, stream) => {
429
+ capturedStream = stream;
430
+ return Promise.resolve(makeMockResult());
431
+ }),
432
+ );
433
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
434
+ const sar = new SubprocessAgentRunner(deps);
435
+
436
+ const fakeStream = { onDelta: vi.fn(), dispose: vi.fn() };
437
+ await sar.run(makeBaseOpts(), new AbortController().signal, undefined, fakeStream as never);
438
+
439
+ expect(capturedStream).toBe(fakeStream);
440
+ });
441
+
442
+ it("SAR.run 不传 stream → executeAndAwait 第 4 参为 undefined", async () => {
443
+ let capturedStream: unknown = "sentinel";
444
+ const mockService = createMockService(
445
+ vi.fn().mockImplementation((_opts, _sig, _onEvt, stream) => {
446
+ capturedStream = stream;
447
+ return Promise.resolve(makeMockResult());
448
+ }),
449
+ );
450
+ const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
451
+ const sar = new SubprocessAgentRunner(deps);
452
+
453
+ await sar.run(makeBaseOpts(), new AbortController().signal);
454
+
455
+ expect(capturedStream).toBeUndefined();
456
+ });
457
+ });
389
458
  });