@zhushanwen/pi-subagent-workflow 0.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 (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,294 @@
1
+ // src/interface/subagent-actions.ts
2
+ //
3
+ // subagent tool 的内部 handler + 唯一 adapter。
4
+ //
5
+ // 分层(spec FR-2):
6
+ // 1. startHandler / listHandler / cancelHandler —— 纯领域对象进出,不碰 {content, details}
7
+ // 2. adapter(action, 领域对象) —— 唯一包装为 AgentToolResult<SubagentToolResult>
8
+ //
9
+ // content(JSON 字符串)给 LLM,details(SubagentToolResult)给 renderResult,同源同处生成。
10
+
11
+ import type { AgentToolResult } from "@mariozechner/pi-coding-agent";
12
+
13
+ import { computeElapsedSeconds } from "../execution/execution-record.ts";
14
+ import type { ModelInfo } from "../execution/model-resolver.ts";
15
+ import type { SubagentService } from "../execution/subagent-service.ts";
16
+ import type {
17
+ BgResponse,
18
+ CancelResponse,
19
+ ListResponse,
20
+ SubagentListItem,
21
+ SubagentRecord,
22
+ SubagentToolResult,
23
+ } from "../execution/types.ts";
24
+ import {
25
+ guiComponent,
26
+ type GuiContext,
27
+ guiResult,
28
+ isGuiCapable,
29
+ } from "./gui-adapter.ts";
30
+
31
+ // ============================================================
32
+ // 常量
33
+ // ============================================================
34
+
35
+ /** list 默认 limit。 */
36
+ const DEFAULT_LIST_LIMIT = 20;
37
+ /** list limit 上限。 */
38
+ const MAX_LIST_LIMIT = 100;
39
+
40
+ /** background 启动提示文案(spec FR-3 bgResponse.message)。 */
41
+ const BG_MESSAGE = "detached, will notify on completion";
42
+
43
+ // ============================================================
44
+ // 入参 / 出参类型
45
+ // ============================================================
46
+
47
+ /** start 入参(从 tool params.startParam 来,task 必填)。 */
48
+ export interface StartHandlerInput {
49
+ task?: string;
50
+ agent?: string;
51
+ model?: string;
52
+ thinkingLevel?: string;
53
+ skillPath?: string;
54
+ appendSystemPrompt?: string[];
55
+ schema?: Record<string, unknown>;
56
+ maxTurns?: number;
57
+ graceTurns?: number;
58
+ /** fork 模式:继承主 session 上下文(D-018 两级降级)。 */
59
+ fork?: boolean;
60
+ /** worktree 模式:文件系统隔离运行(D-008 tmpdir)。 */
61
+ worktree?: boolean;
62
+ /** 覆盖子 agent 工作目录(默认 mainCwd)。 */
63
+ cwd?: string;
64
+ }
65
+
66
+ /** start 领域对象(adapter 包成 bgResponse)。 */
67
+ export type StartHandlerResult = {
68
+ kind: "bg";
69
+ subagentId: string;
70
+ sessionFile: string | undefined;
71
+ response: BgResponse;
72
+ };
73
+
74
+ export interface ListHandlerInput {
75
+ includeFinished?: boolean;
76
+ limit?: number;
77
+ }
78
+
79
+ /** list 领域对象(adapter 包成 listResponse,最外层 subagentId/sessionFile 为 null)。 */
80
+ export interface ListHandlerResult {
81
+ response: ListResponse;
82
+ }
83
+
84
+ export interface CancelHandlerInput {
85
+ subagentId?: string;
86
+ }
87
+
88
+ /** cancel 领域对象(adapter 包成 cancelResponse)。 */
89
+ export interface CancelHandlerResult {
90
+ subagentId: string;
91
+ response: CancelResponse;
92
+ }
93
+
94
+ // ============================================================
95
+ // helpers(模块内)
96
+ // ============================================================
97
+
98
+ /**
99
+ * list 数据源(诚实声明 G3-003):
100
+ * collectRecords(limit, statusFilter) 合并内存(running) + 磁盘(sessions/*.jsonl 重建)。
101
+ * 磁盘源天然跨 session 可见——/new /resume /fork 后前 session 的终态 record 仍在
102
+ * sessions 目录里(直到 30 天 GC)。内存源仅当前 session 的 running record。
103
+ * 不新增 sessionId 到 ExecutionRecord(YAGNI,修跨 session 清理是独立问题)。
104
+ */
105
+
106
+ /** SubagentRecord → SubagentListItem(8 字段,duration 实时计算)。 */
107
+ function recordToListItem(r: SubagentRecord): SubagentListItem {
108
+ return {
109
+ subagentId: r.id,
110
+ agent: r.agent,
111
+ status: r.status,
112
+ mode: r.mode,
113
+ duration: computeElapsedSeconds(r),
114
+ model: r.model,
115
+ totalTokens: r.totalTokens,
116
+ sessionFile: r.sessionFile,
117
+ };
118
+ }
119
+
120
+ // ============================================================
121
+ // start handler
122
+ // ============================================================
123
+
124
+ export async function startHandler(
125
+ service: SubagentService,
126
+ input: StartHandlerInput | undefined,
127
+ signal: AbortSignal | undefined,
128
+ ctxModel?: ModelInfo,
129
+ ): Promise<StartHandlerResult> {
130
+ if (!input) throw new Error("startParam is required for action:'start'");
131
+ // task 必填 + 空白校验(G-008)
132
+ const task = input.task?.trim();
133
+ if (!task) throw new Error("startParam.task is required (and must not be whitespace-only)");
134
+
135
+ const handle = await service.execute({
136
+ task,
137
+ agent: input.agent,
138
+ model: input.model,
139
+ thinkingLevel: input.thinkingLevel,
140
+ skillPath: input.skillPath,
141
+ appendSystemPrompt: input.appendSystemPrompt,
142
+ schema: input.schema,
143
+ maxTurns: input.maxTurns,
144
+ graceTurns: input.graceTurns,
145
+ fork: input.fork,
146
+ worktree: input.worktree,
147
+ cwd: input.cwd,
148
+ ctxModel,
149
+ signal,
150
+ // background 不回流 onUpdate:detached 运行,完成由 notify 驱动新 turn。
151
+ onUpdate: undefined,
152
+ });
153
+
154
+ return {
155
+ kind: "bg",
156
+ subagentId: handle.subagentId,
157
+ sessionFile: handle.sessionFile,
158
+ response: {
159
+ status: "running",
160
+ mode: "background",
161
+ message: BG_MESSAGE,
162
+ },
163
+ };
164
+ }
165
+
166
+ // ============================================================
167
+ // list handler
168
+ // ============================================================
169
+
170
+ export function listHandler(
171
+ service: SubagentService,
172
+ input: ListHandlerInput | undefined,
173
+ ): ListHandlerResult {
174
+ const includeFinished = input?.includeFinished === true;
175
+ // limit 夹紧:默认 20,范围 [1, 100]
176
+ const rawLimit = input?.limit ?? DEFAULT_LIST_LIMIT;
177
+ const limit = Math.max(1, Math.min(rawLimit, MAX_LIST_LIMIT));
178
+
179
+ // collectRecords 是 service 核心能力:statusFilter 决定 running-only 还是全部。
180
+ // 防截断(先多取再过滤)已下沉到 store 层——这里直接传 limit + filter。
181
+ const filter = includeFinished ? "all" : "running";
182
+ const all = service.collectRecords(limit, filter);
183
+ const items: SubagentListItem[] = all.map(recordToListItem);
184
+ const running = items.filter((i) => i.status === "running").length;
185
+
186
+ return { response: { running, items } };
187
+ }
188
+
189
+ // ============================================================
190
+ // cancel handler
191
+ // ============================================================
192
+
193
+ export async function cancelHandler(
194
+ service: SubagentService,
195
+ input: CancelHandlerInput | undefined,
196
+ ): Promise<CancelHandlerResult> {
197
+ const id = input?.subagentId?.trim();
198
+ if (!id) throw new Error("cancelParam.subagentId is required for action:'cancel'");
199
+
200
+ // step 1: id 不存在(findRecord 只查内存 running record,不从 session.jsonl 重建)
201
+ const rec = service.findRecord(id);
202
+ if (!rec) throw new Error(`No subagent record with id "${id}"`);
203
+ // step 2: controller 检查(controller 为 undefined 表示 record 已终态或未启动)
204
+ if (rec.mode !== "background") {
205
+ throw new Error(`Cannot cancel subagent ${id} (unsupported mode: ${rec.mode})`);
206
+ }
207
+ // step 3: service.cancel boolean(list-view 契约不变);false = 已终态(CAS 抢锁失败)。
208
+ // 注意:不嵌入 rec.status——findRecord 快照可能已过期(TOCTOU:cancel 期间 detached
209
+ // 路径 CAS 到 done/failed)。重新查当前状态,避免「status: running」与「already finished」矛盾。
210
+ if (!service.cancel(id)) {
211
+ // CAS 失败 = record 在 cancel 期间被 detached 路径 finalize(done/failed)。
212
+ // re-query 查当前真实状态。终态 record 被 archive 立即移出内存,
213
+ // 诚实报告 "unknown (evicted from memory)" 而非回落到可能过期的 rec.status(BL-3)。
214
+ const now = service.findRecord(id);
215
+ const statusDesc = now ? now.status : "unknown (evicted from memory)";
216
+ throw new Error(`Subagent ${id} could not be cancelled (it likely just finished; status: ${statusDesc})`);
217
+ }
218
+ return { subagentId: id, response: { cancelled: true } };
219
+ }
220
+
221
+ // ============================================================
222
+ // adapter(领域对象 → SubagentToolResult + {content, details})
223
+ // ============================================================
224
+
225
+ /**
226
+ * action ↔ domain 配对的承重类型(替代三处松散 `as`)。
227
+ * 调用方必须传匹配的 {action, domain}——TS 在调用点校验,错配编译报错。
228
+ */
229
+ type AdapterInput =
230
+ | { action: "start"; domain: StartHandlerResult }
231
+ | { action: "list"; domain: ListHandlerResult }
232
+ | { action: "cancel"; domain: CancelHandlerResult };
233
+
234
+ export function adapter(
235
+ input: AdapterInput,
236
+ ctx?: GuiContext,
237
+ ): AgentToolResult<SubagentToolResult> {
238
+ const { action } = input;
239
+ let result: SubagentToolResult;
240
+ if (action === "start") {
241
+ const d = input.domain;
242
+ result = { action, subagentId: d.subagentId, sessionFile: d.sessionFile ?? null, bgResponse: d.response };
243
+ } else if (action === "list") {
244
+ result = { action, subagentId: null, sessionFile: null, listResponse: input.domain.response };
245
+ } else {
246
+ result = { action, subagentId: input.domain.subagentId, sessionFile: null, cancelResponse: input.domain.response };
247
+ }
248
+
249
+ // content JSON:LLM 看的结构化结果(schema 模式 parsedOutput 作为嵌套 JSON 值可接受)。
250
+ const text = JSON.stringify(result);
251
+
252
+ // GUI 协议:RPC 模式下附加结构化渲染数据
253
+ const details: Record<string, unknown> = { ...result };
254
+ if (ctx && isGuiCapable(ctx)) {
255
+ details.__gui__ = guiResult(buildGuiComponent(action, input, result));
256
+ }
257
+
258
+ return {
259
+ content: [{ type: "text", text }],
260
+ details: details as unknown as SubagentToolResult,
261
+ };
262
+ }
263
+
264
+ /** 按 action 构造对应的 GuiComponent。 */
265
+ function buildGuiComponent(
266
+ action: string,
267
+ input: AdapterInput,
268
+ _result: SubagentToolResult,
269
+ ) {
270
+ if (action === "start") {
271
+ return guiComponent("subagent-trace", {
272
+ agent: "subagent",
273
+ status: "running" as const,
274
+ });
275
+ }
276
+ if (action === "list") {
277
+ const listResp = input.domain as ListHandlerResult;
278
+ return guiComponent("task-list", {
279
+ title: `Subagents (${listResp.response.running} running)`,
280
+ items: listResp.response.items.map((it) => ({
281
+ label: `${it.agent} · ${it.subagentId}`,
282
+ status: it.status === "running" ? "in_progress" as const
283
+ : it.status === "done" ? "completed" as const
284
+ : it.status === "failed" ? "failed" as const
285
+ : "pending" as const,
286
+ })),
287
+ summary: `${listResp.response.running}/${listResp.response.items.length} running`,
288
+ });
289
+ }
290
+ // cancel
291
+ return guiComponent("stats-line", {
292
+ items: [{ label: "cancelled", value: (input.domain as CancelHandlerResult).subagentId, severity: "warn" }],
293
+ });
294
+ }
@@ -0,0 +1,294 @@
1
+ // src/interface/subagent-tool.ts
2
+ //
3
+ // `subagent` LLM 工具。薄壳——参数解析 + 调 runtime.execute。
4
+ // 不创建 state、不节流 onUpdate、不持久化(全部在 runtime 层统一)。
5
+ //
6
+ // 设计说明:renderCall/renderResult/execute 三个回调均抽成模块级 const +
7
+ // 顶层 type alias。原因:stub 的 registerTool(tool: unknown) 参数是 unknown,
8
+ // 在其对象字面量内直接标注从 pi-coding-agent 导入的泛型(AgentToolResult<X>、
9
+ // Theme、ExtensionContext)会触发 TS2307 误报(probe5d/5f 验证)。
10
+ // 抽到顶层后参数类型由 alias 提供,绕过该 quirk。
11
+
12
+ import type { Component } from "@earendil-works/pi-tui";
13
+ import { StringEnum } from "@mariozechner/pi-ai";
14
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
15
+ import { Type } from "@sinclair/typebox";
16
+
17
+ import { getSubagentService } from "../execution/subagent-service.ts";
18
+ import type { SubagentToolResult } from "../execution/types.ts";
19
+ import { extractAgentName } from "./format.ts";
20
+ import { adapter, cancelHandler, listHandler, startHandler } from "./subagent-actions.ts";
21
+ import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
22
+
23
+ // ============================================================
24
+ // 回调类型(抽 alias 绕 registerTool(unknown) 的 TS2307 误报)
25
+ // ============================================================
26
+
27
+ /**
28
+ * execute 回调的 params 类型(手写副本——stub registerTool 是 unknown,
29
+ * 无法从 SubagentParams schema 反向推断参数类型)。
30
+ * action 与对应 param 不匹配时 handler 内 throw。
31
+ */
32
+ interface StartParam {
33
+ task: string;
34
+ agent?: string;
35
+ model?: string;
36
+ thinkingLevel?: string;
37
+ skillPath?: string;
38
+ appendSystemPrompt?: string[];
39
+ schema?: Record<string, unknown>;
40
+ maxTurns?: number;
41
+ graceTurns?: number;
42
+ fork?: boolean;
43
+ worktree?: boolean;
44
+ cwd?: string;
45
+ }
46
+
47
+ interface ListParam {
48
+ includeFinished?: boolean;
49
+ limit?: number;
50
+ }
51
+
52
+ interface CancelParam {
53
+ subagentId: string;
54
+ }
55
+
56
+ interface SubagentExecuteParams {
57
+ action: "start" | "list" | "cancel";
58
+ startParam?: StartParam;
59
+ listParam?: ListParam;
60
+ cancelParam?: CancelParam;
61
+ }
62
+
63
+ type SubagentExecuteCb = (
64
+ toolCallId: string,
65
+ params: SubagentExecuteParams,
66
+ signal: AbortSignal | undefined,
67
+ onUpdate?: (partialResult: AgentToolResult<SubagentToolResult>) => void,
68
+ // ctx 在 SDK 契约里必填;此处保持 optional 以兼容 onUpdate? 在前(TS 参数顺序约束),
69
+ // 结构兼容——registerTool(unknown) 不校验,运行时 SDK 必传入。
70
+ ctx?: ExtensionContext,
71
+ ) => Promise<AgentToolResult<SubagentToolResult>>;
72
+
73
+ type SubagentRenderCallCb = (args: unknown, theme: Theme, ctx: RenderContext) => Component;
74
+
75
+ type SubagentRenderResultCb = (
76
+ result: AgentToolResult<SubagentToolResult>,
77
+ options: { expanded: boolean; isPartial: boolean },
78
+ theme: Theme,
79
+ ctx: RenderContext,
80
+ ) => Component;
81
+
82
+ // ============================================================
83
+ // Params schema
84
+ // ============================================================
85
+
86
+ /** Params schema(模块内消费,未导出)。 */
87
+ const SubagentParams = Type.Object({
88
+ action: StringEnum(["start", "list", "cancel"], {
89
+ description: "Operation: 'start' runs a subagent, 'list' shows running subagents (optional includeFinished), 'cancel' stops a background subagent by id.",
90
+ }),
91
+ startParam: Type.Optional(Type.Object({
92
+ task: Type.String({
93
+ description: "The task for the subagent to execute (required for action:'start'). Whitespace-only is rejected.",
94
+ }),
95
+ agent: Type.Optional(Type.String({
96
+ description: 'Agent name (system prompt + tools). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Available: general-purpose (default fallback), worker, researcher, scout, planner, reviewer, oracle, context-builder. Custom agents configurable.',
97
+ })),
98
+ model: Type.Optional(Type.String({
99
+ 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.',
100
+ })),
101
+ thinkingLevel: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const)),
102
+ skillPath: Type.Optional(Type.String()),
103
+ appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
104
+ schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
105
+ maxTurns: Type.Optional(Type.Number({
106
+ description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
107
+ })),
108
+ graceTurns: Type.Optional(Type.Number({
109
+ description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
110
+ })),
111
+ fork: Type.Optional(Type.Boolean({
112
+ description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing. Use worktree:true (requires fork:true) for file-system isolation.",
113
+ })),
114
+ worktree: Type.Optional(Type.Boolean({
115
+ description: "Worktree isolation (requires fork:true): run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session. Prevents concurrent file-write conflicts between parent and subagent. Only takes effect when fork:true; passing worktree:true without fork:true throws an error.",
116
+ })),
117
+ cwd: Type.Optional(Type.String({
118
+ description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
119
+ })),
120
+ })),
121
+ listParam: Type.Optional(Type.Object({
122
+ includeFinished: Type.Optional(Type.Boolean({
123
+ description: "Include finished (done/failed/cancelled) records. Default false (running only).",
124
+ })),
125
+ limit: Type.Optional(Type.Number({
126
+ description: "Max items to return. Default 20, clamped to [1, 100].",
127
+ })),
128
+ })),
129
+ cancelParam: Type.Optional(Type.Object({
130
+ subagentId: Type.String({
131
+ description: "The subagentId to cancel (required for action:'cancel'). Only background subagents can be cancelled.",
132
+ }),
133
+ })),
134
+ });
135
+
136
+ // ============================================================
137
+ // renderCall 预解析 helper
138
+ // ============================================================
139
+
140
+ // extractAgentName 已上移到 ../tui/format.ts 共享(tool-render / subagent-tool 复用)。
141
+
142
+ /** exhaustiveness 承重 helper:default 分支把 action 收敛为 never,新增 action 时 tsc 报错。 */
143
+ function assertNever(value: never): string {
144
+ return String(value);
145
+ }
146
+
147
+ /** unknown 是否为含 model/thinkingLevel 的对象(类型守卫,替代全可选结构 `as`)。 */
148
+ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?: unknown } {
149
+ return typeof a === "object" && a !== null;
150
+ }
151
+
152
+ /** unknown args 是否含 startParam(类型守卫,替代 `in` 后的 `as`)。 */
153
+ function hasStartParam(a: unknown): a is { startParam?: unknown } {
154
+ return typeof a === "object" && a !== null && "startParam" in a;
155
+ }
156
+
157
+ /** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。 */
158
+ function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
159
+ if (!isModelOverrideObj(args)) return undefined;
160
+ const override: { model?: string; thinkingLevel?: string } = {};
161
+ if (typeof args.model === "string" && args.model.length > 0) override.model = args.model;
162
+ if (typeof args.thinkingLevel === "string" && args.thinkingLevel.length > 0) override.thinkingLevel = args.thinkingLevel;
163
+ return Object.keys(override).length > 0 ? override : undefined;
164
+ }
165
+
166
+ // ============================================================
167
+ // 注册
168
+ // ============================================================
169
+
170
+ /** 注册 `subagent` 工具。由工厂调用。 */
171
+ export function registerSubagentTool(pi: ExtensionAPI): void {
172
+ pi.registerTool({
173
+ name: "subagent",
174
+ label: "Subagent",
175
+ description: `Delegate a task to a specialized subagent via an explicit action.
176
+
177
+ CRITICAL — this tool is registered with executionMode "sequential": multiple \`subagent\` calls in the SAME message run one-after-another, NOT in parallel. The first must finish before the next starts. To get real concurrency, all start actions run in background mode — background calls return immediately and the underlying tasks run concurrently in the pool (default maxConcurrent=6; extras queue).
178
+
179
+ ## Actions
180
+
181
+ - action:"start" — run a subagent. Pass startParam: { task, agent?, ... }. The subagent always runs in background: it returns a subagentId immediately, runs detached, and keeps running even if you stop. On completion a message is auto-injected that triggers a new turn so you can process the result.
182
+ - action:"list" — list subagents. Pass listParam: { includeFinished?: boolean, limit?: number }. Default: running only, limit 20. Each item includes a sessionFile path — read it with the \`read\` tool for full detail (the jsonl is append-only, flushed in real time). Ignores startParam/cancelParam.
183
+ - action:"cancel" — cancel a background subagent. Pass cancelParam: { subagentId }. Only background subagents can be cancelled. Ignores startParam/listParam.
184
+
185
+ ## After launching — do NOT wait
186
+
187
+ Completion auto-notifies you (a message is injected that wakes your next turn). So:
188
+ - DO NOT sleep, busy-wait, or poll in a loop after launching. There is no poll action — use action:"list" only when you concretely need the current state.
189
+ - DO useful non-overlapping work if you have any.
190
+ - Otherwise STOP. Stopping is correct — the completion notification will wake you. It is not giving up.
191
+
192
+ ## Calling patterns
193
+
194
+ - single — one subagent for one task (the common case).
195
+ - chain — dependent steps where B needs A's output: send the next start only after A's completion notification.
196
+ - parallel / fan-out — N independent tasks concurrently: send N \`subagent\` calls with action:"start" in the SAME message. Each returns a subagentId at once; tasks run concurrently. Then do other work, or just stop.
197
+ - background — one long-running task you don't want to block on: action:"start", then move on. Cancel later with action:"cancel" if the direction is wrong.
198
+
199
+ ## Anti-patterns
200
+
201
+ - Launching background, then sleeping/polling instead of working or stopping.
202
+
203
+ ## Nested spawning
204
+
205
+ A subagent MAY itself call the \`subagent\` tool (nested delegation is supported; each level spawns its own child process). A subagent sees its nesting depth in the environment block ("Depth: N/10") — you may spawn deeper while N < 10. The 11th nesting level is refused with a clear "nesting depth 11 > 10" or "fork depth 10 >= 10" error and fails the subagent gracefully (does not crash the parent). Do NOT refuse to spawn a sub-subagent by assuming it is disallowed — it is not; only the depth limit applies.`,
206
+ executionMode: "sequential",
207
+ parameters: SubagentParams,
208
+ renderCall: subagentRenderCall,
209
+ renderResult: subagentRenderResult,
210
+ execute: executeSubagent,
211
+ });
212
+ }
213
+
214
+ // ============================================================
215
+ // 回调实现(模块级 const)
216
+ // ============================================================
217
+
218
+ // ponytail: renderCall 每次 TUI invalidate 都触发,同一解析错误会重复刷屏。
219
+ // 按错误消息去重(Set),session 内只报第一次。错误消息含 modelStr,足够区分。
220
+ const reportedRenderErrors = new Set<string>();
221
+
222
+ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
223
+ // 预解析 model(同步):让标题行能显示 model/thinking,不必等 execute。
224
+ // resolveModel 三层:override → agentConfig.model → 主 agent model(session 缓存)。
225
+ // 主 agent model 由 ModelConfigService 缓存(session_start 注入,model_select 刷新),
226
+ // 补偿 renderCall 的 ToolRenderContext 不含 model 的 SDK 限制。
227
+ // service 未就绪 / 缓存为空 / 解析失败 → 降级不显示 model。
228
+ const startParam = hasStartParam(args) ? args.startParam : undefined;
229
+ const agent = extractAgentName(startParam);
230
+ const override = extractModelOverride(startParam);
231
+ let resolved: { model: string; thinkingLevel?: string } | undefined;
232
+ try {
233
+ const service = getSubagentService();
234
+ const r = service?.resolveModel(agent, override);
235
+ if (r) resolved = { model: `${r.model.provider}/${r.model.id}`, thinkingLevel: r.thinkingLevel };
236
+ } catch (err) {
237
+ // service 未注册 / modelRegistry 未注入 / 无可用 model → 降级不显示 model(renderCall 不应崩)。
238
+ // 去重:同一 err.message 只 console.debug 一次,避免 TUI invalidate 反复刷屏。
239
+ const msg = err instanceof Error ? err.message : String(err);
240
+ if (!reportedRenderErrors.has(msg)) {
241
+ reportedRenderErrors.add(msg);
242
+ void err; // 显式确认忽略:renderCall 降级是设计意图,不阻断渲染
243
+ console.debug("[subagents] renderCall model resolution failed, degrading:", err);
244
+ }
245
+ }
246
+ return renderSubagentCall(args, theme, ctx, resolved);
247
+ };
248
+
249
+ const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ctx) =>
250
+ renderSubagentResult(result, options, theme, ctx);
251
+
252
+ /**
253
+ * execute 实现(action 路由 + adapter)。
254
+ *
255
+ * ╔══════════════════════════════════════════════════════════════════╗
256
+ * ║ service = getSubagentService() —— 未初始化 throw ║
257
+ * ║ ║
258
+ * ║ switch(params.action): ║
259
+ * ║ "start" → startHandler(service, params.startParam, signal) → 领域对象 ║
260
+ * ║ "list" → listHandler(service, params.listParam) → 领域对象 ║
261
+ * ║ "cancel" → cancelHandler(service, params.cancelParam) → 领域对象║
262
+ * ║ ║
263
+ * ║ result = adapter(action, 领域对象) ║
264
+ * ║ return { content: [{text: JSON.stringify(result)}], details: result }║
265
+ * ╚══════════════════════════════════════════════════════════════════╝
266
+ *
267
+ * handler 返回纯领域对象(不碰 {content, details}),adapter 唯一包装。
268
+ * content(JSON 字符串)给 LLM,details(领域对象 + action)给 renderResult,同源。
269
+ */
270
+ const executeSubagent: SubagentExecuteCb = async (
271
+ _toolCallId,
272
+ params,
273
+ signal,
274
+ _onUpdate,
275
+ _ctx,
276
+ ) => {
277
+ // background 模式:execute 立即返回,detached 运行不向 tool 层回流 onUpdate
278
+ //(完成由 notify 驱动新 turn)。onUpdate 参数保留以兼容 SDK 回调签名,但不消费。
279
+ const service = getSubagentService();
280
+ if (!service) throw new Error("subagents runtime not initialized");
281
+
282
+ switch (params.action) {
283
+ case "start":
284
+ return adapter({ action: "start", domain: await startHandler(service, params.startParam, signal, _ctx?.model) }, _ctx);
285
+ case "list":
286
+ return adapter({ action: "list", domain: listHandler(service, params.listParam) }, _ctx);
287
+ case "cancel":
288
+ return adapter({ action: "cancel", domain: await cancelHandler(service, params.cancelParam) }, _ctx);
289
+ default:
290
+ // assertNever:让 exhaustiveness 成为承重约束——新增 action 时 tsc 报错,
291
+ // 而非悄悄落入此分支。
292
+ throw new Error(`Unknown subagent action: ${assertNever(params.action)}`);
293
+ }
294
+ };
@@ -0,0 +1,30 @@
1
+ // src/commands/subagents.ts
2
+ //
3
+ // /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
4
+ //
5
+ // 解析:args[0] 直接作可选 <id>(聚焦该 record)。
6
+
7
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
+
9
+ import { getSubagentService } from "../execution/subagent-service.ts";
10
+ import { createSubagentsView } from "./list-view.ts";
11
+
12
+ /** 注册 /subagents 命令(= list overlay)。 */
13
+ export function registerSubagentsCommand(pi: ExtensionAPI): void {
14
+ pi.registerCommand("subagents", {
15
+ description: "Subagents: /subagents [<id>]",
16
+ handler: async (argsStr: string, ctx: ExtensionCommandContext) => {
17
+ if (!ctx.hasUI) {
18
+ ctx.ui.notify("/subagents requires an interactive UI", "error");
19
+ return;
20
+ }
21
+ const service = getSubagentService();
22
+ if (!service) {
23
+ ctx.ui.notify("subagents execution runtime not ready (session not started)", "error");
24
+ return;
25
+ }
26
+ const args = argsStr.trim().split(/\s+/).filter(Boolean);
27
+ await createSubagentsView(service, ctx.ui.theme, ctx, args[0]);
28
+ },
29
+ });
30
+ }