@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
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// pi 子进程 stdout JSON 事件流的解析器。Core 叶子原语(仅依赖 types.ts)。
|
|
4
4
|
//
|
|
5
|
-
// spawn 改造的基座模块。pi --mode
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// spawn 改造的基座模块。session-runner runSpawn 用 `pi --mode rpc` spawn 子进程。
|
|
6
|
+
// RPC mode 不向 stdout 输出 header 行(只有 json/print mode 才输出),故 runSpawn 额外
|
|
7
|
+
// 通过 get_state RPC 握手回填 sessionFile/sessionId。两种 stdout 行形态本模块统一解析:
|
|
8
|
+
// 1. header 行(json/print mode 首行):{ type: "session", id, timestamp, cwd, ... }
|
|
9
|
+
// —— session 元信息,含 session id(RPC mode 不发,靠 get_state 握手替代)
|
|
8
10
|
// 2. 事件行:{ type: "tool_execution_start" | "message_end" | ..., ... }
|
|
9
11
|
// —— 与 in-process session.subscribe 收到的 SdkEvent 同源同构
|
|
10
12
|
//
|
|
@@ -30,10 +32,68 @@ export interface SpawnSessionHeader {
|
|
|
30
32
|
readonly version?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/** Pi 原生 extension_ui_request 的方法特定字段(按 method 平铺)。
|
|
36
|
+
* 与 Pi rpc-types.ts L230-265 的 RpcExtensionUIRequest 1:1 对应。
|
|
37
|
+
* method 是判别字段;每个变体仅列出该 method 的已知字段(可选字段保持可选)。
|
|
38
|
+
* 未知 method 走 string fallback(保留 raw 字段,避免协议演进时丢字段)。 */
|
|
39
|
+
export type ExtensionUiRequest =
|
|
40
|
+
| { method: "select"; title: string; options: string[]; timeout?: number }
|
|
41
|
+
| { method: "confirm"; title: string; message: string; timeout?: number }
|
|
42
|
+
| { method: "input"; title: string; placeholder?: string; timeout?: number }
|
|
43
|
+
| { method: "editor"; title: string; prefill?: string }
|
|
44
|
+
| { method: "notify"; message: string; notifyType?: "info" | "warning" | "error" }
|
|
45
|
+
| { method: "setStatus"; statusKey: string; statusText: string | undefined }
|
|
46
|
+
| {
|
|
47
|
+
method: "setWidget";
|
|
48
|
+
widgetKey: string;
|
|
49
|
+
widgetLines: string[] | undefined;
|
|
50
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
51
|
+
}
|
|
52
|
+
| { method: "setTitle"; title: string }
|
|
53
|
+
| { method: "set_editor_text"; text: string }
|
|
54
|
+
// 未知 method fallback:保留原始字段,避免协议演进时丢信息
|
|
55
|
+
| { method: string; raw: Record<string, unknown> };
|
|
56
|
+
|
|
57
|
+
/** 解析后的 extension_ui_request 顶层形状(type 守卫用)。
|
|
58
|
+
* id 和 method 在顶层,method 特定字段平铺(与 Pi 原生格式一致)。 */
|
|
59
|
+
interface ExtensionUiRequestEnvelope {
|
|
60
|
+
type: "extension_ui_request";
|
|
61
|
+
id: string;
|
|
62
|
+
method: string;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pi 原生 RPC response 顶层形状(type 守卫用)。
|
|
67
|
+
* 与 Pi rpc-types.ts 的 RpcResponse 一致:type:"response" + command + success。 */
|
|
68
|
+
interface RpcResponseEnvelope {
|
|
69
|
+
type: "response";
|
|
70
|
+
command: string;
|
|
71
|
+
success: boolean;
|
|
72
|
+
id?: string;
|
|
73
|
+
data?: unknown;
|
|
74
|
+
error?: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** parseSpawnLine 的分类结果。
|
|
79
|
+
*
|
|
80
|
+
* 关键改动(W1 协议层重写):
|
|
81
|
+
* - extension_ui_request 分支:从 {id, params:Record} 改为 {id, request: ExtensionUiRequest}
|
|
82
|
+
* (request 按 method 平铺,与 Pi rpc-types.ts 1:1)
|
|
83
|
+
* - response 分支:从 {id, result, error} 改为 {id?, command, success, data?, error?}
|
|
84
|
+
* (Pi 原生 RpcResponse 格式,SR-1 根因 1b) */
|
|
34
85
|
export type ParsedSpawnLine =
|
|
35
86
|
| { kind: "header"; header: SpawnSessionHeader }
|
|
36
87
|
| { kind: "event"; event: SdkEvent }
|
|
88
|
+
| {
|
|
89
|
+
kind: "response";
|
|
90
|
+
id?: string;
|
|
91
|
+
command: string;
|
|
92
|
+
success: boolean;
|
|
93
|
+
data?: unknown;
|
|
94
|
+
error?: string;
|
|
95
|
+
}
|
|
96
|
+
| { kind: "extension_ui_request"; id: string; request: ExtensionUiRequest }
|
|
37
97
|
| { kind: "invalid"; raw: string; error: string };
|
|
38
98
|
|
|
39
99
|
/**
|
|
@@ -52,15 +112,149 @@ function isSessionHeader(obj: unknown): obj is SpawnSessionHeader {
|
|
|
52
112
|
);
|
|
53
113
|
}
|
|
54
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 判断解析出的 JSON 是否为 Pi 原生 RPC response。
|
|
117
|
+
*
|
|
118
|
+
* SR-1 根因 1b 修复:旧守卫判 JSON-RPC 2.0(jsonrpc + id + result/error),
|
|
119
|
+
* 但 Pi 实际发 {type:"response", command, success, data?, error?}。
|
|
120
|
+
* 新守卫只认 Pi 原生格式:
|
|
121
|
+
* - type === "response"
|
|
122
|
+
* - command: string(调用的命令名,如 "run_tool")
|
|
123
|
+
* - success: boolean
|
|
124
|
+
* id 可选(通知型 response 无 id)。
|
|
125
|
+
*
|
|
126
|
+
* 旧 JSON-RPC 2.0 response({jsonrpc, id, result})不再被识别 → 落 invalid 分支。
|
|
127
|
+
*/
|
|
128
|
+
function isRpcResponse(obj: unknown): obj is RpcResponseEnvelope {
|
|
129
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
130
|
+
const r = obj as Record<string, unknown>;
|
|
131
|
+
return (
|
|
132
|
+
r.type === "response" &&
|
|
133
|
+
typeof r.command === "string" &&
|
|
134
|
+
typeof r.success === "boolean"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 判断解析出的 JSON 是否为 Pi 原生 extension_ui_request。
|
|
140
|
+
*
|
|
141
|
+
* 关键改动(W1):旧守卫判 JSON-RPC 2.0(jsonrpc + method:"extension_ui_request" + params),
|
|
142
|
+
* 但 Pi 实际发平铺格式 {type:"extension_ui_request", id, method, ...method特定字段}。
|
|
143
|
+
* 新守卫:
|
|
144
|
+
* - type === "extension_ui_request"(顶层 type 字段,非 method 字段值)
|
|
145
|
+
* - id: string
|
|
146
|
+
* - method: string(select/confirm/.../set_editor_text 等具体方法名)
|
|
147
|
+
*
|
|
148
|
+
* 删掉 jsonrpc 守卫(Pi 不发 JSON-RPC 2.0 envelope)和 params 守卫(字段平铺,无 params 包裹)。
|
|
149
|
+
* 旧 JSON-RPC 2.0 格式({jsonrpc, method:"extension_ui_request", params})不再被识别。
|
|
150
|
+
*/
|
|
151
|
+
function isExtensionUiRequest(obj: unknown): obj is ExtensionUiRequestEnvelope {
|
|
152
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
153
|
+
const r = obj as Record<string, unknown>;
|
|
154
|
+
return (
|
|
155
|
+
r.type === "extension_ui_request" &&
|
|
156
|
+
typeof r.id === "string" &&
|
|
157
|
+
typeof r.method === "string"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 从已通过 isExtensionUiRequest 守卫的 envelope 构造 ExtensionUiRequest 变体。
|
|
163
|
+
*
|
|
164
|
+
* 按 method 平铺提取字段(与 Pi rpc-types.ts L230-265 1:1)。已知 method
|
|
165
|
+
* 走对应变体;未知 method 走 string fallback(保留 raw 字段全量字段)。
|
|
166
|
+
*
|
|
167
|
+
* 字段类型容错:协议字段类型不符(如 options 非数组)时,该字段降级为空数组/undefined
|
|
168
|
+
*(数组类字段做元素类型过滤,剔除非字符串元素),仍归类为已知 method(不丢 method 信息)。
|
|
169
|
+
*/
|
|
170
|
+
function buildExtensionUiRequest(env: ExtensionUiRequestEnvelope): ExtensionUiRequest {
|
|
171
|
+
const r: Record<string, unknown> = env;
|
|
172
|
+
switch (env.method) {
|
|
173
|
+
case "select":
|
|
174
|
+
return {
|
|
175
|
+
method: "select",
|
|
176
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
177
|
+
options: Array.isArray(r.options)
|
|
178
|
+
? r.options.filter((x): x is string => typeof x === "string")
|
|
179
|
+
: [],
|
|
180
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
181
|
+
};
|
|
182
|
+
case "confirm":
|
|
183
|
+
return {
|
|
184
|
+
method: "confirm",
|
|
185
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
186
|
+
message: typeof r.message === "string" ? r.message : "",
|
|
187
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
188
|
+
};
|
|
189
|
+
case "input":
|
|
190
|
+
return {
|
|
191
|
+
method: "input",
|
|
192
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
193
|
+
...(typeof r.placeholder === "string" ? { placeholder: r.placeholder } : {}),
|
|
194
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
195
|
+
};
|
|
196
|
+
case "editor":
|
|
197
|
+
return {
|
|
198
|
+
method: "editor",
|
|
199
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
200
|
+
...(typeof r.prefill === "string" ? { prefill: r.prefill } : {}),
|
|
201
|
+
};
|
|
202
|
+
case "notify":
|
|
203
|
+
return {
|
|
204
|
+
method: "notify",
|
|
205
|
+
message: typeof r.message === "string" ? r.message : "",
|
|
206
|
+
...(r.notifyType === "info" || r.notifyType === "warning" || r.notifyType === "error"
|
|
207
|
+
? { notifyType: r.notifyType }
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
case "setStatus":
|
|
211
|
+
return {
|
|
212
|
+
method: "setStatus",
|
|
213
|
+
statusKey: typeof r.statusKey === "string" ? r.statusKey : "",
|
|
214
|
+
statusText: typeof r.statusText === "string" ? r.statusText : undefined,
|
|
215
|
+
};
|
|
216
|
+
case "setWidget": {
|
|
217
|
+
const placement = r.widgetPlacement;
|
|
218
|
+
const widgetLines = Array.isArray(r.widgetLines)
|
|
219
|
+
? r.widgetLines.filter((x): x is string => typeof x === "string")
|
|
220
|
+
: undefined;
|
|
221
|
+
return {
|
|
222
|
+
method: "setWidget",
|
|
223
|
+
widgetKey: typeof r.widgetKey === "string" ? r.widgetKey : "",
|
|
224
|
+
widgetLines,
|
|
225
|
+
...(placement === "aboveEditor" || placement === "belowEditor"
|
|
226
|
+
? { widgetPlacement: placement }
|
|
227
|
+
: {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "setTitle":
|
|
231
|
+
return {
|
|
232
|
+
method: "setTitle",
|
|
233
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
234
|
+
};
|
|
235
|
+
case "set_editor_text":
|
|
236
|
+
return {
|
|
237
|
+
method: "set_editor_text",
|
|
238
|
+
text: typeof r.text === "string" ? r.text : "",
|
|
239
|
+
};
|
|
240
|
+
default:
|
|
241
|
+
// 未知 method:保留全部原始字段,避免协议演进时丢信息
|
|
242
|
+
return { method: env.method, raw: r };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
55
246
|
/**
|
|
56
247
|
* 解析 pi stdout 的一行。
|
|
57
248
|
*
|
|
58
249
|
* @param line stdout 的一行(不含换行符;空行返回 null)
|
|
59
250
|
* @returns 分类结果,或 null(空行/仅空白)
|
|
60
251
|
*
|
|
61
|
-
*
|
|
252
|
+
* 分类规则(判定顺序关键,见 W1 bug 修复):
|
|
62
253
|
* - 空白行 → null(pi 可能输出空行,跳过)
|
|
63
|
-
* - 合法 JSON + type:"session" +
|
|
254
|
+
* - 合法 JSON + type:"session" + 必需字段 → header
|
|
255
|
+
* - 合法 JSON + type:"extension_ui_request" + id + method → extension_ui_request
|
|
256
|
+
* (必须在 event 分支之前,否则被 typeof obj.type===string 吞为 event)
|
|
257
|
+
* - 合法 JSON + type:"response" + command + success → response
|
|
64
258
|
* - 合法 JSON + 有 type 字段 → event(SdkEvent,type schema 由调用方校验)
|
|
65
259
|
* - 合法 JSON 但无 type → invalid
|
|
66
260
|
* - 非法 JSON → invalid(记录 error,不抛——单行损坏不应中断整个流)
|
|
@@ -87,6 +281,25 @@ export function parseSpawnLine(line: string): ParsedSpawnLine | null {
|
|
|
87
281
|
return { kind: "header", header: obj };
|
|
88
282
|
}
|
|
89
283
|
|
|
284
|
+
// extension_ui_request:必须在 event 分支之前判定(W1 判定顺序 bug 修复)。
|
|
285
|
+
// 原因:extension_ui_request 也有 type 字段,若 event 分支(typeof obj.type===string)
|
|
286
|
+
// 在前,会被当 event 静默吞掉。现在先于 event 判定,命中后按 method 构造 request。
|
|
287
|
+
if (isExtensionUiRequest(obj)) {
|
|
288
|
+
return { kind: "extension_ui_request", id: obj.id, request: buildExtensionUiRequest(obj) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// RPC response:Pi 原生格式 {type:"response", command, success, data?, error?}
|
|
292
|
+
if (isRpcResponse(obj)) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "response",
|
|
295
|
+
...(typeof obj.id === "string" ? { id: obj.id } : {}),
|
|
296
|
+
command: obj.command,
|
|
297
|
+
success: obj.success,
|
|
298
|
+
...(obj.data !== undefined ? { data: obj.data } : {}),
|
|
299
|
+
...(typeof obj.error === "string" ? { error: obj.error } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
90
303
|
// 事件行:必须有 type 字段(SdkEvent 契约)
|
|
91
304
|
if (
|
|
92
305
|
typeof obj === "object" &&
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/execution/stdin-writer.ts
|
|
2
|
+
//
|
|
3
|
+
// 向 rpc 子进程 stdin 写入命令的 helper 集合。
|
|
4
|
+
//
|
|
5
|
+
// pi --mode rpc 通过 stdin 的 JSON RpcCommand / RpcExtensionUIResponse 驱动:
|
|
6
|
+
// - extension_ui_response(主进程回答子进程的 UI 请求,如 ask_user)
|
|
7
|
+
// - prompt(驱动子进程开始处理 task)
|
|
8
|
+
// 两者共用 child.stdin.write + 背压检查,提取到此模块统一维护。
|
|
9
|
+
|
|
10
|
+
import type { ChildProcess } from "node:child_process";
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import type { UiResponse } from "./dialog-queue.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 按 UiResponse 形状构造 Pi 原生 extension_ui_response 并写 stdin。
|
|
17
|
+
*
|
|
18
|
+
* SR-5:ack(fire-and-forget)不写 stdin——Pi 对 fire-and-forget method 不期待响应,
|
|
19
|
+
* 写入会触发协议错配。其他三种 shape(value/confirmed/cancelled)按对应字段写。
|
|
20
|
+
*
|
|
21
|
+
* [R1] 背压检查:child.stdin.write 返回 false 时记 warn(不阻塞,内核缓冲会随后排空)。
|
|
22
|
+
* [R2] 序列化在本函数内逐分支完成。JSON.stringify 可能抛错(out.value 含循环引用 /
|
|
23
|
+
* BigInt 等不可序列化结构),try/catch 降级为 cancelled——宁可取消单次 dialog 也不让
|
|
24
|
+
* 父进程崩溃(UI 请求通道不应被脏数据拖垮)。
|
|
25
|
+
*
|
|
26
|
+
* @param child 子进程(stdin 写入响应)
|
|
27
|
+
* @param id 请求 id(关联 response)
|
|
28
|
+
* @param out UiResponse({value}/{confirmed}/{cancelled}/{ack})
|
|
29
|
+
* @param signal abort signal(已 aborted 时跳过写入)
|
|
30
|
+
*/
|
|
31
|
+
export function respond(child: ChildProcess, id: string, out: UiResponse, signal?: AbortSignal): void {
|
|
32
|
+
if (signal?.aborted) return;
|
|
33
|
+
let line: string | undefined;
|
|
34
|
+
try {
|
|
35
|
+
if ("value" in out) line = JSON.stringify({ type: "extension_ui_response", id, value: out.value });
|
|
36
|
+
else if ("confirmed" in out) line = JSON.stringify({ type: "extension_ui_response", id, confirmed: out.confirmed });
|
|
37
|
+
else if ("cancelled" in out) line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// [R2] out.value 含循环引用/BigInt 等不可序列化结构——降级 cancelled,避免父进程崩溃。
|
|
40
|
+
console.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled:`, err);
|
|
41
|
+
line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
|
|
42
|
+
}
|
|
43
|
+
// ack: fire-and-forget,不写 stdin(SR-5)
|
|
44
|
+
if (line === undefined) return;
|
|
45
|
+
writeStdinLine(child, line, `ui response for request ${id}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* spawn 后向 rpc 子进程 stdin 写 prompt 命令,驱动 agent 开始处理 task。
|
|
50
|
+
*
|
|
51
|
+
* pi 的 runRpcMode 只通过 stdin RpcCommand 驱动——positional task arg / -p flag
|
|
52
|
+
* 在 rpc mode 下被 resolveAppMode 无视。必须在 spawn 后主动喂 prompt 命令,
|
|
53
|
+
* 否则子进程阻塞等 stdin、永不进入推理(totalTokens 恒 0)。
|
|
54
|
+
*
|
|
55
|
+
* 时机:spawn 后立即写。stdin 是 pipe,内核缓冲保证数据不丢;
|
|
56
|
+
* pi 在 await rebindSession() 后才挂 stdin reader(rpc-mode.ts:778-781),
|
|
57
|
+
* reader 处理 prompt 时 session 已就绪。
|
|
58
|
+
*
|
|
59
|
+
* @param child 子进程(stdin 写入 prompt 命令)
|
|
60
|
+
* @param task 完整 task 文本(含 schema 指令)
|
|
61
|
+
*/
|
|
62
|
+
export function sendPromptCommand(child: ChildProcess, task: string): void {
|
|
63
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
64
|
+
const command = JSON.stringify({
|
|
65
|
+
id: crypto.randomUUID(),
|
|
66
|
+
type: "prompt",
|
|
67
|
+
message: task,
|
|
68
|
+
});
|
|
69
|
+
writeStdinLine(child, command, "prompt command");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 向 rpc 子进程 stdin 写 get_state 命令,查询 sessionFile/sessionId。
|
|
74
|
+
*
|
|
75
|
+
* FR-4: RPC get_state 握手。当 stdout header 未携带 sessionFile 时,
|
|
76
|
+
* 通过此命令向子进程查询当前 session 状态。子进程收到后返回
|
|
77
|
+
* {type:"response", command:"get_state", success:true, data:{sessionFile, sessionId}}。
|
|
78
|
+
*
|
|
79
|
+
* @param child 子进程(stdin 写入 get_state 命令)
|
|
80
|
+
* @returns 请求 id(用于匹配 response)
|
|
81
|
+
*/
|
|
82
|
+
export function sendGetStateCommand(child: ChildProcess): string {
|
|
83
|
+
const id = crypto.randomUUID();
|
|
84
|
+
const command = JSON.stringify({
|
|
85
|
+
id,
|
|
86
|
+
type: "get_state",
|
|
87
|
+
});
|
|
88
|
+
writeStdinLine(child, command, "get_state command");
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 向子进程 stdin 写一行(自动补换行),带背压检查。
|
|
94
|
+
*
|
|
95
|
+
* [R1] write 返回 false 时记 warn(不阻塞,内核缓冲会随后排空)。
|
|
96
|
+
* stdin 已关闭/销毁时跳过——respond 已检查 signal,sendPromptCommand 已检查 destroyed。
|
|
97
|
+
*
|
|
98
|
+
* @param child 子进程
|
|
99
|
+
* @param line JSON 行(不含换行)
|
|
100
|
+
* @param warnTag warn 日志的语义标记
|
|
101
|
+
*/
|
|
102
|
+
function writeStdinLine(child: ChildProcess, line: string, warnTag: string): void {
|
|
103
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
104
|
+
const ok = child.stdin.write(line + "\n");
|
|
105
|
+
if (!ok) console.warn(`[subagents] stdin backpressure on ${warnTag}`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent text_delta streaming sink。
|
|
3
|
+
*
|
|
4
|
+
* background subagent 执行期间,session-runner 的 agentEvent 出口把每个 text_delta
|
|
5
|
+
* 传到 SubagentStream.onDelta。本模块做 100ms 时间窗合并后,通过 StreamSink.setWidget
|
|
6
|
+
* 转发到 RPC stdout(经 ctx.ui.setWidget → extension_ui_request 通道)。
|
|
7
|
+
*
|
|
8
|
+
* SubagentStream 是一个生命周期对象——内聚 buffer/timer 状态 + onDelta/dispose 方法。
|
|
9
|
+
* 调用方(subagent-service)创建后只需在 text_delta 时调 onDelta、终态时调 dispose,
|
|
10
|
+
* 不需要拆散 push/clear 两个函数跨层透传。
|
|
11
|
+
*
|
|
12
|
+
* 设计要点:
|
|
13
|
+
* - leading edge:第一个 delta 立即 flush(前端尽快看到开始)
|
|
14
|
+
* - trailing edge:后续 delta 追加 buffer,timer 到期后 flush
|
|
15
|
+
* - 每次 flush 把 buffer 的累积全文 split("\n") 传给 setWidget
|
|
16
|
+
* - dispose 清除 widget + 清 timer
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** UI streaming sink 的最小接口(ctx.ui.setWidget 的 duck-typed 子集)。
|
|
20
|
+
*
|
|
21
|
+
* 当前只有一个 adapter(index.ts session_start 包装 ctx.ui.setWidget)。
|
|
22
|
+
* 保留接口而非裸函数类型,因为 StreamSink 的语义是「UI sink 契约」——
|
|
23
|
+
* 测试 mock 和未来可能的第二 sink(如写文件)都走此契约。 */
|
|
24
|
+
export interface StreamSink {
|
|
25
|
+
setWidget(key: string, lines: string[] | undefined): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** delta 合并窗口时间(ms)。与 onEventThrottled 的节流间隔对齐。 */
|
|
29
|
+
const STREAM_FLUSH_MS = 100;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* subagent text_delta streaming 生命周期对象。
|
|
33
|
+
*
|
|
34
|
+
* 创建后:
|
|
35
|
+
* - `onDelta(delta)`:session-runner 每次 text_delta 调
|
|
36
|
+
* - `dispose()`:subagent 终态时调,清除 widget + 清 timer
|
|
37
|
+
*
|
|
38
|
+
* buffer/timer 状态全部内聚在此对象,调用方不需要关心合并逻辑。
|
|
39
|
+
*/
|
|
40
|
+
export class SubagentStream {
|
|
41
|
+
private readonly widgetKey: string;
|
|
42
|
+
private readonly sink: StreamSink;
|
|
43
|
+
private buffer = "";
|
|
44
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
45
|
+
private hasFlushed = false;
|
|
46
|
+
private disposed = false;
|
|
47
|
+
|
|
48
|
+
constructor(recordId: string, sink: StreamSink) {
|
|
49
|
+
this.widgetKey = `subagent-stream-${recordId}`;
|
|
50
|
+
this.sink = sink;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 接收一个 text_delta 增量。空串静默丢弃(不消耗 leading edge)。 */
|
|
54
|
+
onDelta(delta: string): void {
|
|
55
|
+
if (this.disposed || delta.length === 0) return;
|
|
56
|
+
this.buffer += delta;
|
|
57
|
+
if (!this.hasFlushed) {
|
|
58
|
+
// leading edge:第一个 delta 立即 flush
|
|
59
|
+
this.hasFlushed = true;
|
|
60
|
+
this.flush();
|
|
61
|
+
} else if (this.timer === undefined) {
|
|
62
|
+
// trailing edge:后续 delta 经 timer 合并
|
|
63
|
+
this.timer = setTimeout(() => this.flush(), STREAM_FLUSH_MS);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 终态清理:清除 widget + 清 timer(幂等)。 */
|
|
68
|
+
dispose(): void {
|
|
69
|
+
if (this.disposed) return;
|
|
70
|
+
this.disposed = true;
|
|
71
|
+
if (this.timer !== undefined) {
|
|
72
|
+
clearTimeout(this.timer);
|
|
73
|
+
this.timer = undefined;
|
|
74
|
+
}
|
|
75
|
+
this.sink.setWidget(this.widgetKey, undefined);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private flush(): void {
|
|
79
|
+
this.timer = undefined;
|
|
80
|
+
if (this.buffer.length === 0 || this.disposed) return;
|
|
81
|
+
this.sink.setWidget(this.widgetKey, this.buffer.split("\n"));
|
|
82
|
+
}
|
|
83
|
+
}
|