@zhushanwen/pi-subagent-workflow 8.4.0 → 8.5.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 (101) hide show
  1. package/package.json +18 -4
  2. package/relay/relay.mjs +390 -0
  3. package/skills/subagent-ext-config/SKILL.md +80 -0
  4. package/src/execution/__tests__/agent-registry.test.ts +110 -0
  5. package/src/execution/__tests__/chat-engine-routing.test.ts +597 -0
  6. package/src/execution/__tests__/execution-record.test.ts +127 -1
  7. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  8. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  9. package/src/execution/__tests__/relay-env.test.ts +42 -0
  10. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  11. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  12. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  13. package/src/execution/__tests__/subprocess-agent-runner.test.ts +53 -5
  14. package/src/execution/agent-registry.ts +10 -0
  15. package/src/execution/config.ts +25 -2
  16. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  17. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  18. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  19. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  20. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  21. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  22. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  23. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  24. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  25. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  26. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  27. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  28. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  29. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  30. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  31. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  32. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  33. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  34. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  35. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  36. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  37. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  38. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  39. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  40. package/src/execution/engine/common/data-dir.ts +62 -0
  41. package/src/execution/engine/common/errors.ts +183 -0
  42. package/src/execution/engine/common/event-journal.ts +254 -0
  43. package/src/execution/engine/common/journal-replay.ts +62 -0
  44. package/src/execution/engine/common/kill-chain.ts +221 -0
  45. package/src/execution/engine/common/nesting-guard.ts +50 -0
  46. package/src/execution/engine/common/persona-router.ts +108 -0
  47. package/src/execution/engine/common/pool-manager.ts +226 -0
  48. package/src/execution/engine/common/schema-emulation.ts +189 -0
  49. package/src/execution/engine/common/session-view-projection.ts +51 -0
  50. package/src/execution/engine/engine-discovery.ts +65 -0
  51. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  52. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  53. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  54. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  55. package/src/execution/engine/engines/pi/reader.ts +48 -0
  56. package/src/execution/engine/engines/pi/registration.ts +35 -0
  57. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  58. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  59. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  60. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  61. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  62. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  63. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  64. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  65. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +567 -0
  66. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  67. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  68. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  69. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  70. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  71. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  72. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  73. package/src/execution/engine/engines/zcode/zcode-engine.ts +648 -0
  74. package/src/execution/engine/host-task-spec.ts +47 -0
  75. package/src/execution/engine/model-prompt.ts +59 -0
  76. package/src/execution/engine/paths.ts +42 -0
  77. package/src/execution/engine/port.ts +153 -0
  78. package/src/execution/engine/registry.ts +123 -0
  79. package/src/execution/engine/routing.ts +218 -0
  80. package/src/execution/engine/types.ts +304 -0
  81. package/src/execution/execute-options-mapper.ts +5 -1
  82. package/src/execution/execution-record.ts +6 -0
  83. package/src/execution/model-resolver.ts +6 -0
  84. package/src/execution/pi-invocation.ts +32 -2
  85. package/src/execution/record-entry.ts +14 -0
  86. package/src/execution/record-store.ts +34 -0
  87. package/src/execution/relay-env.ts +37 -0
  88. package/src/execution/session-runner.ts +24 -0
  89. package/src/execution/stream-sink.ts +26 -0
  90. package/src/execution/subagent-service.ts +249 -11
  91. package/src/execution/subprocess-agent-runner.ts +196 -14
  92. package/src/execution/types.ts +56 -0
  93. package/src/index.ts +46 -1
  94. package/src/interface/subagent-actions.ts +3 -0
  95. package/src/interface/subagent-tool.ts +6 -0
  96. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +5 -2
  97. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +3 -3
  98. package/src/orchestration/models/types.ts +7 -0
  99. package/src/orchestration/worker-script-builder.ts +5 -2
  100. package/src/shared/meta-parser.ts +5 -1
  101. package/src/shared/resource-meta.ts +5 -0
@@ -88,6 +88,15 @@ export type ExecutionMode = "background";
88
88
  *
89
89
  * 设计:AgentEvent 携带 updateFromEvent 收口进 record 所需的**全部数据**——
90
90
  * tool_end 带 result(供 turn.toolCalls 存完整 ToolCall),无需翻译层旁路累积。
91
+ *
92
+ * ACP 词汇对照(D11 注记级校准,零行为变更;新引擎实现者按本表对齐语义,
93
+ * 详见 docs/architecture/subagent-engine-gui-visibility.md §3.3 D11):
94
+ * text_delta / thinking_delta ↔ ACP content blocks(text / thinking)
95
+ * tool_start / tool_end ↔ ACP tool_call / tool_call_update
96
+ * turn_end / message_end ↔ ACP prompt turn 终态(stop_reason + usage)
97
+ * compaction ↔ ACP session/compaction
98
+ * 本协议以 pi 为语义锚点(D3)——命名不迁移,对照表仅保证未来 AcpEngine 适配器
99
+ * 与跨引擎 trace 映射的翻译成本最低。
91
100
  */
92
101
  export type AgentEvent =
93
102
  | { type: "tool_start"; toolName: string; args?: unknown }
@@ -384,6 +393,23 @@ export interface ExecutionRecord {
384
393
  * 向后兼容:旧 record 无此字段,按默认值处理。
385
394
  */
386
395
  readonly idleTimeoutMs?: number;
396
+ /**
397
+ * 实际执行引擎 id(P4 路由留痕,D9①)。创建时确定不可变;缺省(存量 record)
398
+ * = pi 投影(消费方零迁移)。持久化经 subagent-record entry。
399
+ */
400
+ readonly engine?: string;
401
+ /**
402
+ * 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎)。GUI 警告条数据源;
403
+ * 缺省 = 无 fallback。持久化经 subagent-record entry。
404
+ */
405
+ readonly engineFallback?: { from: string; reason: string };
406
+ /**
407
+ * 引擎自描述定位符(U2:非 pi run resolve 后回填、终态迁移落 entry 前——run 前
408
+ * 缺省不可用)。sessionRef 整体透传(失败终态 sessionId 缺失时仍回填已有部分,
409
+ * 读侧①级降②级的防御形态);journalPath 为 retarget 后实际落盘路径。pi 分支不
410
+ * 回填(sessionFile 即定位符)。持久化经 subagent-record entry。
411
+ */
412
+ engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
387
413
 
388
414
  // ── 状态(实时更新)──
389
415
  status: ExecutionStatus;
@@ -563,6 +589,16 @@ export interface ExecuteOptions {
563
589
  * 优先级:参数 > env XYZ_SUBAGENT_IDLE_TIMEOUT_MS > 默认 300000ms。
564
590
  */
565
591
  idleTimeoutMs?: number;
592
+ /**
593
+ * 实际执行引擎 id(P4 路由留痕):pi 引擎由 PiEngine.run 在还原 opts 时写入;
594
+ * 缺省(历史调用方不设)= pi 投影。createRecordForMode 读入 record identity。
595
+ */
596
+ engine?: string;
597
+ /**
598
+ * 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎时由路由层写入)。
599
+ * from = 请求引擎 id,reason 恒 'engine_probe_failed'(GUI 警告条数据源)。
600
+ */
601
+ engineFallback?: { from: string; reason: string };
566
602
  // 注:fork 深度不从外部传入(曾暴露 parentForkDepth,改用 ALS 后 execute 内部从调用链派生,
567
603
  // 公开字段成为死字段误导调用方,已移除)。深度限制检查见 session-runner.ts 内部 RunOptions.parentForkDepth
568
604
  // (与历史残留的 types.ts RunOptions 同名不同 interface——后者已删除)。
@@ -731,6 +767,19 @@ export interface SubagentRecord {
731
767
  externalInstance?: AliveMarker;
732
768
  /** fork 模式下的 worktree handle。 */
733
769
  worktreeHandle?: WorktreeHandle;
770
+ /**
771
+ * 实际执行引擎 id(P4 路由留痕)。缺省 = pi 投影(存量 record 零迁移);
772
+ * GUI 警告条/引擎标记的数据源之一。
773
+ */
774
+ engine?: string;
775
+ /** 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎)。GUI 警告条数据源。 */
776
+ engineFallback?: { from: string; reason: string };
777
+ /**
778
+ * 引擎自描述定位符(U1:EngineHandleData 的持久化消费面子集,引擎无关——
779
+ * sessionRef 整体透传不枚举内部键)。read 降级链①②级的数据源(runtime
780
+ * subagent-engine-history);缺省 = pi(走 JSONL 直读链)。
781
+ */
782
+ engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
734
783
  }
735
784
 
736
785
  // ============================================================
@@ -747,6 +796,13 @@ export interface SubagentRecord {
747
796
  export interface SubagentsGlobalConfig {
748
797
  version: number;
749
798
  maxConcurrent: number;
799
+ /**
800
+ * 全局默认执行引擎(D9 三层优先级的最底层:调用参数 > agent frontmatter > 本值)。
801
+ * 缺省 'pi'(P4 路由层 DEFAULT_ENGINE_ID)。加载期只做类型校验,注册表校验归路由层。
802
+ */
803
+ defaultEngine?: string;
804
+ /** 引擎路由策略(D9①):strict=true 时一切 probe 失败直接报错(不 fallback)。 */
805
+ engineRouting?: { strict: boolean };
750
806
  }
751
807
 
752
808
  // ============================================================
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
 
19
- import type { ExtensionAPI, ExtensionContext, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
19
+ import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
20
20
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
21
21
  import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
22
22
 
@@ -24,6 +24,15 @@ import { bestEffort } from "./execution/best-effort.ts";
24
24
  // ═══ execution/ 层(subagents 核心 + 运行时) ═══
25
25
  import { getOrCreateChannelRegistry } from "./execution/channel-registry-access.ts";
26
26
  import { DialogGlobalQueue } from "./execution/dialog-queue.ts";
27
+ // [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
28
+ import { syncEnginesFile } from "./execution/engine/engine-discovery.ts";
29
+ // [U7] 引擎模型段注入(defaultEngine 非 pi 时 system prompt 补 <available_<engine>_models>)
30
+ import { buildEngineModelsPromptAppend } from "./execution/engine/model-prompt.ts";
31
+ // [P1 引擎接线] 组合根登记 'pi' 引擎进 registry(引擎获取统一经 getEngine,缺省 id 'pi')
32
+ import { registerPiEngine } from "./execution/engine/engines/pi/registration.ts";
33
+ // [P3 引擎接线] 组合根登记 'zcode' 引擎(spawn 单轮模式;engineDataDir 默认走
34
+ // common/data-dir SSOT,见 engines/zcode/registration.ts)
35
+ import { registerZcodeEngine } from "./execution/engine/engines/zcode/registration.ts";
27
36
  import { createUiRequestHandlerForMode } from "./execution/ui-request-handler-factory.ts";
28
37
  import {
29
38
  getModelConfigService,
@@ -160,6 +169,22 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
160
169
  // 的 getLogger("subagents") 也能走 appendEntry。
161
170
  setPiHandle(pi);
162
171
 
172
+ // [P1 引擎接线] 组合根登记缺省引擎:进程级 SubagentService 单例(session_start 注入)
173
+ // 经 registry 以 'pi' 暴露——引擎获取从此统一走 getEngine(DEFAULT_ENGINE_ID),上层
174
+ // 不再硬编码「spawn pi」。幂等(registerEngine 覆盖语义),工厂惰性解析服务单例。
175
+ // P4 配置路由(agent frontmatter engine 字段 + 三层优先级)在本登记之上消费。
176
+ registerPiEngine();
177
+
178
+ // [P3 引擎接线] 登记 'zcode'(幂等同上)。惰性工厂:不触发 CLI/凭据探测,引擎被
179
+ // 实际选用(P4 路由或显式 getEngine('zcode'))才解析 deps。
180
+ registerZcodeEngine();
181
+
182
+ // [U7b] 引擎列表在 extension 模块加载时即同步 engines.json(不等 session_start——
183
+ // 用户体验拍板 2026-08-25:xyz-agent 打开后激活任意 session 的第一时间(含 TUI 等价
184
+ // 场景)GUI 引擎选择器就该有数据;session_start 处保留幂等重写兜底 jiti 双路径/
185
+ // 模块重载场景的刷新)。
186
+ syncEnginesFile(getAgentDir());
187
+
163
188
  // ════════════════════════════════════════════════════════════
164
189
  // subagents 域:tool + command + messageRenderer
165
190
  // ════════════════════════════════════════════════════════════
@@ -332,6 +357,10 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
332
357
  const sessionId = ctx.sessionManager.getSessionId();
333
358
  lsRef.lastSessionId = sessionId;
334
359
 
360
+ // [U7] 引擎列表同步 engines.json(幂等零写 + fail-safe;组合根注册已在
361
+ // extension 工厂体完成,此处 registry 已含全部引擎)
362
+ syncEnginesFile(agentDir);
363
+
335
364
  // skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
336
365
  // 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
337
366
  // 包的候选目录)。session 内复用收益不变(IF8/DM3 消重发生在同 session 的重复调用)。
@@ -559,6 +588,22 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
559
588
  });
560
589
  });
561
590
 
591
+ // ════════════════════════════════════════════════════════════
592
+ // [U7] before_agent_start:引擎模型段注入(defaultEngine 非 pi 且引擎实现
593
+ // listModels 时追加 <available_<engine>_models>——每 turn 重判 config,改配置
594
+ // 后下一 turn 即生效;fail-safe 任何异常不注入不阻塞 agent loop)
595
+ // ════════════════════════════════════════════════════════════
596
+ pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {
597
+ try {
598
+ const service = getModelConfigService();
599
+ const append = service === null ? "" : buildEngineModelsPromptAppend(service.getGlobalConfig().defaultEngine);
600
+ if (append === "" || typeof event.systemPrompt !== "string") return undefined;
601
+ return { systemPrompt: `${event.systemPrompt}\n\n${append}` };
602
+ } catch {
603
+ return undefined;
604
+ }
605
+ });
606
+
562
607
  // ════════════════════════════════════════════════════════════
563
608
  // model_select:用户切换 model 时刷新缓存
564
609
  // ════════════════════════════════════════════════════════════
@@ -81,6 +81,8 @@ export interface StartHandlerInput {
81
81
  conversation?: boolean;
82
82
  /** 空闲超时毫秒数(仅 conversation 模式有意义,覆盖默认 5min)。 */
83
83
  idleTimeoutMs?: number;
84
+ /** 执行引擎(D4 三层路由第一层:本参数 > agent frontmatter engine > config defaultEngine)。 */
85
+ engine?: string;
84
86
  }
85
87
 
86
88
  /** start 领域对象(adapter 包成 bgResponse)。 */
@@ -219,6 +221,7 @@ export async function startHandler(
219
221
  cwd: input.cwd,
220
222
  conversation: input.conversation,
221
223
  idleTimeoutMs: input.idleTimeoutMs,
224
+ engine: input.engine,
222
225
  ctxModel,
223
226
  signal,
224
227
  // background detached 运行,完成由 notify 驱动新 turn。
@@ -127,6 +127,12 @@ const SubagentParams = Type.Object({
127
127
  "Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
128
128
  "Only meaningful with conversation:true; ignored for one-shot subagents.",
129
129
  })),
130
+ engine: Type.Optional(StringEnum(["pi", "zcode"], {
131
+ description:
132
+ "Execution engine for this subagent. Omit to inherit the global config. " +
133
+ "Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
134
+ "Non-pi engines do not support conversation/fork/worktree (rejected before the subagent is created).",
135
+ })),
130
136
  // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
131
137
  listParam: Type.Optional(Type.Object({
132
138
  includeFinished: Type.Optional(Type.Boolean({
@@ -12,7 +12,7 @@ const { parentPort: _parentPort, workerData: _workerData } = require("node:worke
12
12
  const _workerLogs = [];
13
13
  // IF6(#12): known agent() fields — hoisted to module scope, built once per worker
14
14
  // (was rebuilt inside agent() on every call; field set is call-invariant).
15
- const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);
15
+ const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);
16
16
  function _pushWorkerLog(level, args) {
17
17
  try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }
18
18
  }
@@ -153,6 +153,8 @@ function _safePost(msg, context) {
153
153
  scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,
154
154
  phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,
155
155
  thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,
156
+ // P4 D9③:step 级 engine 显式指定(仅限必须某引擎独有能力的场景)
157
+ engine: (secondArg && typeof secondArg === "object" && secondArg.engine) || undefined,
156
158
  };
157
159
  } else if (typeof firstArg === "object" && firstArg !== null) {
158
160
  if (firstArg.prompt) {
@@ -174,6 +176,7 @@ function _safePost(msg, context) {
174
176
  worktree: firstArg.worktree,
175
177
  returnMeta: firstArg.returnMeta,
176
178
  thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL,
179
+ engine: firstArg.engine,
177
180
  };
178
181
  } else {
179
182
  opts = firstArg;
@@ -190,7 +193,7 @@ function _safePost(msg, context) {
190
193
  // Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)
191
194
  const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));
192
195
  if (_unknownFields.length > 0) {
193
- _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel"]);
196
+ _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);
194
197
  }
195
198
 
196
199
  const callId = _callIdCounter;
@@ -100,11 +100,11 @@ describe("buildWorkerScript — _KNOWN_FIELDS module scope 提升(IF6)", ()
100
100
  expect(agentBody).toContain("_KNOWN_FIELDS.has(k)");
101
101
  });
102
102
 
103
- it("字段集合内容逐字段一致(16 known fields 不丢失)", () => {
103
+ it("字段集合内容逐字段一致(17 known fields 不丢失——P4 增 engine)", () => {
104
104
  expect(script).toContain(
105
- 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
105
+ 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);',
106
106
  );
107
107
  // unknown-fields 警告文案不变(known 列表仍全量)
108
- expect(script).toMatch(/Known fields:.*returnMeta.*thinkingLevel/);
108
+ expect(script).toMatch(/Known fields:.*returnMeta.*thinkingLevel.*engine/);
109
109
  });
110
110
  });
@@ -143,6 +143,13 @@ export interface AgentCallOpts {
143
143
  cwd?: string;
144
144
  /** Inherit parent session context (fork mode). Independent of worktree (file isolation). */
145
145
  fork?: boolean;
146
+ /**
147
+ * 执行引擎 id(P4 D9 三层优先级的第一层:调用参数级,workflow step 显式指定)。
148
+ * 仅限「必须某引擎独有能力」的场景使用并注释原因(D9③ workflow 脚本不写死
149
+ * engine——环境差异由 frontmatter/全局默认承载);透传链 worker-script-builder
150
+ * agent() → execute-agent-call → SAR 路由层。
151
+ */
152
+ engine?: string;
146
153
  /** Filesystem isolation: when true, creates a new git worktree for the agent. Independent of fork. */
147
154
  worktree?: boolean;
148
155
  /** When true, agent() resolves {value, sessionFile, worktreePath, error} instead of a bare value.
@@ -66,7 +66,7 @@ const WORKER_TEMPLATE_PRE = [
66
66
  'const _workerLogs = [];',
67
67
  '// IF6(#12): known agent() fields — hoisted to module scope, built once per worker',
68
68
  '// (was rebuilt inside agent() on every call; field set is call-invariant).',
69
- 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
69
+ 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);',
70
70
  'function _pushWorkerLog(level, args) {',
71
71
  ' try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }',
72
72
  '}',
@@ -210,6 +210,8 @@ const WORKER_TEMPLATE_PRE = [
210
210
  ' scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,\n' +
211
211
  ' phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,',
212
212
  ' thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,',
213
+ ' // P4 D9③:step 级 engine 显式指定(仅限必须某引擎独有能力的场景)',
214
+ ' engine: (secondArg && typeof secondArg === "object" && secondArg.engine) || undefined,',
213
215
  ' };',
214
216
  ' } else if (typeof firstArg === "object" && firstArg !== null) {',
215
217
  ' if (firstArg.prompt) {',
@@ -231,6 +233,7 @@ const WORKER_TEMPLATE_PRE = [
231
233
  ' worktree: firstArg.worktree,',
232
234
  ' returnMeta: firstArg.returnMeta,',
233
235
  ' thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL,',
236
+ ' engine: firstArg.engine,',
234
237
  ' };',
235
238
  ' } else {',
236
239
  ' opts = firstArg;',
@@ -247,7 +250,7 @@ const WORKER_TEMPLATE_PRE = [
247
250
  ' // Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)',
248
251
  ' const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));',
249
252
  ' if (_unknownFields.length > 0) {',
250
- ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel"]);',
253
+ ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);',
251
254
  ' }',
252
255
  '',
253
256
  ' const callId = _callIdCounter;',
@@ -88,7 +88,9 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
88
88
 
89
89
  if (kind === "workflow") {
90
90
  // minor-2:agent 专属字段不可出现在 workflow(串类 reject)
91
- if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined) return null;
91
+ if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined || o.engine !== undefined) {
92
+ return null;
93
+ }
92
94
  // phases 必填数组,元素为 string | {title:string, detail?:string}
93
95
  if (!Array.isArray(o.phases)) return null;
94
96
  const phases: WorkflowMeta["phases"] = [];
@@ -156,6 +158,7 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
156
158
  }
157
159
  }
158
160
  const model = isString(o.model) ? o.model : undefined;
161
+ const engine = isString(o.engine) ? o.engine : undefined;
159
162
 
160
163
  const meta: AgentMeta = {
161
164
  kind: "agent",
@@ -164,6 +167,7 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
164
167
  ...(examples !== undefined ? { examples } : {}),
165
168
  ...(tools !== undefined ? { tools } : {}),
166
169
  ...(model !== undefined ? { model } : {}),
170
+ ...(engine !== undefined ? { engine } : {}),
167
171
  ...(when !== undefined ? { when } : {}),
168
172
  ...(notFor !== undefined ? { notFor } : {}),
169
173
  };
@@ -54,6 +54,11 @@ export interface AgentMeta extends ResourceMetaBase {
54
54
  /** 供 AgentRegistry 执行侧 spawn 时注入,不进 system prompt 注入段。 */
55
55
  tools?: string[];
56
56
  model?: string;
57
+ /**
58
+ * 执行引擎 id(D9 per-agent 主通道:调用参数 engine > 本字段 > 全局默认)。
59
+ * 与 model 字段同风格——路由字段(不进 system prompt),执行侧(P4 路由层)消费。
60
+ */
61
+ engine?: string;
57
62
  }
58
63
 
59
64
  /** 判别联合(kind 判别)。 */