@zhushanwen/pi-subagent-cli 0.1.1
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/bin/pi-subagent-cli.mjs +41 -0
- package/dist/chunk-SDXGJXQR.js +2502 -0
- package/dist/index.cjs +2538 -0
- package/dist/index.d.cts +784 -0
- package/dist/index.d.ts +784 -0
- package/dist/index.js +90 -0
- package/dist/main.cjs +2425 -0
- package/dist/main.d.cts +2 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +20 -0
- package/package.json +79 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2538 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
ChatSessionRegistry: () => ChatSessionRegistry,
|
|
34
|
+
DETERMINISTIC_SCHEMA_FAILURE_PREFIX: () => DETERMINISTIC_SCHEMA_FAILURE_PREFIX,
|
|
35
|
+
EPIPE_FAILURE_THRESHOLD: () => EPIPE_FAILURE_THRESHOLD,
|
|
36
|
+
EngineProtocolServer: () => EngineProtocolServer,
|
|
37
|
+
PI_ADAPTER_VERSION: () => PI_ADAPTER_VERSION,
|
|
38
|
+
PI_ENGINE_ID: () => PI_ENGINE_ID,
|
|
39
|
+
PI_POOL_KEY: () => PI_POOL_KEY,
|
|
40
|
+
PiEngine: () => PiEngine,
|
|
41
|
+
STALE_CONTEXT_PATTERNS: () => STALE_CONTEXT_PATTERNS,
|
|
42
|
+
WRAP_UP_HINT: () => WRAP_UP_HINT,
|
|
43
|
+
applySchemaEnvToChildEnv: () => applySchemaEnvToChildEnv,
|
|
44
|
+
buildEnvBlock: () => buildEnvBlock,
|
|
45
|
+
buildSpawnArgs: () => buildSpawnArgs,
|
|
46
|
+
classifyFailureKind: () => classifyFailureKind,
|
|
47
|
+
clearEpipeFailure: () => clearEpipeFailure,
|
|
48
|
+
collectOutcome: () => collectOutcome,
|
|
49
|
+
createDefaultPiEngine: () => createDefaultPiEngine,
|
|
50
|
+
createTurnLimiter: () => createTurnLimiter,
|
|
51
|
+
createUiRequestQueue: () => createUiRequestQueue,
|
|
52
|
+
deriveSessionFilePath: () => deriveSessionFilePath,
|
|
53
|
+
describeMissingParsedOutput: () => describeMissingParsedOutput,
|
|
54
|
+
extractParsedOutput: () => extractParsedOutput,
|
|
55
|
+
findSessionFileByHeaderId: () => findSessionFileByHeaderId,
|
|
56
|
+
getActiveChild: () => getActiveChild,
|
|
57
|
+
getPiInvocation: () => getPiInvocation,
|
|
58
|
+
isDeterministicSchemaFailureMsg: () => isDeterministicSchemaFailureMsg,
|
|
59
|
+
isStaleContextErrorMsg: () => isStaleContextErrorMsg,
|
|
60
|
+
killAllActiveChildren: () => killAllActiveChildren,
|
|
61
|
+
mapAssistantMessageDelta: () => mapAssistantMessageDelta,
|
|
62
|
+
mirrorMainProcessFlags: () => mirrorMainProcessFlags,
|
|
63
|
+
neutralizeStalePatterns: () => neutralizeStalePatterns,
|
|
64
|
+
parseChannel: () => import_subagent_engine_sdk9.parseChannel,
|
|
65
|
+
parseSpawnLine: () => parseSpawnLine,
|
|
66
|
+
parseSpawnModelRef: () => parseSpawnModelRef,
|
|
67
|
+
performGetStateHandshake: () => performGetStateHandshake,
|
|
68
|
+
recordEpipeFailure: () => recordEpipeFailure,
|
|
69
|
+
replayJournalToSessionView: () => replayJournalToSessionView,
|
|
70
|
+
requestGetStateOnce: () => requestGetStateOnce,
|
|
71
|
+
resetAllEpipeFailures: () => resetAllEpipeFailures,
|
|
72
|
+
respond: () => respond,
|
|
73
|
+
runSpawnOnce: () => runSpawnOnce,
|
|
74
|
+
sendGetStateCommand: () => sendGetStateCommand,
|
|
75
|
+
sendPromptCommand: () => sendPromptCommand
|
|
76
|
+
});
|
|
77
|
+
module.exports = __toCommonJS(src_exports);
|
|
78
|
+
|
|
79
|
+
// src/server.ts
|
|
80
|
+
var import_subagent_engine_sdk20 = require("@zhushanwen/subagent-engine-sdk");
|
|
81
|
+
|
|
82
|
+
// src/constants.ts
|
|
83
|
+
var PI_ENGINE_ID = "pi";
|
|
84
|
+
var PI_ADAPTER_VERSION = "1.0.0";
|
|
85
|
+
var PI_POOL_KEY = "shared";
|
|
86
|
+
var SCHEMA_ENV_VAR = "PI_WORKFLOW_SCHEMA";
|
|
87
|
+
var SCHEMA_ENV_MAX_KIB = 256;
|
|
88
|
+
var BYTES_PER_KIB = 1024;
|
|
89
|
+
var SCHEMA_ENV_MAX_BYTES = SCHEMA_ENV_MAX_KIB * BYTES_PER_KIB;
|
|
90
|
+
var PI_KILL_GRACE_MS = 3e4;
|
|
91
|
+
|
|
92
|
+
// src/pi-engine.ts
|
|
93
|
+
var import_node_child_process2 = require("child_process");
|
|
94
|
+
var import_subagent_engine_sdk17 = require("@zhushanwen/subagent-engine-sdk");
|
|
95
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
96
|
+
var path4 = __toESM(require("path"), 1);
|
|
97
|
+
var import_subagent_engine_sdk18 = require("@zhushanwen/subagent-engine-sdk");
|
|
98
|
+
|
|
99
|
+
// src/error-message.ts
|
|
100
|
+
var import_subagent_engine_sdk = require("@zhushanwen/subagent-engine-sdk");
|
|
101
|
+
|
|
102
|
+
// src/pi-invocation.ts
|
|
103
|
+
var fs = __toESM(require("fs"), 1);
|
|
104
|
+
var path = __toESM(require("path"), 1);
|
|
105
|
+
|
|
106
|
+
// src/relay-env.ts
|
|
107
|
+
var import_subagent_engine_sdk2 = require("@zhushanwen/subagent-engine-sdk");
|
|
108
|
+
|
|
109
|
+
// src/pi-invocation.ts
|
|
110
|
+
var BUN_VIRTUAL_PREFIX = "/$bunfs/root/";
|
|
111
|
+
var SELF_CLI_BIN_BASENAME = "pi-subagent-cli.mjs";
|
|
112
|
+
function isGenericRuntime(execPath) {
|
|
113
|
+
const execName = path.basename(execPath).toLowerCase();
|
|
114
|
+
return /^(node|bun)(\.exe)?$/.test(execName);
|
|
115
|
+
}
|
|
116
|
+
var scriptExistsCache;
|
|
117
|
+
function currentScriptExists() {
|
|
118
|
+
const currentScript = process.argv[1];
|
|
119
|
+
if (scriptExistsCache === void 0 || scriptExistsCache.script !== currentScript) {
|
|
120
|
+
scriptExistsCache = {
|
|
121
|
+
script: currentScript,
|
|
122
|
+
exists: currentScript !== void 0 && !currentScript.startsWith(BUN_VIRTUAL_PREFIX) && fs.existsSync(currentScript)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return scriptExistsCache.exists;
|
|
126
|
+
}
|
|
127
|
+
function getPiInvocation(userArgs, opts) {
|
|
128
|
+
const currentScript = process.argv[1];
|
|
129
|
+
const isBunVirtualScript = currentScript?.startsWith(BUN_VIRTUAL_PREFIX);
|
|
130
|
+
const isSelfCliBin = currentScript !== void 0 && path.basename(currentScript) === SELF_CLI_BIN_BASENAME;
|
|
131
|
+
if (opts?.relay !== false && (0, import_subagent_engine_sdk2.isRelayActive)(process.env)) {
|
|
132
|
+
const relayNode = process.env[import_subagent_engine_sdk2.RELAY_ENV_NODE];
|
|
133
|
+
const relayScript = process.env[import_subagent_engine_sdk2.RELAY_ENV_SCRIPT];
|
|
134
|
+
if (relayNode !== void 0 && relayScript !== void 0) {
|
|
135
|
+
return { command: relayNode, args: [relayScript, ...userArgs] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (currentScript && !isBunVirtualScript && !isSelfCliBin && currentScriptExists()) {
|
|
139
|
+
return { command: process.execPath, args: [currentScript, ...userArgs] };
|
|
140
|
+
}
|
|
141
|
+
if (!isGenericRuntime(process.execPath)) {
|
|
142
|
+
return { command: process.execPath, args: userArgs };
|
|
143
|
+
}
|
|
144
|
+
return { command: "pi", args: userArgs };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/stdin-writer.ts
|
|
148
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
149
|
+
var import_subagent_engine_sdk3 = require("@zhushanwen/subagent-engine-sdk");
|
|
150
|
+
var logger = (0, import_subagent_engine_sdk3.getLogger)("subagents");
|
|
151
|
+
var epipeConsecutiveFailures = /* @__PURE__ */ new Map();
|
|
152
|
+
var EPIPE_FAILURE_THRESHOLD = 2;
|
|
153
|
+
function recordEpipeFailure(recordId) {
|
|
154
|
+
const count = (epipeConsecutiveFailures.get(recordId) ?? 0) + 1;
|
|
155
|
+
epipeConsecutiveFailures.set(recordId, count);
|
|
156
|
+
return count;
|
|
157
|
+
}
|
|
158
|
+
function clearEpipeFailure(recordId) {
|
|
159
|
+
epipeConsecutiveFailures.delete(recordId);
|
|
160
|
+
}
|
|
161
|
+
function resetAllEpipeFailures() {
|
|
162
|
+
epipeConsecutiveFailures.clear();
|
|
163
|
+
}
|
|
164
|
+
function respond(child, id, out, signal) {
|
|
165
|
+
if (signal?.aborted) return;
|
|
166
|
+
let line;
|
|
167
|
+
try {
|
|
168
|
+
if ("value" in out) line = JSON.stringify({ type: "extension_ui_response", id, value: out.value });
|
|
169
|
+
else if ("confirmed" in out) line = JSON.stringify({ type: "extension_ui_response", id, confirmed: out.confirmed });
|
|
170
|
+
else if ("cancelled" in out) line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled`, {
|
|
173
|
+
detail: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
174
|
+
});
|
|
175
|
+
line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
|
|
176
|
+
}
|
|
177
|
+
if (line === void 0) return;
|
|
178
|
+
writeStdinLine(child, line, `ui response for request ${id}`);
|
|
179
|
+
}
|
|
180
|
+
function sendPromptCommand(child, task, options) {
|
|
181
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
182
|
+
const payload = {
|
|
183
|
+
id: crypto.randomUUID(),
|
|
184
|
+
type: "prompt",
|
|
185
|
+
message: task
|
|
186
|
+
};
|
|
187
|
+
if (options?.streamingBehavior) {
|
|
188
|
+
payload.streamingBehavior = options.streamingBehavior;
|
|
189
|
+
}
|
|
190
|
+
writeStdinLine(child, JSON.stringify(payload), "prompt command");
|
|
191
|
+
}
|
|
192
|
+
function sendGetStateCommand(child) {
|
|
193
|
+
const id = crypto.randomUUID();
|
|
194
|
+
const command = JSON.stringify({
|
|
195
|
+
id,
|
|
196
|
+
type: "get_state"
|
|
197
|
+
});
|
|
198
|
+
writeStdinLine(child, command, "get_state command");
|
|
199
|
+
return id;
|
|
200
|
+
}
|
|
201
|
+
function writeStdinLine(child, line, warnTag) {
|
|
202
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
203
|
+
try {
|
|
204
|
+
const ok = child.stdin.write(line + "\n");
|
|
205
|
+
if (!ok) logger.warn(`[subagents] stdin backpressure on ${warnTag}`);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err !== null && typeof err === "object" && "code" in err && (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED")) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[subagents] EPIPE on stdin write (${warnTag}): pipe broken, child process likely exited. Recovery: treat as dead process and resume via cold path.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
logger.warn(`[subagents] unexpected stdin write error on ${warnTag}`, {
|
|
213
|
+
detail: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/spawn-runner.ts
|
|
219
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
220
|
+
var import_node_path = require("path");
|
|
221
|
+
var import_subagent_engine_sdk13 = require("@zhushanwen/subagent-engine-sdk");
|
|
222
|
+
|
|
223
|
+
// src/argv-mirror.ts
|
|
224
|
+
var VALUED_FLAGS = /* @__PURE__ */ new Set([
|
|
225
|
+
"--extension",
|
|
226
|
+
"-e",
|
|
227
|
+
"--skill",
|
|
228
|
+
"--model",
|
|
229
|
+
"--system-prompt",
|
|
230
|
+
"--append-system-prompt",
|
|
231
|
+
"--tools",
|
|
232
|
+
"-t",
|
|
233
|
+
"--exclude-tools",
|
|
234
|
+
"-xt",
|
|
235
|
+
"--fork",
|
|
236
|
+
"--session-dir",
|
|
237
|
+
"--mode",
|
|
238
|
+
"--thinking",
|
|
239
|
+
"--models"
|
|
240
|
+
]);
|
|
241
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Map([
|
|
242
|
+
["--no-extensions", "noExtensions"],
|
|
243
|
+
["-ne", "noExtensions"],
|
|
244
|
+
["--approve", "approve"],
|
|
245
|
+
["-a", "approve"],
|
|
246
|
+
["--no-context-files", "noContextFiles"],
|
|
247
|
+
["-nc", "noContextFiles"]
|
|
248
|
+
]);
|
|
249
|
+
var ARGV_FLAG_START = 2;
|
|
250
|
+
var memoArgv;
|
|
251
|
+
var memoResult;
|
|
252
|
+
function mirrorMainProcessFlags(argv) {
|
|
253
|
+
if (memoResult !== void 0 && memoArgv === argv) return memoResult;
|
|
254
|
+
const result = parseMirrorFlagArgs(
|
|
255
|
+
argv.length > ARGV_FLAG_START ? argv.slice(ARGV_FLAG_START) : []
|
|
256
|
+
);
|
|
257
|
+
memoArgv = argv;
|
|
258
|
+
memoResult = result;
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
function parseMirrorFlagArgs(flagArgs) {
|
|
262
|
+
const flags = {
|
|
263
|
+
noExtensions: false,
|
|
264
|
+
approve: false,
|
|
265
|
+
noContextFiles: false
|
|
266
|
+
};
|
|
267
|
+
const extensionPaths = [];
|
|
268
|
+
for (let i = 0; i < flagArgs.length; i++) {
|
|
269
|
+
const tok = flagArgs[i];
|
|
270
|
+
const booleanKey = BOOLEAN_FLAGS.get(tok);
|
|
271
|
+
if (booleanKey !== void 0) {
|
|
272
|
+
flags[booleanKey] = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const eqMatch = /^(--extension|-e)=(.*)$/.exec(tok);
|
|
276
|
+
if (eqMatch) {
|
|
277
|
+
const val = eqMatch[2];
|
|
278
|
+
if (val) extensionPaths.push(val);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (tok === "--extension" || tok === "-e") {
|
|
282
|
+
const next = flagArgs[i + 1];
|
|
283
|
+
if (next !== void 0 && !next.startsWith("--") && next.length > 0) {
|
|
284
|
+
extensionPaths.push(next);
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (VALUED_FLAGS.has(tok)) {
|
|
290
|
+
i++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
noExtensions: flags.noExtensions,
|
|
296
|
+
approve: flags.approve,
|
|
297
|
+
extensionPaths,
|
|
298
|
+
noContextFiles: flags.noContextFiles
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/active-children.ts
|
|
303
|
+
var import_subagent_engine_sdk4 = require("@zhushanwen/subagent-engine-sdk");
|
|
304
|
+
var activeChildren = /* @__PURE__ */ new Map();
|
|
305
|
+
function registerActiveChild(recordId, child) {
|
|
306
|
+
activeChildren.set(recordId, child);
|
|
307
|
+
}
|
|
308
|
+
function unregisterActiveChild(recordId, child) {
|
|
309
|
+
if (activeChildren.get(recordId) === child) activeChildren.delete(recordId);
|
|
310
|
+
}
|
|
311
|
+
function getActiveChild(recordId) {
|
|
312
|
+
return activeChildren.get(recordId);
|
|
313
|
+
}
|
|
314
|
+
function killAllActiveChildren(signal = "SIGTERM") {
|
|
315
|
+
let killed = 0;
|
|
316
|
+
for (const [recordId, child] of activeChildren) {
|
|
317
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
318
|
+
killed++;
|
|
319
|
+
child.kill(signal);
|
|
320
|
+
void (0, import_subagent_engine_sdk4.killChain)(child, {
|
|
321
|
+
graceMs: PI_KILL_GRACE_MS,
|
|
322
|
+
unrefTimers: true,
|
|
323
|
+
escalationNote: `child ${recordId} (source: dispose killAll)`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
activeChildren.delete(recordId);
|
|
327
|
+
}
|
|
328
|
+
return killed;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/output-collector.ts
|
|
332
|
+
var STALE_CONTEXT_PATTERNS = [
|
|
333
|
+
"ctx is stale",
|
|
334
|
+
"stale after session replacement",
|
|
335
|
+
"context canceled",
|
|
336
|
+
"aborted"
|
|
337
|
+
];
|
|
338
|
+
function isStaleContextErrorMsg(msg) {
|
|
339
|
+
if (!msg) return false;
|
|
340
|
+
const lower = msg.toLowerCase();
|
|
341
|
+
return STALE_CONTEXT_PATTERNS.some((p) => lower.includes(p));
|
|
342
|
+
}
|
|
343
|
+
var DETERMINISTIC_SCHEMA_FAILURE_PREFIX = "Structured output failed deterministically:";
|
|
344
|
+
function isDeterministicSchemaFailureMsg(msg) {
|
|
345
|
+
if (!msg) return false;
|
|
346
|
+
return msg.includes(DETERMINISTIC_SCHEMA_FAILURE_PREFIX);
|
|
347
|
+
}
|
|
348
|
+
function classifyFailureKind(msg) {
|
|
349
|
+
if (msg === void 0) return void 0;
|
|
350
|
+
if (isStaleContextErrorMsg(msg)) return "stale_context";
|
|
351
|
+
if (isDeterministicSchemaFailureMsg(msg)) return "schema_deterministic";
|
|
352
|
+
return "unknown";
|
|
353
|
+
}
|
|
354
|
+
var STRUCTURED_OUTPUT_TOOL = "structured-output";
|
|
355
|
+
var FAILED_SO_SUMMARY_MAX_CHARS = 300;
|
|
356
|
+
function extractParsedOutput(toolCalls) {
|
|
357
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
358
|
+
const tc = toolCalls[i];
|
|
359
|
+
if (tc.toolName === STRUCTURED_OUTPUT_TOOL && tc.isError !== true && tc.result?.details !== void 0) {
|
|
360
|
+
return tc.result.details;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function neutralizeStalePatterns(text) {
|
|
366
|
+
let out = text;
|
|
367
|
+
for (const pattern of STALE_CONTEXT_PATTERNS) {
|
|
368
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
369
|
+
out = out.replace(new RegExp(escaped, "gi"), "[redacted]");
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function describeMissingParsedOutput(toolCalls) {
|
|
374
|
+
if (extractParsedOutput(toolCalls) !== void 0) return void 0;
|
|
375
|
+
const soCalls = toolCalls.filter((tc) => tc.toolName === STRUCTURED_OUTPUT_TOOL);
|
|
376
|
+
if (soCalls.length === 0) {
|
|
377
|
+
return `${DETERMINISTIC_SCHEMA_FAILURE_PREFIX} Agent finished without producing a structured output: the structured-output tool was never called. Recovery: verify the structured-output extension is installed and enabled for this agent, and that the agent's final answer conforms to the requested schema.`;
|
|
378
|
+
}
|
|
379
|
+
const failed = soCalls.filter((tc) => tc.isError === true);
|
|
380
|
+
if (failed.length > 0) {
|
|
381
|
+
const last = failed[failed.length - 1];
|
|
382
|
+
const lastErrorSummary = neutralizeStalePatterns(summarizeToolContent(last.result?.content));
|
|
383
|
+
const failureKind = lastErrorSummary.toLowerCase().includes("validation failed") ? "schema validation" : "execution failure";
|
|
384
|
+
return `${DETERMINISTIC_SCHEMA_FAILURE_PREFIX} Agent finished without a valid structured output: ${failed.length} structured-output call(s) failed (${failureKind}). Last error: ${lastErrorSummary}`;
|
|
385
|
+
}
|
|
386
|
+
return "Agent finished without a valid structured output: structured-output was called but none of the successful calls carried result details.";
|
|
387
|
+
}
|
|
388
|
+
function summarizeToolContent(content) {
|
|
389
|
+
let summary = "(no detail)";
|
|
390
|
+
if (Array.isArray(content)) {
|
|
391
|
+
const texts = content.map((item) => item && typeof item === "object" && "text" in item ? String(item.text) : "").filter((t) => t !== "");
|
|
392
|
+
if (texts.length > 0) summary = texts.join(" ");
|
|
393
|
+
} else if (typeof content === "string" && content !== "") {
|
|
394
|
+
summary = content;
|
|
395
|
+
}
|
|
396
|
+
return summary.length > FAILED_SO_SUMMARY_MAX_CHARS ? `${summary.slice(0, FAILED_SO_SUMMARY_MAX_CHARS)}...` : summary;
|
|
397
|
+
}
|
|
398
|
+
function getFullText(record) {
|
|
399
|
+
return record.turns.map((t) => t.text).filter((t) => t !== void 0 && t.length > 0).join("\n\n");
|
|
400
|
+
}
|
|
401
|
+
function getAllToolCalls(record) {
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const turn of record.turns) {
|
|
404
|
+
out.push(...turn.toolCalls);
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
function aggregateUsageDelta(record) {
|
|
409
|
+
let input = 0;
|
|
410
|
+
let output = 0;
|
|
411
|
+
let cacheRead = 0;
|
|
412
|
+
let cacheWrite = 0;
|
|
413
|
+
let cost = 0;
|
|
414
|
+
let hasCost = false;
|
|
415
|
+
let any = false;
|
|
416
|
+
for (const turn of record.turns) {
|
|
417
|
+
const u = turn.usageDelta;
|
|
418
|
+
if (u === void 0) continue;
|
|
419
|
+
any = true;
|
|
420
|
+
input += u.input ?? 0;
|
|
421
|
+
output += u.output ?? 0;
|
|
422
|
+
cacheRead += u.cacheRead ?? 0;
|
|
423
|
+
cacheWrite += u.cacheWrite ?? 0;
|
|
424
|
+
if (u.cost !== void 0) {
|
|
425
|
+
cost += u.cost;
|
|
426
|
+
hasCost = true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!any) return void 0;
|
|
430
|
+
return { input, output, cacheRead, cacheWrite, ...hasCost ? { cost } : {} };
|
|
431
|
+
}
|
|
432
|
+
function collectOutcome(record, args) {
|
|
433
|
+
const toolCalls = getAllToolCalls(record);
|
|
434
|
+
const parsedOutput = extractParsedOutput(toolCalls);
|
|
435
|
+
let success = args.success;
|
|
436
|
+
let error = args.error;
|
|
437
|
+
if (args.schemaExpected === true && success && parsedOutput === void 0) {
|
|
438
|
+
success = false;
|
|
439
|
+
error = describeMissingParsedOutput(toolCalls) ?? error;
|
|
440
|
+
}
|
|
441
|
+
if (success && record.lastError !== void 0) {
|
|
442
|
+
success = false;
|
|
443
|
+
error = record.lastError;
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
content: getFullText(record),
|
|
447
|
+
turns: record.turnCount,
|
|
448
|
+
durationMs: Date.now() - args.startTime,
|
|
449
|
+
success,
|
|
450
|
+
error,
|
|
451
|
+
sessionId: args.sessionId,
|
|
452
|
+
sessionFile: args.sessionFile,
|
|
453
|
+
toolCalls,
|
|
454
|
+
parsedOutput,
|
|
455
|
+
usage: aggregateUsageDelta(record),
|
|
456
|
+
// [D5-③] 失败分诊结构化:error 最终确定后一次分类写入
|
|
457
|
+
failureKind: error !== void 0 ? classifyFailureKind(error) : void 0
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/spawn-args.ts
|
|
462
|
+
var import_node_child_process = require("child_process");
|
|
463
|
+
var import_subagent_engine_sdk5 = require("@zhushanwen/subagent-engine-sdk");
|
|
464
|
+
var logger2 = (0, import_subagent_engine_sdk5.getLogger)("session-runner");
|
|
465
|
+
var THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "max"];
|
|
466
|
+
function asThinkingLevel(v) {
|
|
467
|
+
return typeof v === "string" && THINKING_LEVELS.includes(v) ? v : void 0;
|
|
468
|
+
}
|
|
469
|
+
function parseSpawnModelRef(ref) {
|
|
470
|
+
if (ref === void 0 || ref.trim() === "") return void 0;
|
|
471
|
+
const slash = ref.indexOf("/");
|
|
472
|
+
if (slash <= 0 || slash === ref.length - 1) return void 0;
|
|
473
|
+
return { provider: ref.slice(0, slash), id: ref.slice(slash + 1) };
|
|
474
|
+
}
|
|
475
|
+
function buildSpawnArgs(params) {
|
|
476
|
+
const args = ["--mode", "rpc", "--session-dir", params.sessionDir];
|
|
477
|
+
if (params.sessionFile) {
|
|
478
|
+
args.push("--session", params.sessionFile);
|
|
479
|
+
}
|
|
480
|
+
args.push("--model", `${params.modelRef.provider}/${params.modelRef.id}`);
|
|
481
|
+
if (params.thinkingLevel) {
|
|
482
|
+
const lastIdx = args.length - 1;
|
|
483
|
+
args[lastIdx] = `${args[lastIdx]}:${params.thinkingLevel}`;
|
|
484
|
+
}
|
|
485
|
+
if (params.agentTools && params.agentTools.length > 0) {
|
|
486
|
+
args.push("--tools", params.agentTools.join(","));
|
|
487
|
+
}
|
|
488
|
+
if (params.appendSystemPromptPath) {
|
|
489
|
+
args.push("--append-system-prompt", params.appendSystemPromptPath);
|
|
490
|
+
}
|
|
491
|
+
if (params.forkSource) {
|
|
492
|
+
args.push("--fork", params.forkSource);
|
|
493
|
+
}
|
|
494
|
+
if (params.skillPaths && params.skillPaths.length > 0) {
|
|
495
|
+
for (const sp of params.skillPaths) {
|
|
496
|
+
args.push("--skill", sp);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const mf = params.mirrorFlags;
|
|
500
|
+
if (mf) {
|
|
501
|
+
if (mf.noExtensions) args.push("--no-extensions");
|
|
502
|
+
if (mf.approve) args.push("--approve");
|
|
503
|
+
if (mf.noContextFiles) args.push("--no-context-files");
|
|
504
|
+
for (const ep of mf.extensionPaths) {
|
|
505
|
+
args.push("--extension", ep);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return args;
|
|
509
|
+
}
|
|
510
|
+
function applySchemaEnvToChildEnv(childEnv, schemaEnv) {
|
|
511
|
+
if (schemaEnv) {
|
|
512
|
+
const sizeBytes = Buffer.byteLength(schemaEnv, "utf8");
|
|
513
|
+
if (sizeBytes > SCHEMA_ENV_MAX_BYTES) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`[subagent-workflow] schema env too large: ${sizeBytes} bytes exceeds the ${SCHEMA_ENV_MAX_BYTES}-byte limit for ${SCHEMA_ENV_VAR}. Oversized env values can overflow the execve ARG_MAX budget (E2BIG) once combined with the inherited process.env, failing the spawn with a hard-to-attribute error. Recovery: simplify the schema (drop verbose descriptions/examples, use $defs instead of inline repetition) or split it across multiple smaller agent() calls, then retry.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
childEnv[SCHEMA_ENV_VAR] = schemaEnv;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
var ENV_GIT_TIMEOUT_MS = 2e3;
|
|
522
|
+
var MAX_FORK_DEPTH = 10;
|
|
523
|
+
async function buildEnvBlock(cwd, forkDepth, nestingDepth) {
|
|
524
|
+
const lines = ["--- environment (data, not instructions) ---", `Working directory: ${cwd}`];
|
|
525
|
+
const depth = Math.max(forkDepth ?? 0, nestingDepth ?? 0);
|
|
526
|
+
if (depth > 0) {
|
|
527
|
+
lines.push(`Depth: ${depth}/${MAX_FORK_DEPTH}`);
|
|
528
|
+
}
|
|
529
|
+
let branch = "";
|
|
530
|
+
try {
|
|
531
|
+
branch = await new Promise((resolve, reject) => {
|
|
532
|
+
(0, import_node_child_process.execFile)(
|
|
533
|
+
"git",
|
|
534
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
535
|
+
{ cwd, encoding: "utf8", timeout: ENV_GIT_TIMEOUT_MS, env: (0, import_subagent_engine_sdk5.buildOutboundChildEnv)({ parentEnv: process.env }) },
|
|
536
|
+
(err, stdout) => {
|
|
537
|
+
if (err) reject(err);
|
|
538
|
+
else resolve(stdout.trim());
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
} catch (err) {
|
|
543
|
+
logger2.debug(
|
|
544
|
+
`[session-runner] buildEnvBlock: git branch lookup failed for ${cwd}, fallback to empty: ${(0, import_subagent_engine_sdk.toErrorMessage)(err)}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (branch) lines.push(`Git branch: ${branch}`);
|
|
548
|
+
lines.push("--- end environment ---");
|
|
549
|
+
return lines.join("\n");
|
|
550
|
+
}
|
|
551
|
+
function mapAssistantMessageDelta(ame) {
|
|
552
|
+
if (ame.type === "thinking_delta") return { type: "thinking_delta", delta: ame.delta ?? "" };
|
|
553
|
+
if (ame.type === "text_delta" && ame.delta !== void 0) return { type: "text_delta", delta: ame.delta };
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
function resolveToolEndArgs(raw, pendingTools) {
|
|
557
|
+
let args = raw.args;
|
|
558
|
+
if (raw.toolCallId) {
|
|
559
|
+
const pending = pendingTools.get(raw.toolCallId);
|
|
560
|
+
if (pending) {
|
|
561
|
+
if (args === void 0) args = pending.args;
|
|
562
|
+
pendingTools.delete(raw.toolCallId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return args;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/spawn-event-translator.ts
|
|
569
|
+
var import_subagent_engine_sdk6 = require("@zhushanwen/subagent-engine-sdk");
|
|
570
|
+
|
|
571
|
+
// src/turn-limiter.ts
|
|
572
|
+
var WRAP_UP_MESSAGE = [
|
|
573
|
+
"You have reached your turn limit. Wrap up now:",
|
|
574
|
+
"1. Summarize what you have completed (with evidence: file paths, command output).",
|
|
575
|
+
"2. List what remains undone and why.",
|
|
576
|
+
"3. State the single most important next step for whoever continues.",
|
|
577
|
+
"Do NOT claim the task is complete if any part remains unfinished."
|
|
578
|
+
].join(" ");
|
|
579
|
+
var WRAP_UP_HINT = [
|
|
580
|
+
`You have a turn limit. As you sense you are approaching it, proactively wrap up:`,
|
|
581
|
+
"1. Summarize what you have completed (with evidence: file paths, command output).",
|
|
582
|
+
"2. List what remains undone and why.",
|
|
583
|
+
"3. State the single most important next step for whoever continues.",
|
|
584
|
+
"Do NOT claim the task is complete if any part remains unfinished."
|
|
585
|
+
].join(" ");
|
|
586
|
+
function createTurnLimiter(opts) {
|
|
587
|
+
let steered = false;
|
|
588
|
+
let aborted = false;
|
|
589
|
+
const limit = opts.maxTurns > 0 ? opts.maxTurns : Infinity;
|
|
590
|
+
const grace = opts.graceTurns > 0 ? opts.graceTurns : 0;
|
|
591
|
+
const onTurnEnd = (turn) => {
|
|
592
|
+
if (aborted || !Number.isFinite(limit)) return;
|
|
593
|
+
if (!steered && turn >= limit) {
|
|
594
|
+
steered = true;
|
|
595
|
+
opts.steer(WRAP_UP_MESSAGE);
|
|
596
|
+
}
|
|
597
|
+
if (steered && turn >= limit + grace) {
|
|
598
|
+
aborted = true;
|
|
599
|
+
opts.abort();
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
const reset = () => {
|
|
603
|
+
steered = false;
|
|
604
|
+
aborted = false;
|
|
605
|
+
};
|
|
606
|
+
return {
|
|
607
|
+
onTurnEnd,
|
|
608
|
+
reset,
|
|
609
|
+
get didSteer() {
|
|
610
|
+
return steered;
|
|
611
|
+
},
|
|
612
|
+
get didAbort() {
|
|
613
|
+
return aborted;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/spawn-event-translator.ts
|
|
619
|
+
var DEFAULT_GRACE_TURNS = 2;
|
|
620
|
+
function handleToolExecutionStart(raw, pendingTools, agentEvent) {
|
|
621
|
+
const toolName = raw.toolName ?? "";
|
|
622
|
+
if (raw.toolCallId) {
|
|
623
|
+
pendingTools.set(raw.toolCallId, { toolName, args: raw.args });
|
|
624
|
+
}
|
|
625
|
+
agentEvent({ type: "tool_start", toolName, args: raw.args });
|
|
626
|
+
}
|
|
627
|
+
function handleToolExecutionEnd(raw, pendingTools, agentEvent) {
|
|
628
|
+
agentEvent({
|
|
629
|
+
type: "tool_end",
|
|
630
|
+
toolName: raw.toolName ?? "",
|
|
631
|
+
args: resolveToolEndArgs(raw, pendingTools),
|
|
632
|
+
result: raw.result,
|
|
633
|
+
isError: raw.isError
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function handleAssistantMessageUpdate(raw, agentEvent) {
|
|
637
|
+
const mapped = mapAssistantMessageDelta(raw.assistantMessageEvent ?? {});
|
|
638
|
+
if (mapped) agentEvent(mapped);
|
|
639
|
+
}
|
|
640
|
+
function handleAgentEnd(raw, opts) {
|
|
641
|
+
if (raw.willRetry !== true) opts.onAgentEnd?.();
|
|
642
|
+
}
|
|
643
|
+
function handleAgentSettled(record, limiter, opts) {
|
|
644
|
+
if (opts.onAgentSettled !== void 0) {
|
|
645
|
+
record.turnCount = 0;
|
|
646
|
+
limiter.reset();
|
|
647
|
+
opts.onAgentSettled();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function accumulateMessageEnd(raw, agentEvent) {
|
|
651
|
+
const msg = raw.message;
|
|
652
|
+
if (msg?.usage) {
|
|
653
|
+
const { cost: costObj } = msg.usage;
|
|
654
|
+
const usage = {
|
|
655
|
+
input: msg.usage.input ?? 0,
|
|
656
|
+
output: msg.usage.output ?? 0,
|
|
657
|
+
cacheRead: msg.usage.cacheRead ?? 0,
|
|
658
|
+
cacheWrite: msg.usage.cacheWrite ?? 0,
|
|
659
|
+
...costObj?.total !== void 0 ? { cost: costObj.total } : {}
|
|
660
|
+
};
|
|
661
|
+
agentEvent({ type: "message_end", usage });
|
|
662
|
+
}
|
|
663
|
+
const stopReason = msg?.stopReason;
|
|
664
|
+
if (stopReason === "error" || stopReason === "aborted") {
|
|
665
|
+
const errMsg = msg?.errorMessage ?? raw.reason ?? stopReason;
|
|
666
|
+
agentEvent({ type: "error", message: errMsg });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function createSdkEventTranslator(record, opts) {
|
|
670
|
+
const pendingTools = /* @__PURE__ */ new Map();
|
|
671
|
+
const limiter = createTurnLimiter({
|
|
672
|
+
maxTurns: opts.maxTurns ?? 0,
|
|
673
|
+
graceTurns: opts.graceTurns ?? DEFAULT_GRACE_TURNS,
|
|
674
|
+
steer: () => {
|
|
675
|
+
},
|
|
676
|
+
abort: opts.abort
|
|
677
|
+
});
|
|
678
|
+
const agentEvent = (event) => {
|
|
679
|
+
(0, import_subagent_engine_sdk6.updateFromEvent)(record, event);
|
|
680
|
+
if (event.type === "turn_end") limiter.onTurnEnd(record.turnCount);
|
|
681
|
+
if (event.type === "text_delta") opts.onDelta?.(event.delta);
|
|
682
|
+
opts.onEvent(event);
|
|
683
|
+
};
|
|
684
|
+
return (raw) => {
|
|
685
|
+
switch (raw.type) {
|
|
686
|
+
case "tool_execution_start":
|
|
687
|
+
handleToolExecutionStart(raw, pendingTools, agentEvent);
|
|
688
|
+
return;
|
|
689
|
+
case "tool_execution_end":
|
|
690
|
+
handleToolExecutionEnd(raw, pendingTools, agentEvent);
|
|
691
|
+
return;
|
|
692
|
+
case "message_update":
|
|
693
|
+
handleAssistantMessageUpdate(raw, agentEvent);
|
|
694
|
+
return;
|
|
695
|
+
case "turn_end":
|
|
696
|
+
agentEvent({ type: "turn_end" });
|
|
697
|
+
return;
|
|
698
|
+
case "agent_end":
|
|
699
|
+
handleAgentEnd(raw, opts);
|
|
700
|
+
return;
|
|
701
|
+
case "agent_settled":
|
|
702
|
+
handleAgentSettled(record, limiter, opts);
|
|
703
|
+
return;
|
|
704
|
+
case "message_end":
|
|
705
|
+
accumulateMessageEnd(raw, agentEvent);
|
|
706
|
+
return;
|
|
707
|
+
case "compaction_start":
|
|
708
|
+
agentEvent({ type: "compaction" });
|
|
709
|
+
return;
|
|
710
|
+
default:
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/spawn-run-pump.ts
|
|
717
|
+
var import_subagent_engine_sdk7 = require("@zhushanwen/subagent-engine-sdk");
|
|
718
|
+
|
|
719
|
+
// src/get-state-handshake.ts
|
|
720
|
+
var GET_STATE_MAX_RETRIES = 3;
|
|
721
|
+
var GET_STATE_RETRY_INTERVAL_MS = 500;
|
|
722
|
+
var GET_STATE_TIMEOUT_MS = 2e3;
|
|
723
|
+
function extractGetStateFields(data, into) {
|
|
724
|
+
if (data && typeof data === "object") {
|
|
725
|
+
const d = data;
|
|
726
|
+
if (typeof d.sessionFile === "string" && d.sessionFile.length > 0) {
|
|
727
|
+
into.sessionFile = d.sessionFile;
|
|
728
|
+
}
|
|
729
|
+
if (typeof d.sessionId === "string" && d.sessionId.length > 0) {
|
|
730
|
+
into.sessionId = d.sessionId;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function performGetStateHandshake(child, addResponseListener) {
|
|
735
|
+
return new Promise((resolve) => {
|
|
736
|
+
const collected = {};
|
|
737
|
+
let attempts = 0;
|
|
738
|
+
let resolved = false;
|
|
739
|
+
function tryOnce() {
|
|
740
|
+
if (resolved) return;
|
|
741
|
+
attempts++;
|
|
742
|
+
const reqId = sendGetStateCommand(child);
|
|
743
|
+
let pendingRetry;
|
|
744
|
+
const timer = setTimeout(() => {
|
|
745
|
+
pendingRetry = void 0;
|
|
746
|
+
if (attempts < GET_STATE_MAX_RETRIES && !resolved) {
|
|
747
|
+
pendingRetry = setTimeout(() => tryOnce(), GET_STATE_RETRY_INTERVAL_MS);
|
|
748
|
+
pendingRetry.unref();
|
|
749
|
+
} else if (!resolved) {
|
|
750
|
+
resolved = true;
|
|
751
|
+
resolve(collected);
|
|
752
|
+
}
|
|
753
|
+
}, GET_STATE_TIMEOUT_MS);
|
|
754
|
+
timer.unref();
|
|
755
|
+
addResponseListener(reqId, (data) => {
|
|
756
|
+
if (resolved) return;
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
if (pendingRetry) clearTimeout(pendingRetry);
|
|
759
|
+
extractGetStateFields(data, collected);
|
|
760
|
+
if (collected.sessionFile) {
|
|
761
|
+
resolved = true;
|
|
762
|
+
resolve(collected);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
tryOnce();
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function requestGetStateOnce(child, addResponseListener, timeoutMs) {
|
|
770
|
+
return new Promise((resolve) => {
|
|
771
|
+
let settled = false;
|
|
772
|
+
let removeListener = () => {
|
|
773
|
+
};
|
|
774
|
+
const finish = (r) => {
|
|
775
|
+
if (settled) return;
|
|
776
|
+
settled = true;
|
|
777
|
+
clearTimeout(timer);
|
|
778
|
+
removeListener();
|
|
779
|
+
resolve(r);
|
|
780
|
+
};
|
|
781
|
+
let reqId;
|
|
782
|
+
try {
|
|
783
|
+
reqId = sendGetStateCommand(child);
|
|
784
|
+
} catch {
|
|
785
|
+
resolve({});
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
removeListener = addResponseListener(reqId, (data) => {
|
|
789
|
+
const r = {};
|
|
790
|
+
extractGetStateFields(data, r);
|
|
791
|
+
finish(r);
|
|
792
|
+
}) ?? (() => {
|
|
793
|
+
});
|
|
794
|
+
const timer = setTimeout(() => finish({}), timeoutMs);
|
|
795
|
+
timer.unref();
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/spawn-event-adapter.ts
|
|
800
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
801
|
+
var path2 = __toESM(require("path"), 1);
|
|
802
|
+
function isSessionHeader(obj) {
|
|
803
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
804
|
+
const r = obj;
|
|
805
|
+
return r.type === "session" && typeof r.id === "string" && typeof r.timestamp === "string" && typeof r.cwd === "string";
|
|
806
|
+
}
|
|
807
|
+
function isRpcResponse(obj) {
|
|
808
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
809
|
+
const r = obj;
|
|
810
|
+
return r.type === "response" && typeof r.command === "string" && typeof r.success === "boolean";
|
|
811
|
+
}
|
|
812
|
+
function isExtensionUiRequest(obj) {
|
|
813
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
814
|
+
const r = obj;
|
|
815
|
+
return r.type === "extension_ui_request" && typeof r.id === "string" && typeof r.method === "string";
|
|
816
|
+
}
|
|
817
|
+
function reqStr(r, key) {
|
|
818
|
+
const v = r[key];
|
|
819
|
+
return typeof v === "string" ? v : "";
|
|
820
|
+
}
|
|
821
|
+
function strArrayField(r, key, onMissing) {
|
|
822
|
+
const v = r[key];
|
|
823
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : onMissing;
|
|
824
|
+
}
|
|
825
|
+
function optTimeout(r) {
|
|
826
|
+
return typeof r.timeout === "number" ? { timeout: r.timeout } : {};
|
|
827
|
+
}
|
|
828
|
+
function optPlaceholder(r) {
|
|
829
|
+
return typeof r.placeholder === "string" ? { placeholder: r.placeholder } : {};
|
|
830
|
+
}
|
|
831
|
+
function optPrefill(r) {
|
|
832
|
+
return typeof r.prefill === "string" ? { prefill: r.prefill } : {};
|
|
833
|
+
}
|
|
834
|
+
function optNotifyType(r) {
|
|
835
|
+
const v = r.notifyType;
|
|
836
|
+
return v === "info" || v === "warning" || v === "error" ? { notifyType: v } : {};
|
|
837
|
+
}
|
|
838
|
+
function optWidgetPlacement(r) {
|
|
839
|
+
const v = r.widgetPlacement;
|
|
840
|
+
return v === "aboveEditor" || v === "belowEditor" ? { widgetPlacement: v } : {};
|
|
841
|
+
}
|
|
842
|
+
function buildExtensionUiRequest(env) {
|
|
843
|
+
const r = env;
|
|
844
|
+
switch (env.method) {
|
|
845
|
+
case "select":
|
|
846
|
+
return {
|
|
847
|
+
method: "select",
|
|
848
|
+
title: reqStr(r, "title"),
|
|
849
|
+
options: strArrayField(r, "options", []),
|
|
850
|
+
...optTimeout(r)
|
|
851
|
+
};
|
|
852
|
+
case "confirm":
|
|
853
|
+
return {
|
|
854
|
+
method: "confirm",
|
|
855
|
+
title: reqStr(r, "title"),
|
|
856
|
+
message: reqStr(r, "message"),
|
|
857
|
+
...optTimeout(r)
|
|
858
|
+
};
|
|
859
|
+
case "input":
|
|
860
|
+
return {
|
|
861
|
+
method: "input",
|
|
862
|
+
title: reqStr(r, "title"),
|
|
863
|
+
...optPlaceholder(r),
|
|
864
|
+
...optTimeout(r)
|
|
865
|
+
};
|
|
866
|
+
case "editor":
|
|
867
|
+
return {
|
|
868
|
+
method: "editor",
|
|
869
|
+
title: reqStr(r, "title"),
|
|
870
|
+
...optPrefill(r)
|
|
871
|
+
};
|
|
872
|
+
case "notify":
|
|
873
|
+
return {
|
|
874
|
+
method: "notify",
|
|
875
|
+
message: reqStr(r, "message"),
|
|
876
|
+
...optNotifyType(r)
|
|
877
|
+
};
|
|
878
|
+
case "setStatus":
|
|
879
|
+
return {
|
|
880
|
+
method: "setStatus",
|
|
881
|
+
statusKey: reqStr(r, "statusKey"),
|
|
882
|
+
statusText: typeof r.statusText === "string" ? r.statusText : void 0
|
|
883
|
+
};
|
|
884
|
+
case "setWidget":
|
|
885
|
+
return {
|
|
886
|
+
method: "setWidget",
|
|
887
|
+
widgetKey: reqStr(r, "widgetKey"),
|
|
888
|
+
widgetLines: strArrayField(r, "widgetLines", void 0),
|
|
889
|
+
...optWidgetPlacement(r)
|
|
890
|
+
};
|
|
891
|
+
case "setTitle":
|
|
892
|
+
return {
|
|
893
|
+
method: "setTitle",
|
|
894
|
+
title: reqStr(r, "title")
|
|
895
|
+
};
|
|
896
|
+
case "set_editor_text":
|
|
897
|
+
return {
|
|
898
|
+
method: "set_editor_text",
|
|
899
|
+
text: reqStr(r, "text")
|
|
900
|
+
};
|
|
901
|
+
default:
|
|
902
|
+
return { method: env.method, raw: r };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function parseSpawnLine(line) {
|
|
906
|
+
const trimmed = line.trim();
|
|
907
|
+
if (trimmed === "") return null;
|
|
908
|
+
let obj;
|
|
909
|
+
try {
|
|
910
|
+
obj = JSON.parse(trimmed);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
return {
|
|
913
|
+
kind: "invalid",
|
|
914
|
+
raw: trimmed,
|
|
915
|
+
error: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (isSessionHeader(obj)) {
|
|
919
|
+
return { kind: "header", header: obj };
|
|
920
|
+
}
|
|
921
|
+
if (isExtensionUiRequest(obj)) {
|
|
922
|
+
return { kind: "extension_ui_request", id: obj.id, request: buildExtensionUiRequest(obj) };
|
|
923
|
+
}
|
|
924
|
+
if (isRpcResponse(obj)) {
|
|
925
|
+
return {
|
|
926
|
+
kind: "response",
|
|
927
|
+
...typeof obj.id === "string" ? { id: obj.id } : {},
|
|
928
|
+
command: obj.command,
|
|
929
|
+
success: obj.success,
|
|
930
|
+
...obj.data !== void 0 ? { data: obj.data } : {},
|
|
931
|
+
...typeof obj.error === "string" ? { error: obj.error } : {}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (typeof obj === "object" && obj !== null && typeof obj.type === "string") {
|
|
935
|
+
return { kind: "event", event: obj };
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
kind: "invalid",
|
|
939
|
+
raw: trimmed,
|
|
940
|
+
error: "JSON missing string 'type' field"
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function deriveSessionFilePath(header, sessionDir) {
|
|
944
|
+
const fileTimestamp = header.timestamp.replace(/[:.]/g, "-");
|
|
945
|
+
return `${sessionDir}/${fileTimestamp}_${header.id}.jsonl`;
|
|
946
|
+
}
|
|
947
|
+
function findSessionFileByHeaderId(sessionDir, sessionId) {
|
|
948
|
+
try {
|
|
949
|
+
const files = fs2.readdirSync(sessionDir);
|
|
950
|
+
const match = files.find((f) => f.endsWith(`_${sessionId}.jsonl`));
|
|
951
|
+
return match ? path2.join(sessionDir, match) : void 0;
|
|
952
|
+
} catch {
|
|
953
|
+
return void 0;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/spawn-run-pump.ts
|
|
958
|
+
var logger3 = (0, import_subagent_engine_sdk7.getLogger)("session-runner");
|
|
959
|
+
var INVALID_LINE_LOG_CHARS = 160;
|
|
960
|
+
var SIGNAL_EXIT_CODE_BASE = 128;
|
|
961
|
+
function createSessionIdentityTracker(sessionDir, callbacks) {
|
|
962
|
+
let sessionId;
|
|
963
|
+
let sessionFile;
|
|
964
|
+
const stateListeners = /* @__PURE__ */ new Map();
|
|
965
|
+
const applyGetStateFields = (fields) => {
|
|
966
|
+
if (fields.sessionId !== void 0 && sessionId === void 0) sessionId = fields.sessionId;
|
|
967
|
+
if (fields.sessionFile !== void 0 && fields.sessionFile !== sessionFile) {
|
|
968
|
+
sessionFile = fields.sessionFile;
|
|
969
|
+
callbacks.onHandleReady?.({
|
|
970
|
+
sessionRef: {
|
|
971
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
972
|
+
sessionFile: fields.sessionFile
|
|
973
|
+
},
|
|
974
|
+
poolKey: "shared"
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
return {
|
|
979
|
+
get sessionId() {
|
|
980
|
+
return sessionId;
|
|
981
|
+
},
|
|
982
|
+
get sessionFile() {
|
|
983
|
+
return sessionFile;
|
|
984
|
+
},
|
|
985
|
+
noteHeader(header) {
|
|
986
|
+
sessionId = header.id;
|
|
987
|
+
sessionFile = deriveSessionFilePath(header, sessionDir);
|
|
988
|
+
callbacks.onHandleReady?.({
|
|
989
|
+
sessionRef: {
|
|
990
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
991
|
+
...sessionFile !== void 0 ? { sessionFile } : {}
|
|
992
|
+
},
|
|
993
|
+
poolKey: "shared"
|
|
994
|
+
});
|
|
995
|
+
},
|
|
996
|
+
addStateListener(id, resolver) {
|
|
997
|
+
stateListeners.set(id, (data) => {
|
|
998
|
+
const picked = {};
|
|
999
|
+
extractGetStateFields(data, picked);
|
|
1000
|
+
applyGetStateFields(picked);
|
|
1001
|
+
resolver(data);
|
|
1002
|
+
});
|
|
1003
|
+
},
|
|
1004
|
+
dispatchStateResponse(id, success, data) {
|
|
1005
|
+
if (id === void 0 || !stateListeners.has(id)) return;
|
|
1006
|
+
const resolver = stateListeners.get(id);
|
|
1007
|
+
stateListeners.delete(id);
|
|
1008
|
+
resolver?.(success ? data : void 0);
|
|
1009
|
+
},
|
|
1010
|
+
applyGetStateFields,
|
|
1011
|
+
fallbackSessionFileByHeaderId() {
|
|
1012
|
+
if (sessionFile === void 0 && sessionId !== void 0) {
|
|
1013
|
+
sessionFile = findSessionFileByHeaderId(sessionDir, sessionId);
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
clearStateListeners() {
|
|
1017
|
+
stateListeners.clear();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function createLineConsumer(deps) {
|
|
1022
|
+
const { recordId, identity, handleSdkEvent, enqueueUi } = deps;
|
|
1023
|
+
return (line) => {
|
|
1024
|
+
const parsed = parseSpawnLine(line);
|
|
1025
|
+
if (parsed === null) return;
|
|
1026
|
+
switch (parsed.kind) {
|
|
1027
|
+
case "header":
|
|
1028
|
+
identity.noteHeader(parsed.header);
|
|
1029
|
+
return;
|
|
1030
|
+
case "event":
|
|
1031
|
+
handleSdkEvent(parsed.event);
|
|
1032
|
+
return;
|
|
1033
|
+
case "response":
|
|
1034
|
+
identity.dispatchStateResponse(parsed.id, parsed.success, parsed.data);
|
|
1035
|
+
return;
|
|
1036
|
+
case "extension_ui_request":
|
|
1037
|
+
enqueueUi(parsed.id, parsed.request);
|
|
1038
|
+
return;
|
|
1039
|
+
case "invalid":
|
|
1040
|
+
logger3.debug(
|
|
1041
|
+
`[session-runner] invalid stdout line from ${recordId} (ignored): ${parsed.raw.slice(0, INVALID_LINE_LOG_CHARS)}`,
|
|
1042
|
+
{ error: parsed.error }
|
|
1043
|
+
);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function reportChildExited(child, recordId, callbacks, code, signal) {
|
|
1049
|
+
if (child.pid === void 0) return;
|
|
1050
|
+
callbacks.onChildStateChanged?.({
|
|
1051
|
+
pid: child.pid,
|
|
1052
|
+
recordId,
|
|
1053
|
+
state: "exited",
|
|
1054
|
+
killed: child.killed,
|
|
1055
|
+
...code !== null ? { exitCode: code } : {},
|
|
1056
|
+
...signal !== null ? { signal } : {}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
function normalizeExitCode(endedCleanly, code, signal) {
|
|
1060
|
+
return endedCleanly ? 0 : code ?? (signal !== null ? SIGNAL_EXIT_CODE_BASE : 0);
|
|
1061
|
+
}
|
|
1062
|
+
function createCloseFinalizer(deps, resolveExit) {
|
|
1063
|
+
const { child, recordId, callbacks, identity, stderrTee, runEnd } = deps;
|
|
1064
|
+
return (code, signal) => {
|
|
1065
|
+
identity.clearStateListeners();
|
|
1066
|
+
stderrTee?.close();
|
|
1067
|
+
unregisterActiveChild(recordId, child);
|
|
1068
|
+
reportChildExited(child, recordId, callbacks, code, signal);
|
|
1069
|
+
identity.fallbackSessionFileByHeaderId();
|
|
1070
|
+
resolveExit(normalizeExitCode(runEnd.endedCleanly, code, signal));
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function wireChildStdoutPump(deps) {
|
|
1074
|
+
const { child } = deps;
|
|
1075
|
+
return new Promise((resolveExit) => {
|
|
1076
|
+
deps.runEnd.resolveChatRun = resolveExit;
|
|
1077
|
+
const consumeLine = createLineConsumer(deps);
|
|
1078
|
+
const onClose = createCloseFinalizer(deps, resolveExit);
|
|
1079
|
+
(0, import_subagent_engine_sdk7.pumpNdjsonLines)(child.stdout, consumeLine);
|
|
1080
|
+
child.once("close", onClose);
|
|
1081
|
+
child.once("error", (err) => {
|
|
1082
|
+
logger3.error(`[session-runner] child ${deps.recordId} error event`, {
|
|
1083
|
+
detail: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
1084
|
+
});
|
|
1085
|
+
onClose(null, null);
|
|
1086
|
+
});
|
|
1087
|
+
child.stdin?.on("error", (err) => {
|
|
1088
|
+
if (err !== null && typeof err === "object" && "code" in err && (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED")) {
|
|
1089
|
+
recordEpipeFailure(deps.recordId);
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function reportChildSpawned(child, recordId, callbacks) {
|
|
1095
|
+
if (child.pid === void 0) return;
|
|
1096
|
+
callbacks.onChildSpawned?.(child.pid, recordId);
|
|
1097
|
+
callbacks.onChildStateChanged?.({
|
|
1098
|
+
pid: child.pid,
|
|
1099
|
+
recordId,
|
|
1100
|
+
state: "running",
|
|
1101
|
+
killed: false
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/temp-prompt.ts
|
|
1106
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
1107
|
+
var os = __toESM(require("os"), 1);
|
|
1108
|
+
var path3 = __toESM(require("path"), 1);
|
|
1109
|
+
|
|
1110
|
+
// src/best-effort.ts
|
|
1111
|
+
var import_subagent_engine_sdk8 = require("@zhushanwen/subagent-engine-sdk");
|
|
1112
|
+
|
|
1113
|
+
// src/temp-prompt.ts
|
|
1114
|
+
async function writePromptToTempFile(agentName, prompt) {
|
|
1115
|
+
const dir = await fs3.promises.mkdtemp(path3.join(os.tmpdir(), "pi-subagent-"));
|
|
1116
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
1117
|
+
const filePath = path3.join(dir, `prompt-${safeName}.md`);
|
|
1118
|
+
await fs3.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
|
|
1119
|
+
return { dir, filePath };
|
|
1120
|
+
}
|
|
1121
|
+
async function cleanupTempPrompt(file) {
|
|
1122
|
+
try {
|
|
1123
|
+
await fs3.promises.rm(file.dir, { recursive: true, force: true });
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
(0, import_subagent_engine_sdk8.bestEffort)(err, `cleanup temp prompt dir ${file.dir}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/ui-request-queue.ts
|
|
1130
|
+
var import_subagent_engine_sdk10 = require("@zhushanwen/subagent-engine-sdk");
|
|
1131
|
+
|
|
1132
|
+
// src/ui-channels.ts
|
|
1133
|
+
var import_subagent_engine_sdk9 = require("@zhushanwen/subagent-engine-sdk");
|
|
1134
|
+
|
|
1135
|
+
// src/ui-request-queue.ts
|
|
1136
|
+
var logger4 = (0, import_subagent_engine_sdk10.getLogger)("subagents");
|
|
1137
|
+
function createUiRequestQueue(child, deps) {
|
|
1138
|
+
const abortController = new AbortController();
|
|
1139
|
+
const queue = [];
|
|
1140
|
+
let processing = false;
|
|
1141
|
+
let closed = false;
|
|
1142
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
1143
|
+
function processNext() {
|
|
1144
|
+
if (processing || queue.length === 0 || closed) return;
|
|
1145
|
+
processing = true;
|
|
1146
|
+
const { id, request, signal } = queue.shift();
|
|
1147
|
+
handleUiRequest(child, id, request, deps, signal, warnedKeys).catch((err) => {
|
|
1148
|
+
const m = (0, import_subagent_engine_sdk.toErrorMessage)(err);
|
|
1149
|
+
logger4.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
|
|
1150
|
+
}).finally(() => {
|
|
1151
|
+
processing = false;
|
|
1152
|
+
processNext();
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
const onClose = () => {
|
|
1156
|
+
if (closed) return;
|
|
1157
|
+
closed = true;
|
|
1158
|
+
abortController.abort();
|
|
1159
|
+
queue.length = 0;
|
|
1160
|
+
};
|
|
1161
|
+
child.on("close", onClose);
|
|
1162
|
+
child.on("error", onClose);
|
|
1163
|
+
return function enqueue(id, request) {
|
|
1164
|
+
if (closed) return;
|
|
1165
|
+
queue.push({ id, request, signal: abortController.signal });
|
|
1166
|
+
processNext();
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
async function handleUiRequest(child, id, request, deps, signal, warnedKeys) {
|
|
1170
|
+
const handler = deps.uiRequestHandler;
|
|
1171
|
+
if (!handler) {
|
|
1172
|
+
const key = child.pid?.toString() ?? id;
|
|
1173
|
+
if (!warnedKeys.has(key)) {
|
|
1174
|
+
warnedKeys.add(key);
|
|
1175
|
+
logger4.warn(
|
|
1176
|
+
`[subagents] no ui request handler bound (host/askUser unsupported?); subsequent requests from this child will be auto-cancelled`,
|
|
1177
|
+
{ pid: child.pid, method: request.method }
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
respond(child, id, { cancelled: true }, signal);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const { channel, channelPayload } = (0, import_subagent_engine_sdk9.parseChannel)(request);
|
|
1184
|
+
const uiReq = {
|
|
1185
|
+
id,
|
|
1186
|
+
method: request.method,
|
|
1187
|
+
...child.pid !== void 0 ? { _childPid: child.pid } : {},
|
|
1188
|
+
...channel !== void 0 ? { channel } : {},
|
|
1189
|
+
...channelPayload !== void 0 ? { channelPayload } : {},
|
|
1190
|
+
...extractMethodFields(request)
|
|
1191
|
+
};
|
|
1192
|
+
try {
|
|
1193
|
+
const result = await handler(uiReq);
|
|
1194
|
+
if (signal?.aborted) return;
|
|
1195
|
+
respond(child, id, result, signal);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
if (signal?.aborted) return;
|
|
1198
|
+
logger4.error("[subagents] uiRequestHandler threw", {
|
|
1199
|
+
detail: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
1200
|
+
});
|
|
1201
|
+
respond(child, id, { cancelled: true }, signal);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function isStringValue(value) {
|
|
1205
|
+
return typeof value === "string";
|
|
1206
|
+
}
|
|
1207
|
+
function isNumberValue(value) {
|
|
1208
|
+
return typeof value === "number";
|
|
1209
|
+
}
|
|
1210
|
+
function isArrayValue(value) {
|
|
1211
|
+
return Array.isArray(value);
|
|
1212
|
+
}
|
|
1213
|
+
var METHOD_FIELD_COPY_SPECS = [
|
|
1214
|
+
{ key: "title", guard: isStringValue },
|
|
1215
|
+
{ key: "options", guard: isArrayValue },
|
|
1216
|
+
{ key: "message", guard: isStringValue },
|
|
1217
|
+
{ key: "placeholder", guard: isStringValue },
|
|
1218
|
+
{ key: "prefill", guard: isStringValue },
|
|
1219
|
+
{ key: "notifyType", guard: isStringValue },
|
|
1220
|
+
{ key: "statusKey", guard: isStringValue },
|
|
1221
|
+
{ key: "statusText", guard: null },
|
|
1222
|
+
{ key: "widgetKey", guard: isStringValue },
|
|
1223
|
+
{ key: "widgetLines", guard: null },
|
|
1224
|
+
{ key: "widgetPlacement", guard: null },
|
|
1225
|
+
{ key: "text", guard: isStringValue },
|
|
1226
|
+
{ key: "timeout", guard: isNumberValue }
|
|
1227
|
+
];
|
|
1228
|
+
function extractMethodFields(req) {
|
|
1229
|
+
const out = {};
|
|
1230
|
+
const source = req;
|
|
1231
|
+
for (const { key, guard } of METHOD_FIELD_COPY_SPECS) {
|
|
1232
|
+
if (!(key in source)) continue;
|
|
1233
|
+
const value = source[key];
|
|
1234
|
+
if (guard !== null && !guard(value)) continue;
|
|
1235
|
+
out[key] = value;
|
|
1236
|
+
}
|
|
1237
|
+
return out;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/logs/stderr-rotation.ts
|
|
1241
|
+
var import_subagent_engine_sdk11 = require("@zhushanwen/subagent-engine-sdk");
|
|
1242
|
+
var import_subagent_engine_sdk12 = require("@zhushanwen/subagent-engine-sdk");
|
|
1243
|
+
var PI_STDERR_LOG_PREFIX = "pi-task-stderr-";
|
|
1244
|
+
function stderrLogPathFor(engineDataDir, pid) {
|
|
1245
|
+
return (0, import_subagent_engine_sdk11.stderrLogPathFor)(engineDataDir, pid, PI_STDERR_LOG_PREFIX);
|
|
1246
|
+
}
|
|
1247
|
+
function cleanupSiblingStderrLogs(logPath, env, now) {
|
|
1248
|
+
return (0, import_subagent_engine_sdk11.cleanupSiblingStderrLogs)(logPath, env, PI_STDERR_LOG_PREFIX, now);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/spawn-runner.ts
|
|
1252
|
+
var logger5 = (0, import_subagent_engine_sdk13.getLogger)("session-runner");
|
|
1253
|
+
function buildChildEnv(params) {
|
|
1254
|
+
const extras = {};
|
|
1255
|
+
applySchemaEnvToChildEnv(extras, params.schemaEnv);
|
|
1256
|
+
const childEnv = (0, import_subagent_engine_sdk13.buildOutboundChildEnv)({ parentEnv: process.env, extras });
|
|
1257
|
+
if ((0, import_subagent_engine_sdk2.isRelayActive)(process.env)) {
|
|
1258
|
+
const rootId = params.sessionRootId ?? process.env["PI_SUBAGENT_ROOT_SESSION_ID"];
|
|
1259
|
+
if (rootId !== void 0 && rootId !== "") childEnv[import_subagent_engine_sdk2.RELAY_ENV_SESSION_ID] = rootId;
|
|
1260
|
+
childEnv[import_subagent_engine_sdk2.RELAY_ENV_RECORD_ID] = params.recordId;
|
|
1261
|
+
}
|
|
1262
|
+
return childEnv;
|
|
1263
|
+
}
|
|
1264
|
+
function createStderrTee(child) {
|
|
1265
|
+
if (child.stderr === null) return void 0;
|
|
1266
|
+
let dataDir;
|
|
1267
|
+
try {
|
|
1268
|
+
dataDir = (0, import_subagent_engine_sdk13.resolveEngineDataDir)(process.env);
|
|
1269
|
+
} catch {
|
|
1270
|
+
return void 0;
|
|
1271
|
+
}
|
|
1272
|
+
const pid = child.pid;
|
|
1273
|
+
if (pid === void 0) return void 0;
|
|
1274
|
+
const logPath = stderrLogPathFor(dataDir, pid);
|
|
1275
|
+
let stream = null;
|
|
1276
|
+
let failed = false;
|
|
1277
|
+
child.stderr.setEncoding("utf8");
|
|
1278
|
+
child.stderr.on("data", (chunk) => {
|
|
1279
|
+
if (failed) return;
|
|
1280
|
+
try {
|
|
1281
|
+
if (stream === null) {
|
|
1282
|
+
fs4.mkdirSync((0, import_node_path.dirname)(logPath), { recursive: true });
|
|
1283
|
+
stream = fs4.createWriteStream(logPath, { flags: "a" });
|
|
1284
|
+
stream.on("error", () => {
|
|
1285
|
+
failed = true;
|
|
1286
|
+
});
|
|
1287
|
+
cleanupSiblingStderrLogs(logPath, process.env);
|
|
1288
|
+
}
|
|
1289
|
+
stream.write(chunk);
|
|
1290
|
+
if ((0, import_subagent_engine_sdk12.rotateStderrLogIfNeeded)(logPath, (0, import_subagent_engine_sdk12.stderrRotationParams)(process.env))) {
|
|
1291
|
+
stream.end();
|
|
1292
|
+
stream = null;
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
failed = true;
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
return {
|
|
1299
|
+
close() {
|
|
1300
|
+
try {
|
|
1301
|
+
stream?.end();
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
logger5.debug(`[session-runner] stderr tee close best-effort failed: ${(0, import_subagent_engine_sdk.toErrorMessage)(err)}`);
|
|
1304
|
+
}
|
|
1305
|
+
stream = null;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async function writeAppendPromptFile(params) {
|
|
1310
|
+
const appendParts = [await buildEnvBlock(params.cwd)];
|
|
1311
|
+
if (params.maxTurns && params.maxTurns > 0) appendParts.push(WRAP_UP_HINT);
|
|
1312
|
+
if (params.appendSystemPrompt) appendParts.push(...params.appendSystemPrompt);
|
|
1313
|
+
return appendParts.length > 0 ? await writePromptToTempFile(params.agentName, appendParts.join("\n\n")) : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
function buildTranslatorOpts(params, callbacks, killChild, runEnd) {
|
|
1316
|
+
const chatMode = params.chatMode === true;
|
|
1317
|
+
return {
|
|
1318
|
+
maxTurns: params.maxTurns,
|
|
1319
|
+
graceTurns: params.graceTurns,
|
|
1320
|
+
onEvent: callbacks.onEvent,
|
|
1321
|
+
onDelta: callbacks.onDelta,
|
|
1322
|
+
abort: () => killChild("turn limiter abort"),
|
|
1323
|
+
onAgentEnd: chatMode ? () => {
|
|
1324
|
+
runEnd.endedCleanly = true;
|
|
1325
|
+
callbacks.onChatRoundEnd?.();
|
|
1326
|
+
} : () => {
|
|
1327
|
+
runEnd.endedCleanly = true;
|
|
1328
|
+
killChild("agent_end final kill");
|
|
1329
|
+
},
|
|
1330
|
+
...chatMode ? {
|
|
1331
|
+
onAgentSettled: () => {
|
|
1332
|
+
runEnd.endedCleanly = true;
|
|
1333
|
+
callbacks.onChatAgentSettled?.();
|
|
1334
|
+
runEnd.resolveChatRun?.(0);
|
|
1335
|
+
}
|
|
1336
|
+
} : {}
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
async function runSpawnOnce(params, callbacks) {
|
|
1340
|
+
const record = (0, import_subagent_engine_sdk13.createReplayRecord)();
|
|
1341
|
+
const startTime = Date.now();
|
|
1342
|
+
const modelRef = parseSpawnModelRef(params.model);
|
|
1343
|
+
const tempFile = await writeAppendPromptFile(params);
|
|
1344
|
+
if (modelRef === void 0) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`[pi-subagent-cli] run requires a canonical model ref ("provider/id") in ctx.model, got: ${JSON.stringify(params.model)}. Recovery: the host must resolve the model before dispatching (run.params.ctx.model); check the engine routing layer.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
try {
|
|
1350
|
+
const args = buildSpawnArgs({
|
|
1351
|
+
modelRef,
|
|
1352
|
+
thinkingLevel: asThinkingLevel(params.thinkingLevel),
|
|
1353
|
+
agentTools: params.agentTools,
|
|
1354
|
+
appendSystemPromptPath: tempFile?.filePath,
|
|
1355
|
+
sessionDir: params.sessionDir,
|
|
1356
|
+
sessionFile: params.resumeSessionFile,
|
|
1357
|
+
forkSource: params.forkSource,
|
|
1358
|
+
skillPaths: params.skillPaths,
|
|
1359
|
+
mirrorFlags: params.mirrorFlags ?? mirrorMainProcessFlags(process.argv)
|
|
1360
|
+
});
|
|
1361
|
+
const invocation = getPiInvocation(args);
|
|
1362
|
+
const child = (0, import_subagent_engine_sdk13.spawnEngineChild)({
|
|
1363
|
+
command: invocation.command,
|
|
1364
|
+
args: invocation.args,
|
|
1365
|
+
env: buildChildEnv(params),
|
|
1366
|
+
cwd: params.cwd,
|
|
1367
|
+
...params.signal !== void 0 ? { signal: params.signal } : {}
|
|
1368
|
+
});
|
|
1369
|
+
const killChild = (source) => {
|
|
1370
|
+
void (0, import_subagent_engine_sdk13.killChain)(child, {
|
|
1371
|
+
graceMs: PI_KILL_GRACE_MS,
|
|
1372
|
+
unrefTimers: true,
|
|
1373
|
+
escalationNote: `child ${params.recordId} (source: ${source})`
|
|
1374
|
+
});
|
|
1375
|
+
};
|
|
1376
|
+
const runEnd = { endedCleanly: false };
|
|
1377
|
+
const handleSdkEvent = createSdkEventTranslator(
|
|
1378
|
+
record,
|
|
1379
|
+
buildTranslatorOpts(params, callbacks, killChild, runEnd)
|
|
1380
|
+
);
|
|
1381
|
+
const stderrTee = createStderrTee(child);
|
|
1382
|
+
if (stderrTee === void 0 && child.stderr !== null) {
|
|
1383
|
+
child.stderr.resume();
|
|
1384
|
+
}
|
|
1385
|
+
registerActiveChild(params.recordId, child);
|
|
1386
|
+
reportChildSpawned(child, params.recordId, callbacks);
|
|
1387
|
+
const enqueueUi = createUiRequestQueue(child, {
|
|
1388
|
+
...callbacks.askUser !== void 0 ? { uiRequestHandler: callbacks.askUser } : {}
|
|
1389
|
+
});
|
|
1390
|
+
const identity = createSessionIdentityTracker(params.sessionDir, callbacks);
|
|
1391
|
+
const exitPromise = wireChildStdoutPump({
|
|
1392
|
+
child,
|
|
1393
|
+
recordId: params.recordId,
|
|
1394
|
+
callbacks,
|
|
1395
|
+
identity,
|
|
1396
|
+
handleSdkEvent,
|
|
1397
|
+
enqueueUi,
|
|
1398
|
+
stderrTee,
|
|
1399
|
+
runEnd
|
|
1400
|
+
});
|
|
1401
|
+
void performGetStateHandshake(child, identity.addStateListener).then((r) => {
|
|
1402
|
+
identity.applyGetStateFields(r);
|
|
1403
|
+
});
|
|
1404
|
+
sendPromptCommand(child, params.task);
|
|
1405
|
+
const exitCode = await exitPromise;
|
|
1406
|
+
clearEpipeFailure(params.recordId);
|
|
1407
|
+
const outcome = collectOutcome(record, {
|
|
1408
|
+
startTime,
|
|
1409
|
+
success: exitCode === 0,
|
|
1410
|
+
error: exitCode === 0 ? void 0 : `pi child exited with code ${exitCode}`,
|
|
1411
|
+
sessionId: identity.sessionId ?? "",
|
|
1412
|
+
sessionFile: identity.sessionFile,
|
|
1413
|
+
...params.schemaEnv !== void 0 ? { schemaExpected: true } : {}
|
|
1414
|
+
});
|
|
1415
|
+
return { ...outcome, sessionId: identity.sessionId };
|
|
1416
|
+
} finally {
|
|
1417
|
+
if (tempFile !== void 0) await cleanupTempPrompt(tempFile);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/chat-session.ts
|
|
1422
|
+
var import_subagent_engine_sdk14 = require("@zhushanwen/subagent-engine-sdk");
|
|
1423
|
+
var logger6 = (0, import_subagent_engine_sdk14.getLogger)("chat-session");
|
|
1424
|
+
var CHAT_KILL_GRACE_MS = 3e4;
|
|
1425
|
+
function emptyUsage() {
|
|
1426
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: void 0 };
|
|
1427
|
+
}
|
|
1428
|
+
function toAgentUsage(acc) {
|
|
1429
|
+
return {
|
|
1430
|
+
input: acc.input,
|
|
1431
|
+
output: acc.output,
|
|
1432
|
+
cacheRead: acc.cacheRead,
|
|
1433
|
+
cacheWrite: acc.cacheWrite,
|
|
1434
|
+
...acc.cost !== void 0 ? { cost: acc.cost } : {}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function anchorOf(session) {
|
|
1438
|
+
return {
|
|
1439
|
+
sessionRef: {
|
|
1440
|
+
recordId: session.recordId,
|
|
1441
|
+
...session.sessionFile !== void 0 ? { sessionFile: session.sessionFile } : {}
|
|
1442
|
+
},
|
|
1443
|
+
poolKey: PI_POOL_KEY
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function roundKeyOf(session) {
|
|
1447
|
+
return session.firstRoundDone ? { recordId: session.recordId } : { runId: session.spawnRunId };
|
|
1448
|
+
}
|
|
1449
|
+
function roundAbortedError(recordId, source) {
|
|
1450
|
+
return {
|
|
1451
|
+
code: "engine_round_aborted",
|
|
1452
|
+
message: `chat round for ${recordId} was aborted by host (${source}): in-flight round terminated before settling`,
|
|
1453
|
+
recovery: `The session file on disk is intact; dispatch a new run with ctx chat resume to cold-resume the conversation, or start a new chat session.`
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function roundCrashedError(recordId, exit) {
|
|
1457
|
+
const detail = exit.signal !== void 0 ? `signal ${exit.signal}` : `exit code ${String(exit.exitCode)}`;
|
|
1458
|
+
return {
|
|
1459
|
+
code: "engine_round_crashed",
|
|
1460
|
+
message: `pi child for chat session ${recordId} died mid-round (${detail}); last streamed output is incomplete`,
|
|
1461
|
+
recovery: `Inspect the engine stderr tee (logs/pi-task-stderr-<pid>.log) for the crash cause, then cold-resume via a new run with ctx chat resume if the session file is intact.`
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function roundEpipeExhaustedError(recordId, count) {
|
|
1465
|
+
return {
|
|
1466
|
+
code: "engine_round_epipe_exhausted",
|
|
1467
|
+
message: `EPIPE fallback exhausted for ${recordId}: ${count} consecutive stdin write failures (child process likely exited)`,
|
|
1468
|
+
recovery: `Use interact close to clean up the dead session, then dispatch a new run with ctx chat resume (or a fresh chat session).`
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function coldPathReject(recordId) {
|
|
1472
|
+
return {
|
|
1473
|
+
ok: false,
|
|
1474
|
+
code: "engine_session_not_resumable",
|
|
1475
|
+
message: `the pi session behind record ${recordId} has no live process in this engine instance (cold path). Recovery: dispatch a new run with ctx chat resume (pi --session cold resume), or start a new chat session.`
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
var ChatSessionRegistry = class {
|
|
1479
|
+
executor;
|
|
1480
|
+
channels;
|
|
1481
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1482
|
+
constructor(deps = {}) {
|
|
1483
|
+
this.executor = deps.spawnRunner ?? runSpawnOnce;
|
|
1484
|
+
}
|
|
1485
|
+
/** server 构造后注入反向通道发射面(未注入 = 无宿主面:帧丢弃 + askUser 自动 cancelled)。 */
|
|
1486
|
+
bindHostChannels(channels) {
|
|
1487
|
+
this.channels = channels;
|
|
1488
|
+
}
|
|
1489
|
+
/** recordId 是否有活会话(interact 路由判据;不建立会话)。 */
|
|
1490
|
+
has(recordId) {
|
|
1491
|
+
const s = this.sessions.get(recordId);
|
|
1492
|
+
return s !== void 0 && !s.closed;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* run 会话形态入口(首轮 + 冷续):spawn 长驻子进程 + 首轮 prompt,本轮 agent_settled
|
|
1496
|
+
* (真空闲)时 resolve(exit 0 口径,进程保活——对齐 inproc chatMode 首轮 resolve 语义)。
|
|
1497
|
+
* resume 锚点存在 = 冷续(--session 续写原文件),不存在 = 首轮新建。
|
|
1498
|
+
*/
|
|
1499
|
+
async startRound(params, opts) {
|
|
1500
|
+
const stale = this.sessions.get(params.recordId);
|
|
1501
|
+
if (stale !== void 0 && !stale.closed) {
|
|
1502
|
+
stale.killReason = "superseded";
|
|
1503
|
+
const oldChild = getActiveChild(params.recordId);
|
|
1504
|
+
if (oldChild !== void 0 && !oldChild.killed) {
|
|
1505
|
+
void (0, import_subagent_engine_sdk14.killChain)(oldChild, {
|
|
1506
|
+
graceMs: CHAT_KILL_GRACE_MS,
|
|
1507
|
+
unrefTimers: true,
|
|
1508
|
+
escalationNote: `chat session ${params.recordId} (source: superseded by cold resume)`
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
this.sessions.delete(params.recordId);
|
|
1512
|
+
}
|
|
1513
|
+
const session = {
|
|
1514
|
+
recordId: params.recordId,
|
|
1515
|
+
spawnRunId: opts.runId,
|
|
1516
|
+
firstRoundDone: false,
|
|
1517
|
+
roundActive: true,
|
|
1518
|
+
armedSeq: 1,
|
|
1519
|
+
settledSeq: void 0,
|
|
1520
|
+
usage: emptyUsage(),
|
|
1521
|
+
roundTerminalWaiters: /* @__PURE__ */ new Set(),
|
|
1522
|
+
closeAfterRound: false,
|
|
1523
|
+
closed: false,
|
|
1524
|
+
killReason: void 0,
|
|
1525
|
+
streamEnabled: opts.stream !== void 0
|
|
1526
|
+
};
|
|
1527
|
+
this.sessions.set(params.recordId, session);
|
|
1528
|
+
const callbacks = {
|
|
1529
|
+
onEvent: (event) => {
|
|
1530
|
+
this.accumulateUsage(session, event);
|
|
1531
|
+
if (!session.firstRoundDone) opts.onEvent(event);
|
|
1532
|
+
},
|
|
1533
|
+
onHandleReady: (partial) => {
|
|
1534
|
+
const sf = partial.sessionRef.sessionFile;
|
|
1535
|
+
if (typeof sf === "string") session.sessionFile = sf;
|
|
1536
|
+
const sid = partial.sessionRef.sessionId;
|
|
1537
|
+
if (typeof sid === "string") session.sessionId = sid;
|
|
1538
|
+
opts.onHandleReady?.(partial);
|
|
1539
|
+
},
|
|
1540
|
+
onChildSpawned: opts.onChildSpawned,
|
|
1541
|
+
onChildStateChanged: (p) => {
|
|
1542
|
+
if (p.state === "exited") this.handleChildExited(session, p);
|
|
1543
|
+
},
|
|
1544
|
+
onDelta: (delta) => {
|
|
1545
|
+
if (!session.firstRoundDone) opts.stream?.onDelta(delta);
|
|
1546
|
+
else if (session.streamEnabled) this.channels?.streamDelta({ recordId: session.recordId, delta });
|
|
1547
|
+
},
|
|
1548
|
+
askUser: (req) => {
|
|
1549
|
+
if (this.channels === void 0) return Promise.resolve({ cancelled: true });
|
|
1550
|
+
return this.channels.askUser(session.spawnRunId, req);
|
|
1551
|
+
},
|
|
1552
|
+
onChatRoundEnd: () => this.handleRoundEnd(session),
|
|
1553
|
+
onChatAgentSettled: () => this.handleAgentSettled(session)
|
|
1554
|
+
};
|
|
1555
|
+
try {
|
|
1556
|
+
return await this.executor({ ...params, chatMode: true }, callbacks);
|
|
1557
|
+
} finally {
|
|
1558
|
+
session.firstRoundDone = true;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* interact message 热路径:prompt + streamingBehavior 直写 stdin(pi 权威裁决
|
|
1563
|
+
* busy/idle:busy 时 followUp 入队 / steer 抢占,idle 时开新 turn——pi 上游语义,
|
|
1564
|
+
* abort 不清 followUp 队列)。冷路径(无活进程)→ engine_session_not_resumable +
|
|
1565
|
+
* 冷续指引(宿主发新 run chat+resume 接续,D1-A)。
|
|
1566
|
+
*/
|
|
1567
|
+
deliverMessage(recordId, text, interrupt) {
|
|
1568
|
+
const session = this.sessions.get(recordId);
|
|
1569
|
+
const child = getActiveChild(recordId);
|
|
1570
|
+
if (session === void 0 || session.closed || child === void 0 || child.killed) {
|
|
1571
|
+
return coldPathReject(recordId);
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
sendPromptCommand(child, text, {
|
|
1575
|
+
streamingBehavior: interrupt ? "steer" : "followUp"
|
|
1576
|
+
});
|
|
1577
|
+
clearEpipeFailure(recordId);
|
|
1578
|
+
session.roundActive = true;
|
|
1579
|
+
session.armedSeq += 1;
|
|
1580
|
+
return { ok: true, delivered: true };
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
1583
|
+
const count = recordEpipeFailure(recordId);
|
|
1584
|
+
if (count >= EPIPE_FAILURE_THRESHOLD) {
|
|
1585
|
+
clearEpipeFailure(recordId);
|
|
1586
|
+
session.roundActive = false;
|
|
1587
|
+
this.emitPhase(session, { phase: "failed", error: roundEpipeExhaustedError(recordId, count) });
|
|
1588
|
+
return {
|
|
1589
|
+
ok: false,
|
|
1590
|
+
code: "engine_interact_failed",
|
|
1591
|
+
message: `EPIPE fallback exhausted for ${recordId}: ${count} consecutive failures. Recovery: close the session, then cold-resume via run chat resume or start a new chat.`
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
ok: false,
|
|
1596
|
+
code: "engine_session_not_resumable",
|
|
1597
|
+
message: `EPIPE on hot path for ${recordId} (stdin pipe broken, child likely exited; attempt ${count}/${EPIPE_FAILURE_THRESHOLD}). Recovery: retry after close, or cold-resume via run chat resume.`
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
ok: false,
|
|
1602
|
+
code: "engine_interact_failed",
|
|
1603
|
+
message: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* interact close:force = 立即杀链收割(在途轮 → failed aborted);缺省 = 优雅关闭
|
|
1609
|
+
* (在途轮标记 closeAfterRound,agent_settled 收口后收割——对齐 inproc closeSubagent
|
|
1610
|
+
* 的 force 分流);idle 态(无在途轮)立即收割(对齐 closeChatIdle 的进程回收)。
|
|
1611
|
+
*/
|
|
1612
|
+
close(recordId, force) {
|
|
1613
|
+
const session = this.sessions.get(recordId);
|
|
1614
|
+
const child = getActiveChild(recordId);
|
|
1615
|
+
if (session === void 0 || session.closed || child === void 0) {
|
|
1616
|
+
return { ok: true, delivered: true };
|
|
1617
|
+
}
|
|
1618
|
+
if (force || !session.roundActive) {
|
|
1619
|
+
if (session.roundActive) session.killReason = "close";
|
|
1620
|
+
void (0, import_subagent_engine_sdk14.killChain)(child, {
|
|
1621
|
+
graceMs: CHAT_KILL_GRACE_MS,
|
|
1622
|
+
unrefTimers: true,
|
|
1623
|
+
escalationNote: `chat session ${recordId} (source: interact close${force ? " force" : ""})`
|
|
1624
|
+
});
|
|
1625
|
+
return { ok: true, delivered: true };
|
|
1626
|
+
}
|
|
1627
|
+
session.closeAfterRound = true;
|
|
1628
|
+
return { ok: true, delivered: true };
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* interact cancel(D3 协议层收敛语义,对齐 run 域 cancel 形态):
|
|
1632
|
+
* 受理(SIGTERM——run 域 AbortController→SIGTERM 同构)→ 等待目标轮次终态相位
|
|
1633
|
+
* (roundLifecycle settled/idle/failed)→ 超 CANCEL_SETTLE_GRACE_MS 未收敛走与
|
|
1634
|
+
* run 域同构的杀链升级(SIGTERM → grace → SIGKILL)。应答在收敛/升级完成后返回
|
|
1635
|
+
* (协议层收敛语义);无在途轮时无收敛对象,受理即返回(进程回收经 close 事件收口)。
|
|
1636
|
+
*/
|
|
1637
|
+
async cancel(recordId) {
|
|
1638
|
+
const session = this.sessions.get(recordId);
|
|
1639
|
+
if (session === void 0 || session.closed) {
|
|
1640
|
+
return { ok: true, delivered: true };
|
|
1641
|
+
}
|
|
1642
|
+
const child = getActiveChild(recordId);
|
|
1643
|
+
if (child === void 0) {
|
|
1644
|
+
this.sessions.delete(recordId);
|
|
1645
|
+
return { ok: true, delivered: true };
|
|
1646
|
+
}
|
|
1647
|
+
const roundInFlight = session.roundActive;
|
|
1648
|
+
const settle = roundInFlight ? this.waitForRoundTerminal(session, import_subagent_engine_sdk14.CANCEL_SETTLE_GRACE_MS) : void 0;
|
|
1649
|
+
session.killReason = "cancel";
|
|
1650
|
+
child.kill("SIGTERM");
|
|
1651
|
+
if (settle === void 0) {
|
|
1652
|
+
return { ok: true, delivered: true };
|
|
1653
|
+
}
|
|
1654
|
+
if (await settle) {
|
|
1655
|
+
return { ok: true, delivered: true };
|
|
1656
|
+
}
|
|
1657
|
+
if (session.closed) {
|
|
1658
|
+
return { ok: true, delivered: true };
|
|
1659
|
+
}
|
|
1660
|
+
logger6.warn(
|
|
1661
|
+
`[chat-session] cancel for ${recordId} did not settle within ${import_subagent_engine_sdk14.CANCEL_SETTLE_GRACE_MS}ms, escalating kill chain`
|
|
1662
|
+
);
|
|
1663
|
+
const reaped = this.waitForRoundTerminal(session, import_subagent_engine_sdk14.CANCEL_SETTLE_GRACE_MS + CHAT_KILL_GRACE_MS);
|
|
1664
|
+
void (0, import_subagent_engine_sdk14.killChain)(child, {
|
|
1665
|
+
graceMs: CHAT_KILL_GRACE_MS,
|
|
1666
|
+
unrefTimers: true,
|
|
1667
|
+
escalationNote: `chat session ${recordId} (source: interact cancel settle timeout)`
|
|
1668
|
+
});
|
|
1669
|
+
await reaped;
|
|
1670
|
+
return { ok: true, delivered: true };
|
|
1671
|
+
}
|
|
1672
|
+
// ── 内部:轮次相位处置 ──
|
|
1673
|
+
/** agent_end(非 willRetry,队列排空):本轮收敛 → settled 相位(带本轮用量)。 */
|
|
1674
|
+
handleRoundEnd(session) {
|
|
1675
|
+
if (session.closed || !session.roundActive) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
session.roundActive = false;
|
|
1679
|
+
session.settledSeq = session.armedSeq;
|
|
1680
|
+
const usage = toAgentUsage(session.usage);
|
|
1681
|
+
session.lastRoundUsage = usage;
|
|
1682
|
+
session.usage = emptyUsage();
|
|
1683
|
+
this.emitPhase(session, { phase: "settled", usage });
|
|
1684
|
+
}
|
|
1685
|
+
/** agent_settled(真空闲):idle 相位(usage + anchor 回填)+ 优雅关闭收割。 */
|
|
1686
|
+
handleAgentSettled(session) {
|
|
1687
|
+
if (session.closed) return;
|
|
1688
|
+
if (session.armedSeq === session.settledSeq) {
|
|
1689
|
+
session.roundActive = false;
|
|
1690
|
+
}
|
|
1691
|
+
this.emitPhase(session, {
|
|
1692
|
+
phase: "idle",
|
|
1693
|
+
...session.lastRoundUsage !== void 0 ? { usage: session.lastRoundUsage } : {},
|
|
1694
|
+
anchor: anchorOf(session)
|
|
1695
|
+
});
|
|
1696
|
+
if (session.closeAfterRound) {
|
|
1697
|
+
session.closeAfterRound = false;
|
|
1698
|
+
const child = getActiveChild(session.recordId);
|
|
1699
|
+
if (child !== void 0 && !child.killed) {
|
|
1700
|
+
void (0, import_subagent_engine_sdk14.killChain)(child, {
|
|
1701
|
+
graceMs: CHAT_KILL_GRACE_MS,
|
|
1702
|
+
unrefTimers: true,
|
|
1703
|
+
escalationNote: `chat session ${session.recordId} (source: close after round)`
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
/** 子进程退出:会话消亡;在途轮 → failed 相位(引擎主动杀 = aborted / 否则 crashed)。 */
|
|
1709
|
+
handleChildExited(session, exit) {
|
|
1710
|
+
if (session.closed) return;
|
|
1711
|
+
session.closed = true;
|
|
1712
|
+
const hadRound = session.roundActive;
|
|
1713
|
+
session.roundActive = false;
|
|
1714
|
+
if (this.sessions.get(session.recordId) === session) {
|
|
1715
|
+
this.sessions.delete(session.recordId);
|
|
1716
|
+
}
|
|
1717
|
+
if (!hadRound) return;
|
|
1718
|
+
if (session.killReason !== void 0) {
|
|
1719
|
+
this.emitPhase(session, { phase: "failed", error: roundAbortedError(session.recordId, session.killReason) });
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
this.emitPhase(session, { phase: "failed", error: roundCrashedError(session.recordId, exit) });
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* 相位发射(关联键 = 首轮 runId / 续聊 recordId)+ 轮终等待体逐一 resolve。
|
|
1726
|
+
* superseded 会话抑制宿主发射(S5,守卫放单一咽喉点而非仅 handleChildExited):
|
|
1727
|
+
* 同 recordId 冷续 run 已重建会话,旧会话残留终态帧(killChain 杀死在途轮的 failed、
|
|
1728
|
+
* SIGTERM 优雅收口竞态下的 settled+idle——pi trap 语义先收口再退)以 recordId 键发射
|
|
1729
|
+
* 会与新会话在途轮同键串扰,core 可能把旧轮终态误配到新轮(误终态化)。superseded
|
|
1730
|
+
* 信号本身 = 新 run 的 start(宿主主动发起),core 无需旧会话帧。等待体仍逐一
|
|
1731
|
+
* resolve:cancel 等待中 killReason 被后续冷续覆写为 superseded 的极端时序下收敛
|
|
1732
|
+
* 不悬挂。
|
|
1733
|
+
*
|
|
1734
|
+
* [F-4 S6 引擎半边] 旧轮的 idle 相位(agent_settled 空闲边界)在 settled→idle 间隙
|
|
1735
|
+
* 宿主已投递新轮时抑制宿主发射(`armedSeq !== settledSeq` = settled 以来有新轮 arm,
|
|
1736
|
+
* 该 idle 帧属旧轮——时序恒晚于新轮投递)。与 S6 的 armedSeq 守卫(handleAgentSettled
|
|
1737
|
+
* 保 settled 相位、不清新轮 roundActive)同模式:旧 idle 帧若放行,core 的
|
|
1738
|
+
* handleChatRoundPhase(idle) 无轮次身份可判——disarmRoundFromProtocol 拆掉新轮中段
|
|
1739
|
+
* 守护 + armChatIdleTimer 挂 5min idle timer → 新轮进行中超 5min 被误杀。新轮的
|
|
1740
|
+
* idle 由新轮自身 agent_settled 发射(其 armedSeq === settledSeq)。等待体仍逐一
|
|
1741
|
+
* resolve(在途 cancel 的收敛兜底语义不变)。
|
|
1742
|
+
*/
|
|
1743
|
+
emitPhase(session, phase) {
|
|
1744
|
+
if (phase.phase === "idle" && session.armedSeq !== session.settledSeq) {
|
|
1745
|
+
logger6.debug(
|
|
1746
|
+
`[chat-session] suppress stale idle phase for ${session.recordId} (armedSeq=${session.armedSeq} advanced past settledSeq=${session.settledSeq} \u2014 a new round was armed in the settled\u2192idle gap; its own agent_settled will emit idle)`
|
|
1747
|
+
);
|
|
1748
|
+
} else if (session.killReason === "superseded") {
|
|
1749
|
+
logger6.debug(
|
|
1750
|
+
`[chat-session] suppress ${phase.phase} phase for superseded session ${session.recordId} (record cold-resumed by a new run)`
|
|
1751
|
+
);
|
|
1752
|
+
} else {
|
|
1753
|
+
this.channels?.roundLifecycle({ ...roundKeyOf(session), ...phase });
|
|
1754
|
+
}
|
|
1755
|
+
for (const w of session.roundTerminalWaiters) w();
|
|
1756
|
+
session.roundTerminalWaiters.clear();
|
|
1757
|
+
}
|
|
1758
|
+
/** message_end 用量累加(轮内增量;interact 轮无 event 通知通道,用量经相位帧回填)。 */
|
|
1759
|
+
accumulateUsage(session, event) {
|
|
1760
|
+
if (event.type !== "message_end" || event.usage === void 0) return;
|
|
1761
|
+
session.usage.input += event.usage.input;
|
|
1762
|
+
session.usage.output += event.usage.output;
|
|
1763
|
+
session.usage.cacheRead += event.usage.cacheRead;
|
|
1764
|
+
session.usage.cacheWrite += event.usage.cacheWrite;
|
|
1765
|
+
if (event.usage.cost !== void 0) {
|
|
1766
|
+
session.usage.cost = (session.usage.cost ?? 0) + event.usage.cost;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/** 轮终等待(cancel 收敛判据;超时 resolve false——不清理其他等待体)。 */
|
|
1770
|
+
waitForRoundTerminal(session, timeoutMs) {
|
|
1771
|
+
return new Promise((resolve) => {
|
|
1772
|
+
const done = () => {
|
|
1773
|
+
clearTimeout(timer);
|
|
1774
|
+
resolve(true);
|
|
1775
|
+
};
|
|
1776
|
+
const timer = setTimeout(() => {
|
|
1777
|
+
session.roundTerminalWaiters.delete(done);
|
|
1778
|
+
resolve(false);
|
|
1779
|
+
}, timeoutMs);
|
|
1780
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1781
|
+
session.roundTerminalWaiters.add(done);
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
// src/read-fallback.ts
|
|
1787
|
+
var import_node_fs = require("fs");
|
|
1788
|
+
var import_subagent_engine_sdk15 = require("@zhushanwen/subagent-engine-sdk");
|
|
1789
|
+
var import_subagent_engine_sdk16 = require("@zhushanwen/subagent-engine-sdk");
|
|
1790
|
+
var logger7 = (0, import_subagent_engine_sdk15.getLogger)("pi-engine/read-fallback");
|
|
1791
|
+
function replayJournalToSessionView(handle, engineId) {
|
|
1792
|
+
const journalPath = handle.data.journalPath;
|
|
1793
|
+
if (journalPath === void 0 || journalPath === "") return void 0;
|
|
1794
|
+
let raw;
|
|
1795
|
+
try {
|
|
1796
|
+
raw = (0, import_node_fs.readFileSync)(journalPath, "utf8");
|
|
1797
|
+
} catch {
|
|
1798
|
+
return void 0;
|
|
1799
|
+
}
|
|
1800
|
+
const events = [];
|
|
1801
|
+
for (const line of raw.split("\n")) {
|
|
1802
|
+
const trimmed = line.trim();
|
|
1803
|
+
if (trimmed === "") continue;
|
|
1804
|
+
try {
|
|
1805
|
+
const obj = JSON.parse(trimmed);
|
|
1806
|
+
const candidate = typeof obj === "object" && obj !== null && "event" in obj ? obj.event : obj;
|
|
1807
|
+
if (isAgentEvent(candidate)) events.push(candidate);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
logger7.debug(`[read-fallback] skip corrupt journal line: ${(0, import_subagent_engine_sdk.toErrorMessage)(err)}`);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (events.length === 0) return void 0;
|
|
1813
|
+
const sessionId = (0, import_subagent_engine_sdk16.sessionIdFromHandle)(handle.data);
|
|
1814
|
+
return (0, import_subagent_engine_sdk16.eventsToSessionView)(events, engineId, sessionId);
|
|
1815
|
+
}
|
|
1816
|
+
function isAgentEvent(x) {
|
|
1817
|
+
if (typeof x !== "object" || x === null || !("type" in x)) return false;
|
|
1818
|
+
const t = x.type;
|
|
1819
|
+
return t === "tool_start" || t === "tool_end" || t === "text_delta" || t === "thinking_delta" || t === "turn_end" || t === "message_end" || t === "compaction" || t === "error";
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/pi-engine.ts
|
|
1823
|
+
var logger8 = (0, import_subagent_engine_sdk18.getLogger)("pi-engine");
|
|
1824
|
+
var PROBE_VERSION_TIMEOUT_MS = 1e4;
|
|
1825
|
+
function resolveSessionDir(dataDir, cwd) {
|
|
1826
|
+
const encoded = cwd.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
1827
|
+
return path4.join(dataDir, "subagents", "sessions", encoded);
|
|
1828
|
+
}
|
|
1829
|
+
function resolveDataRoot(explicit) {
|
|
1830
|
+
if (explicit !== void 0 && explicit !== "") return explicit;
|
|
1831
|
+
const env = process.env.XYZ_AGENT_DATA_DIR;
|
|
1832
|
+
return env !== void 0 && env !== "" ? env : void 0;
|
|
1833
|
+
}
|
|
1834
|
+
var PiEngine = class {
|
|
1835
|
+
id = PI_ENGINE_ID;
|
|
1836
|
+
deps;
|
|
1837
|
+
probeCache;
|
|
1838
|
+
/** [v1.x] chat 会话注册表(长驻会话状态面——见 chat-session.ts 文件头)。 */
|
|
1839
|
+
chatSessions;
|
|
1840
|
+
constructor(deps = {}) {
|
|
1841
|
+
this.deps = deps;
|
|
1842
|
+
this.chatSessions = new ChatSessionRegistry(
|
|
1843
|
+
deps.spawnRunner !== void 0 ? { spawnRunner: deps.spawnRunner } : {}
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
/** pi 链路实际接通的能力(与 core PiEngine.capabilities() 逐位一致——manifest 同源)。 */
|
|
1847
|
+
capabilities() {
|
|
1848
|
+
return {
|
|
1849
|
+
// PI_WORKFLOW_SCHEMA env 注入 + structured-output 扩展(方案 A 唯一校验权威)
|
|
1850
|
+
schemaEnforcement: "native",
|
|
1851
|
+
// pi RPC 有 steer,但 spawn 链路未接通(turn-limiter steer no-op)
|
|
1852
|
+
steer: "unsupported",
|
|
1853
|
+
// chatMode idle 复用 + message/close/cancel 交互面已接通(v1.x 协议路径:
|
|
1854
|
+
// run 会话形态 + chat-session 会话管理器承载轮次)
|
|
1855
|
+
conversation: "native",
|
|
1856
|
+
// persona 经 --skill / --append-system-prompt flag 通道注入
|
|
1857
|
+
personaInjection: "flag",
|
|
1858
|
+
// 30+ 事件流(六引擎最细粒度)
|
|
1859
|
+
eventGranularity: "stream",
|
|
1860
|
+
// 无 OS sandbox;worktree 隔离(公共层)= emulated
|
|
1861
|
+
sandbox: "emulated",
|
|
1862
|
+
// pi session JSONL 完整重建(session-reconstructor)
|
|
1863
|
+
sessionRead: "full",
|
|
1864
|
+
// chatMode 同进程 idle 复用(热)+ --session 冷续写
|
|
1865
|
+
resume: "native",
|
|
1866
|
+
// 现链路 abort = SIGTERM(pi 子进程 trap 后 graceful shutdown)
|
|
1867
|
+
interrupt: "kill-only",
|
|
1868
|
+
// argv-mirror 镜像主进程 --approve 等 flag
|
|
1869
|
+
permissionMode: "native",
|
|
1870
|
+
// turn limiter + spawn watchdog 估算兑现轮数上限
|
|
1871
|
+
maxTurns: true
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
/** 探针(D7):invocation 可解析(二进制/脚本存在)+ 版本解析。relay:false 显式
|
|
1875
|
+
* 直连——探针测 pi 本体可解析性,经 relay 探到的是 runtime 健康,语义错位。 */
|
|
1876
|
+
async probe(opts) {
|
|
1877
|
+
if (!opts?.force && this.probeCache) return this.probeCache;
|
|
1878
|
+
const invocation = getPiInvocation(["--version"], { relay: false });
|
|
1879
|
+
const invocationOk = isInvocationResolvable(invocation);
|
|
1880
|
+
const checks = [
|
|
1881
|
+
{
|
|
1882
|
+
name: "invocation",
|
|
1883
|
+
ok: invocationOk,
|
|
1884
|
+
detail: invocationOk ? `${invocation.command} ${invocation.args.join(" ")}` : `cannot resolve pi executable: ${invocation.command} (script missing and not on PATH)`
|
|
1885
|
+
}
|
|
1886
|
+
];
|
|
1887
|
+
let engineVersion = "";
|
|
1888
|
+
if (invocationOk) {
|
|
1889
|
+
const runVersion = this.deps.probeVersion ?? defaultProbeVersion;
|
|
1890
|
+
const version = await runVersion(invocation);
|
|
1891
|
+
const versionOk = version !== void 0 && version.length > 0;
|
|
1892
|
+
checks.push({
|
|
1893
|
+
name: "version",
|
|
1894
|
+
ok: versionOk,
|
|
1895
|
+
detail: versionOk ? version : "pi --version returned empty or failed"
|
|
1896
|
+
});
|
|
1897
|
+
engineVersion = version ?? "";
|
|
1898
|
+
}
|
|
1899
|
+
const ok = checks.every((c) => c.ok);
|
|
1900
|
+
const report = {
|
|
1901
|
+
ok,
|
|
1902
|
+
engineVersion,
|
|
1903
|
+
checks,
|
|
1904
|
+
...ok ? {} : {
|
|
1905
|
+
error: {
|
|
1906
|
+
code: "engine_probe_failed",
|
|
1907
|
+
recovery: `Run \`${invocation.command} --version\` to confirm the pi binary works, then retry the probe. If the binary is missing, reinstall pi (npm i -g @earendil-works/pi-coding-agent) or fix PATH.`
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
this.probeCache = report;
|
|
1912
|
+
return report;
|
|
1913
|
+
}
|
|
1914
|
+
/** run 的引擎侧实现:协议 RunParams → spawn-runner 单次执行。
|
|
1915
|
+
*
|
|
1916
|
+
* run 期间事件经 ctx.onEvent(server 层 → `event` 通知);session 身份经
|
|
1917
|
+
* ctx.onHandleReady(→ host/handleReady);子进程 pid/状态经镜像回调上报。
|
|
1918
|
+
* 抛错语义(core PiEngine.run ①):prepare 期失败 reject,不产生 handle。
|
|
1919
|
+
*
|
|
1920
|
+
* [v1.x] ctx.chat 存在 = chat 会话形态(首轮/冷续):委托 chat-session 长驻执行
|
|
1921
|
+
* (agent_end 不 kill、agent_settled resolve——本轮收口进程保活,续聊经 interact)。 */
|
|
1922
|
+
async run(task, ctx) {
|
|
1923
|
+
const dataDir = resolveEngineDataRootOrThrow(this.deps.dataDir);
|
|
1924
|
+
ctx.onPoolResolved?.(PI_POOL_KEY);
|
|
1925
|
+
const cwd = task.cwd ?? process.cwd();
|
|
1926
|
+
if (ctx.chat !== void 0) {
|
|
1927
|
+
return this.runChatRound(task, ctx, dataDir, cwd);
|
|
1928
|
+
}
|
|
1929
|
+
const spawn = this.deps.spawnRunner ?? runSpawnOnce;
|
|
1930
|
+
const result = await spawn(
|
|
1931
|
+
buildOneShotRunParams(task, ctx, dataDir, cwd),
|
|
1932
|
+
buildRunCallbacks(ctx, this.askUserHandler)
|
|
1933
|
+
);
|
|
1934
|
+
return buildEngineRunResult(ctx.taskId, result);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* [v1.x] run 会话形态主体(首轮/冷续,chat-domain 设计 §3.2 D1-A):spawn 长驻子进程
|
|
1938
|
+
* + 首轮 prompt,本轮 agent_settled(真空闲)resolve(进程保活)。record 锚定键 =
|
|
1939
|
+
* chat.recordId(区别于一次性 run 的 runId 锚定——interact/roundLifecycle 据此定位);
|
|
1940
|
+
* resume 锚点存在 = 冷续(--session 续写原文件),不存在 = 首轮新建。
|
|
1941
|
+
*/
|
|
1942
|
+
async runChatRound(task, ctx, dataDir, cwd) {
|
|
1943
|
+
const chat = ctx.chat;
|
|
1944
|
+
const resumeFile = refString(chat.resume?.sessionRef ?? {}, "sessionFile");
|
|
1945
|
+
const result = await this.chatSessions.startRound(
|
|
1946
|
+
buildChatRoundParams(task, ctx, dataDir, cwd, chat.recordId, resumeFile),
|
|
1947
|
+
buildChatRoundCallbacks(ctx)
|
|
1948
|
+
);
|
|
1949
|
+
return buildEngineRunResult(chat.recordId, result);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* D1 交互控制面。[v1.x] chat 会话(chat-session 命中)优先路由:
|
|
1953
|
+
* - message:热路径 sendPromptCommand + streamingBehavior(interrupt=steer 抢占/
|
|
1954
|
+
* 缺省 followUp 排队——pi 上游语义)+ EPIPE 兜底(耗尽 → roundLifecycle failed);
|
|
1955
|
+
* 冷路径(进程死)→ engine_session_not_resumable + 冷续指引(宿主发新 run chat+resume);
|
|
1956
|
+
* - close:force=杀链立即收割 / 缺省=优雅(轮收口后收割);
|
|
1957
|
+
* - cancel:D3 收敛语义(受理 → 等轮终相位 → 超 CANCEL_SETTLE_GRACE_MS 杀链升级)。
|
|
1958
|
+
*
|
|
1959
|
+
* 未命中(一次性 run 的活跃子进程 / 冷句柄)走下方既有路径:message 热路径直写、
|
|
1960
|
+
* close = SIGTERM、cancel = SIGTERM(无收敛等待——run 域 cancel 帧另有 AbortController
|
|
1961
|
+
* 通道,收敛由 run 应答承载)。
|
|
1962
|
+
*/
|
|
1963
|
+
async interact(handle, action) {
|
|
1964
|
+
try {
|
|
1965
|
+
const recordId = refString(handle.data.sessionRef, "recordId");
|
|
1966
|
+
if (recordId === void 0) return notResumable(handle);
|
|
1967
|
+
if (this.chatSessions.has(recordId)) return this.interactChatSession(recordId, action);
|
|
1968
|
+
return this.interactRunDomain(recordId, action);
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
return {
|
|
1971
|
+
ok: false,
|
|
1972
|
+
code: "engine_interact_failed",
|
|
1973
|
+
message: (0, import_subagent_engine_sdk.toErrorMessage)(err)
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
/** chat 会话命中分支(长驻):cancel/close/message 直派 chat-session
|
|
1978
|
+
* (close force=杀链立即收割 / 缺省=优雅;message streamingBehavior 由 interrupt 决定)。 */
|
|
1979
|
+
async interactChatSession(recordId, action) {
|
|
1980
|
+
if (action.kind === "cancel") return this.chatSessions.cancel(recordId);
|
|
1981
|
+
if (action.kind === "close") return this.chatSessions.close(recordId, action.payload?.force === true);
|
|
1982
|
+
return this.chatSessions.deliverMessage(recordId, action.payload, action.interrupt === true);
|
|
1983
|
+
}
|
|
1984
|
+
/** 未命中分支:一次性 run 的活跃子进程 / 冷句柄——message 热路径直写 stdin、
|
|
1985
|
+
* close/cancel = SIGTERM(run 域 cancel 帧另有 AbortController 通道,收敛由 run 应答承载)。 */
|
|
1986
|
+
interactRunDomain(recordId, action) {
|
|
1987
|
+
if (action.kind === "cancel") {
|
|
1988
|
+
const child = getActiveChild(recordId);
|
|
1989
|
+
if (child === void 0) return { ok: true, delivered: true };
|
|
1990
|
+
child.kill("SIGTERM");
|
|
1991
|
+
return { ok: true, delivered: true };
|
|
1992
|
+
}
|
|
1993
|
+
if (action.kind === "close") {
|
|
1994
|
+
killRecordChild(recordId);
|
|
1995
|
+
return { ok: true, delivered: true };
|
|
1996
|
+
}
|
|
1997
|
+
return this.deliverHotPathMessage(recordId, action);
|
|
1998
|
+
}
|
|
1999
|
+
/** message 热路径(进程活)sendPromptCommand 直写 stdin + EPIPE 兜底
|
|
2000
|
+
* (连续失败达阈值 → 抛错升级为 engine_interact_failed;未达 → not_resumable 降级)。 */
|
|
2001
|
+
deliverHotPathMessage(recordId, action) {
|
|
2002
|
+
const child = getActiveChild(recordId);
|
|
2003
|
+
if (child === void 0 || child.killed) {
|
|
2004
|
+
return {
|
|
2005
|
+
ok: false,
|
|
2006
|
+
code: "engine_session_not_resumable",
|
|
2007
|
+
message: `the pi session behind this handle has no live process in this engine instance (cold path). Recovery: dispatch a new run with ctx resume (pi --session cold resume), or start a new subagent.`
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
sendPromptCommand(child, action.payload, {
|
|
2012
|
+
streamingBehavior: action.interrupt === true ? "steer" : "followUp"
|
|
2013
|
+
});
|
|
2014
|
+
clearEpipeFailure(recordId);
|
|
2015
|
+
return { ok: true, delivered: true };
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
if (err instanceof Error && err.message.includes("EPIPE")) {
|
|
2018
|
+
const count = recordEpipeFailure(recordId);
|
|
2019
|
+
if (count >= EPIPE_FAILURE_THRESHOLD) {
|
|
2020
|
+
clearEpipeFailure(recordId);
|
|
2021
|
+
throw new Error(
|
|
2022
|
+
`[subagents] EPIPE fallback exhausted for ${recordId}: ${count} consecutive EPIPE failures. Recovery: use action:'close' to clean up, then action:'start' a new subagent.`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
ok: false,
|
|
2027
|
+
code: "engine_session_not_resumable",
|
|
2028
|
+
message: `EPIPE on hot path for ${recordId} (stdin pipe broken, child likely exited; attempt ${count}/${EPIPE_FAILURE_THRESHOLD}). Recovery: retry after close, or start a new subagent run.`
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
throw err;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* read 三级降级(协议化形态):②级 journal 重放(SDK journal-replay 纯投影)
|
|
2036
|
+
* → ③级 outcome-only。①级 pi 原生读取(session-reconstructor)按 §2.7 保持
|
|
2037
|
+
* core,不随迁(deviations 登记)。
|
|
2038
|
+
*/
|
|
2039
|
+
async read(handle) {
|
|
2040
|
+
const sessionFile = refString(handle.data.sessionRef, "sessionFile");
|
|
2041
|
+
const journaled = replayJournalToSessionView(handle, PI_ENGINE_ID);
|
|
2042
|
+
if (journaled !== void 0) return journaled;
|
|
2043
|
+
void sessionFile;
|
|
2044
|
+
return { engineId: PI_ENGINE_ID, turns: [], source: "outcome-only" };
|
|
2045
|
+
}
|
|
2046
|
+
/** dispose:全量收割活跃子进程 + 清 EPIPE 计数(幂等)。 */
|
|
2047
|
+
async dispose() {
|
|
2048
|
+
killAllActiveChildren();
|
|
2049
|
+
resetAllEpipeFailures();
|
|
2050
|
+
}
|
|
2051
|
+
// ── 反向通道注入面(server 构造后接线 host/askUser) ──
|
|
2052
|
+
askUserHandler;
|
|
2053
|
+
/** server 层注入 host/askUser 两阶段等待体(ui-request-queue 消费)。 */
|
|
2054
|
+
bindAskUser(handler) {
|
|
2055
|
+
this.askUserHandler = handler;
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* server 层注入 [v1.x] chat 会话的反向通道发射面(host/roundLifecycle、recordId 键
|
|
2059
|
+
* host/streamDelta、会话级 host/askUser)。会话跨 run 存活,绑定是引擎进程生命周期
|
|
2060
|
+
* 级(区别于一次性 run 的 per-run askUser 绑定)。
|
|
2061
|
+
*/
|
|
2062
|
+
bindHostChannels(channels) {
|
|
2063
|
+
this.chatSessions.bindHostChannels(channels);
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
function resolveEngineDataRootOrThrow(explicit) {
|
|
2067
|
+
const dataDir = resolveDataRoot(explicit);
|
|
2068
|
+
if (dataDir !== void 0) return dataDir;
|
|
2069
|
+
throw new import_subagent_engine_sdk18.EngineSdkError(
|
|
2070
|
+
"engine_not_found",
|
|
2071
|
+
"pi-subagent-cli cannot resolve the engine data root (neither initialize hostInfo.dataRoot nor env XYZ_AGENT_DATA_DIR is set)",
|
|
2072
|
+
"The host must pass hostInfo.dataRoot at initialize, or inject XYZ_AGENT_DATA_DIR into the engine child env. Expected shape: <xyz-agent dataDir> (sessions live under <dataDir>/subagents/sessions/)."
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
function buildOneShotRunParams(task, ctx, dataDir, cwd) {
|
|
2076
|
+
return {
|
|
2077
|
+
recordId: ctx.taskId,
|
|
2078
|
+
task: task.prompt,
|
|
2079
|
+
agentName: task.description ?? task.agent ?? "workflow-agent",
|
|
2080
|
+
model: ctx.ctxModel !== void 0 ? `${ctx.ctxModel.provider}/${ctx.ctxModel.id}` : task.model,
|
|
2081
|
+
...task.thinkingLevel !== void 0 ? { thinkingLevel: task.thinkingLevel } : {},
|
|
2082
|
+
sessionDir: resolveSessionDir(dataDir, cwd),
|
|
2083
|
+
cwd,
|
|
2084
|
+
...task.schemaEnv !== void 0 ? { schemaEnv: task.schemaEnv } : {},
|
|
2085
|
+
...task.maxTurns !== void 0 ? { maxTurns: task.maxTurns } : {},
|
|
2086
|
+
...task.graceTurns !== void 0 ? { graceTurns: task.graceTurns } : {},
|
|
2087
|
+
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
2088
|
+
...task.skillPath !== void 0 ? { skillPaths: [task.skillPath] } : {},
|
|
2089
|
+
...task.appendSystemPrompt !== void 0 ? { appendSystemPrompt: task.appendSystemPrompt } : {},
|
|
2090
|
+
// fork-from 显式分叉源(协议 task.forkSource → --fork;一次性 run 承载——
|
|
2091
|
+
// fork-from 无 chatMode,chat 轮续写走 resumeSessionFile 通路互不相交)。
|
|
2092
|
+
...task.forkSource !== void 0 ? { forkSource: task.forkSource } : {}
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function buildRunCallbacks(ctx, askUserHandler) {
|
|
2096
|
+
return {
|
|
2097
|
+
onEvent: (event) => ctx.onEvent?.(event),
|
|
2098
|
+
onHandleReady: (partial) => ctx.onHandleReady?.(partial),
|
|
2099
|
+
onChildSpawned: (pid) => {
|
|
2100
|
+
ctx.onChildSpawned?.({ pid, killed: false });
|
|
2101
|
+
},
|
|
2102
|
+
...ctx.stream !== void 0 ? { onDelta: (delta) => ctx.stream?.onDelta(delta) } : {},
|
|
2103
|
+
...askUserHandler !== void 0 ? { askUser: (req) => askUserHandler(req) } : {}
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function buildChatRoundParams(task, ctx, dataDir, cwd, recordId, resumeFile) {
|
|
2107
|
+
return {
|
|
2108
|
+
recordId,
|
|
2109
|
+
task: task.prompt,
|
|
2110
|
+
agentName: task.description ?? task.agent ?? "chat-agent",
|
|
2111
|
+
model: ctx.ctxModel !== void 0 ? `${ctx.ctxModel.provider}/${ctx.ctxModel.id}` : task.model,
|
|
2112
|
+
...task.thinkingLevel !== void 0 ? { thinkingLevel: task.thinkingLevel } : {},
|
|
2113
|
+
sessionDir: resolveSessionDir(dataDir, cwd),
|
|
2114
|
+
cwd,
|
|
2115
|
+
...task.schemaEnv !== void 0 ? { schemaEnv: task.schemaEnv } : {},
|
|
2116
|
+
...task.maxTurns !== void 0 ? { maxTurns: task.maxTurns } : {},
|
|
2117
|
+
...task.graceTurns !== void 0 ? { graceTurns: task.graceTurns } : {},
|
|
2118
|
+
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
2119
|
+
...task.skillPath !== void 0 ? { skillPaths: [task.skillPath] } : {},
|
|
2120
|
+
...task.appendSystemPrompt !== void 0 ? { appendSystemPrompt: task.appendSystemPrompt } : {},
|
|
2121
|
+
...resumeFile !== void 0 ? { resumeSessionFile: resumeFile } : {}
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function buildChatRoundCallbacks(ctx) {
|
|
2125
|
+
return {
|
|
2126
|
+
runId: ctx.taskId,
|
|
2127
|
+
onEvent: (event) => ctx.onEvent?.(event),
|
|
2128
|
+
...ctx.stream !== void 0 ? { stream: ctx.stream } : {},
|
|
2129
|
+
onHandleReady: (partial) => ctx.onHandleReady?.(partial),
|
|
2130
|
+
onChildSpawned: (pid) => {
|
|
2131
|
+
ctx.onChildSpawned?.({ pid, killed: false });
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function buildEngineRunResult(recordId, result) {
|
|
2136
|
+
return {
|
|
2137
|
+
handle: {
|
|
2138
|
+
data: {
|
|
2139
|
+
v: 1,
|
|
2140
|
+
engineId: PI_ENGINE_ID,
|
|
2141
|
+
sessionRef: {
|
|
2142
|
+
recordId,
|
|
2143
|
+
...result.sessionId !== void 0 ? { sessionId: result.sessionId } : {},
|
|
2144
|
+
...result.sessionFile !== void 0 ? { sessionFile: result.sessionFile } : {}
|
|
2145
|
+
},
|
|
2146
|
+
poolKey: PI_POOL_KEY,
|
|
2147
|
+
adapterVersion: PI_ADAPTER_VERSION
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
outcome: toOutcome(result)
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function toOutcome(result) {
|
|
2154
|
+
return {
|
|
2155
|
+
content: result.content,
|
|
2156
|
+
...result.failureKind !== void 0 ? { failureKind: result.failureKind } : {},
|
|
2157
|
+
...result.parsedOutput !== void 0 ? { parsedOutput: result.parsedOutput } : {},
|
|
2158
|
+
...result.usage !== void 0 ? { usage: toOutcomeUsage(result) } : {},
|
|
2159
|
+
durationMs: result.durationMs,
|
|
2160
|
+
...result.error !== void 0 ? { error: result.error } : {},
|
|
2161
|
+
...result.sessionId !== void 0 ? { sessionId: result.sessionId } : {},
|
|
2162
|
+
...result.sessionFile !== void 0 ? { sessionFile: result.sessionFile } : {},
|
|
2163
|
+
toolCalls: result.toolCalls.map((tc) => ({
|
|
2164
|
+
name: tc.toolName,
|
|
2165
|
+
input: tc.args === void 0 ? "" : JSON.stringify(tc.args)
|
|
2166
|
+
})),
|
|
2167
|
+
engineId: PI_ENGINE_ID
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
function toOutcomeUsage(result) {
|
|
2171
|
+
const u = result.usage;
|
|
2172
|
+
return {
|
|
2173
|
+
input: u.input,
|
|
2174
|
+
output: u.output,
|
|
2175
|
+
cacheRead: u.cacheRead,
|
|
2176
|
+
cacheWrite: u.cacheWrite,
|
|
2177
|
+
cost: u.cost ?? 0,
|
|
2178
|
+
contextTokens: u.input + u.cacheRead,
|
|
2179
|
+
turns: result.turns
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
function refString(ref, key) {
|
|
2183
|
+
const v = ref[key];
|
|
2184
|
+
return typeof v === "string" ? v : void 0;
|
|
2185
|
+
}
|
|
2186
|
+
function killRecordChild(recordId) {
|
|
2187
|
+
const child = getActiveChild(recordId);
|
|
2188
|
+
if (child === void 0 || child.killed) return;
|
|
2189
|
+
child.kill("SIGTERM");
|
|
2190
|
+
}
|
|
2191
|
+
function notResumable(handle) {
|
|
2192
|
+
return {
|
|
2193
|
+
ok: false,
|
|
2194
|
+
code: "engine_session_not_resumable",
|
|
2195
|
+
message: `the pi session behind this handle is not resumable via interact (record not found for sessionRef ${JSON.stringify(handle.data.sessionRef)}). Recovery: use a cold resume path (pi --session with the session file), or start a new subagent.`
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
async function defaultProbeVersion(invocation) {
|
|
2199
|
+
try {
|
|
2200
|
+
return await new Promise((resolve, reject) => {
|
|
2201
|
+
(0, import_node_child_process2.execFile)(
|
|
2202
|
+
invocation.command,
|
|
2203
|
+
invocation.args,
|
|
2204
|
+
{ encoding: "utf8", timeout: PROBE_VERSION_TIMEOUT_MS, env: (0, import_subagent_engine_sdk17.buildOutboundChildEnv)({ parentEnv: process.env }) },
|
|
2205
|
+
(err, stdout) => {
|
|
2206
|
+
if (err) reject(err);
|
|
2207
|
+
else resolve(stdout.trim().split("\n")[0]?.trim() || void 0);
|
|
2208
|
+
}
|
|
2209
|
+
);
|
|
2210
|
+
});
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
logger8.debug(
|
|
2213
|
+
`[pi-engine] probe version check failed (best-effort continue): ${(0, import_subagent_engine_sdk.toErrorMessage)(err)}`
|
|
2214
|
+
);
|
|
2215
|
+
return void 0;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
function isInvocationResolvable(invocation) {
|
|
2219
|
+
if (invocation.command !== "pi") return true;
|
|
2220
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2221
|
+
for (const dir of pathEnv.split(path4.delimiter)) {
|
|
2222
|
+
if (dir === "") continue;
|
|
2223
|
+
try {
|
|
2224
|
+
if (fs5.existsSync(path4.join(dir, "pi"))) return true;
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
logger8.debug(
|
|
2227
|
+
`[pi-engine] PATH dir probe failed (continue scanning): ${dir}: ${(0, import_subagent_engine_sdk.toErrorMessage)(err)}`
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/port-types.ts
|
|
2235
|
+
var import_subagent_engine_sdk19 = require("@zhushanwen/subagent-engine-sdk");
|
|
2236
|
+
|
|
2237
|
+
// src/server.ts
|
|
2238
|
+
var REVERSE_TIMEOUT_DEFAULT_MS = 6e4;
|
|
2239
|
+
function createDefaultPiEngine() {
|
|
2240
|
+
return new PiEngine();
|
|
2241
|
+
}
|
|
2242
|
+
var EngineProtocolServer = class {
|
|
2243
|
+
write;
|
|
2244
|
+
engine;
|
|
2245
|
+
reverseClock;
|
|
2246
|
+
reverseTimeoutMs;
|
|
2247
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
2248
|
+
reversePending = /* @__PURE__ */ new Map();
|
|
2249
|
+
/** 10 正向方法 → EnginePort 装配表(构造期冻结;表驱动分发)。 */
|
|
2250
|
+
dispatchTable;
|
|
2251
|
+
revSeq = 0;
|
|
2252
|
+
initialized = false;
|
|
2253
|
+
constructor(opts) {
|
|
2254
|
+
this.write = opts.write;
|
|
2255
|
+
this.engine = opts.engine ?? createDefaultPiEngine();
|
|
2256
|
+
this.reverseClock = opts.reverseClock;
|
|
2257
|
+
this.reverseTimeoutMs = opts.reverseTimeoutMs ?? REVERSE_TIMEOUT_DEFAULT_MS;
|
|
2258
|
+
this.dispatchTable = this.buildDispatchTable();
|
|
2259
|
+
this.engine.bindHostChannels?.({
|
|
2260
|
+
streamDelta: (p) => {
|
|
2261
|
+
void this.reverseRequestInternal("host/streamDelta", p);
|
|
2262
|
+
},
|
|
2263
|
+
roundLifecycle: (p) => {
|
|
2264
|
+
void this.reverseRequestInternal("host/roundLifecycle", p);
|
|
2265
|
+
},
|
|
2266
|
+
askUser: (runId, request) => this.reverseRequestInternal("host/askUser", { runId, request })
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
/** 入站帧消费(请求帧 + 反向请求应答帧;main.ts 的行解析器拆行后喂入)。 */
|
|
2270
|
+
handleFrame(frame) {
|
|
2271
|
+
if ((0, import_subagent_engine_sdk20.isResponseFrame)(frame)) {
|
|
2272
|
+
this.settleReverse(frame.id, frame);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if ((0, import_subagent_engine_sdk20.isReverseRequestFrame)(frame)) {
|
|
2276
|
+
this.write({
|
|
2277
|
+
id: 0,
|
|
2278
|
+
error: {
|
|
2279
|
+
code: "engine_protocol_bad_frame",
|
|
2280
|
+
message: `unexpected reverse request frame from host: ${String(frame.method)}`,
|
|
2281
|
+
recovery: "The engine protocol v1 only carries host/* requests engine\u2192host."
|
|
2282
|
+
}
|
|
2283
|
+
});
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (typeof frame === "object" && frame !== null && "id" in frame && "method" in frame && typeof frame.method === "string") {
|
|
2287
|
+
const { id, method, params } = frame;
|
|
2288
|
+
if (typeof id === "number") {
|
|
2289
|
+
void this.dispatch(id, method, params).then(
|
|
2290
|
+
(result) => this.write({ id, result }),
|
|
2291
|
+
(err) => this.write({ id, error: toProtocolError(err) })
|
|
2292
|
+
);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/** 10 正向方法 → EnginePort 装配表(协议载荷 cast 收敛在各方法适配行)。 */
|
|
2298
|
+
buildDispatchTable() {
|
|
2299
|
+
return {
|
|
2300
|
+
initialize: (params) => this.initialize(params),
|
|
2301
|
+
probe: (params) => this.engine.probe(typeof params === "object" && params !== null ? params : void 0),
|
|
2302
|
+
run: (params) => this.run(params),
|
|
2303
|
+
cancel: (params) => this.cancel(params),
|
|
2304
|
+
interact: (params) => this.interact(params),
|
|
2305
|
+
read: (params) => this.read(params),
|
|
2306
|
+
listModels: () => ({ models: this.engine.listModels?.() ?? null }),
|
|
2307
|
+
validateModel: (params) => this.validateModel(params),
|
|
2308
|
+
dispose: async () => {
|
|
2309
|
+
await this.engine.dispose?.();
|
|
2310
|
+
return { ok: true };
|
|
2311
|
+
},
|
|
2312
|
+
ping: () => ({ pong: true })
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
/** 10 正向方法分发(表驱动;未知方法 → engine_protocol_unknown_method)。 */
|
|
2316
|
+
async dispatch(id, method, params) {
|
|
2317
|
+
const handler = this.dispatchTable[method];
|
|
2318
|
+
if (handler === void 0) {
|
|
2319
|
+
throw new import_subagent_engine_sdk20.EngineSdkError(
|
|
2320
|
+
"engine_protocol_unknown_method",
|
|
2321
|
+
`unknown protocol method: ${method} (request id ${id})`,
|
|
2322
|
+
"The engine speaks protocol v1; check the installed engine package version vs the host."
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
return handler(params);
|
|
2326
|
+
}
|
|
2327
|
+
// ── initialize:版本协商(越界 → engine_protocol_mismatch)+ 能力应答 ──
|
|
2328
|
+
initialize(params) {
|
|
2329
|
+
if (params?.protocolVersion !== import_subagent_engine_sdk20.ENGINE_PROTOCOL_VERSION) {
|
|
2330
|
+
throw new import_subagent_engine_sdk20.EngineSdkError(
|
|
2331
|
+
"engine_protocol_mismatch",
|
|
2332
|
+
`host protocol version ${String(params?.protocolVersion)} is not compatible with engine protocol v${import_subagent_engine_sdk20.ENGINE_PROTOCOL_VERSION}`,
|
|
2333
|
+
`Upgrade the engine package or the host so both speak protocol v${import_subagent_engine_sdk20.ENGINE_PROTOCOL_VERSION}.`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
this.initialized = true;
|
|
2337
|
+
const models = this.engine.listModels?.() ?? null;
|
|
2338
|
+
return {
|
|
2339
|
+
protocolVersion: import_subagent_engine_sdk20.ENGINE_PROTOCOL_VERSION,
|
|
2340
|
+
engineId: this.engine.id,
|
|
2341
|
+
engineVersion: PI_ADAPTER_VERSION,
|
|
2342
|
+
adapterVersion: PI_ADAPTER_VERSION,
|
|
2343
|
+
capabilities: this.engine.capabilities(),
|
|
2344
|
+
...models !== null ? { models: models.map((m) => ({ id: m.id })) } : {}
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
// ── run:协议载荷 → 本地 AgentCallOpts/RunContext;事件 → 通知/host 通道 ──
|
|
2348
|
+
async run(params) {
|
|
2349
|
+
if (!this.initialized) {
|
|
2350
|
+
throw new import_subagent_engine_sdk20.EngineSdkError(
|
|
2351
|
+
"engine_protocol_not_initialized",
|
|
2352
|
+
"run before initialize is a protocol violation",
|
|
2353
|
+
"The host must complete the initialize handshake before dispatching runs."
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
if (params.chat !== void 0) this.assertChatRunFrame(params.chat);
|
|
2357
|
+
const { runId, task, ctx } = params;
|
|
2358
|
+
const controller = new AbortController();
|
|
2359
|
+
this.activeRuns.set(runId, { controller, seq: 0 });
|
|
2360
|
+
const isChatRun = params.chat !== void 0;
|
|
2361
|
+
if (!isChatRun) {
|
|
2362
|
+
this.engine.bindAskUser?.(
|
|
2363
|
+
(request) => this.reverseRequestInternal("host/askUser", { runId, request })
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
const fullTask = { ...task, ...ctx.model !== void 0 ? { model: ctx.model } : {} };
|
|
2367
|
+
try {
|
|
2368
|
+
const r = await this.engine.run(
|
|
2369
|
+
fullTask,
|
|
2370
|
+
this.buildRunContext(params, controller, params.chat?.recordId)
|
|
2371
|
+
);
|
|
2372
|
+
return { handle: r.handle.data, outcome: r.outcome };
|
|
2373
|
+
} finally {
|
|
2374
|
+
this.activeRuns.delete(runId);
|
|
2375
|
+
if (!isChatRun) this.engine.bindAskUser?.(void 0);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/** run.chat 帧校验 + chat 能力位 gate(A6 方向防御):recordId 非空 + conversation
|
|
2379
|
+
* 位 unsupported 同步拒——判据单源 = SDK assertChatConversationSupported(与 core
|
|
2380
|
+
* capability-gate 同一能力位,防两侧判据漂移)。本引擎 manifest 声明 native,
|
|
2381
|
+
* 此处仅防御 manifest/实装漂移。 */
|
|
2382
|
+
assertChatRunFrame(chat) {
|
|
2383
|
+
if (typeof chat.recordId !== "string" || chat.recordId === "") {
|
|
2384
|
+
throw new import_subagent_engine_sdk20.EngineSdkError(
|
|
2385
|
+
"engine_protocol_bad_frame",
|
|
2386
|
+
`run.chat requires a non-empty recordId (got: ${JSON.stringify(chat.recordId)})`,
|
|
2387
|
+
"The host must mint a record id before dispatching a chat-form run; it keys interact routing and roundLifecycle association."
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
(0, import_subagent_engine_sdk20.assertChatConversationSupported)(this.engine.id, this.engine.capabilities());
|
|
2391
|
+
}
|
|
2392
|
+
/** RunContext 装配(协议 ctx 还原 + host/* 反向通道接线;事件 seq 由 emitEvent 计数)。 */
|
|
2393
|
+
buildRunContext(params, controller, chatRecordId) {
|
|
2394
|
+
const { runId, ctx } = params;
|
|
2395
|
+
const ctxModel = (0, import_subagent_engine_sdk19.parseCtxModel)(ctx.ctxModel);
|
|
2396
|
+
const stream = ctx.streamMode === "stream" ? { onDelta: (delta) => {
|
|
2397
|
+
void this.reverseRequestInternal("host/streamDelta", { runId, delta });
|
|
2398
|
+
} } : void 0;
|
|
2399
|
+
return {
|
|
2400
|
+
taskId: runId,
|
|
2401
|
+
poolKey: ctx.poolKey,
|
|
2402
|
+
signal: controller.signal,
|
|
2403
|
+
onEvent: (event) => this.emitEvent(runId, event),
|
|
2404
|
+
...ctxModel !== void 0 ? { ctxModel } : {},
|
|
2405
|
+
...stream !== void 0 ? { stream } : {},
|
|
2406
|
+
...ctx.schemaEnv !== void 0 ? { schemaEnv: ctx.schemaEnv } : {},
|
|
2407
|
+
...ctx.engineFallback !== void 0 ? { engineFallback: ctx.engineFallback } : {},
|
|
2408
|
+
...params.chat !== void 0 ? { chat: params.chat } : {},
|
|
2409
|
+
onPoolResolved: (poolKey) => {
|
|
2410
|
+
void this.reverseRequestInternal("host/poolResolved", { runId, poolKey });
|
|
2411
|
+
},
|
|
2412
|
+
onHandleReady: (partial) => {
|
|
2413
|
+
void this.reverseRequestInternal("host/handleReady", { runId, sessionRef: partial.sessionRef, poolKey: partial.poolKey });
|
|
2414
|
+
},
|
|
2415
|
+
onChildSpawned: (child) => {
|
|
2416
|
+
if (child.pid === void 0) return;
|
|
2417
|
+
void this.reverseRequestInternal("host/childSpawned", { pid: child.pid, recordId: chatRecordId ?? runId });
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
cancel(params) {
|
|
2422
|
+
const active = this.activeRuns.get(params.runId);
|
|
2423
|
+
if (active !== void 0) active.controller.abort(new Error(`cancelled by host: ${params.reason}`));
|
|
2424
|
+
return { ok: true };
|
|
2425
|
+
}
|
|
2426
|
+
async interact(params) {
|
|
2427
|
+
return this.engine.interact({ data: params.handle }, params.action);
|
|
2428
|
+
}
|
|
2429
|
+
read(params) {
|
|
2430
|
+
return this.engine.read({ data: params.handle });
|
|
2431
|
+
}
|
|
2432
|
+
validateModel(params) {
|
|
2433
|
+
if (this.engine.validateModel === void 0) {
|
|
2434
|
+
throw new import_subagent_engine_sdk20.EngineSdkError(
|
|
2435
|
+
"engine_capability_unsupported",
|
|
2436
|
+
"engine does not implement validateModel",
|
|
2437
|
+
"Manifest omits modelCatalog; model validation falls back to run-time engine checks."
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
return this.engine.validateModel(params.modelRef);
|
|
2441
|
+
}
|
|
2442
|
+
// ── 出站:事件通知 + 反向请求客户端 ──
|
|
2443
|
+
emitEvent(runId, event) {
|
|
2444
|
+
const active = this.activeRuns.get(runId);
|
|
2445
|
+
const seq = active !== void 0 ? ++active.seq : 0;
|
|
2446
|
+
this.write({ method: "event", params: { runId, seq, event } });
|
|
2447
|
+
}
|
|
2448
|
+
/** 反向请求发送(公开面:main.ts 的 host/log 桥接消费;内部 run 通道同路)。 */
|
|
2449
|
+
reverseRequest(method, params) {
|
|
2450
|
+
return this.reverseRequestInternal(method, params);
|
|
2451
|
+
}
|
|
2452
|
+
reverseRequestInternal(method, params) {
|
|
2453
|
+
const id = `rev-${++this.revSeq}`;
|
|
2454
|
+
return new Promise((resolve, reject) => {
|
|
2455
|
+
const timer = setTimeout(() => {
|
|
2456
|
+
this.reversePending.delete(id);
|
|
2457
|
+
this.reverseClock?.settled(id);
|
|
2458
|
+
reject(new Error(`reverse request ${method} (${id}) timed out after ${this.reverseTimeoutMs}ms`));
|
|
2459
|
+
}, this.reverseTimeoutMs);
|
|
2460
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
2461
|
+
this.reversePending.set(id, { resolve, reject, timer });
|
|
2462
|
+
this.reverseClock?.started(id);
|
|
2463
|
+
this.write({ id, method, params });
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
/** 反向请求应答落位。
|
|
2467
|
+
*
|
|
2468
|
+
* ack 两阶段(R9-2):人机交互通道(host/askUser / host/permission)宿主先回
|
|
2469
|
+
* `{ack:true}`——只 ack 计时面(移出 in-flight 自灭计时),**不终结等待**;最终
|
|
2470
|
+
* 结果帧才 settle。数据面通道宿主直接回终态({ok:true} 等),ack 即 settle。 */
|
|
2471
|
+
settleReverse(id, frame) {
|
|
2472
|
+
const pending = this.reversePending.get(String(id));
|
|
2473
|
+
if (pending === void 0) return;
|
|
2474
|
+
this.reverseClock?.acked(String(id));
|
|
2475
|
+
if (frame.error === void 0 && typeof frame.result === "object" && frame.result !== null && "ack" in frame.result && frame.result.ack === true) {
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
this.reversePending.delete(String(id));
|
|
2479
|
+
clearTimeout(pending.timer);
|
|
2480
|
+
this.reverseClock?.settled(String(id));
|
|
2481
|
+
if (frame.error !== void 0) pending.reject(new Error(`reverse request ${String(id)} rejected: ${(0, import_subagent_engine_sdk.toErrorMessage)(frame.error)}`));
|
|
2482
|
+
else pending.resolve(frame.result);
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
function toProtocolError(err) {
|
|
2486
|
+
if (err instanceof import_subagent_engine_sdk20.EngineSdkError) return err.toStructured();
|
|
2487
|
+
return {
|
|
2488
|
+
code: "engine_run_failed",
|
|
2489
|
+
message: (0, import_subagent_engine_sdk.toErrorMessage)(err),
|
|
2490
|
+
recovery: "Check the engine process logs (host/log stream + stderr) and rerun; if persistent, reinstall or upgrade the engine package."
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2494
|
+
0 && (module.exports = {
|
|
2495
|
+
ChatSessionRegistry,
|
|
2496
|
+
DETERMINISTIC_SCHEMA_FAILURE_PREFIX,
|
|
2497
|
+
EPIPE_FAILURE_THRESHOLD,
|
|
2498
|
+
EngineProtocolServer,
|
|
2499
|
+
PI_ADAPTER_VERSION,
|
|
2500
|
+
PI_ENGINE_ID,
|
|
2501
|
+
PI_POOL_KEY,
|
|
2502
|
+
PiEngine,
|
|
2503
|
+
STALE_CONTEXT_PATTERNS,
|
|
2504
|
+
WRAP_UP_HINT,
|
|
2505
|
+
applySchemaEnvToChildEnv,
|
|
2506
|
+
buildEnvBlock,
|
|
2507
|
+
buildSpawnArgs,
|
|
2508
|
+
classifyFailureKind,
|
|
2509
|
+
clearEpipeFailure,
|
|
2510
|
+
collectOutcome,
|
|
2511
|
+
createDefaultPiEngine,
|
|
2512
|
+
createTurnLimiter,
|
|
2513
|
+
createUiRequestQueue,
|
|
2514
|
+
deriveSessionFilePath,
|
|
2515
|
+
describeMissingParsedOutput,
|
|
2516
|
+
extractParsedOutput,
|
|
2517
|
+
findSessionFileByHeaderId,
|
|
2518
|
+
getActiveChild,
|
|
2519
|
+
getPiInvocation,
|
|
2520
|
+
isDeterministicSchemaFailureMsg,
|
|
2521
|
+
isStaleContextErrorMsg,
|
|
2522
|
+
killAllActiveChildren,
|
|
2523
|
+
mapAssistantMessageDelta,
|
|
2524
|
+
mirrorMainProcessFlags,
|
|
2525
|
+
neutralizeStalePatterns,
|
|
2526
|
+
parseChannel,
|
|
2527
|
+
parseSpawnLine,
|
|
2528
|
+
parseSpawnModelRef,
|
|
2529
|
+
performGetStateHandshake,
|
|
2530
|
+
recordEpipeFailure,
|
|
2531
|
+
replayJournalToSessionView,
|
|
2532
|
+
requestGetStateOnce,
|
|
2533
|
+
resetAllEpipeFailures,
|
|
2534
|
+
respond,
|
|
2535
|
+
runSpawnOnce,
|
|
2536
|
+
sendGetStateCommand,
|
|
2537
|
+
sendPromptCommand
|
|
2538
|
+
});
|