@zhushanwen/pi-subagent-workflow 7.4.0 → 8.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.
Files changed (166) hide show
  1. package/README.md +15 -0
  2. package/package.json +7 -4
  3. package/scripts/rfl.mjs +308 -0
  4. package/skills/workflow-script-format/SKILL.md +1 -1
  5. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  6. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  7. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  8. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  9. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  10. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  11. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  12. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  13. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  14. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  15. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  16. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  17. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  18. package/src/execution/__tests__/execution-record.test.ts +199 -0
  19. package/src/execution/__tests__/finalize-record.test.ts +197 -15
  20. package/src/execution/__tests__/format.test.ts +131 -7
  21. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  22. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
  24. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  26. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  27. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  28. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  29. package/src/execution/__tests__/list-component.test.ts +59 -5
  30. package/src/execution/__tests__/list-fields.test.ts +109 -0
  31. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  32. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  33. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  34. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  35. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  36. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  37. package/src/execution/__tests__/record-store.test.ts +442 -54
  38. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  39. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  40. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  41. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  42. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  43. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  44. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  45. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  46. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  47. package/src/execution/__tests__/session-pending.test.ts +61 -2
  48. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  49. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  50. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  51. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  52. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  53. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  54. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  55. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  56. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  57. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  59. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  60. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  61. package/src/execution/__tests__/tool-action.test.ts +12 -10
  62. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  63. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  64. package/src/execution/__tests__/worktree-manager.test.ts +300 -90
  65. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
  66. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  67. package/src/execution/__tests__/worktree-registry.test.ts +72 -34
  68. package/src/execution/agent-result-mapper.ts +4 -1
  69. package/src/execution/argv-mirror.ts +21 -2
  70. package/src/execution/channel-registry-access.ts +3 -1
  71. package/src/execution/execution-record.ts +126 -9
  72. package/src/execution/finalize-record.ts +99 -13
  73. package/src/execution/idle-gc.ts +47 -0
  74. package/src/execution/lifecycle-manager.ts +491 -0
  75. package/src/execution/lifecycle-predicates.ts +65 -0
  76. package/src/execution/manifest-store.ts +61 -16
  77. package/src/execution/model-resolver.ts +26 -5
  78. package/src/execution/notifier.ts +69 -12
  79. package/src/execution/pi-invocation.ts +21 -1
  80. package/src/execution/record-entry.ts +118 -0
  81. package/src/execution/record-store.ts +844 -108
  82. package/src/execution/session-pending.ts +121 -49
  83. package/src/execution/session-reconstructor.ts +224 -7
  84. package/src/execution/session-runner.ts +713 -316
  85. package/src/execution/sessions-index.ts +304 -0
  86. package/src/execution/stdin-writer.ts +93 -7
  87. package/src/execution/stream-sink.ts +20 -3
  88. package/src/execution/subagent-service.ts +917 -138
  89. package/src/execution/temp-prompt.ts +8 -3
  90. package/src/execution/turn-limiter.ts +14 -0
  91. package/src/execution/types.ts +218 -19
  92. package/src/execution/worktree-manager.ts +449 -59
  93. package/src/execution/worktree-registry.ts +97 -29
  94. package/src/index.ts +318 -20
  95. package/src/injectors/subagent-list-injector.ts +26 -8
  96. package/src/injectors/workflow-list-injector.ts +25 -8
  97. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  98. package/src/interface/__tests__/tool-render.test.ts +10 -8
  99. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
  100. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
  101. package/src/interface/bg-notify-render.ts +32 -8
  102. package/src/interface/command-actions.ts +26 -7
  103. package/src/interface/commands.ts +21 -22
  104. package/src/interface/format.ts +53 -20
  105. package/src/interface/gui-mappers.ts +6 -8
  106. package/src/interface/helpers.ts +170 -10
  107. package/src/interface/list-component.ts +53 -14
  108. package/src/interface/subagent-actions.ts +235 -17
  109. package/src/interface/subagent-tool.ts +81 -16
  110. package/src/interface/subagents.ts +2 -1
  111. package/src/interface/tool-render.ts +21 -27
  112. package/src/interface/tool-workflow-script.ts +29 -33
  113. package/src/interface/tool-workflow.ts +67 -100
  114. package/src/interface/views/WorkflowsView.ts +89 -32
  115. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  116. package/src/interface/views/detail-content.ts +1 -1
  117. package/src/interface/views/format.ts +3 -3
  118. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
  119. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  120. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  121. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  122. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  123. package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
  124. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  128. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  129. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
  130. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  131. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  132. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  133. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
  134. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  135. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  136. package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
  137. package/src/orchestration/agent-opts-resolver.ts +4 -1
  138. package/src/orchestration/args-validator.ts +2 -2
  139. package/src/orchestration/config-loader.ts +30 -1
  140. package/src/orchestration/error-recovery.ts +137 -30
  141. package/src/orchestration/execute-agent-call.ts +44 -10
  142. package/src/orchestration/jsonl-run-store.ts +397 -71
  143. package/src/orchestration/launcher.ts +7 -1
  144. package/src/orchestration/lifecycle.ts +145 -133
  145. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  146. package/src/orchestration/models/budget.ts +1 -1
  147. package/src/orchestration/models/run-runtime.ts +15 -17
  148. package/src/orchestration/models/run-spec.ts +2 -2
  149. package/src/orchestration/models/run-state.ts +3 -3
  150. package/src/orchestration/models/trace.ts +95 -15
  151. package/src/orchestration/models/types.ts +8 -9
  152. package/src/orchestration/models/workflow-run.ts +50 -71
  153. package/src/orchestration/models/workflow-script.ts +32 -1
  154. package/src/orchestration/skill-discovery.ts +30 -0
  155. package/src/orchestration/worker-handle.ts +1 -1
  156. package/src/orchestration/worker-host.ts +1 -1
  157. package/src/orchestration/worker-script-builder.ts +38 -11
  158. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  159. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  160. package/src/shared/__tests__/resource-discovery.test.ts +79 -0
  161. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  162. package/src/shared/agent-ref.ts +22 -1
  163. package/src/shared/resource-discovery.ts +162 -59
  164. package/src/shared/schema-jsonify.ts +56 -0
  165. package/workflows/review-fix-loop-utils.cjs +542 -32
  166. package/workflows/review-fix-loop.js +462 -109
@@ -21,7 +21,7 @@ import { getSubagentService } from "../execution/subagent-service.ts";
21
21
  import type { SubagentToolResult } from "../execution/types.ts";
22
22
  import { extractAgentName } from "./format.ts";
23
23
  import { toGuiCtx } from "./gui-mappers.ts";
24
- import { adapter, cancelHandler, listHandler, startHandler } from "./subagent-actions.ts";
24
+ import { adapter, cancelHandler, closeHandler, listHandler, messageHandler, startHandler } from "./subagent-actions.ts";
25
25
  import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
26
26
 
27
27
  // ============================================================
@@ -71,8 +71,8 @@ type SubagentRenderResultCb = (
71
71
  // (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
72
72
  // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
73
73
  const SubagentParams = Type.Object({
74
- action: StringEnum(["start", "list", "cancel"], {
75
- description: "Operation: 'start' runs a subagent, 'list' shows running subagents (optional includeFinished), 'cancel' stops a background subagent by id.",
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
76
  }),
77
77
  // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
78
78
  // Missing/empty task or slug throws at runtime (startHandler).
@@ -93,7 +93,7 @@ const SubagentParams = Type.Object({
93
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
94
  })),
95
95
  thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
96
- description: "Thinking depth override (derived from THINKING_ORDER SSOT, includes 'max'). Omit to inherit the main agent's thinking level.",
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
97
  })),
98
98
  skillPath: Type.Optional(Type.String()),
99
99
  appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
@@ -113,6 +113,20 @@ const SubagentParams = Type.Object({
113
113
  cwd: Type.Optional(Type.String({
114
114
  description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
115
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
+ })),
116
130
  // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
117
131
  listParam: Type.Optional(Type.Object({
118
132
  includeFinished: Type.Optional(Type.Boolean({
@@ -128,6 +142,29 @@ const SubagentParams = Type.Object({
128
142
  description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
129
143
  }),
130
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
+ })),
131
168
  });
132
169
 
133
170
  // ============================================================
@@ -142,13 +179,13 @@ function assertNever(value: never): string {
142
179
  }
143
180
 
144
181
  /** Subagent action 字面量联合(与 parameters schema 的 StringEnum 取值一致)。 */
145
- type SubagentAction = "start" | "list" | "cancel";
182
+ type SubagentAction = "start" | "list" | "cancel" | "message" | "close";
146
183
 
147
184
  /** 类型守卫:把 schema 投影出的 string 形式 action 收窄回字面量联合。
148
185
  * typebox v1 的 StringEnum Static 退化为 string,需运行时校验 + 类型收窄
149
186
  * 才能恢复 switch 的 exhaustiveness 约束。 */
150
187
  function isSubagentAction(value: string): value is SubagentAction {
151
- return value === "start" || value === "list" || value === "cancel";
188
+ return value === "start" || value === "list" || value === "cancel" || value === "message" || value === "close";
152
189
  }
153
190
 
154
191
  /** unknown 是否为含 model/thinkingLevel 的对象(类型守卫,替代全可选结构 `as`)。 */
@@ -182,26 +219,36 @@ CRITICAL — executionMode "sequential": multiple \`subagent\` calls in the SAME
182
219
 
183
220
  ## When to delegate
184
221
 
185
- Delegate when the task needs a distinct specialized role, context isolation (fork/worktree), or parallelism while you do other work. Delegate FIRST when the task involves any of: reading 3+ files, writing 100+ lines of implementation, parallel research, or specialized review — doing these yourself floods your context with implementation detail and loses the orchestration view.
222
+ Delegate when the task needs a distinct specialized role, context isolation (fork/worktree), or parallelism while you do other work. Delegate FIRST when the task involves any of: reading 3+ files, writing 100+ lines of implementation, parallel research, or specialized review — doing these yourself floods your context.
223
+
224
+ ## Before starting — list first
225
+
226
+ action:"list" before action:"start" — a reusable running subagent may exist; compaction can swallow its id.
186
227
 
187
228
  ## Actions
188
229
 
189
- - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd. Background only: returns a subagentId immediately, notifies on completion.
230
+ - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, conversation, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion.
231
+ - action:"message" — send a follow-up message to a running subagent (conversation-mode or one-shot); it keeps the full context across rounds. REQUIRED messageParam: { subagentId, text }. Optional: interrupt (default false). The reply auto-notifies when the round completes.
232
+ - action:"close" — end a running subagent and release its resources. REQUIRED closeParam: { subagentId }. Optional: force (default false; true terminates mid-round immediately). Always close when done.
190
233
  - action:"list" — list subagents. Pass listParam: { includeFinished?, limit? } (all optional). Read an item's sessionFile for full detail.
191
- - action:"cancel" — cancel a background subagent. REQUIRED cancelParam: { subagentId }.
234
+ - action:"cancel" — stop a background subagent (legacy verb; for conversation-mode use close). REQUIRED cancelParam: { subagentId }.
192
235
 
193
236
  ## Examples
194
237
 
195
238
  \`\`\`
196
239
  {"action":"start","task":"<your task>","slug":"<kebab-case>"}
197
240
  {"action":"start","task":"...","slug":"fix-login","agent":"coder","model":"anthropic/claude-3.5-sonnet","fork":true}
241
+ {"action":"start","task":"review iteratively","slug":"review","conversation":true}
242
+ {"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"now also handle the empty-list case"}}
243
+ {"action":"message","messageParam":{"subagentId":"sa-550e8400","text":"stop, switch direction to X","interrupt":true}}
244
+ {"action":"close","closeParam":{"subagentId":"sa-550e8400"}}
198
245
  {"action":"list","listParam":{"includeFinished":false,"limit":20}}
199
246
  {"action":"cancel","cancelParam":{"subagentId":"sa-550e8400"}}
200
247
  \`\`\`
201
248
 
202
249
  ## After launching — do NOT wait
203
250
 
204
- Completion auto-notifies you (steer wakes next turn, even mid-poll). So:
251
+ Completion auto-notifies you (steer wakes the next turn):
205
252
  - DO NOT sleep, busy-wait, or poll — there is no poll action; use action:"list" only when you concretely need state.
206
253
  - DO useful non-overlapping work, otherwise STOP.
207
254
  - On auto-injected completion: process directly. The notification IS the confirmation — do NOT call action:"list" to re-confirm.
@@ -209,25 +256,39 @@ Completion auto-notifies you (steer wakes next turn, even mid-poll). So:
209
256
 
210
257
  ## Anti-patterns
211
258
 
212
- - Forgetting the REQUIRED top-level task/slug fields for action:"start" — both must be present at the top level (not nested).
259
+ - Forgetting the REQUIRED top-level task/slug fields for action:"start" (not nested).
213
260
  - Over-generalizing the flatten: ONLY start fields are top-level. list and cancel params stay nested under listParam / cancelParam (e.g. {"action":"list","listParam":{"includeFinished":true}}, NOT {"action":"list","includeFinished":true}).
214
261
  - Launching background, then sleeping/polling instead of working or stopping.
215
262
  - Treating subagent results as authoritative without verification.
216
263
  - Canceling by guessing a subagentId instead of using action:"list" first.
217
264
 
265
+ ## Continuous chat (conversation mode)
266
+
267
+ For multi-round work, set conversation:true on start. The subagent stays available across replies — action:"message" continues with full context retained, action:"close" releases it. Always close when finished.
268
+
269
+ When to use:
270
+ - ✅ Multi-round collaboration (review/fix loops) → conversation:true
271
+ - ✅ Long-interval rounds (>5min apart) → conversation:true + idleTimeoutMs increased
272
+ - ❌ Single exploration/lookup → default (one-shot)
273
+
274
+ idleTimeoutMs: per-subagent idle timeout (default 300000 / 5min). Env XYZ_SUBAGENT_IDLE_TIMEOUT_MS sets the global default; per-call param takes precedence.
275
+
218
276
  ## You cannot
219
277
 
220
- - Get a synchronous/inline result — always background, returns a subagentId immediately.
221
- - Pause or resume a subagent (only cancel).
278
+ - Get a synchronous/inline result — start always returns a subagentId immediately (background).
222
279
  - Read mid-flight streaming output — wait for the completion notification.
223
280
 
224
281
  ## Calling patterns
225
282
 
226
- Single (one subagent, one task) is the common case. Chain dependent tasks: send the next start after the prior completion. Run N independent tasks concurrently: send N action:"start" calls in the SAME message — each returns a subagentId at once. Start long tasks and move on; cancel if the direction changes.
283
+ Chain dependent tasks: send the next start after prior completion. Run N independent tasks concurrently: N action:"start" calls in the SAME message. Cancel if direction changes.
284
+
285
+ ## Nested spawning (recursion)
286
+
287
+ A subagent MAY call the \`subagent\` tool itself (depth appears in the environment block as "Depth: N/10"). The hard cap is 10 levels — depth 11 fails as a tool error, NOT a reason to avoid nesting entirely (Do NOT refuse a sub-subagent).
227
288
 
228
- ## Nested spawning
289
+ Recursion is for TREE-SHAPED work only: a task that decomposes naturally into independent, independently-verifiable sub-tasks. Each level's \`task\` must be SELF-CONTAINED — the child does not see your conversation (unless fork:true). Each level must have its own acceptance criteria, or errors compound silently down the chain.
229
290
 
230
- A subagent MAY call the \`subagent\` tool itself (each level spawns its own child process). Nesting depth appears in the environment block ("Depth: N/10") spawn deeper while N < 10; the 11th level fails gracefully. Do NOT refuse a sub-subagent only the depth limit applies.`,
291
+ Do NOT recurse when: the work is linear/flat (use chain or parallel instead); the child needs your context to do the job; or you are delegating the judgment/decision your own level is responsible for. Depth should match the task tree (2-3 levels for most work; deep trees only when the decomposition genuinely demands it) 10 is a safety rail against infinite delegation loops, not a budget to spend. Prefer fork:false in recursion: fork chains copy parent history at every level and blow up context volume linearly.`,
231
292
  executionMode: "sequential",
232
293
  parameters: SubagentParams,
233
294
  renderCall: subagentRenderCall,
@@ -321,6 +382,10 @@ const executeSubagent: SubagentExecuteCb = async (
321
382
  return adapter({ action: "list", domain: listHandler(service, params.listParam) }, toGuiCtx(_ctx));
322
383
  case "cancel":
323
384
  return adapter({ action: "cancel", domain: await cancelHandler(service, params.cancelParam) }, toGuiCtx(_ctx));
385
+ case "message":
386
+ return adapter({ action: "message", domain: await messageHandler(service, params.messageParam) }, toGuiCtx(_ctx));
387
+ case "close":
388
+ return adapter({ action: "close", domain: await closeHandler(service, params.closeParam) }, toGuiCtx(_ctx));
324
389
  default:
325
390
  // assertNever:让 exhaustiveness 成为承重约束——新增 action 时 tsc 报错,
326
391
  // 而非悄悄落入此分支。
@@ -8,6 +8,7 @@
8
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
9
 
10
10
  import { getSubagentService } from "../execution/subagent-service.ts";
11
+ import { displayAgentName } from "../shared/agent-ref.ts";
11
12
  import { parseSubagentRpcCommand } from "./command-actions.ts";
12
13
  import { LIST_LIMIT } from "./list-shared.ts";
13
14
  import { createSubagentsView } from "./list-view.ts";
@@ -39,7 +40,7 @@ export function registerSubagentsCommand(pi: ExtensionAPI): void {
39
40
  return records.map((r) => ({
40
41
  label: r.id,
41
42
  value: r.id,
42
- description: `${r.agent} [${r.status}]`,
43
+ description: `${displayAgentName(r.agent)} [${r.status}]`,
43
44
  }));
44
45
  } catch {
45
46
  // 拿不到运行时数据(service disposed 等)→ 静默降级,补全失败不影响 command
@@ -25,6 +25,7 @@ import type {
25
25
  ListResponse,
26
26
  SubagentToolResult,
27
27
  } from "../execution/types.ts";
28
+ import { displayAgentName } from "../shared/agent-ref.ts";
28
29
  import {
29
30
  extractAgentName,
30
31
  firstLine,
@@ -45,13 +46,19 @@ const STREAM_PREFIX = " ⎿ ";
45
46
  /** footer 用的纯空格缩进(与 STREAM_PREFIX 等宽 4 列,但不带 ⎿)。 */
46
47
  const FOOTER_PREFIX = " ";
47
48
 
49
+ /** process.stdout.columns 不可用(非 TTY 等)时的终端宽度兜底(列)。 */
50
+ const DEFAULT_TERM_WIDTH_COLUMNS = 120;
51
+
52
+ /** 终端宽度扣减(列):Pi Box paddingX=1(左右各 1 列)+ 安全余量。 */
53
+ const TERM_WIDTH_BOX_INSET_COLUMNS = 4;
54
+
48
55
  /**
49
56
  * 获取终端宽度(参照 nicobailon getTermWidth)。
50
57
  * truncLine 需要在创建 Text 之前执行——此时 Pi 的 Box.render(contentWidth) 还未调用,
51
- * 只能从 process.stdout 估算。-4 对应 Pi Box paddingX=1(左右各 1 列)+ 安全余量。
58
+ * 只能从 process.stdout 估算。
52
59
  */
53
60
  function getTermWidth(): number {
54
- return (process.stdout.columns || 120) - 4;
61
+ return (process.stdout.columns || DEFAULT_TERM_WIDTH_COLUMNS) - TERM_WIDTH_BOX_INSET_COLUMNS;
55
62
  }
56
63
 
57
64
  // ============================================================
@@ -75,9 +82,9 @@ export interface RenderContext {
75
82
  // ============================================================
76
83
 
77
84
  /**
78
- * renderCall:tool 标题行(agent + model + thinking,不变信息)。
85
+ * renderCall:tool 标题行(agent + model + thinking 等级,不变信息)。
79
86
  *
80
- * "subagent worker · glm-5.2 · thinking high"
87
+ * "subagent worker · glm-5.2 · high"
81
88
  *
82
89
  * model/thinkingLevel 由调用方(subagent-tool.ts 的闭包)预解析后传入,
83
90
  * 因为 renderCall 在 execute 前调用,但 model 解析是同步的(只读配置)。
@@ -94,11 +101,13 @@ export function renderSubagentCall(
94
101
  const t = theme as ThemeLike;
95
102
  // args 结构(拍平后):{ action:"start", agent, task, slug, ... }(见 subagent-tool.ts schema)。
96
103
  // 13 字段直接在顶层,extractAgentName / slug / task 都从 args 顶层提取,
97
- // 对齐 nicobailon 的 renderCall 多行布局。
98
- const agent = extractAgentName(args);
104
+ // 对齐 nicobailon 的 renderCall 多行布局。agent ref 是绝对路径,显示取 basename 短名
105
+ // (displayAgentName);extractAgentName 原值另被 subagent-tool 的 resolveModel 消费,不动。
106
+ const agent = displayAgentName(extractAgentName(args));
99
107
  // slug:从顶层 args 提取(必填字段),非空时在 agent 后用 · 分隔展示。
108
+ // 断言目标含必填 slug 字段(in 守卫已确认存在),对齐 format.ts extractAgentName 先例。
100
109
  const slug = typeof args === "object" && args !== null && "slug" in args
101
- ? (args as { slug?: unknown }).slug
110
+ ? (args as { slug: unknown }).slug
102
111
  : undefined;
103
112
  const slugStr = typeof slug === "string" ? slug.trim() : "";
104
113
  const parts = slugStr
@@ -111,26 +120,12 @@ export function renderSubagentCall(
111
120
  parts.push(t.fg("dim", " ("));
112
121
  parts.push(t.fg("accent", resolved.model));
113
122
  if (resolved.thinkingLevel) {
114
- parts.push(t.fg("dim", ` · thinking ${resolved.thinkingLevel})`));
123
+ parts.push(t.fg("dim", ` · ${resolved.thinkingLevel})`));
115
124
  } else {
116
125
  parts.push(t.fg("dim", ")"));
117
126
  }
118
127
  }
119
128
 
120
- // task preview 行——对齐 nicobailon:renderCall 输出多行(标题 + \n + task 预览)。
121
- // 实验假设:call 多行让首帧(无 result)与后续帧(有 result)的高度跳变模式
122
- // 与 nicobailon 一致,可能影响 pi diff 引擎的行对齐路径。preview 截断到 60 字符。
123
- const task = typeof args === "object" && args !== null && "task" in args
124
- ? (args as { task?: unknown }).task
125
- : undefined;
126
- if (typeof task === "string" && task.length > 0) {
127
- // task 取首行——prompt 常含换行(多行指令),直接 slice 会保留 \n,
128
- // 渲染时意外换行破坏 tool block 行对齐。
129
- const taskFirst = task.split("\n").find((l) => l.trim())?.trim() ?? "";
130
- const preview = taskFirst.length > 60 ? `${taskFirst.slice(0, 60)}...` : taskFirst;
131
- if (preview) parts.push(`\n ${t.fg("dim", preview)}`);
132
- }
133
-
134
129
  return new Text(parts.join(""), 0, 0);
135
130
  }
136
131
 
@@ -215,10 +210,9 @@ function buildCompactLines(d: SubagentToolResult, theme: ThemeLike): string[] {
215
210
  }
216
211
  // ── start 分支:background ──
217
212
  if ("bgResponse" in d) {
218
- const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
219
213
  return [truncLine(
220
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}${slugPart}`
221
- + ` ${theme.fg("dim", "· running detached · will notify on completion")}`,
214
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
215
+ + ` ${theme.fg("dim", "· detached")}`,
222
216
  width,
223
217
  )];
224
218
  }
@@ -244,9 +238,9 @@ function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
244
238
  const lines: string[] = [];
245
239
  // bg 占位 expanded 与 compact 同(一次性 block 无细节可展开)
246
240
  if ("bgResponse" in d) {
247
- const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
248
241
  lines.push(truncLine(
249
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}${slugPart}`,
242
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
243
+ + ` ${theme.fg("dim", "· detached")}`,
250
244
  width,
251
245
  ));
252
246
  return lines;
@@ -211,8 +211,9 @@ export function registerWorkflowScriptTool(
211
211
  result = await actionList(registry);
212
212
  break;
213
213
  default:
214
- result = textResult(`Unknown action: ${String(params.action)}`, true);
215
- break;
214
+ // 防御性(schema StringEnum 先拦):throw(W4b)——pi 只对 execute throw 置
215
+ // isError:true,返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483)。
216
+ throw new Error(`Unknown action: ${String(params.action)}`);
216
217
  }
217
218
  // GUI 协议:RPC 模式下附加 __gui__ 到 details
218
219
  return withScriptGui(result, toGuiCtx(ctx));
@@ -241,28 +242,27 @@ export function registerWorkflowScriptTool(
241
242
 
242
243
  export function actionGenerate(params: ScriptParams, signal: AbortSignal | undefined): TextContent {
243
244
  if (signal?.aborted) {
244
- return textResult("Operation aborted before start", true);
245
+ // throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)
246
+ throw new Error("Operation aborted before start");
245
247
  }
246
248
  const name = params.name;
247
249
  const script = params.script;
248
250
  if (!name || !script) {
249
- return textResult("generate requires 'name' and 'script' parameters", true);
251
+ throw new Error("generate requires 'name' and 'script' parameters");
250
252
  }
251
253
 
252
254
  // 1. Reject ESM syntax (Worker runs CJS); 'export const meta' 例外
253
255
  const stripped = script.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
254
256
  if (/\bimport\s+(?:type\s+)?[\w{*]/.test(stripped)) {
255
- return textResult(
257
+ throw new Error(
256
258
  "Script uses ESM 'import' syntax. Workflow scripts run in a CJS Worker — use require() instead.",
257
- true,
258
259
  );
259
260
  }
260
261
  const hasExportMeta = /\bexport\s+const\s+meta\s*=/.test(stripped);
261
262
  const otherExports = stripped.match(/\bexport\s+(?:const|let|var|function|default|\{)/g);
262
263
  if (otherExports && !hasExportMeta) {
263
- return textResult(
264
+ throw new Error(
264
265
  "Script uses ESM 'export' (non-meta). Use 'const meta = {...}' at top level instead.",
265
- true,
266
266
  );
267
267
  }
268
268
 
@@ -270,17 +270,15 @@ export function actionGenerate(params: ScriptParams, signal: AbortSignal | undef
270
270
  const hasPiMeta = /\/\*\s*@pi-meta\s*\n/.test(script);
271
271
  const hasLegacyMeta = script.includes("const meta") || script.includes("export const meta");
272
272
  if (!hasPiMeta && !hasLegacyMeta) {
273
- return textResult(
273
+ throw new Error(
274
274
  "Script must contain a meta declaration: a /* @pi-meta */ YAML block comment (preferred) or legacy const meta = { ... }. The block has the form: a block comment starting with /* @pi-meta followed by YAML (name/description/phases/parameters?/usage?), closed by */ on its own line.",
275
- true,
276
275
  );
277
276
  }
278
277
 
279
278
  // 3. Check agent usage
280
279
  if (!/\bagent\s*\(/.test(stripped)) {
281
- return textResult(
280
+ throw new Error(
282
281
  "Script does not contain any agent() calls. A workflow must call agent() at least once.",
283
- true,
284
282
  );
285
283
  }
286
284
 
@@ -290,7 +288,7 @@ export function actionGenerate(params: ScriptParams, signal: AbortSignal | undef
290
288
  new Function(`(async () => { ${cjsScript} })();`);
291
289
  } catch (err: unknown) {
292
290
  const msg = err instanceof Error ? err.message : String(err);
293
- return textResult(`Syntax error in script: ${msg}`, true);
291
+ throw new Error(`Syntax error in script: ${msg}`);
294
292
  }
295
293
 
296
294
  // 4b. Round-trip: validate /* @pi-meta */ YAML before writing (v5 §4.7 / ERR4 — report linePos, don't write bad files)
@@ -300,9 +298,8 @@ export function actionGenerate(params: ScriptParams, signal: AbortSignal | undef
300
298
  const loc = "linePos" in detailed && detailed.linePos
301
299
  ? ` (line ${detailed.linePos.line}, col ${detailed.linePos.col})`
302
300
  : "";
303
- return textResult(
301
+ throw new Error(
304
302
  `Generated /* @pi-meta */ YAML cannot be parsed${loc}: ${detailed.error}. Common causes: YAML indent errors, patternProperties regex must use double backslash (\\d not \d), or a stray star-slash inside the YAML body. Fix the meta block and retry.`,
305
- true,
306
303
  );
307
304
  }
308
305
  }
@@ -332,7 +329,7 @@ async function actionLint(
332
329
  ): Promise<TextContent> {
333
330
  const name = params.name;
334
331
  if (!name) {
335
- return textResult("lint requires 'name' parameter", true);
332
+ throw new Error("lint requires 'name' parameter");
336
333
  }
337
334
  const source = await loadScriptSource(name, registry);
338
335
  if (!source) {
@@ -341,9 +338,8 @@ async function actionLint(
341
338
  const suggestions = available
342
339
  .map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
343
340
  .join("\n");
344
- return textResult(
341
+ throw new Error(
345
342
  `Workflow '${name}' not found or not available.\nAvailable:\n${suggestions || " (none)"}`,
346
- true,
347
343
  );
348
344
  }
349
345
 
@@ -386,7 +382,7 @@ async function loadScriptSource(
386
382
  async function actionSave(params: ScriptParams): Promise<TextContent> {
387
383
  const name = params.name;
388
384
  if (!name) {
389
- return textResult("save requires 'name' parameter (tmp script name)", true);
385
+ throw new Error("save requires 'name' parameter (tmp script name)");
390
386
  }
391
387
  try {
392
388
  const result = await saveWorkflow(name, params.newName);
@@ -395,12 +391,10 @@ async function actionSave(params: ScriptParams): Promise<TextContent> {
395
391
  details: { action: "save", name, ok: true },
396
392
  };
397
393
  } catch (err: unknown) {
394
+ // throw(W4):pi 只对 execute throw 置 isError:true(返回值里的 isError 被
395
+ // agent-loop 丢弃,agent-loop.js:453-483)——文案原样进 toolResult。
398
396
  const msg = err instanceof Error ? err.message : String(err);
399
- return {
400
- content: [{ type: "text", text: `Save failed: ${msg}` }],
401
- details: { action: "save", name, ok: false },
402
- isError: true,
403
- };
397
+ throw new Error(`Save failed: ${msg}`);
404
398
  }
405
399
  }
406
400
 
@@ -413,7 +407,7 @@ function actionDelete(
413
407
  ): TextContent {
414
408
  const name = params.name;
415
409
  if (!name) {
416
- return textResult("delete requires 'name' parameter", true);
410
+ throw new Error("delete requires 'name' parameter");
417
411
  }
418
412
  // deleteWorkflow 内部检查 isRunning(防止删运行中脚本)
419
413
  try {
@@ -425,12 +419,9 @@ function actionDelete(
425
419
  details: { action: "delete", name, ok: true },
426
420
  };
427
421
  } catch (err: unknown) {
422
+ // throw(W4):同 save——pi 契约只有 throw 才置 isError:true。
428
423
  const msg = err instanceof Error ? err.message : String(err);
429
- return {
430
- content: [{ type: "text", text: `Delete failed: ${msg}` }],
431
- details: { action: "delete", name, ok: false },
432
- isError: true,
433
- };
424
+ throw new Error(`Delete failed: ${msg}`);
434
425
  }
435
426
  }
436
427
 
@@ -451,17 +442,22 @@ async function actionList(registry: WorkflowScriptRegistry): Promise<TextContent
451
442
  details: { action: "list", count: available.length },
452
443
  };
453
444
  } catch (err: unknown) {
445
+ // throw(W4b):list 失败改 throw(原 return isError 被 pi 丢弃),文案保持
454
446
  const msg = err instanceof Error ? err.message : String(err);
455
- return textResult(`List failed: ${msg}`, true);
447
+ throw new Error(`List failed: ${msg}`);
456
448
  }
457
449
  }
458
450
 
459
451
  // ── helper ───────────────────────────────────────────────────
460
452
 
461
- function textResult(text: string, isError = false): TextContent {
453
+ /**
454
+ * 构造纯文本非错误结果(W4b:isError 参数已删除——pi 只对 execute throw 置
455
+ * isError:true,返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483),
456
+ * 错误一律 throw,编译器兜底防回潮)。
457
+ */
458
+ function textResult(text: string): TextContent {
462
459
  return {
463
460
  content: [{ type: "text", text }],
464
461
  details: undefined,
465
- isError: isError || undefined,
466
462
  };
467
463
  }