@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.
- package/agents/context-builder.md +1 -3
- package/agents/oracle.md +2 -2
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +13 -3
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +40 -8
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +60 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +26 -4
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/record-store.ts +2 -0
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +12 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +68 -43
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/index.ts +15 -2
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +35 -22
- package/src/interface/subagent-tool.ts +54 -23
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- 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
|
|
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
|
|
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.
|
package/agents/researcher.md
CHANGED
package/agents/reviewer.md
CHANGED
|
@@ -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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "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
|
-
"
|
|
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:
|
|
116
|
-
(review, file) => agent({ prompt: `Fix ${file}: ${JSON.stringify(review)}`, description:
|
|
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
|
-
>
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
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).
|
|
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
|
|
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("
|
|
72
|
-
const opts: AgentCallOpts = { ...baseOpts,
|
|
88
|
+
it("thinkingLevel 透传 (M1)", () => {
|
|
89
|
+
const opts: AgentCallOpts = { ...baseOpts, thinkingLevel: "high" };
|
|
73
90
|
const result = mapToExecuteOptions(opts);
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
});
|