@zhushanwen/pi-subagent-workflow 0.1.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 +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-script-format
|
|
3
|
+
description: >-
|
|
4
|
+
Reference for writing workflow JS scripts. Auto-loaded when using workflow-generate
|
|
5
|
+
or writing/editing workflow scripts for Pi. Covers runtime environment, injected
|
|
6
|
+
globals, constraints, and script patterns. Not for general coding or subagent usage.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workflow Script Format Reference
|
|
10
|
+
|
|
11
|
+
## Runtime Environment
|
|
12
|
+
|
|
13
|
+
- Script runs inside an **async IIFE in a Worker thread**. Top-level await IS supported.
|
|
14
|
+
- **DO NOT use `import`/`export` (ESM) syntax**. Use `require()` for Node.js built-ins.
|
|
15
|
+
- The script's **`return` value IS captured** and sent back to the main thread.
|
|
16
|
+
|
|
17
|
+
### [MANDATORY] Do NOT wrap your script in another async IIFE
|
|
18
|
+
|
|
19
|
+
The worker already wraps your script in an async IIFE. If you add your own `(async function main() { ... })();` wrapper **without `await`**, the worker's outer IIFE resolves immediately (fire-and-forget), posts `return` to the main thread, and the main thread tears down the runtime — **killing any in-flight `agent()` subprocess via SIGKILL within ~2ms**.
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// ❌ WRONG: bare IIFE — outer worker IIFE doesn't await this, posts return immediately
|
|
23
|
+
(async function main() {
|
|
24
|
+
const result = await agent({ prompt: 'analyze' }); // subprocess killed ~2ms after spawn
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
// ✅ CORRECT: top-level await directly (the worker wraps this in its own async IIFE)
|
|
28
|
+
const result = await agent({ prompt: 'analyze' });
|
|
29
|
+
|
|
30
|
+
// ✅ ALSO OK: awaited IIFE (rarely needed — prefer top-level await)
|
|
31
|
+
await (async function main() {
|
|
32
|
+
const result = await agent({ prompt: 'analyze' });
|
|
33
|
+
})();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This is enforced by `lintScript`:
|
|
37
|
+
- **error** (workflow refuses to run): bare IIFE as a standalone statement + contains agent/parallel/pipeline.
|
|
38
|
+
- **warning** (workflow runs, but flagged): IIFE assigned to a variable or returned from a function + contains agent/parallel/pipeline. Review whether the surrounding code actually awaits the Promise; if not, the same kill-on-spawn bug applies.
|
|
39
|
+
|
|
40
|
+
## Required: Meta Declaration
|
|
41
|
+
|
|
42
|
+
Every script MUST declare `meta` at the top level:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const meta = { name: 'workflow-name', description: '...', phases: ['phase1', 'phase2'] };
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`name` must match the filename stem. `phases` is for display only.
|
|
49
|
+
|
|
50
|
+
## Injected Globals (pre-defined, do NOT redeclare)
|
|
51
|
+
|
|
52
|
+
### `agent(...)` — Call an AI agent
|
|
53
|
+
|
|
54
|
+
支持三种签名:
|
|
55
|
+
- `agent(promptString)` — 最简,prompt 字符串,返回 content 字符串
|
|
56
|
+
- `agent(promptString, { label?, schema?, ... })` — 字符串 + opts(`label` 是 `description` 的别名)
|
|
57
|
+
- `agent({ prompt, schema?, description?, agent?, skill?, timeoutMs?, model?, scene? })` — 完整 opts 对象
|
|
58
|
+
|
|
59
|
+
Returns `parsedOutput` (structured data when schema provided) or `content` (string).
|
|
60
|
+
|
|
61
|
+
**[MANDATORY] Structured output rule:** When you need JSON/structured data from an agent, you MUST pass `schema`. The `schema` parameter triggers a tool-call mechanism where the LLM calls a `structured-output` tool to return validated JSON — this is reliable. NEVER ask the agent to "output JSON in a code block" or use regex to extract JSON from text.
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// ✅ CORRECT: use schema parameter — returns parsed JS object directly
|
|
65
|
+
const result = await agent({
|
|
66
|
+
prompt: 'Analyze this code and rate it',
|
|
67
|
+
schema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
score: { type: 'number' },
|
|
71
|
+
issues: { type: 'array', items: { type: 'string' } },
|
|
72
|
+
},
|
|
73
|
+
required: ['score'],
|
|
74
|
+
},
|
|
75
|
+
description: 'code-analysis',
|
|
76
|
+
});
|
|
77
|
+
// result is already a parsed object: { score: 8, issues: [...] }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// ❌ WRONG: prompt-based JSON extraction — fragile, LLM often wraps in markdown
|
|
82
|
+
const result = await agent({ prompt: 'Analyze code. Output JSON: { "score": N }' });
|
|
83
|
+
// result is a string, you'd need regex to extract — DON'T do this
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `parallel(calls)` — Run multiple agent calls concurrently
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const [r1, r2, r3] = await parallel([
|
|
90
|
+
agent({ prompt: 'Review file A', description: 'review-a' }),
|
|
91
|
+
agent({ prompt: 'Review file B', description: 'review-b' }),
|
|
92
|
+
agent({ prompt: 'Review file C', description: 'review-c' }),
|
|
93
|
+
]);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
并发默认上限 6(ConcurrencyPool 限流,`maxConcurrent=6` 来源 ADR-030 决策 3),超出自动排队。元素也可以是返回 Promise 的函数,会被直接调用:
|
|
97
|
+
|
|
98
|
+
### `pipeline(...)` — Execute stages sequentially
|
|
99
|
+
|
|
100
|
+
**模式一:顺序模式** — 传入 stage 数组,每个 stage 收到上一个 stage 的结果:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const final = await pipeline([
|
|
104
|
+
() => agent({ prompt: 'Analyze code', description: 'analyze' }),
|
|
105
|
+
(prev) => agent({ prompt: `Write tests for: ${prev}`, description: 'test-gen' }),
|
|
106
|
+
]);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**模式二:笛卡尔积模式** — 传入 items 数组 + 多个 stage,对每个 item 依次跑完所有 stage(批处理杀手锏):
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
// 对每个 file 依次跑 review → fix
|
|
113
|
+
await pipeline(
|
|
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}` }),
|
|
117
|
+
);
|
|
118
|
+
// stage 函数签名:(prevResult, currentItem) => result;第一个 stage 只收 currentItem
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `workflow(name, args?)` — Call another workflow (nested orchestration)
|
|
122
|
+
|
|
123
|
+
调用已定义的子 workflow(by name),实现 workflow 嵌套编排(顺序 chain / 并行 parallel / scatter-gather / map-reduce)。被调用的 workflow 必须已通过 `workflow-script save` 或放在 `.pi/workflows/` / `~/.pi/agent/workflows/` 可被发现。
|
|
124
|
+
|
|
125
|
+
**签名**:`workflow(name: string, args?: object) => Promise<AgentResult>`
|
|
126
|
+
|
|
127
|
+
**参数**:
|
|
128
|
+
- `name` — 目标 workflow 的名称(`meta.name`,即文件名 stem)
|
|
129
|
+
- `args` — 传给子 workflow 的参数对象,子 workflow 内通过 `$ARGS` 读取
|
|
130
|
+
|
|
131
|
+
**返回值**:`AgentResult`,与 `agent()` 返回结构同构:
|
|
132
|
+
- `content: string` — 子 workflow 的 return 值(字符串化)
|
|
133
|
+
- `parsedOutput?: unknown` — 子 workflow return 的对象(当 return 是对象时)
|
|
134
|
+
- `usage?: {...}` — token 消耗
|
|
135
|
+
- `error?: string` — 失败原因(成功时无此字段)
|
|
136
|
+
|
|
137
|
+
**嵌套配额**:`workflow()` 调用走同一 ConcurrencyPool,按 depth 分层分配配额(`max(1, 6 - depth)`,保底 1 槽防饿死)。`parallel()` 内的 `workflow()` 调用共享父 workflow 的配额池,超出自动排队(不报错)。嵌套深度受 `MAX_FORK_DEPTH` 护栏保护(见 ADR-030 决策 3)。
|
|
138
|
+
|
|
139
|
+
**返回值**:`workflow()` 返回 `AgentResult` 对象(与 `agent()` 一致):
|
|
140
|
+
- 成功:`{ content: string, parsedOutput?: object }`——content 是子 workflow execute() 返回值的 JSON 字符串;parsedOutput 是返回值为对象时的原样回传
|
|
141
|
+
- 失败:`{ content: "", error: string }`——子 workflow 未找到/lint 失败/执行异常/被 abort
|
|
142
|
+
|
|
143
|
+
**循环检测**:`workflow()` 自动追踪调用链(A→B→C),如果目标 name 已在当前调用链中(如 A→B→A),立即返回 error result(`Circular workflow call detected: A → B → A`),不执行子 workflow。
|
|
144
|
+
|
|
145
|
+
**预算继承**:子 workflow 的 token 预算继承父 workflow 的剩余预算。子 workflow 消耗的 tokens/cost 执行后累加回父 workflow 的预算池。父 workflow abort 时子 workflow 级联 abort。
|
|
146
|
+
|
|
147
|
+
**chain 基础示例**(顺序:每步输出作下步输入):
|
|
148
|
+
```javascript
|
|
149
|
+
const a = await workflow("extract", { source: inputPath });
|
|
150
|
+
const b = await workflow("transform", { raw: a.content });
|
|
151
|
+
const c = await workflow("load", { normalized: b.content });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**parallel 基础示例**(并行:多个独立子 workflow 同时跑):
|
|
155
|
+
```javascript
|
|
156
|
+
const results = await parallel(
|
|
157
|
+
tasks.map((t) => workflow(t, { target }))
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> 完整模式模板(chain / parallel / scatter-gather / map-reduce,含 `meta` + `$ARGS` + try-catch 错误处理)见 `extensions/subagent-workflow/examples/`。本段教 API,examples 教模式。
|
|
162
|
+
|
|
163
|
+
### Other globals
|
|
164
|
+
|
|
165
|
+
- `$ARGS` — Object with workflow arguments (from `--args key=val`)
|
|
166
|
+
- `$WORKSPACE` — Absolute path to the project workspace root
|
|
167
|
+
- `$BUDGET` — Budget info(getter + 方法,**不是**扁平字段):
|
|
168
|
+
- `$BUDGET.total` — token 预算上限(未设预算时为 0)
|
|
169
|
+
- `$BUDGET.spent()` — 已用 token
|
|
170
|
+
- `$BUDGET.remaining()` — 剩余 token(最小为 0)
|
|
171
|
+
- 例:`if ($BUDGET.remaining() < 5000) { phase('wrap-up'); }`
|
|
172
|
+
- `phase(name)` — 设置当前阶段名,影响 TUI 分组显示。`meta.phases` 只是声明,TUI 实际分组靠运行时 `phase()` 调用或 agent opts 的 `phase` 字段
|
|
173
|
+
- `log(msg)` — 输出诊断信息(收集到 workerLogs,失败时附在错误消息里,不泄漏到主进程 stderr)
|
|
174
|
+
- `module.exports = { meta, execute }` — 脚本可导出 `execute({ agent, parallel, pipeline, phase, log, $ARGS, $WORKSPACE, $BUDGET })`,运行时会自动调用(兼容 Claude Code 写法)
|
|
175
|
+
|
|
176
|
+
### `description` naming convention [MANDATORY]
|
|
177
|
+
|
|
178
|
+
`description` 用作 TUI 显示的 agent 标识,必须简短可读。规则:kebab-case,单词间用 `-` 分隔,不含 round/iteration 后缀。
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// ✅ CORRECT: kebab-case,单词间用 - 分隔
|
|
182
|
+
agent({ prompt: '...', description: 'review-business-logic' });
|
|
183
|
+
agent({ prompt: '...', description: 'fix-imports' });
|
|
184
|
+
agent({ prompt: '...', description: 'parse-must-fix' });
|
|
185
|
+
|
|
186
|
+
// ❌ WRONG: 无分隔符拼接(不可读)
|
|
187
|
+
agent({ prompt: '...', description: 'reviewbusinesslogic' });
|
|
188
|
+
agent({ prompt: '...', description: 'fiximports' });
|
|
189
|
+
|
|
190
|
+
// ❌ WRONG: 冗长描述(不是 label 用途)
|
|
191
|
+
agent({ prompt: '...', description: 'Review business logic against spec requirements' });
|
|
192
|
+
|
|
193
|
+
// ❌ WRONG: 不必要的 round/iteration 后缀(TUI 自带序号)
|
|
194
|
+
agent({ prompt: '...', description: 'review-business-logic-round-1' });
|
|
195
|
+
// ✅ CORRECT: 去掉 round 后缀
|
|
196
|
+
agent({ prompt: '...', description: 'review-business-logic' });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Constraints
|
|
200
|
+
|
|
201
|
+
- `agent()` calls **must be deterministic in order** for pause/resume to work correctly. 根因:调用结果按单调递增的 callId(从 0 起、按调用顺序)缓存,pause 时杀 Worker 但保留 callCache,resume 时按 callId 重放。`parallel()` 内的调用顺序不能随机,否则重放会错位命中旧结果。注意:无 script hash 校验,**改脚本后 resume 会用旧结果**,开发期改脚本应重新 run。
|
|
202
|
+
- `parallel()` 并发默认上限 6(ConcurrencyPool,超出自动排队;来源 ADR-030 决策 3)。
|
|
203
|
+
- Throwing an error aborts the workflow (after retries).
|
|
204
|
+
- Use `require()` for Node.js built-ins: `const fs = require('node:fs');`
|
|
205
|
+
|
|
206
|
+
## Complete Example
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const meta = { name: 'review-fix-loop', description: 'Loop: review → fix → commit until clean', phases: ['review-fix'] };
|
|
210
|
+
|
|
211
|
+
const MAX_ROUNDS = 10;
|
|
212
|
+
let round = 0;
|
|
213
|
+
|
|
214
|
+
while (round < MAX_ROUNDS) {
|
|
215
|
+
round++;
|
|
216
|
+
const result = await agent({
|
|
217
|
+
prompt: `Round ${round}: Review git diff main...HEAD. Fix all issues. Commit with: fix: review round ${round}.`,
|
|
218
|
+
schema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
mustFix: { type: 'number', description: 'Number of MUST-fix issues found' },
|
|
222
|
+
suggestions: { type: 'number', description: 'Number of suggestions' },
|
|
223
|
+
summary: { type: 'string' },
|
|
224
|
+
},
|
|
225
|
+
required: ['mustFix'],
|
|
226
|
+
},
|
|
227
|
+
description: `review-${round}`,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// result is already a parsed object thanks to schema
|
|
231
|
+
if (result.mustFix === 0) break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { rounds: round, clean: true };
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Script Size Guideline
|
|
238
|
+
|
|
239
|
+
Keep scripts under **100 lines**. Scripts are orchestration glue, not business logic.
|
|
240
|
+
If a script exceeds 100 lines, split into multiple smaller workflow scripts.
|
|
241
|
+
|
|
242
|
+
## Verification Patterns
|
|
243
|
+
|
|
244
|
+
Workflow nodes should be **verifiable** — every critical execution path needs a check that the AI's output is correct. Two patterns are supported:
|
|
245
|
+
|
|
246
|
+
### Pattern A: Node-Internal Verification
|
|
247
|
+
|
|
248
|
+
Embed self-check instructions directly in the prompt and require a structured output that includes validation. Best for: trivial classification, single-step lookups, format checks.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// Example: classify severity of a code review finding
|
|
252
|
+
const result = await agent({
|
|
253
|
+
prompt: `Classify the severity of this finding: "${findingText}".
|
|
254
|
+
The selfCheck field MUST reflect whether severity and reason are both present and consistent.`,
|
|
255
|
+
schema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
severity: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
259
|
+
reason: { type: 'string' },
|
|
260
|
+
selfCheck: { type: 'object', properties: { valid: { type: 'boolean' }, reason: { type: 'string' } } },
|
|
261
|
+
},
|
|
262
|
+
required: ['severity', 'reason', 'selfCheck'],
|
|
263
|
+
},
|
|
264
|
+
description: 'classify-severity',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (!result.selfCheck.valid) {
|
|
268
|
+
throw new Error(`self-check failed: ${result.selfCheck.reason}`);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Pros:** Single call, low overhead. **Cons:** Self-check is part of the same call — AI can lie about validation.
|
|
273
|
+
|
|
274
|
+
### Pattern B: Follow-up Verify Node
|
|
275
|
+
|
|
276
|
+
A second `agent()` call that explicitly verifies the previous result. Best for: critical mutations, data transforms, anything where errors propagate downstream.
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
// Example: review a file, then verify the review is complete
|
|
280
|
+
const review = await agent({
|
|
281
|
+
prompt: `Review ${file} for issues. Report each finding with severity and reason.`,
|
|
282
|
+
schema: {
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: {
|
|
285
|
+
findings: { type: 'array', items: { type: 'object', properties: { severity: { type: 'string' }, reason: { type: 'string' } } } } },
|
|
286
|
+
},
|
|
287
|
+
required: ['findings'],
|
|
288
|
+
},
|
|
289
|
+
description: `review-${file}`,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const verify = await agent({
|
|
293
|
+
prompt: `You are verifying a code review. The previous output was:
|
|
294
|
+
${JSON.stringify(review)}
|
|
295
|
+
Did the review cover: (1) all functions in the file, (2) at least 3 potential issues, (3) severity rating for each?`,
|
|
296
|
+
schema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
valid: { type: 'boolean' },
|
|
300
|
+
missingItems: { type: 'array', items: { type: 'string' } },
|
|
301
|
+
},
|
|
302
|
+
required: ['valid'],
|
|
303
|
+
},
|
|
304
|
+
description: `verify-review-${file}`,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!verify.valid) {
|
|
308
|
+
throw new Error(`verification failed for ${file}: ${verify.missingItems.join(', ')}`);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Pros:** Independent check, harder to game. **Cons:** Doubles agent calls.
|
|
313
|
+
|
|
314
|
+
### Decision Tree
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
Is the step a critical data transform or mutation?
|
|
318
|
+
├─ YES → Use Pattern B (follow-up verify)
|
|
319
|
+
└─ NO
|
|
320
|
+
├─ Trivial classification / format check?
|
|
321
|
+
│ └─ YES → Use Pattern A (node-internal)
|
|
322
|
+
└─ Read-only / informational?
|
|
323
|
+
└─ No verification needed
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Anti-pattern
|
|
327
|
+
|
|
328
|
+
- **Never skip verification entirely on critical execution paths** — even with strong prompts, AI outputs are probabilistic. A verify step catches hallucinations before they propagate.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/__tests__/agent-registry.test.ts
|
|
2
|
+
//
|
|
3
|
+
// AgentRegistry 测试(ADR-031 统一资源发现版)。
|
|
4
|
+
//
|
|
5
|
+
// agent 发现走 shared/resource-discovery,扫描路径由 workspaceRoot + agentDir 推导:
|
|
6
|
+
// - project 级:workspaceRoot/.pi/agents/ + workspaceRoot/.agents/agents/
|
|
7
|
+
// - user 级:agentDir/agents/ + ~/.agents/agents/
|
|
8
|
+
// - npm/dev:agentDir/npm/node_modules/*/ + agentDir/extensions/*/
|
|
9
|
+
//
|
|
10
|
+
// 测试用 tmp 目录作 workspaceRoot,在约定路径下放 agent 文件验证发现 + 优先级。
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
+
|
|
17
|
+
import type { BuiltinAgentRegistry } from "../agent-registry.ts";
|
|
18
|
+
import { AgentRegistry, createPackageBuiltinRegistry, parseAgentFrontmatter } from "../agent-registry.ts";
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// helpers
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
function tmpWorkspace(): string {
|
|
25
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "agent-reg-test-"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeAgent(dir: string, name: string, body: string): string {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
const filePath = path.join(dir, `${name}.md`);
|
|
31
|
+
fs.writeFileSync(filePath, body, "utf-8");
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const emptyBuiltin: BuiltinAgentRegistry = { get: () => undefined, list: () => [] };
|
|
36
|
+
|
|
37
|
+
/** 构造 AgentRegistry,workspaceRoot=ws,agentDir=ws/.fake-agent(隔离 user 级) */
|
|
38
|
+
function newRegistry(ws: string): AgentRegistry {
|
|
39
|
+
return new AgentRegistry({
|
|
40
|
+
workspaceRoot: ws,
|
|
41
|
+
agentDir: path.join(ws, ".fake-agent"),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// parseAgentFrontmatter
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
describe("parseAgentFrontmatter", () => {
|
|
50
|
+
it("parses name from filename + body as systemPrompt when no frontmatter", () => {
|
|
51
|
+
const cfg = parseAgentFrontmatter("/x/worker.md", "You are a worker.");
|
|
52
|
+
expect(cfg.name).toBe("worker");
|
|
53
|
+
expect(cfg.systemPrompt).toBe("You are a worker.");
|
|
54
|
+
});
|
|
55
|
+
it("extracts model/thinkingLevel/tools from frontmatter", () => {
|
|
56
|
+
const cfg = parseAgentFrontmatter("/x/coder.md", `---
|
|
57
|
+
model: anthropic/claude-sonnet-4-5
|
|
58
|
+
thinkingLevel: high
|
|
59
|
+
tools: bash, read, edit
|
|
60
|
+
---
|
|
61
|
+
You write code.`);
|
|
62
|
+
expect(cfg.name).toBe("coder");
|
|
63
|
+
expect(cfg.model).toBe("anthropic/claude-sonnet-4-5");
|
|
64
|
+
expect(cfg.thinkingLevel).toBe("high");
|
|
65
|
+
expect(cfg.tools).toEqual(["bash", "read", "edit"]);
|
|
66
|
+
expect(cfg.systemPrompt).toBe("You write code.");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// AgentRegistry.discoverAll — 统一资源发现
|
|
72
|
+
// ============================================================
|
|
73
|
+
|
|
74
|
+
describe("AgentRegistry.discoverAll", () => {
|
|
75
|
+
let ws: string;
|
|
76
|
+
beforeEach(() => { ws = tmpWorkspace(); });
|
|
77
|
+
afterEach(() => { fs.rmSync(ws, { recursive: true, force: true }); });
|
|
78
|
+
|
|
79
|
+
it("discovers all .md agents in project .pi/agents/", () => {
|
|
80
|
+
const piAgents = path.join(ws, ".pi", "agents");
|
|
81
|
+
writeAgent(piAgents, "worker", "do work");
|
|
82
|
+
writeAgent(piAgents, "scout", "explore");
|
|
83
|
+
const reg = newRegistry(ws);
|
|
84
|
+
reg.discoverAll(emptyBuiltin);
|
|
85
|
+
expect(reg.list().sort()).toEqual(["scout", "worker"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("project .agents/agents overrides project .pi/agents on name clash (priority)", () => {
|
|
89
|
+
writeAgent(path.join(ws, ".pi", "agents"), "worker", "pi-body");
|
|
90
|
+
writeAgent(path.join(ws, ".agents", "agents"), "worker", "agents-body");
|
|
91
|
+
const reg = newRegistry(ws);
|
|
92
|
+
reg.discoverAll(emptyBuiltin);
|
|
93
|
+
// .agents 优先级高于 .pi(buildScanTargets 顺序:project-pi 先于 project-agents)
|
|
94
|
+
expect(reg.get("worker")?.systemPrompt).toBe("agents-body");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("file agents override builtin on name clash", () => {
|
|
98
|
+
writeAgent(path.join(ws, ".pi", "agents"), "worker", "file-worker");
|
|
99
|
+
const builtin: BuiltinAgentRegistry = {
|
|
100
|
+
get: (n) => (n === "worker" ? { name: "worker", systemPrompt: "builtin-worker" } : undefined),
|
|
101
|
+
list: () => ["worker"],
|
|
102
|
+
};
|
|
103
|
+
const reg = newRegistry(ws);
|
|
104
|
+
reg.discoverAll(builtin);
|
|
105
|
+
expect(reg.get("worker")?.systemPrompt).toBe("file-worker");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("builtin fills in when no file agent exists", () => {
|
|
109
|
+
const builtin: BuiltinAgentRegistry = {
|
|
110
|
+
get: (n) => (n === "oracle" ? { name: "oracle", systemPrompt: "builtin-oracle" } : undefined),
|
|
111
|
+
list: () => ["oracle"],
|
|
112
|
+
};
|
|
113
|
+
const reg = newRegistry(ws);
|
|
114
|
+
reg.discoverAll(builtin);
|
|
115
|
+
expect(reg.get("oracle")?.systemPrompt).toBe("builtin-oracle");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("get with require=true throws listing discovered agents", () => {
|
|
119
|
+
writeAgent(path.join(ws, ".pi", "agents"), "worker", "x");
|
|
120
|
+
const reg = newRegistry(ws);
|
|
121
|
+
reg.discoverAll(emptyBuiltin);
|
|
122
|
+
expect(() => reg.get("nonexistent", true)).toThrow(/Agent "nonexistent" not found.*Discovered: worker/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("ignores files not ending in .md, starting with _, or .chain.md", () => {
|
|
126
|
+
const piAgents = path.join(ws, ".pi", "agents");
|
|
127
|
+
writeAgent(piAgents, "real", "body");
|
|
128
|
+
writeAgent(piAgents, "_skip", "ignored");
|
|
129
|
+
writeAgent(piAgents, "trace", "ignored"); // trace.chain.md → 被跳过
|
|
130
|
+
fs.renameSync(path.join(piAgents, "trace.md"), path.join(piAgents, "trace.chain.md"));
|
|
131
|
+
fs.writeFileSync(path.join(piAgents, "readme.txt"), "not an agent");
|
|
132
|
+
const reg = newRegistry(ws);
|
|
133
|
+
reg.discoverAll(emptyBuiltin);
|
|
134
|
+
expect(reg.list()).toEqual(["real"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("nonexistent directory is silently skipped", () => {
|
|
138
|
+
// workspaceRoot 下无任何 agents 目录 → 空结果,不抛错
|
|
139
|
+
const reg = newRegistry(ws);
|
|
140
|
+
expect(() => reg.discoverAll(emptyBuiltin)).not.toThrow();
|
|
141
|
+
expect(reg.list()).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// createPackageBuiltinRegistry — 包内 agents/ 扫描(走 pi.agents manifest)
|
|
147
|
+
// ============================================================
|
|
148
|
+
|
|
149
|
+
describe("createPackageBuiltinRegistry", () => {
|
|
150
|
+
it("discovers packaged agents/*.md (worker, reviewer, scout, etc.)", () => {
|
|
151
|
+
// [HISTORICAL] S6: 包内 agents/ 此前未被接通——discoverAll 从未调用,
|
|
152
|
+
// 导致 pi install 后包内 agent 定义开箱不可用。
|
|
153
|
+
const builtin = createPackageBuiltinRegistry();
|
|
154
|
+
const names = builtin.list();
|
|
155
|
+
// 包内至少有 worker/reviewer/scout 等核心 agent
|
|
156
|
+
expect(names).toEqual(expect.arrayContaining(["worker", "reviewer", "scout", "researcher", "planner", "oracle", "context-builder"]));
|
|
157
|
+
// 每个 agent 都有 systemPrompt
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
const cfg = builtin.get(name);
|
|
160
|
+
expect(cfg).toBeDefined();
|
|
161
|
+
expect(cfg?.systemPrompt.length).toBeGreaterThan(0);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/execution/__tests__/agent-result-mapper.test.ts
|
|
2
|
+
//
|
|
3
|
+
// T3.14 (NFR-兼容性): AgentResult 映射字段对齐 —— D-A10 纯函数单测
|
|
4
|
+
// T3.1 (正常): executeAndAwait 返回 content → 间接覆盖(subagent-service.test.ts 集成验证)
|
|
5
|
+
//
|
|
6
|
+
// 测试范围: mapToWorkflowAgentResult 所有字段映射分支(success/error/usage/toolCalls)
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { mapToWorkflowAgentResult } from "../agent-result-mapper.ts";
|
|
11
|
+
import type { AgentResult as SubagentsAgentResult, AgentUsageTotal, ToolCall } from "../types.ts";
|
|
12
|
+
|
|
13
|
+
describe("mapToWorkflowAgentResult (D-A10)", () => {
|
|
14
|
+
const minimalResult: SubagentsAgentResult = {
|
|
15
|
+
text: "Hello",
|
|
16
|
+
turns: 3,
|
|
17
|
+
durationMs: 1200,
|
|
18
|
+
success: true,
|
|
19
|
+
sessionId: "session-123",
|
|
20
|
+
toolCalls: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ── T3.14: 正常成功路径 ──
|
|
24
|
+
|
|
25
|
+
it("映射成功结果: text→content, durationMs/sessionId 透传", () => {
|
|
26
|
+
const result = mapToWorkflowAgentResult(minimalResult);
|
|
27
|
+
expect(result.content).toBe("Hello");
|
|
28
|
+
expect(result.durationMs).toBe(1200);
|
|
29
|
+
expect(result.sessionId).toBe("session-123");
|
|
30
|
+
expect(result.error).toBeUndefined();
|
|
31
|
+
expect(result.parsedOutput).toBeUndefined();
|
|
32
|
+
expect(result.usage).toBeUndefined();
|
|
33
|
+
expect(result.toolCalls).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("映射 parsedOutput 透传(structured-output 契约 BC-8)", () => {
|
|
37
|
+
const parsedData = { score: 0.95, label: "positive" };
|
|
38
|
+
const r: SubagentsAgentResult = { ...minimalResult, parsedOutput: parsedData };
|
|
39
|
+
const result = mapToWorkflowAgentResult(r);
|
|
40
|
+
expect(result.parsedOutput).toEqual(parsedData);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── T3.14: 失败路径 ──
|
|
44
|
+
|
|
45
|
+
it("映射失败: success=false 且 error → 填入 error 字段", () => {
|
|
46
|
+
const r: SubagentsAgentResult = { ...minimalResult, success: false, error: "timeout" };
|
|
47
|
+
const result = mapToWorkflowAgentResult(r);
|
|
48
|
+
expect(result.error).toBe("timeout");
|
|
49
|
+
expect(result.content).toBe("Hello"); // content 仍保留(可能有部分输出)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("映射失败: success=false 但无 error → error=undefined", () => {
|
|
53
|
+
const r: SubagentsAgentResult = { ...minimalResult, success: false };
|
|
54
|
+
const result = mapToWorkflowAgentResult(r);
|
|
55
|
+
expect(result.error).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("映射成功: success=true 且 error 存在 → error=undefined(不误填)", () => {
|
|
59
|
+
const r: SubagentsAgentResult = { ...minimalResult, success: true, error: "stale" };
|
|
60
|
+
const result = mapToWorkflowAgentResult(r);
|
|
61
|
+
expect(result.error).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── T3.14: usage 映射 ──
|
|
65
|
+
|
|
66
|
+
it("映射 usage: AgentUsageTotal → AgentUsage(字段形状转换)", () => {
|
|
67
|
+
const usage: AgentUsageTotal = {
|
|
68
|
+
input: 1000,
|
|
69
|
+
output: 500,
|
|
70
|
+
cacheRead: 200,
|
|
71
|
+
cacheWrite: 100,
|
|
72
|
+
total: 1800,
|
|
73
|
+
cost: 0.005,
|
|
74
|
+
};
|
|
75
|
+
const r: SubagentsAgentResult = { ...minimalResult, usage };
|
|
76
|
+
const result = mapToWorkflowAgentResult(r);
|
|
77
|
+
expect(result.usage).toEqual({
|
|
78
|
+
input: 1000,
|
|
79
|
+
output: 500,
|
|
80
|
+
cacheRead: 200,
|
|
81
|
+
cacheWrite: 100,
|
|
82
|
+
cost: 0.005,
|
|
83
|
+
contextTokens: 1800, // total → contextTokens
|
|
84
|
+
turns: 3, // 来自 AgentResult.turns
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("映射 usage: 无 usage → usage undefined", () => {
|
|
89
|
+
const result = mapToWorkflowAgentResult(minimalResult);
|
|
90
|
+
expect(result.usage).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── T3.14: toolCalls 映射 ──
|
|
94
|
+
|
|
95
|
+
it("映射 toolCalls: ToolCall → ToolCallEntry(name/input 形状转换)", () => {
|
|
96
|
+
const calls: ToolCall[] = [
|
|
97
|
+
{ toolName: "read", args: { path: "/a.txt" } },
|
|
98
|
+
{ toolName: "bash", args: { command: "ls" }, result: { content: ["ok"] }, isError: false },
|
|
99
|
+
];
|
|
100
|
+
const r: SubagentsAgentResult = { ...minimalResult, toolCalls: calls };
|
|
101
|
+
const result = mapToWorkflowAgentResult(r);
|
|
102
|
+
expect(result.toolCalls).toBeDefined();
|
|
103
|
+
expect(result.toolCalls!).toHaveLength(2);
|
|
104
|
+
expect(result.toolCalls![0]).toEqual({ name: "read", input: '{"path":"/a.txt"}' });
|
|
105
|
+
expect(result.toolCalls![1].name).toBe("bash");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("映射 toolCalls: args=undefined → input=''", () => {
|
|
109
|
+
const calls: ToolCall[] = [{ toolName: "list" }];
|
|
110
|
+
const r: SubagentsAgentResult = { ...minimalResult, toolCalls: calls };
|
|
111
|
+
const result = mapToWorkflowAgentResult(r);
|
|
112
|
+
expect(result.toolCalls![0].input).toBe("");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("映射 toolCalls: 长 args 截断(>500 chars)", () => {
|
|
116
|
+
const longStr = "x".repeat(600);
|
|
117
|
+
const calls: ToolCall[] = [{ toolName: "write", args: { content: longStr } }];
|
|
118
|
+
const r: SubagentsAgentResult = { ...minimalResult, toolCalls: calls };
|
|
119
|
+
const result = mapToWorkflowAgentResult(r);
|
|
120
|
+
expect(result.toolCalls![0].input.length).toBeLessThanOrEqual(503); // 500 + "..."
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("映射 toolCalls: 无 toolCalls → undefined", () => {
|
|
124
|
+
const r: SubagentsAgentResult = { ...minimalResult, toolCalls: undefined };
|
|
125
|
+
const result = mapToWorkflowAgentResult(r);
|
|
126
|
+
expect(result.toolCalls).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|