@zhushanwen/pi-subagent-workflow 8.4.0 → 8.6.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 (171) hide show
  1. package/package.json +22 -7
  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__/bg-notify-render.test.ts +73 -0
  6. package/src/execution/__tests__/chat-engine-routing.test.ts +601 -0
  7. package/src/execution/__tests__/delivery-methods.test.ts +38 -1
  8. package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
  9. package/src/execution/__tests__/execution-record.test.ts +237 -1
  10. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  11. package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
  12. package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
  13. package/src/execution/__tests__/index-session-start.test.ts +86 -7
  14. package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
  15. package/src/execution/__tests__/list-fields.test.ts +45 -14
  16. package/src/execution/__tests__/model-resolver.test.ts +57 -5
  17. package/src/execution/__tests__/notifier-flush.test.ts +64 -26
  18. package/src/execution/__tests__/notify-ledger.test.ts +826 -0
  19. package/src/execution/__tests__/output-collector.test.ts +299 -2
  20. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  21. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  22. package/src/execution/__tests__/relay-env.test.ts +42 -0
  23. package/src/execution/__tests__/rpc-mode.test.ts +1 -1
  24. package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
  25. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  26. package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
  27. package/src/execution/__tests__/spawn-args.test.ts +37 -26
  28. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  29. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  30. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  31. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  32. package/src/execution/__tests__/subprocess-agent-runner.test.ts +147 -6
  33. package/src/execution/__tests__/timeout-integration.test.ts +220 -2
  34. package/src/execution/__tests__/tool-action.test.ts +92 -1
  35. package/src/execution/agent-registry.ts +16 -0
  36. package/src/execution/argv-mirror.ts +5 -1
  37. package/src/execution/concurrency-pool.ts +1 -1
  38. package/src/execution/config.ts +25 -2
  39. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  40. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  41. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  42. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  43. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  44. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  45. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  46. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  47. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  48. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  49. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  50. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  51. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  52. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  53. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  54. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  55. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  56. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  57. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  58. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  59. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  60. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  61. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  62. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  63. package/src/execution/engine/common/data-dir.ts +62 -0
  64. package/src/execution/engine/common/errors.ts +183 -0
  65. package/src/execution/engine/common/event-journal.ts +254 -0
  66. package/src/execution/engine/common/journal-replay.ts +62 -0
  67. package/src/execution/engine/common/kill-chain.ts +221 -0
  68. package/src/execution/engine/common/nesting-guard.ts +50 -0
  69. package/src/execution/engine/common/persona-router.ts +108 -0
  70. package/src/execution/engine/common/pool-manager.ts +226 -0
  71. package/src/execution/engine/common/schema-emulation.ts +189 -0
  72. package/src/execution/engine/common/session-view-projection.ts +51 -0
  73. package/src/execution/engine/engine-discovery.ts +65 -0
  74. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  75. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  76. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  77. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  78. package/src/execution/engine/engines/pi/reader.ts +48 -0
  79. package/src/execution/engine/engines/pi/registration.ts +35 -0
  80. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  81. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  82. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  83. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  84. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  85. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  86. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  87. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  88. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  89. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  90. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  91. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  92. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  93. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  94. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  95. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  96. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  97. package/src/execution/engine/host-task-spec.ts +47 -0
  98. package/src/execution/engine/model-prompt.ts +59 -0
  99. package/src/execution/engine/paths.ts +42 -0
  100. package/src/execution/engine/port.ts +153 -0
  101. package/src/execution/engine/registry.ts +123 -0
  102. package/src/execution/engine/routing.ts +218 -0
  103. package/src/execution/engine/types.ts +309 -0
  104. package/src/execution/execute-options-mapper.ts +13 -8
  105. package/src/execution/execution-record.ts +66 -1
  106. package/src/execution/lifecycle-manager.ts +23 -1
  107. package/src/execution/model-config-service.ts +16 -1
  108. package/src/execution/model-resolver.ts +37 -59
  109. package/src/execution/notifier.ts +105 -35
  110. package/src/execution/notify-ledger.ts +580 -0
  111. package/src/execution/output-collector.ts +143 -3
  112. package/src/execution/pi-invocation.ts +32 -2
  113. package/src/execution/record-entry.ts +14 -0
  114. package/src/execution/record-store.ts +34 -0
  115. package/src/execution/relay-env.ts +37 -0
  116. package/src/execution/session-runner.ts +328 -71
  117. package/src/execution/stream-sink.ts +26 -0
  118. package/src/execution/subagent-service.ts +273 -13
  119. package/src/execution/subprocess-agent-runner.ts +210 -14
  120. package/src/execution/types.ts +124 -5
  121. package/src/execution/ui-request-queue.ts +14 -4
  122. package/src/index.ts +99 -1
  123. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  124. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  125. package/src/interface/bg-notify-render.ts +33 -12
  126. package/src/interface/helpers.ts +2 -2
  127. package/src/interface/subagent-actions.ts +29 -9
  128. package/src/interface/subagent-tool-schema.ts +156 -0
  129. package/src/interface/subagent-tool.ts +56 -119
  130. package/src/interface/subagents.ts +2 -2
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +19 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
  133. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
  135. package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
  136. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
  137. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
  138. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
  139. package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
  140. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  141. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
  142. package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
  143. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  144. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
  145. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +22 -3
  146. package/src/orchestration/agent-opts-resolver.ts +104 -23
  147. package/src/orchestration/error-recovery.ts +189 -33
  148. package/src/orchestration/execute-agent-call.ts +39 -0
  149. package/src/orchestration/jsonl-run-store.ts +121 -7
  150. package/src/orchestration/launcher.ts +60 -15
  151. package/src/orchestration/lifecycle.ts +10 -7
  152. package/src/orchestration/models/__tests__/budget.test.ts +1 -61
  153. package/src/orchestration/models/budget.ts +5 -35
  154. package/src/orchestration/models/run-runtime.ts +24 -9
  155. package/src/orchestration/models/types.ts +16 -0
  156. package/src/orchestration/script-lint.ts +1 -1
  157. package/src/orchestration/skill-discovery.ts +31 -8
  158. package/src/orchestration/worker-script-builder.ts +19 -3
  159. package/src/shared/__tests__/model-ref.test.ts +306 -0
  160. package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
  161. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  162. package/src/shared/meta-parser.ts +5 -1
  163. package/src/shared/model-ref.ts +286 -0
  164. package/src/shared/resource-meta.ts +5 -0
  165. package/src/shared/schema-env.ts +44 -0
  166. package/src/shared/schema-jsonify.ts +6 -4
  167. package/src/shared/timer-delay.ts +54 -0
  168. package/workflows/review-fix-loop-utils.cjs +9 -7
  169. package/workflows/review-fix-loop.js +20 -12
  170. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  171. package/src/orchestration/concurrency-gate.ts +0 -69
@@ -0,0 +1,156 @@
1
+ // src/interface/subagent-tool-schema.ts
2
+ //
3
+ // `subagent` 工具的参数 schema 纯常量叶子(零运行时依赖,先例 shared/schema-env.ts)。
4
+ //
5
+ // 抽取自 subagent-tool.ts(跨包契约另一半):subagent-tool 依赖树沉重(pi SDK /
6
+ // handler / render 链),structured-output 侧的跨包契约测试若从它 import schema
7
+ // 会把整条依赖树拖进测试进程。本模块只含 schema 常量,运行时 import 仅
8
+ // typebox(Type 构造)与 pi-ai(StringEnum helper),为 structured-output 侧
9
+ //(及任何消费者)的跨包契约测试提供稳定 import 点。
10
+ //
11
+ // [跨包契约] structured-output 的 cross-package-contract.test.ts 经真实 typebox
12
+ // 编译本 schema 并断言 required/description/enum/pattern 存活——SW 自身测试环境
13
+ // 把 typebox alias 到 mock(丢 options),SO 侧测试以真实构造为对照基准。
14
+ //
15
+ // 层归属:Interface(工具 schema 的家)。SLUG_MAX_LENGTH 随 schema 迁入:
16
+ // 它的唯一语义就是 tool schema 的 maxLength(见原 execute-options-mapper 注释),
17
+ // execution 侧经 re-export 保持既有 import 路径不变。
18
+
19
+ import { StringEnum } from "@earendil-works/pi-ai";
20
+ import { type Static, Type } from "typebox";
21
+
22
+ import { THINKING_ORDER } from "../shared/model-ref.ts";
23
+
24
+ /**
25
+ * slug 最大长度(字符)。subagent/workflow 创建时 slug 超过此值会被截断。
26
+ * subagent/workflow tool schema 的 maxLength 引用此常量(单一真相,勿再硬编码)。
27
+ * 历史值 20 偏紧——描述性 slug 如 "audit-structured-output"(23)/"fix-subagent-wf-tools"(21)
28
+ * 会撞上限,放宽到 35 兼顾「短到能塞进 TUI 标题行」与「容纳合理描述性 kebab-case 名」。
29
+ */
30
+ export const SLUG_MAX_LENGTH = 35;
31
+
32
+ // Params schema(跨包契约测试的真实 typebox 校验入口)。
33
+ //
34
+ // action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
35
+ // 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
36
+ // startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
37
+ // 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
38
+ // JSON Schema 无法表达「action 条件必填」)。
39
+ //
40
+ // TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
41
+ // 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
42
+ // (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
43
+ // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
44
+ export const SubagentParams = Type.Object({
45
+ action: StringEnum(["start", "list", "cancel", "message", "close"], {
46
+ description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to a running subagent (one-shot subagents are auto-upgraded to conversation mode on first message), 'close' ends a running subagent (conversation-mode or one-shot).",
47
+ }),
48
+ // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
49
+ // Missing/empty task or slug throws at runtime (startHandler).
50
+ // (flat JSON Schema can't express conditional requirement — see file-level TODO.)
51
+ task: Type.Optional(Type.String({
52
+ description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
53
+ })),
54
+ slug: Type.Optional(Type.String({
55
+ description:
56
+ "REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
57
+ "Shown in TUI to distinguish concurrent subagents.",
58
+ maxLength: SLUG_MAX_LENGTH,
59
+ })),
60
+ agent: Type.Optional(Type.String({
61
+ description: 'Agent ref: absolute path to the agent .md file (use <location> from <available_subagents>). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Do not invent names — only use paths from the injected list.',
62
+ })),
63
+ model: Type.Optional(Type.String({
64
+ description: 'Model override in "provider/modelId" format. CASE-SENSITIVE: the string must equal a registry entry exactly, including letter case (e.g. "zai-coding-cn/GLM-5.3-Flash", NOT "zai-coding-cn/glm-5.3-flash"). A non-exact match is rejected immediately with "Did you mean" suggestions — retry with the exact suggested string; the system never auto-corrects your input. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
65
+ })),
66
+ thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
67
+ description: "Thinking depth override (derived from THINKING_ORDER SSOT, includes 'max'). Omit to default to the model's highest available level (not the main agent's level).",
68
+ })),
69
+ skillPath: Type.Optional(Type.String({
70
+ description:
71
+ "Absolute path to a skill directory, injected into the subagent's pi process via --skill " +
72
+ "(e.g. a path under .agents/skills/ already resolved for the caller). Must be an absolute path; " +
73
+ "'..' traversal segments are rejected.",
74
+ pattern: "^/",
75
+ })),
76
+ appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
77
+ schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
78
+ maxTurns: Type.Optional(Type.Number({
79
+ description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
80
+ })),
81
+ graceTurns: Type.Optional(Type.Number({
82
+ description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
83
+ })),
84
+ fork: Type.Optional(Type.Boolean({
85
+ description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
86
+ })),
87
+ worktree: Type.Optional(Type.Boolean({
88
+ description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork — worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
89
+ })),
90
+ cwd: Type.Optional(Type.String({
91
+ description: 'Override the working directory for the subagent execution. Must be an absolute path (no "~" shorthand, no relative paths); ".." segments are rejected. Defaults to the parent session\'s cwd.',
92
+ pattern: "^/",
93
+ })),
94
+ conversation: Type.Optional(Type.Boolean({
95
+ description:
96
+ "Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
97
+ "\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
98
+ "\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
99
+ "\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
100
+ "Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
101
+ })),
102
+ idleTimeoutMs: Type.Optional(Type.Number({
103
+ description:
104
+ "Idle timeout in milliseconds for conversation-mode subagents. Controls how long an idle subagent (between rounds) stays alive before automatic cleanup. " +
105
+ "Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
106
+ "Pass 0 or a negative value to DISABLE idle cleanup entirely (subagent stays alive until explicitly closed). " +
107
+ "Only meaningful with conversation:true; ignored for one-shot subagents.",
108
+ })),
109
+ engine: Type.Optional(StringEnum(["pi", "zcode"], {
110
+ description:
111
+ "Execution engine for this subagent. Omit to inherit the global config. " +
112
+ "Three-layer priority: this parameter > agent .md frontmatter engine > config.json defaultEngine. " +
113
+ "Non-pi engines do not support conversation/fork/worktree (rejected before the subagent is created).",
114
+ })),
115
+ // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
116
+ listParam: Type.Optional(Type.Object({
117
+ includeFinished: Type.Optional(Type.Boolean({
118
+ description: "Include finished (done/failed/cancelled) records. Default false (running only).",
119
+ })),
120
+ limit: Type.Optional(Type.Number({
121
+ description: "Max items to return. Default 20, clamped to [1, 100].",
122
+ })),
123
+ })),
124
+ // action:"cancel" → cancelParam.subagentId REQUIRED. Throws if missing. Ignored by other actions.
125
+ cancelParam: Type.Optional(Type.Object({
126
+ subagentId: Type.String({
127
+ description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
128
+ }),
129
+ })),
130
+ // action:"message" → messageParam.subagentId + text REQUIRED. Any RUNNING subagent works —
131
+ // one-shot subagents are auto-upgraded to conversation mode on first message (SP-5); ended ones throw.
132
+ messageParam: Type.Optional(Type.Object({
133
+ subagentId: Type.String({
134
+ description: "REQUIRED for action:'message'. The subagentId to message (any running subagent; a one-shot subagent is auto-upgraded to conversation mode on first message, so you may also message one-shot subagents that are still running).",
135
+ }),
136
+ text: Type.String({
137
+ description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
138
+ }),
139
+ interrupt: Type.Optional(Type.Boolean({
140
+ description: "If true, interrupt the subagent's current work immediately (in-progress output stops, it switches to your new message). If false (default), the message is queued and processed after the current round completes. When the subagent is idle (between rounds), interrupt has no effect — the message always starts a new round.",
141
+ })),
142
+ })),
143
+ // action:"close" → closeParam.subagentId REQUIRED. Ends a running subagent (conversation-mode
144
+ // or one-shot — closeSubagent behavior split covers both).
145
+ closeParam: Type.Optional(Type.Object({
146
+ subagentId: Type.String({
147
+ description: "REQUIRED for action:'close'. The subagentId to close (any running subagent, conversation-mode or one-shot).",
148
+ }),
149
+ force: Type.Optional(Type.Boolean({
150
+ description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
151
+ })),
152
+ })),
153
+ });
154
+
155
+ /** Params schema 的 Static 投影(消费方经 `Static<typeof SubagentParams>` 使用,见 subagent-tool.ts)。 */
156
+ export type SubagentParamsStatic = Static<typeof SubagentParams>;
@@ -9,19 +9,19 @@
9
9
  // Theme、ExtensionContext)会触发 TS2307 误报(probe5d/5f 验证)。
10
10
  // 抽到顶层后参数类型由 alias 提供,绕过该 quirk。
11
11
 
12
+ import { isAbsolute } from "node:path";
13
+
12
14
  import type { Component } from "@earendil-works/pi-tui";
13
- import { StringEnum } from "@earendil-works/pi-ai";
14
15
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
15
16
  import { getLogger } from "@zhushanwen/pi-extension-logger";
16
- import { type Static, Type } from "typebox";
17
+ import type { Static } from "typebox";
17
18
 
18
- import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
19
- import { THINKING_ORDER } from "../execution/model-resolver.ts";
20
19
  import { getSubagentService } from "../execution/subagent-service.ts";
21
20
  import type { SubagentToolResult } from "../execution/types.ts";
22
21
  import { extractAgentName } from "./format.ts";
23
22
  import { toGuiCtx } from "./gui-mappers.ts";
24
23
  import { adapter, cancelHandler, closeHandler, listHandler, messageHandler, startHandler } from "./subagent-actions.ts";
24
+ import { SubagentParams } from "./subagent-tool-schema.ts";
25
25
  import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
26
26
 
27
27
  // ============================================================
@@ -54,122 +54,14 @@ type SubagentRenderResultCb = (
54
54
  ctx: RenderContext,
55
55
  ) => Component;
56
56
 
57
- // ============================================================
58
- // Params schema
59
- // ============================================================
60
-
61
- // Params schema(模块内消费,未导出)。
62
- //
63
- // action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
64
- // 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
65
- // startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
66
- // 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
67
- // JSON Schema 无法表达「action 条件必填」)。
68
- //
69
- // TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
70
- // 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
71
- // (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
72
- // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
73
- const SubagentParams = Type.Object({
74
- action: StringEnum(["start", "list", "cancel", "message", "close"], {
75
- description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to a running subagent (one-shot subagents are auto-upgraded to conversation mode on first message), 'close' ends a running subagent (conversation-mode or one-shot).",
76
- }),
77
- // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
78
- // Missing/empty task or slug throws at runtime (startHandler).
79
- // (flat JSON Schema can't express conditional requirement — see file-level TODO.)
80
- task: Type.Optional(Type.String({
81
- description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
82
- })),
83
- slug: Type.Optional(Type.String({
84
- description:
85
- "REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
86
- "Shown in TUI to distinguish concurrent subagents.",
87
- maxLength: SLUG_MAX_LENGTH,
88
- })),
89
- agent: Type.Optional(Type.String({
90
- description: 'Agent ref: absolute path to the agent .md file (use <location> from <available_subagents>). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Do not invent names — only use paths from the injected list.',
91
- })),
92
- model: Type.Optional(Type.String({
93
- description: 'Model override in "provider/modelId" format. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
94
- })),
95
- thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
96
- description: "Thinking depth override (derived from THINKING_ORDER SSOT, includes 'max'). Omit to default to the model's highest available level (not the main agent's level).",
97
- })),
98
- skillPath: Type.Optional(Type.String()),
99
- appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
100
- schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
101
- maxTurns: Type.Optional(Type.Number({
102
- description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
103
- })),
104
- graceTurns: Type.Optional(Type.Number({
105
- description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
106
- })),
107
- fork: Type.Optional(Type.Boolean({
108
- description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
109
- })),
110
- worktree: Type.Optional(Type.Boolean({
111
- description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork — worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
112
- })),
113
- cwd: Type.Optional(Type.String({
114
- description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
115
- })),
116
- conversation: Type.Optional(Type.Boolean({
117
- description:
118
- "Enable continuous chat with this subagent. When true, the subagent stays available after each reply — you can send follow-up messages (action:'message') and it keeps the full conversation context across rounds, with no need to re-spawn or re-explain. " +
119
- "\nUse conversation:true for: multi-round collaboration (iterative review-fix loops, back-and-forth refinement), any task where you expect to send follow-up messages after the initial result. " +
120
- "\nOmit (or false) for: one-shot tasks — single exploration, lookup, file read, code generation that needs no follow-up. The subagent runs once, notifies on completion, and is cleaned up automatically (default). " +
121
- "\nFor long-interval collaboration (each round spaced >5min apart), set conversation:true AND increase idleTimeoutMs to avoid premature timeout. " +
122
- "Cost: a conversation-mode subagent holds resources (memory, and a worktree if enabled) until you explicitly end it with action:'close'. Always close when done.",
123
- })),
124
- idleTimeoutMs: Type.Optional(Type.Number({
125
- description:
126
- "Idle timeout in milliseconds for conversation-mode subagents. Controls how long an idle subagent (between rounds) stays alive before automatic cleanup. " +
127
- "Default: 300000 (5min). Override for long-interval collaboration where each round is spaced >5min apart. " +
128
- "Only meaningful with conversation:true; ignored for one-shot subagents.",
129
- })),
130
- // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
131
- listParam: Type.Optional(Type.Object({
132
- includeFinished: Type.Optional(Type.Boolean({
133
- description: "Include finished (done/failed/cancelled) records. Default false (running only).",
134
- })),
135
- limit: Type.Optional(Type.Number({
136
- description: "Max items to return. Default 20, clamped to [1, 100].",
137
- })),
138
- })),
139
- // action:"cancel" → cancelParam.subagentId REQUIRED. Throws if missing. Ignored by other actions.
140
- cancelParam: Type.Optional(Type.Object({
141
- subagentId: Type.String({
142
- description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
143
- }),
144
- })),
145
- // action:"message" → messageParam.subagentId + text REQUIRED. Any RUNNING subagent works —
146
- // one-shot subagents are auto-upgraded to conversation mode on first message (SP-5); ended ones throw.
147
- messageParam: Type.Optional(Type.Object({
148
- subagentId: Type.String({
149
- description: "REQUIRED for action:'message'. The subagentId to message (any running subagent; a one-shot subagent is auto-upgraded to conversation mode on first message, so you may also message one-shot subagents that are still running).",
150
- }),
151
- text: Type.String({
152
- description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
153
- }),
154
- interrupt: Type.Optional(Type.Boolean({
155
- description: "If true, interrupt the subagent's current work immediately (in-progress output stops, it switches to your new message). If false (default), the message is queued and processed after the current round completes. When the subagent is idle (between rounds), interrupt has no effect — the message always starts a new round.",
156
- })),
157
- })),
158
- // action:"close" → closeParam.subagentId REQUIRED. Ends a running subagent (conversation-mode
159
- // or one-shot — closeSubagent behavior split covers both).
160
- closeParam: Type.Optional(Type.Object({
161
- subagentId: Type.String({
162
- description: "REQUIRED for action:'close'. The subagentId to close (any running subagent, conversation-mode or one-shot).",
163
- }),
164
- force: Type.Optional(Type.Boolean({
165
- description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
166
- })),
167
- })),
168
- });
169
-
170
57
  // ============================================================
171
58
  // renderCall 预解析 helper
172
59
  // ============================================================
60
+ //
61
+ // Params schema(SubagentParams)定义在 ./subagent-tool-schema.ts 纯常量叶子:
62
+ // subagent-tool 依赖树沉重,structured-output 侧跨包契约测试需要零依赖 import
63
+ // schema 常量经真实 typebox 编译校验(SW 自身 vitest 把 typebox alias 到 mock,
64
+ // 丢 options——required/description 断言必须以真实构造为基准)。
173
65
 
174
66
  // extractAgentName 已上移到 ../tui/format.ts 共享(tool-render / subagent-tool 复用)。
175
67
 
@@ -193,6 +85,47 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
193
85
  return typeof a === "object" && a !== null;
194
86
  }
195
87
 
88
+ /**
89
+ * start 路径类参数(skillPath / cwd)运行时守卫:绝对路径 + 禁 `..` 穿越。
90
+ *
91
+ * 校验链事实(pi 0.84.1 实装,登记 PS-20):pi agent-loop 对注册 typebox schema
92
+ * 有运行时强校验——agent-loop.js:403-404 在 beforeToolCall / execute 之前调
93
+ * validateToolArguments(pi-ai validation.js:247:Value.Convert :249 + Compile :210
94
+ * + Check :265,失败 throw `Validation failed for tool` :272-273)→ catch 走
95
+ * immediate error(agent-loop.js:445-451),execute 不被调用。schema 的
96
+ * pattern(skillPath/cwd `^/`)/ maxLength(slug 35)是运行时强制而非仅模型可见
97
+ * 契约;tool-definition-wrapper.js:11 只原样透传 params,校验发生在上游 agent-loop 层。
98
+ *
99
+ * 工具层守卫定位 = defense-in-depth + schema 表达力缺口,非「pi 无校验」:
100
+ * - action 条件必填(task/slug 仅 action=start 必填)flat JSON Schema 表达不了,
101
+ * 只能在 startHandler 运行时校验
102
+ * - `..` 穿越段拒绝超出 pattern 能力(`^/` 放行 "/a/../b"),穿越语义只能在
103
+ * 工具层判——与 slug maxLength 双闸同理(schema 强制之上再叠 handler 兜底)
104
+ *
105
+ * 规则:
106
+ * - 绝对路径(isAbsolute;`~` 缩写不是绝对路径,拒绝并指引展开后重试——
107
+ * 下游 session-runner 把该值原样拼进 `--skill <path>` / spawn cwd,不展开 `~`)
108
+ * - 任意 `..` 路径段拒绝(按 /[\\/] 分段判断而非子串——"a..b" 不是穿越):
109
+ * 相对穿越让子进程读到意图外的目录
110
+ *
111
+ * 校验失败 immediate throw(与 action 枚举守卫同风格):pi 只对 execute throw 置
112
+ * isError:true,错误文案原样进 toolResult。
113
+ */
114
+ function assertSafeStartPath(value: string, param: "skillPath" | "cwd"): void {
115
+ if (value.split(/[\\/]/).includes("..")) {
116
+ throw new Error(
117
+ `${param} must not contain '..' path segments (got "${value}"). ` +
118
+ `Pass a normalized absolute path — traversal segments are rejected.`,
119
+ );
120
+ }
121
+ if (!isAbsolute(value)) {
122
+ throw new Error(
123
+ `${param} must be an absolute path (got "${value}"). ` +
124
+ `Expand '~' yourself and pass the full path, e.g. "/Users/me/project".`,
125
+ );
126
+ }
127
+ }
128
+
196
129
  /** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。
197
130
  * 拍平后 args 已是顶层平铺结构(model/thinkingLevel 直接在 args 上)。 */
198
131
  function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
@@ -237,7 +170,7 @@ action:"list" before action:"start" — a reusable running subagent may exist; c
237
170
 
238
171
  \`\`\`
239
172
  {"action":"start","task":"<your task>","slug":"<kebab-case>"}
240
- {"action":"start","task":"...","slug":"fix-login","agent":"coder","model":"anthropic/claude-3.5-sonnet","fork":true}
173
+ {"action":"start","task":"...","slug":"fix-login","agent":"/abs/path/coder.md","model":"anthropic/claude-3.5-sonnet","fork":true}
241
174
  {"action":"start","task":"review iteratively","slug":"review","conversation":true}
242
175
  {"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"now also handle the empty-list case"}}
243
176
  {"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"stop, switch direction to X","interrupt":true}}
@@ -271,7 +204,7 @@ When to use:
271
204
  - ✅ Long-interval rounds (>5min apart) → conversation:true + idleTimeoutMs increased
272
205
  - ❌ Single exploration/lookup → default (one-shot)
273
206
 
274
- idleTimeoutMs: per-subagent idle timeout (default 300000 / 5min). Env XYZ_SUBAGENT_IDLE_TIMEOUT_MS sets the global default; per-call param takes precedence.
207
+ idleTimeoutMs: per-subagent idle timeout (default 300000 / 5min). Env XYZ_SUBAGENT_IDLE_TIMEOUT_MS sets the global default; per-call param takes precedence. Pass 0 or a negative value to disable idle cleanup entirely.
275
208
 
276
209
  ## You cannot
277
210
 
@@ -375,6 +308,10 @@ const executeSubagent: SubagentExecuteCb = async (
375
308
  }
376
309
  switch (params.action) {
377
310
  case "start":
311
+ // 路径类参数守卫(三通道对称审查 + MF-13):skillPath/cwd 在进入 handler 前
312
+ // immediate throw,不产生半启动 record(与 action 枚举守卫同风格)。
313
+ if (params.skillPath !== undefined) assertSafeStartPath(params.skillPath, "skillPath");
314
+ if (params.cwd !== undefined) assertSafeStartPath(params.cwd, "cwd");
378
315
  // 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
379
316
  // action/listParam/cancelParam 被忽略;task/slug 必填性由 startHandler 校验)。
380
317
  return adapter({ action: "start", domain: await startHandler(service, params, signal, _ctx?.model) }, toGuiCtx(_ctx));
@@ -45,7 +45,7 @@ export interface SubagentDirectiveDetails {
45
45
  * 定向消息注入正在运行的主 agent LLM turn,违反「不经主 agent LLM 直达 subagent」。
46
46
  * 故按调用时刻的权威 streaming 状态(ctx.isIdle(),与 sendCustomMessage 内部
47
47
  * isStreaming 判据精确互补,含 agent_end 后 retry/continuation 窗口)分流:
48
- * - streaming(isMainAgentIdle=false):传 { deliverAs: "nextTurn" }——消息入
48
+ * - streaming(isMainAgentIdle=false):传 { deliverAs: "nextTurn" }——消息入(g4-allow: 交互注入——GUI 定向消息留痕分流,非结果语义通知)
49
49
  * pi 内存 _pendingNextTurnMessages 队列,下个 turn 注入主 agent 上下文;不打断、
50
50
  * 不 steer 当前 turn。注意:该队列不落 entry,留痕延迟到下个 turn
51
51
  * - 非 streaming(isMainAgentIdle=true):不传 options——立即 append entry 留痕
@@ -66,7 +66,7 @@ function emitSubagentDirective(
66
66
  display: false,
67
67
  details,
68
68
  },
69
- isMainAgentIdle ? undefined : { deliverAs: "nextTurn" },
69
+ isMainAgentIdle ? undefined : { deliverAs: "nextTurn" }, // g4-allow: 交互注入——/subagents GUI 定向消息留痕,非结果语义(C-ext-19 禁令边界,见 emitSubagentDirective JSDoc)
70
70
  );
71
71
  }
72
72
 
@@ -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", "maxTurns", "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
  }
@@ -98,6 +98,8 @@ function _safePost(msg, context) {
98
98
  // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。
99
99
  // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,
100
100
  // 不丢失。失败 resolve 为空字符串是既定容错策略。
101
+ // [MF-4] schema 模式下失败时 agent() 仍 resolve(content 回退、不 throw)——
102
+ // 需要检查错误时请用 returnMeta:true(resolve 值含 error 字段)。
101
103
  // parsedOutput: validated data object from structured-output execute().
102
104
  // Fallback to content (raw text) when no schema was requested or on error.
103
105
  // W2 改动 9(b):returnMeta===true 时 resolve {value,sessionFile,worktreePath,error,usage,durationMs,sessionId}
@@ -153,6 +155,11 @@ function _safePost(msg, context) {
153
155
  scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,
154
156
  phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,
155
157
  thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,
158
+ // step 级 turn 上限(turn limiter;显式 0/负 = 显式不限,压过 spawn watchdog env 兑底,SP-6)
159
+ // ?? 语义保真:仅 null/undefined 归 undefined(走 env 兑底),显式 0 保留(U5 参数 > env)
160
+ maxTurns: (secondArg && typeof secondArg === "object" ? secondArg.maxTurns : undefined) ?? undefined,
161
+ // P4 D9③:step 级 engine 显式指定(仅限必须某引擎独有能力的场景)
162
+ engine: (secondArg && typeof secondArg === "object" && secondArg.engine) || undefined,
156
163
  };
157
164
  } else if (typeof firstArg === "object" && firstArg !== null) {
158
165
  if (firstArg.prompt) {
@@ -169,11 +176,13 @@ function _safePost(msg, context) {
169
176
  scene: firstArg.scene,
170
177
  skill: firstArg.skill,
171
178
  timeoutMs: firstArg.timeoutMs,
179
+ maxTurns: firstArg.maxTurns,
172
180
  cwd: firstArg.cwd,
173
181
  fork: firstArg.fork,
174
182
  worktree: firstArg.worktree,
175
183
  returnMeta: firstArg.returnMeta,
176
184
  thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL,
185
+ engine: firstArg.engine,
177
186
  };
178
187
  } else {
179
188
  opts = firstArg;
@@ -190,7 +199,7 @@ function _safePost(msg, context) {
190
199
  // Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)
191
200
  const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));
192
201
  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"]);
202
+ _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, maxTurns, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);
194
203
  }
195
204
 
196
205
  const callId = _callIdCounter;
@@ -326,7 +335,14 @@ module.exports = { execute: async (ctx) => ctx.agent("finalize") };
326
335
  }
327
336
  })().then((result) => {
328
337
  const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
329
- _safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return");
338
+ if (!_safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return")) {
339
+ // [F1] return 值不可克隆(含 function/Symbol/循环引用 → DataCloneError)时 _safePost
340
+ // 只能记日志返回 false——若不补救,worker 将静默 exit(0),主线程收不到任何终态消息,
341
+ // run 永久 running、runAndWait 悬挂。回发可克隆的 error 消息(DataCloneError 详情
342
+ // 已由 _safePost 记入 _workerLogs 随消息带回),让主线程 handleScriptError 接管,
343
+ // run 经既有重试矩阵收敛到终态 failed。
344
+ _safePost({ type: "error", runId, error: "Workflow return value could not be delivered (structured-clone failed) — see workerLogs for the postMessage error", workerLogs: _workerLogs }, "error");
345
+ }
330
346
  }).catch((err) => {
331
347
  const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";
332
348
  _safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");
@@ -48,12 +48,6 @@ function makeRunningRun(runId: string): WorkflowRun {
48
48
  runtime: {
49
49
  controller,
50
50
  worker: { postMessage: vi.fn() },
51
- gate: {
52
- // 直接 await fn():executeAgentCall 内 runner.run reject 会沿 withSlot → 外层 .catch
53
- withSlot: vi.fn(async (fn: () => Promise<void>, _signal: AbortSignal) => {
54
- await fn();
55
- }),
56
- },
57
51
  },
58
52
  transition: vi.fn(),
59
53
  replaceRuntime: vi.fn(),
@@ -46,11 +46,6 @@ function makeRunningRun(runId: string): WorkflowRun {
46
46
  runtime: {
47
47
  controller,
48
48
  worker: { postMessage: vi.fn() },
49
- gate: {
50
- withSlot: vi.fn(async (fn: () => Promise<void>, _signal: AbortSignal) => {
51
- await fn();
52
- }),
53
- },
54
49
  },
55
50
  transition: vi.fn(),
56
51
  replaceRuntime: vi.fn(),