@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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 +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -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
- {"action":"start","task":"...","slug":"fix-login","agent":"worker","model":"anthropic/claude-3.5-sonnet","fork":true}
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,
@@ -75,9 +76,9 @@ export interface RenderContext {
75
76
  // ============================================================
76
77
 
77
78
  /**
78
- * renderCall:tool 标题行(agent + model + thinking,不变信息)。
79
+ * renderCall:tool 标题行(agent + model + thinking 等级,不变信息)。
79
80
  *
80
- * "subagent worker · glm-5.2 · thinking high"
81
+ * "subagent worker · glm-5.2 · high"
81
82
  *
82
83
  * model/thinkingLevel 由调用方(subagent-tool.ts 的闭包)预解析后传入,
83
84
  * 因为 renderCall 在 execute 前调用,但 model 解析是同步的(只读配置)。
@@ -94,8 +95,9 @@ export function renderSubagentCall(
94
95
  const t = theme as ThemeLike;
95
96
  // args 结构(拍平后):{ action:"start", agent, task, slug, ... }(见 subagent-tool.ts schema)。
96
97
  // 13 字段直接在顶层,extractAgentName / slug / task 都从 args 顶层提取,
97
- // 对齐 nicobailon 的 renderCall 多行布局。
98
- const agent = extractAgentName(args);
98
+ // 对齐 nicobailon 的 renderCall 多行布局。agent ref 是绝对路径,显示取 basename 短名
99
+ // (displayAgentName);extractAgentName 原值另被 subagent-tool 的 resolveModel 消费,不动。
100
+ const agent = displayAgentName(extractAgentName(args));
99
101
  // slug:从顶层 args 提取(必填字段),非空时在 agent 后用 · 分隔展示。
100
102
  const slug = typeof args === "object" && args !== null && "slug" in args
101
103
  ? (args as { slug?: unknown }).slug
@@ -111,26 +113,12 @@ export function renderSubagentCall(
111
113
  parts.push(t.fg("dim", " ("));
112
114
  parts.push(t.fg("accent", resolved.model));
113
115
  if (resolved.thinkingLevel) {
114
- parts.push(t.fg("dim", ` · thinking ${resolved.thinkingLevel})`));
116
+ parts.push(t.fg("dim", ` · ${resolved.thinkingLevel})`));
115
117
  } else {
116
118
  parts.push(t.fg("dim", ")"));
117
119
  }
118
120
  }
119
121
 
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
122
  return new Text(parts.join(""), 0, 0);
135
123
  }
136
124
 
@@ -215,10 +203,9 @@ function buildCompactLines(d: SubagentToolResult, theme: ThemeLike): string[] {
215
203
  }
216
204
  // ── start 分支:background ──
217
205
  if ("bgResponse" in d) {
218
- const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
219
206
  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")}`,
207
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
208
+ + ` ${theme.fg("dim", "· detached")}`,
222
209
  width,
223
210
  )];
224
211
  }
@@ -244,9 +231,9 @@ function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
244
231
  const lines: string[] = [];
245
232
  // bg 占位 expanded 与 compact 同(一次性 block 无细节可展开)
246
233
  if ("bgResponse" in d) {
247
- const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
248
234
  lines.push(truncLine(
249
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}${slugPart}`,
235
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
236
+ + ` ${theme.fg("dim", "· detached")}`,
250
237
  width,
251
238
  ));
252
239
  return lines;
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Workflow Extension — workflow tool(5 actions,FR-5 tool 收口)。
2
+ * Workflow Extension — workflow tool(3 actions,FR-5 tool 收口)。
3
3
  *
4
4
  * 合并原 tool-workflow.ts + tool-workflow-run.ts 为单 tool。
5
5
  *
6
6
  * Actions:
7
7
  * - run: registry.get → runWorkflow(直接启动,无需用户确认)
8
8
  * - status: 列出 runs(deps.runs)
9
- * - pause: 调 pauseRun
10
- * - resume: 调 resumeRun
11
9
  * - abort: 调 abortRun
12
10
  *
13
- * **restart 不包含**(D-9 废弃)。
11
+ * **restart 不包含**(D-9 废弃);**pause/resume 不包含**(一次性生命周期——run
12
+ * 不可挂起,提前停止用 abort,要新结果开新 run)。
14
13
  *
15
14
  * 层归属:Interface。依赖 Pi SDK + Engine lifecycle/launcher + helpers。
16
15
  *
@@ -36,7 +35,7 @@ import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
36
35
  import { THINKING_ORDER } from "../execution/model-resolver.ts";
37
36
  import type { LauncherDeps } from "../orchestration/launcher.ts";
38
37
  import { ArgsValidationError } from "../orchestration/args-validator.ts";
39
- import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
38
+ import { abortRun, runWorkflow } from "../orchestration/lifecycle.ts";
40
39
  import type { RunStore } from "../orchestration/models/ports.ts";
41
40
  import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
42
41
  import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
@@ -54,15 +53,11 @@ import { formatElapsed, renderTextFallback } from "./views/format.ts";
54
53
  export type WorkflowAction =
55
54
  | "run"
56
55
  | "status"
57
- | "pause"
58
- | "resume"
59
56
  | "abort";
60
57
 
61
58
  const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
62
59
  "run",
63
60
  "status",
64
- "pause",
65
- "resume",
66
61
  "abort",
67
62
  ];
68
63
 
@@ -80,7 +75,7 @@ const WorkflowParams = Type.Object({
80
75
  }),
81
76
  ),
82
77
  runId: Type.Optional(
83
- Type.String({ description: "Workflow run ID (pause/resume/abort)" }),
78
+ Type.String({ description: "Workflow run ID (abort action)" }),
84
79
  ),
85
80
  args: Type.Optional(
86
81
  Type.Record(Type.String(), Type.Unknown(), {
@@ -96,7 +91,7 @@ const WorkflowParams = Type.Object({
96
91
  description: "Run-level model override in 'provider/modelId' format. When set, all agents spawned by this run inherit it by default (unless a per-call agent() opts.model is set). Omit to inherit the main agent's model.",
97
92
  })),
98
93
  thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
99
- description: "Run-level thinkingLevel override (off/minimal/low/medium/high/xhigh/max). All agents in this run inherit it by default. Omit to inherit the main agent's thinking level.",
94
+ description: "Run-level thinkingLevel override (off/minimal/low/medium/high/xhigh/max). All agents in this run inherit it by default. Omit to default each agent to its model's highest available level.",
100
95
  })),
101
96
  });
102
97
 
@@ -229,7 +224,7 @@ interface RunSummary {
229
224
  export type WorkflowToolDetails =
230
225
  | { action: "run"; runId: string; status: "running" | "not_found" | "invalid_args"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
231
226
  | { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
232
- | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
227
+ | { action: "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
233
228
 
234
229
  /** Result returned by the `workflow` tool's execute. */
235
230
  export interface ToolResult {
@@ -283,14 +278,12 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
283
278
  }),
284
279
  });
285
280
  }
286
- // pause/resume/abort
287
- // abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume 保留 ok
288
- const severity = details.action === "abort" || details.action === "pause" ? "warn" as const : "ok" as const;
281
+ // abort(唯一 lifecycle action):破坏性终止非成功完成,用 warn 与成功区分
289
282
  return guiComponent("stats-line", {
290
283
  items: [{
291
284
  label: details.action,
292
285
  value: details.runId.slice(0, RUNID_SHORT),
293
- severity,
286
+ severity: "warn" as const,
294
287
  }],
295
288
  });
296
289
  }
@@ -298,7 +291,8 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
298
291
  // ── Tool registration ────────────────────────────────────────
299
292
 
300
293
  /**
301
- * 注册 workflow tool(5 actions: run / status / pause / resume / abort)。
294
+ * 注册 workflow tool(3 actions: run / status / abort;pause/resume 已随一次性
295
+ * 生命周期移除——enum 拒绝由 pi 核心校验拦截,见 F3)。
302
296
  *
303
297
  * @param pi ExtensionAPI
304
298
  * @param deps LauncherDeps(LifecycleDeps + registry)
@@ -318,9 +312,9 @@ export function registerWorkflowTool(
318
312
  name: "workflow",
319
313
  label: "Workflow",
320
314
  description:
321
- "Execute and control workflows: run (start), status, pause, resume, abort.\n" +
315
+ "Execute and control workflows: run (start), status, abort.\n" +
322
316
  "Replaces workflow + workflow-run tools.",
323
- promptSnippet: "Run, pause, resume, abort, or check workflow status",
317
+ promptSnippet: "Run, abort, or check workflow status",
324
318
  promptGuidelines: [
325
319
  "PRIORITY: When user says 'workflow', 'run workflow', try run action FIRST.",
326
320
  "All listed workflows run DIRECTLY with action:run — refs/descriptions come from " +
@@ -329,10 +323,11 @@ export function registerWorkflowTool(
329
323
  "workflow-script generate for patterns already covered by available workflows.",
330
324
  "run: pass the absolute .js path from <available_workflows> <location> as name, then start in background (no user confirmation needed).",
331
325
  "Do NOT poll status after starting — results appear automatically via notifyDone.",
326
+ "Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.",
332
327
  "Call shapes (JSON): " +
333
328
  "- run: {\"action\":\"run\",\"name\":\"<script>\",\"args\":{...},\"tokens\":N,\"time\":N,\"model\":\"<provider/modelId>\",\"thinkingLevel\":\"<level>\"}. " +
334
329
  "- status: {\"action\":\"status\"}. " +
335
- "- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}).",
330
+ "- abort: {\"action\":\"abort\",\"runId\":\"<id>\"} (optional: {\"error\":\"<reason>\"}).",
336
331
  "Budget: Do NOT set tokens/time unless the user explicitly requests a limit. Built-in workflows run unlimited by default.",
337
332
  "Model/thinkingLevel: omit by default (inherit main agent's model). Only set model/thinkingLevel when the user explicitly requests a specific model or thinking depth for this run.",
338
333
  "Anti-patterns: Flattening args sub-fields (task/items/...) to the top level — they belong inside args. Calling {\"action\":\"run\"} without name.",
@@ -369,12 +364,6 @@ export function registerWorkflowTool(
369
364
  case "status":
370
365
  result = actionStatus(deps);
371
366
  break;
372
- case "pause":
373
- result = await actionLifecycle("pause", params, deps);
374
- break;
375
- case "resume":
376
- result = await actionLifecycle("resume", params, deps);
377
- break;
378
367
  case "abort":
379
368
  result = await actionLifecycle("abort", params, deps);
380
369
  break;
@@ -551,10 +540,12 @@ function actionStatus(deps: LauncherDeps): ToolResult {
551
540
  };
552
541
  }
553
542
 
554
- // ── pause/resume/abort lifecycle actions ─────────────────────
543
+ // ── abort lifecycle action ────────────────────────────────────
555
544
 
545
+ // 一次性生命周期:abort 是唯一的提前停止方式(pause/resume 已随 D-2 移除),
546
+ // action 参数保留字面量类型与 WorkflowAction 单成员分发对齐。
556
547
  async function actionLifecycle(
557
- action: "pause" | "resume" | "abort",
548
+ action: "abort",
558
549
  params: WorkflowToolParams,
559
550
  deps: LauncherDeps,
560
551
  ): Promise<ToolResult> {
@@ -571,13 +562,7 @@ async function actionLifecycle(
571
562
  }
572
563
  try {
573
564
  const oldStatus = run.state.status;
574
- if (action === "pause") {
575
- await pauseRun(runId, deps);
576
- } else if (action === "resume") {
577
- await resumeRun(runId, deps);
578
- } else {
579
- await abortRun(runId, deps, params.error);
580
- }
565
+ await abortRun(runId, deps, params.error);
581
566
  const newStatus = run.state.status;
582
567
  const reasonSuffix = run.state.reason ? ` (${run.state.reason})` : "";
583
568
  return {