@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.
- package/README.md +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- 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
|
|
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
|
|
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
|
|
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" —
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
58
|
+
* 只能从 process.stdout 估算。
|
|
52
59
|
*/
|
|
53
60
|
function getTermWidth(): number {
|
|
54
|
-
return (process.stdout.columns ||
|
|
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 ·
|
|
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
|
-
|
|
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
|
|
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", ` ·
|
|
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 ?? "?")}
|
|
221
|
-
+ ` ${theme.fg("dim", "·
|
|
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 ?? "?")}
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
447
|
+
throw new Error(`List failed: ${msg}`);
|
|
456
448
|
}
|
|
457
449
|
}
|
|
458
450
|
|
|
459
451
|
// ── helper ───────────────────────────────────────────────────
|
|
460
452
|
|
|
461
|
-
|
|
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
|
}
|