@zhushanwen/pi-subagent-workflow 0.1.0 → 0.3.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 +56 -0
- package/agents/context-builder.md +1 -3
- package/agents/explorer.md +27 -0
- package/agents/oracle.md +2 -2
- package/agents/orchestrator.md +48 -0
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-registry.test.ts +3 -3
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +484 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +233 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/crash-recovery.test.ts +5 -1
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/execute-nesting.test.ts +1 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +41 -9
- package/src/execution/__tests__/finalize-record.test.ts +173 -0
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +59 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +209 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/index-session-start.test.ts +342 -0
- package/src/execution/__tests__/list-component.test.ts +1 -1
- package/src/execution/__tests__/notifier-flush.test.ts +78 -0
- package/src/execution/__tests__/path-encoding.test.ts +30 -1
- package/src/execution/__tests__/record-store.test.ts +86 -2
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +157 -153
- package/src/execution/__tests__/run-spawn-integration.test.ts +85 -151
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +193 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-file-gc.test.ts +46 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +7 -1
- package/src/execution/__tests__/spawn-args.test.ts +14 -19
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/stdin-writer.test.ts +353 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service.test.ts +73 -3
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +27 -5
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +166 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +101 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +1 -1
- package/src/execution/agent-registry.ts +1 -1
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/channel-registry-access.ts +138 -0
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/dialog-queue.ts +329 -0
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/finalize-record.ts +160 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +52 -0
- package/src/execution/manifest-store.ts +206 -0
- package/src/execution/notifier.ts +5 -1
- package/src/execution/path-encoding.ts +18 -0
- package/src/execution/pi-invocation.ts +1 -1
- package/src/execution/record-store.ts +110 -2
- package/src/execution/session-file-gc.ts +25 -3
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +228 -32
- package/src/execution/spawn-event-adapter.ts +219 -6
- package/src/execution/stdin-writer.ts +106 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +230 -235
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +175 -0
- package/src/execution/ui-request-observability.ts +77 -0
- package/src/execution/ui-request-queue.ts +168 -0
- package/src/index.ts +101 -4
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/format.ts +2 -0
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +44 -24
- package/src/interface/subagent-tool.ts +56 -24
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/agents/scout.md +0 -17
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- package/src/interface/gui-adapter.ts +0 -136
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/core/session-runner.ts
|
|
2
2
|
//
|
|
3
|
-
// spawn pi --mode
|
|
3
|
+
// spawn pi --mode rpc 子进程执行 session 的编排器。零 mode 感知。
|
|
4
4
|
//
|
|
5
5
|
// spawn 改造后:session 在独立子进程跑(进程隔离),事件经 stdout JSON 流回流。
|
|
6
6
|
// runSpawn 是唯一执行入口(sync/background 共用)。mode 分叉在 Runtime.execute 顶部。
|
|
@@ -9,35 +9,40 @@
|
|
|
9
9
|
import { type ChildProcess,execFileSync, spawn } from "node:child_process";
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
|
|
12
|
+
import type { ExtensionMode } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
12
14
|
import { writeAliveMarker } from "./alive-store.ts";
|
|
13
|
-
import type
|
|
14
|
-
AgentEvent,
|
|
15
|
-
AgentResult,
|
|
16
|
-
ExecutionRecord,
|
|
17
|
-
SdkEvent,
|
|
18
|
-
WorktreeHandle,
|
|
19
|
-
} from "./types.ts";
|
|
15
|
+
import { type DialogGlobalQueue, type UiRequestHandler } from "./dialog-queue.ts";
|
|
20
16
|
import { updateFromEvent } from "./execution-record.ts";
|
|
21
|
-
import type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "./model-resolver.ts";
|
|
17
|
+
import { type GetStateResult, performGetStateHandshake } from "./get-state-handshake.ts";
|
|
18
|
+
import { willRespondToAskUser } from "./host-mode.ts";
|
|
19
|
+
import type { AgentConfig, ResolvedModel } from "./model-resolver.ts";
|
|
25
20
|
import { collectResult } from "./output-collector.ts";
|
|
26
21
|
import { getSubagentSessionDir } from "./path-encoding.ts";
|
|
27
22
|
import { getPiInvocation } from "./pi-invocation.ts";
|
|
28
23
|
import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
|
|
29
24
|
import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "./session-reconstructor.ts";
|
|
25
|
+
import { sendPromptCommand } from "./stdin-writer.ts";
|
|
30
26
|
import {
|
|
31
27
|
deriveSessionFilePath,
|
|
32
28
|
findSessionFileByHeaderId,
|
|
33
29
|
parseSpawnLine,
|
|
34
30
|
type SpawnSessionHeader,
|
|
35
31
|
} from "./spawn-event-adapter.ts";
|
|
32
|
+
import type { SubagentStream } from "./stream-sink.ts";
|
|
36
33
|
import {
|
|
37
34
|
cleanupTempPrompt,
|
|
38
35
|
writePromptToTempFile,
|
|
39
36
|
} from "./temp-prompt.ts";
|
|
37
|
+
import type {
|
|
38
|
+
AgentEvent,
|
|
39
|
+
AgentResult,
|
|
40
|
+
ExecutionRecord,
|
|
41
|
+
SdkEvent,
|
|
42
|
+
WorktreeHandle,
|
|
43
|
+
} from "./types.ts";
|
|
40
44
|
import { createTurnLimiter, WRAP_UP_HINT } from "./turn-limiter.ts";
|
|
45
|
+
import { createUiRequestQueue } from "./ui-request-queue.ts";
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* 运行时 guard:subscribe 回调收到的 event 形状未知,校验 type 字段后再交给 handle。
|
|
@@ -49,6 +54,21 @@ function isSdkEvent(x: unknown): x is SdkEvent {
|
|
|
49
54
|
return typeof (x as SdkEvent).type === "string";
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* M10:agent_end 事件守卫。抽出前调用处用 `(evt as { type: string }).type === "agent_end"`
|
|
59
|
+
* 和 `(evt as { willRetry?: boolean }).willRetry` 两处结构断言触发 taste/no-unsafe-cast
|
|
60
|
+
*(后者断言到全可选属性等于无校验)。守卫返回后 TS 自动窄化为
|
|
61
|
+
* { type: "agent_end"; willRetry?: boolean },调用处无需任何 cast。
|
|
62
|
+
*/
|
|
63
|
+
function isAgentEndEvt(
|
|
64
|
+
x: unknown,
|
|
65
|
+
): x is { type: "agent_end"; willRetry?: boolean } {
|
|
66
|
+
if (typeof x !== "object" || x === null) return false;
|
|
67
|
+
if (!("type" in x)) return false;
|
|
68
|
+
// `"type" in x` 已窄化,TS 允许直接访问 x.type(无需 cast)
|
|
69
|
+
return x.type === "agent_end";
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
// ============================================================
|
|
53
73
|
// 常量
|
|
54
74
|
// ============================================================
|
|
@@ -88,6 +108,35 @@ function computeWatchdogMs(maxTurns: number | undefined | null): number {
|
|
|
88
108
|
/** stderr 累积上限(字符)。防止失控子进程打满父进程内存。保留尾部便于诊断。 */
|
|
89
109
|
const STDERR_MAX_CHARS = 64 * 1024;
|
|
90
110
|
|
|
111
|
+
// ============================================================
|
|
112
|
+
// W4: ask_user RPC 系统提示词
|
|
113
|
+
// ============================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* ask_user 工具的 RPC 使用指引。当子进程配置了 ask_user tool 时注入 appendParts,
|
|
117
|
+
* 告知 LLM:ask_user 的问题会通过 RPC 转发到主 agent UI,用户在主 agent 界面回答。
|
|
118
|
+
*
|
|
119
|
+
* 背景:spawn 模式下子进程没有 TUI 交互通道,ask_user 走 extension_ui_request RPC 协议
|
|
120
|
+
* 转发到父进程,父进程调用 uiRequestHandler 将问题呈现给用户,收到回答后通过 stdin
|
|
121
|
+
* 回写 JSON-RPC response。LLM 需要知道这个机制存在,才能正确使用 ask_user。
|
|
122
|
+
*/
|
|
123
|
+
export const ASK_USER_RPC_PROMPT = `
|
|
124
|
+
## ask_user Tool Availability
|
|
125
|
+
|
|
126
|
+
The \`ask_user\` tool is available in this session. When you call \`ask_user\`, your questions are forwarded via RPC to the main agent's UI, where the user will see them and provide answers. The response is delivered back to you automatically.
|
|
127
|
+
|
|
128
|
+
**How it works:**
|
|
129
|
+
1. You call \`ask_user\` with structured questions (each with options)
|
|
130
|
+
2. The questions are forwarded to the main agent's UI via RPC
|
|
131
|
+
3. The user sees the questions and selects answers in the main agent interface
|
|
132
|
+
4. The answers are returned to you as the tool result
|
|
133
|
+
|
|
134
|
+
**Important:**
|
|
135
|
+
- The user may take some time to respond — this is normal
|
|
136
|
+
- If the user cancels or the request times out, you'll receive a cancellation notice
|
|
137
|
+
- Use ask_user only when you genuinely cannot resolve ambiguity yourself (see tool description for guidelines)
|
|
138
|
+
`.trim();
|
|
139
|
+
|
|
91
140
|
// ============================================================
|
|
92
141
|
// 孤儿进程兜底(C1)
|
|
93
142
|
// ============================================================
|
|
@@ -102,7 +151,9 @@ const STDERR_MAX_CHARS = 64 * 1024;
|
|
|
102
151
|
// 之后,遍历所有仍存活的子进程(含 sync)发 SIGTERM。正常退出路径(子进程 close)会从 Set 移除,
|
|
103
152
|
// 不受影响。background 子进程可能被 controller.abort 路径先 kill 一次,再被本遍历 kill 一次
|
|
104
153
|
// (对已退出的 child.kill 返回 false,无害)。
|
|
105
|
-
|
|
154
|
+
//
|
|
155
|
+
// [export] 测试可观测(断言 dispose 后 size===0)。业务代码误外部修改。
|
|
156
|
+
export const spawnedChildren = new Set<ChildProcess>();
|
|
106
157
|
|
|
107
158
|
/**
|
|
108
159
|
* kill 所有未退出的 spawned 子进程(dispose 兜底用)。
|
|
@@ -134,6 +185,11 @@ export function killAllSpawnedChildren(signal: NodeJS.Signals = "SIGTERM"): numb
|
|
|
134
185
|
// best-effort:单个 kill 失败不影响其他子进程
|
|
135
186
|
}
|
|
136
187
|
}
|
|
188
|
+
// dispose 全量清理;正常路径的 close/error 事件 delete 保留作 per-child 精细清理,
|
|
189
|
+
// 这里兑底防 close 事件漏触发的极端累积(主进程崩溃后 close 回调可能不再触发,
|
|
190
|
+
// 不 clear 则下次 dispose 会重复向已 kill 的 child 发信号——虽然 killed=true 跳过,
|
|
191
|
+
// 但 Set 无限增长泄漏内存)。
|
|
192
|
+
spawnedChildren.clear();
|
|
137
193
|
return n;
|
|
138
194
|
}
|
|
139
195
|
|
|
@@ -159,6 +215,28 @@ export interface SessionRunnerContext {
|
|
|
159
215
|
* 解耦 Core 与 Runtime——session-runner 不直接依赖 WorktreeManager。
|
|
160
216
|
*/
|
|
161
217
|
onWorktreePid?: (branch: string, pid: number) => void;
|
|
218
|
+
/**
|
|
219
|
+
* UI 请求处理回调。子进程发 extension_ui_request 时调用。
|
|
220
|
+
*
|
|
221
|
+
* 入参 UiRequest(method + channel/channelPayload + method 特定字段),
|
|
222
|
+
* 返回 UiResponse({value}/{confirmed}/{cancelled}/{ack})。
|
|
223
|
+
* 实现方按 req.channel 分发业务路由(ask_user → AskUserComponent)+
|
|
224
|
+
* 默认转发(ctx.ui.*),收到用户回答后 resolve。
|
|
225
|
+
*
|
|
226
|
+
* 未设置时不再静默忽略——console.warn 兜底(FR-9 可观测性),
|
|
227
|
+
* W3 接入 SubagentService.notifyMissingHandler 的 appendEntry。
|
|
228
|
+
*/
|
|
229
|
+
uiRequestHandler?: UiRequestHandler;
|
|
230
|
+
/**
|
|
231
|
+
* L2 跨子进程全局 dialog 串行队列(进程单例,由 SubagentService 注入)。
|
|
232
|
+
*
|
|
233
|
+
* SR-4:child close 时调 rejectChildDialogs 清理该 child 在 L2 的 pending dialog,
|
|
234
|
+
* 防 Promise 永挂(handler 等用户输入永不 settle)导致队列死锁(processing 永远 true,
|
|
235
|
+
* 其他子进程的 dialog 永久阻塞)。未注入(旧调用方/测试)时 onclose 只清 L1。
|
|
236
|
+
*/
|
|
237
|
+
dialogQueue?: DialogGlobalQueue;
|
|
238
|
+
/** 主进程运行模式(W4 守卫:headless 不注入 ask_user RPC 提示词)。 */
|
|
239
|
+
mode?: ExtensionMode;
|
|
162
240
|
}
|
|
163
241
|
|
|
164
242
|
/** SessionRunner.run 的入参。 */
|
|
@@ -181,6 +259,12 @@ export interface RunOptions {
|
|
|
181
259
|
signal: AbortSignal | undefined;
|
|
182
260
|
/** event 回流——SessionRunner 内部 updateFromEvent 后,再回调调用方(widget/notify)。 */
|
|
183
261
|
onEvent: ((event: AgentEvent) => void) | undefined;
|
|
262
|
+
/** text_delta streaming 生命周期对象——在 text_delta 到达 onEvent 之前分流。
|
|
263
|
+
* background 模式下 onEvent=undefined,但 text_delta 仍可通过此对象被消费。
|
|
264
|
+
* 由调用方(subagent-service)创建,内部做时间窗合并后转发到 setWidget。
|
|
265
|
+
* workflow 路径(executeAndAwait)不传此字段——其 onEvent 是开的,
|
|
266
|
+
* text_delta 经 onEvent 到 workflow liveRecord,不走 streaming 通道。 */
|
|
267
|
+
stream?: SubagentStream;
|
|
184
268
|
/** D-A6 bridge: workflow schema JSON 字符串,存在时注入 childEnv.PI_WORKFLOW_SCHEMA。
|
|
185
269
|
* workflow 编排层通过 ExecuteOptions.schemaEnv 透传此处,
|
|
186
270
|
* runSpawn 将其注入子进程环境变量,激活 structured-output 扩展注册 tool。
|
|
@@ -298,7 +382,7 @@ export function buildEnvBlock(
|
|
|
298
382
|
}
|
|
299
383
|
|
|
300
384
|
// ============================================================
|
|
301
|
-
// [SPAWN 改造] runSpawn:spawn pi --mode
|
|
385
|
+
// [SPAWN 改造] runSpawn:spawn pi --mode rpc 子进程执行 session
|
|
302
386
|
// ============================================================
|
|
303
387
|
//
|
|
304
388
|
// 替代 in-process run()。核心差异:session 在独立子进程跑(进程隔离),
|
|
@@ -339,9 +423,11 @@ export function buildSpawnArgs(
|
|
|
339
423
|
forkSource: string | undefined;
|
|
340
424
|
skillPaths: string[] | undefined;
|
|
341
425
|
},
|
|
342
|
-
task: string,
|
|
343
426
|
): string[] {
|
|
344
|
-
|
|
427
|
+
// task 不通过命令行传——pi 的 runRpcMode 只消费 stdin RpcCommand,
|
|
428
|
+
// positional task arg / -p flag 在 rpc mode 下被 resolveAppMode 无视。
|
|
429
|
+
// task 由 runSpawn 内 sendPromptCommand 写 child.stdin 驱动。
|
|
430
|
+
const args: string[] = ["--mode", "rpc", "--session-dir", params.sessionDir];
|
|
345
431
|
if (params.model) args.push("--model", params.model);
|
|
346
432
|
if (params.thinkingLevel && params.model) {
|
|
347
433
|
// thinking level 通过 model 后缀 :level 传递(pi CLI 约定)
|
|
@@ -365,7 +451,6 @@ export function buildSpawnArgs(
|
|
|
365
451
|
args.push("--skill", sp);
|
|
366
452
|
}
|
|
367
453
|
}
|
|
368
|
-
args.push(task);
|
|
369
454
|
return args;
|
|
370
455
|
}
|
|
371
456
|
|
|
@@ -387,15 +472,16 @@ export async function runSpawn(
|
|
|
387
472
|
const pendingTools = new Map<string, { toolName: string; args?: unknown }>();
|
|
388
473
|
|
|
389
474
|
// b. turnLimiter(spawn 版:abort = proc.kill;steer 是 no-op)
|
|
390
|
-
// [M1]
|
|
391
|
-
//
|
|
392
|
-
//
|
|
475
|
+
// [M1] rpc mode 是长驻进程(agent_end 后不自动退出),maxTurns soft limit 依赖
|
|
476
|
+
// graceTurns 后的 abort(proc.kill SIGTERM)兑现。agent 自然结束时由 stdout pump
|
|
477
|
+
// 的 agent_end 拦截 kill(见下方)。steer 通道当前未接通(见下方 steer no-op 注释)。
|
|
393
478
|
let proc: ChildProcess | undefined;
|
|
394
479
|
const limiter = createTurnLimiter({
|
|
395
480
|
maxTurns: opts.maxTurns ?? 0,
|
|
396
481
|
graceTurns: opts.graceTurns ?? DEFAULT_GRACE_TURNS,
|
|
397
482
|
steer: () => {
|
|
398
|
-
// no-op
|
|
483
|
+
// no-op:当前 runSpawn 未接通 rpc stdin steer 通道(rpc mode 支持 steer/followUp,
|
|
484
|
+
// 但未实现写入逻辑)。补偿已在启动时注入 WRAP_UP_HINT 让 agent 主动收尾。
|
|
399
485
|
},
|
|
400
486
|
abort: () => {
|
|
401
487
|
proc?.kill("SIGTERM");
|
|
@@ -474,6 +560,10 @@ export async function runSpawn(
|
|
|
474
560
|
const agentEvent = (event: AgentEvent): void => {
|
|
475
561
|
updateFromEvent(record, event);
|
|
476
562
|
if (event.type === "turn_end") limiter.onTurnEnd(record.turnCount);
|
|
563
|
+
// text_delta 分流到 stream 通道(在 onEvent 之前)。
|
|
564
|
+
// 双通道互斥设计:background 路径 stream 有值、onEvent=undefined;
|
|
565
|
+
// workflow 路径 onEvent 有值、stream=undefined。详见 W3 注释。
|
|
566
|
+
if (event.type === "text_delta") opts.stream?.onDelta(event.delta);
|
|
477
567
|
opts.onEvent?.(event);
|
|
478
568
|
};
|
|
479
569
|
|
|
@@ -499,10 +589,14 @@ export async function runSpawn(
|
|
|
499
589
|
const appendParts: string[] = [buildEnvBlock(ctx.cwd, ownForkDepth, record.depth)];
|
|
500
590
|
if (opts.agentConfig?.systemPrompt) appendParts.push(opts.agentConfig.systemPrompt);
|
|
501
591
|
if (opts.appendSystemPrompt) appendParts.push(...opts.appendSystemPrompt);
|
|
502
|
-
// [M1 补偿]
|
|
503
|
-
//
|
|
504
|
-
// 长期方案:切到 pi --mode rpc(支持运行时 steer),见 follow-up。
|
|
592
|
+
// [M1 补偿] rpc mode 的 steer 通道当前未接通,改为启动时预置 wrap-up 提示——
|
|
593
|
+
// agent 感知接近上限时主动收尾。
|
|
505
594
|
if (opts.maxTurns && opts.maxTurns > 0) appendParts.push(WRAP_UP_HINT);
|
|
595
|
+
// W4: ask_user RPC 使用指引——当子进程配置了 ask_user tool 时,告知 LLM
|
|
596
|
+
// ask_user 的问题会通过 RPC 转发到主 agent UI,用户在主 agent 界面回答。
|
|
597
|
+
if (opts.agentConfig?.tools?.includes("ask_user") && willRespondToAskUser(ctx.mode)) {
|
|
598
|
+
appendParts.push(ASK_USER_RPC_PROMPT);
|
|
599
|
+
}
|
|
506
600
|
if (appendParts.length > 0) {
|
|
507
601
|
tempPromptFile = await writePromptToTempFile(record.agent, appendParts.join("\n\n"));
|
|
508
602
|
}
|
|
@@ -532,7 +626,6 @@ export async function runSpawn(
|
|
|
532
626
|
forkSource,
|
|
533
627
|
skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
|
|
534
628
|
},
|
|
535
|
-
fullTask,
|
|
536
629
|
);
|
|
537
630
|
const invocation = getPiInvocation(spawnArgs);
|
|
538
631
|
|
|
@@ -545,7 +638,7 @@ export async function runSpawn(
|
|
|
545
638
|
const child = spawn(invocation.command, invocation.args, {
|
|
546
639
|
cwd: spawnCwd,
|
|
547
640
|
shell: false,
|
|
548
|
-
stdio: ["
|
|
641
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
549
642
|
env: childEnv,
|
|
550
643
|
});
|
|
551
644
|
proc = child;
|
|
@@ -560,6 +653,11 @@ export async function runSpawn(
|
|
|
560
653
|
child.stdout.setEncoding("utf8");
|
|
561
654
|
child.stderr.setEncoding("utf8");
|
|
562
655
|
|
|
656
|
+
// 喂 prompt 命令驱动子进程开始处理 task。pi runRpcMode 只消费 stdin RpcCommand,
|
|
657
|
+
// 不读 positional arg;必须在 spawn 后主动写,否则子进程阻塞、totalTokens 恒 0。
|
|
658
|
+
// 时机安全:pipe 内核缓冲不丢;pi 在 rebindSession 后才挂 stdin reader。
|
|
659
|
+
sendPromptCommand(child, fullTask);
|
|
660
|
+
|
|
563
661
|
// d. signal → proc.kill 监听(一次性,替代 session.abort)
|
|
564
662
|
const onAbort = (): void => {
|
|
565
663
|
child.kill("SIGTERM");
|
|
@@ -580,8 +678,43 @@ export async function runSpawn(
|
|
|
580
678
|
const watchdog = setTimeout(() => child.kill("SIGTERM"), watchdogMs);
|
|
581
679
|
watchdog.unref();
|
|
582
680
|
|
|
583
|
-
// stdout pump:逐行解析 → handleSdkEvent
|
|
681
|
+
// stdout pump:逐行解析 → handleSdkEvent / enqueueUiRequest
|
|
682
|
+
const enqueueUiRequest = createUiRequestQueue(child, ctx);
|
|
683
|
+
// FR-4: get_state RPC response 监听器(id → resolver)。
|
|
684
|
+
// parseSpawnLine 返回 kind:"response" 时,按 command+id 匹配 resolver。
|
|
685
|
+
const get_stateListeners = new Map<string, (data: unknown) => void>();
|
|
584
686
|
let stdoutBuffer = "";
|
|
687
|
+
|
|
688
|
+
// [#18] 握手状态变量在 stdout handler 注册之前定义,消除"handler 闭包依赖同 tick
|
|
689
|
+
// 后续 const 初始化"的隐式顺序假设——handler 现在直接引用已初始化的变量,不靠
|
|
690
|
+
// "data 事件必然在下一 tick 才触发”的运行时不变式兜底。
|
|
691
|
+
const handshakeResultRef: { current?: GetStateResult } = {};
|
|
692
|
+
let settleHandshake: (() => void) | undefined;
|
|
693
|
+
const handshakeSettled: Promise<void> = new Promise((resolveSettled) => {
|
|
694
|
+
settleHandshake = resolveSettled;
|
|
695
|
+
});
|
|
696
|
+
/** 握手完成统一入口:记录结果 + 回填 sessionFile + 写 alive marker + settle。 */
|
|
697
|
+
const finishHandshake = (r: GetStateResult): void => {
|
|
698
|
+
handshakeResultRef.current = r;
|
|
699
|
+
// 仅当 header 未先行设置 record.sessionFile 时回填(RPC mode 路径)。
|
|
700
|
+
if (r.sessionFile && !record.sessionFile) {
|
|
701
|
+
record.sessionFile = r.sessionFile;
|
|
702
|
+
if (child.pid) {
|
|
703
|
+
try {
|
|
704
|
+
writeAliveMarker(r.sessionFile, {
|
|
705
|
+
pid: child.pid,
|
|
706
|
+
id: r.sessionId ?? record.id,
|
|
707
|
+
startedAt: Date.now(),
|
|
708
|
+
});
|
|
709
|
+
} catch {
|
|
710
|
+
// best-effort:alive marker 失败不影响执行
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
settleHandshake?.();
|
|
715
|
+
settleHandshake = undefined;
|
|
716
|
+
};
|
|
717
|
+
|
|
585
718
|
child.stdout.on("data", (data: string) => {
|
|
586
719
|
stdoutBuffer += data;
|
|
587
720
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -612,13 +745,61 @@ export async function runSpawn(
|
|
|
612
745
|
if (opts.worktree && child.pid) {
|
|
613
746
|
ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
|
|
614
747
|
}
|
|
748
|
+
// FR-4 加速路径:header 到达即 finishHandshake(header 已提供 sessionId,
|
|
749
|
+
// 足以推导 sessionFile + 兜底查找,无需等 get_state response)。
|
|
750
|
+
// [#25] buildSpawnArgs 固定 --mode rpc,RPC mode 不发 header——此分支当前不触发,
|
|
751
|
+
// 仅为未来 mode 回切(如 json mode 调试)保留:届时 header 先到可省去 get_state 握手等待。
|
|
752
|
+
if (settleHandshake) {
|
|
753
|
+
finishHandshake({
|
|
754
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
755
|
+
sessionId: parsed.header.id,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
615
758
|
} else if (parsed.kind === "event") {
|
|
759
|
+
const evt = parsed.event;
|
|
760
|
+
// agent_end(willRetry=false)= agent 自然完成。rpc mode 子进程不自动退出
|
|
761
|
+
//(runRpcMode 末尾 return new Promise(() => {}) 长驻等命令),需主动 kill
|
|
762
|
+
// 触发 close → runSpawn resolve。willRetry=true 时 agent 会重试,不能 kill。
|
|
763
|
+
if (isAgentEndEvt(evt)) {
|
|
764
|
+
if (!evt.willRetry) child.kill("SIGTERM");
|
|
765
|
+
}
|
|
616
766
|
if (isSdkEvent(parsed.event)) handleSdkEvent(parsed.event);
|
|
767
|
+
} else if (parsed.kind === "response") {
|
|
768
|
+
// FR-4: RPC response handling — 匹配 get_state 响应
|
|
769
|
+
if (parsed.command === "get_state" && parsed.success && parsed.id) {
|
|
770
|
+
const resolver = get_stateListeners.get(parsed.id);
|
|
771
|
+
if (resolver) {
|
|
772
|
+
get_stateListeners.delete(parsed.id);
|
|
773
|
+
resolver(parsed.data);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} else if (parsed.kind === "extension_ui_request") {
|
|
777
|
+
// W3: 子进程发 UI 请求(ask_user)。入队 FIFO 串行处理,防止并发询问用户。
|
|
778
|
+
enqueueUiRequest(parsed.id, parsed.request);
|
|
617
779
|
}
|
|
618
780
|
// invalid 行忽略(stdout 可能有调试输出)
|
|
619
781
|
}
|
|
620
782
|
});
|
|
621
783
|
|
|
784
|
+
// FR-4: get_state RPC 握手——spawn 后无条件启动。
|
|
785
|
+
// RPC mode(pi --mode rpc)不向 stdout 输出 header,record.sessionFile 无法靠 header
|
|
786
|
+
// 推导,必须通过 get_state RPC 查询子进程回填。json mode 下 header 会先到达触发提前
|
|
787
|
+
// resolve(加速路径),握手仍启动但无害——response 到达时外层已 resolve,resolver
|
|
788
|
+
// 因 resolved=true 直接 return。
|
|
789
|
+
//
|
|
790
|
+
// 时序:握手在 stdout pump(get_stateListeners 已就绪)之后启动。get_state 命令写入
|
|
791
|
+
// stdin,pi rebindSession 后读取并返回 response,经 stdout pump 匹配 resolver 触发 resolve。
|
|
792
|
+
// close handler await handshakeSettled,保证无论 task 多快结束,close 时 sessionFile 已回填。
|
|
793
|
+
// [#18] 握手状态变量(handshakeResultRef/settleHandshake/handshakeSettled/finishHandshake)
|
|
794
|
+
// 已在上方 stdout handler 注册前定义,此处直接发起握手。
|
|
795
|
+
void performGetStateHandshake(child, (id, resolver) => {
|
|
796
|
+
get_stateListeners.set(id, resolver);
|
|
797
|
+
}).then((r) => {
|
|
798
|
+
// header 加速路径下 settleHandshake 已 undefined,跳过(避免覆盖 header 结果)。
|
|
799
|
+
// 超时兜底(r 为空对象)也经此分支 settle,但 record.sessionFile 不回填。
|
|
800
|
+
if (settleHandshake) finishHandshake(r);
|
|
801
|
+
});
|
|
802
|
+
|
|
622
803
|
child.stderr.on("data", (data: string) => {
|
|
623
804
|
// 截断防 OOM:失控子进程持续打 stderr 会耗尽父进程内存。保留尾部便于诊断。
|
|
624
805
|
stderrBuffer = (stderrBuffer + data).slice(-STDERR_MAX_CHARS);
|
|
@@ -626,9 +807,19 @@ export async function runSpawn(
|
|
|
626
807
|
|
|
627
808
|
// 等待子进程退出
|
|
628
809
|
const exitCode = await new Promise<number>((resolve) => {
|
|
629
|
-
child.on("close", (code: number | null) => {
|
|
810
|
+
child.on("close", async (code: number | null) => {
|
|
630
811
|
// [C1] 子进程已退出,从 orphan-tracking Set 移除(dispose 兜底无需再 kill 它)
|
|
631
812
|
spawnedChildren.delete(child);
|
|
813
|
+
// FR-4: 清理 get_state 监听器(子进程已退出,无更多 response)
|
|
814
|
+
get_stateListeners.clear();
|
|
815
|
+
// FR-4: 子进程已退出,get_state response 不会再来。若握手仍未 settle,立即放弃
|
|
816
|
+
//(record.sessionFile 未回填则走 findSessionFileByHeaderId 兜底)。避免子进程快速
|
|
817
|
+
// 失败/退出场景下 close handler 阻塞等待握手内部 6s 超时。
|
|
818
|
+
settleHandshake?.();
|
|
819
|
+
settleHandshake = undefined;
|
|
820
|
+
// await 立即返回(上方已 settle):保证 header 加速路径或 get_state response 已
|
|
821
|
+
// 完成的回填结果对后续 identity 写入可见。
|
|
822
|
+
await handshakeSettled;
|
|
632
823
|
// 处理 stdout 末尾残留行
|
|
633
824
|
if (stdoutBuffer.trim()) {
|
|
634
825
|
const parsed = parseSpawnLine(stdoutBuffer);
|
|
@@ -653,12 +844,16 @@ export async function runSpawn(
|
|
|
653
844
|
// session.jsonl 由子进程写入,父进程在子进程退出后(写入完成)補写身份条目。
|
|
654
845
|
// reconstructFromFile 依赖 IDENTITY_CUSTOM_TYPE custom entry 重建 record 身份,
|
|
655
846
|
// 缺失则 /subagents list 磁盘源为空(终态 record 全丢失)。[回归修复]
|
|
656
|
-
if (
|
|
657
|
-
// 兜底:deriveSessionFilePath
|
|
847
|
+
if (record.sessionFile) {
|
|
848
|
+
// 兜底:deriveSessionFilePath 推导或握手返回的路径可能不存在(pi 命名规则变化),
|
|
658
849
|
// 用 sessionId 后缀匹配实际文件。匹配到则修正 record.sessionFile。
|
|
850
|
+
// sessionId 来源:header(json mode)优先,其次握手结果(RPC mode)。
|
|
659
851
|
if (!fs.existsSync(record.sessionFile)) {
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
852
|
+
const lookupId = sessionHeader?.id ?? handshakeResultRef.current?.sessionId;
|
|
853
|
+
if (lookupId) {
|
|
854
|
+
const actual = findSessionFileByHeaderId(sessionDir, lookupId);
|
|
855
|
+
if (actual) record.sessionFile = actual;
|
|
856
|
+
}
|
|
662
857
|
}
|
|
663
858
|
// 补写 identity custom entry(子进程已退出,append 安全)。
|
|
664
859
|
if (fs.existsSync(record.sessionFile)) {
|
|
@@ -667,6 +862,7 @@ export async function runSpawn(
|
|
|
667
862
|
agent: record.agent,
|
|
668
863
|
mode: record.mode,
|
|
669
864
|
task: record.task,
|
|
865
|
+
slug: record.slug,
|
|
670
866
|
startedAt: record.startedAt,
|
|
671
867
|
rootSessionId: record.rootSessionId,
|
|
672
868
|
parentRecordId: record.parentRecordId,
|