@zhushanwen/zcode-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/zcode-subagent-cli.mjs +41 -0
- package/dist/chunk-IIAABOER.js +2708 -0
- package/dist/index.cjs +2740 -0
- package/dist/index.d.cts +362 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.js +45 -0
- package/dist/main.cjs +2700 -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/main.cjs
ADDED
|
@@ -0,0 +1,2700 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/main.ts
|
|
26
|
+
var import_subagent_engine_sdk13 = require("@zhushanwen/subagent-engine-sdk");
|
|
27
|
+
|
|
28
|
+
// src/server.ts
|
|
29
|
+
var import_subagent_engine_sdk12 = require("@zhushanwen/subagent-engine-sdk");
|
|
30
|
+
|
|
31
|
+
// src/constants.ts
|
|
32
|
+
var ZCODE_ENGINE_ID = "zcode";
|
|
33
|
+
var ZCODE_ADAPTER_VERSION = "2.0.0";
|
|
34
|
+
var ZCODE_CLI_DEFAULT_PATH = "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
35
|
+
var ZCODE_V2_CONFIG_PATH_SUFFIX = [".zcode", "v2", "config.json"];
|
|
36
|
+
var ZCODE_FALLBACK_DEFAULT_MODEL = "builtin:bigmodel-coding-plan/GLM-5.3";
|
|
37
|
+
var ZCODE_HOST_DB_SUFFIX = [".zcode", "cli", "db", "db.sqlite"];
|
|
38
|
+
var ZCODE_KILL_GRACE_MS = 5e3;
|
|
39
|
+
var ZCODE_ERROR_TAIL_CHARS = 2e3;
|
|
40
|
+
var ZCODE_APPSERVER_REQUEST_TIMEOUT_MS = 15e3;
|
|
41
|
+
var ZCODE_APPSERVER_STDERR_TAIL_CHARS = 400;
|
|
42
|
+
var ZCODE_APPSERVER_STDERR_TAIL_BUFFER_CHARS = 2048;
|
|
43
|
+
var ZCODE_APPSERVER_TURN_READ_TIMEOUT_MS = 5e3;
|
|
44
|
+
var ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS = 1500;
|
|
45
|
+
var ZCODE_TURN_IDLE_TIMEOUT_ENV = "XYZ_ZCODE_TURN_IDLE_TIMEOUT_MS";
|
|
46
|
+
var ZCODE_TURN_MAX_TIMEOUT_ENV = "XYZ_ZCODE_TURN_MAX_TIMEOUT_MS";
|
|
47
|
+
var ZCODE_TURN_IDLE_TIMEOUT_MS = 18e5;
|
|
48
|
+
var ZCODE_TURN_MAX_TIMEOUT_MS = 36e5;
|
|
49
|
+
function parseZcodeTurnTimeoutEnv(raw) {
|
|
50
|
+
if (raw === void 0 || raw === "") return { state: "unset" };
|
|
51
|
+
const parsed = Number(raw);
|
|
52
|
+
if (!Number.isFinite(parsed)) return { state: "invalid" };
|
|
53
|
+
return { state: "valid", ms: parsed };
|
|
54
|
+
}
|
|
55
|
+
var ZCODE_SHARED_POOL_KEY = "shared";
|
|
56
|
+
var ZCODE_APPSERVER_STOP_TIMEOUT_MS = 3e3;
|
|
57
|
+
var ZCODE_APPSERVER_ABORT_GRACE_MS = 3e3;
|
|
58
|
+
var ZCODE_APPSERVER_ERR_MODEL_CONFIG_MISSING = -32603;
|
|
59
|
+
var ZCODE_APPSERVER_ERR_BUSY_SESSION = -32010;
|
|
60
|
+
var ZCODE_APPSERVER_HARVEST_GRACE_MS = 1e3;
|
|
61
|
+
|
|
62
|
+
// src/registration.ts
|
|
63
|
+
var import_subagent_engine_sdk10 = require("@zhushanwen/subagent-engine-sdk");
|
|
64
|
+
|
|
65
|
+
// src/zcode-engine.ts
|
|
66
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
67
|
+
var path4 = __toESM(require("path"), 1);
|
|
68
|
+
var import_subagent_engine_sdk8 = require("@zhushanwen/subagent-engine-sdk");
|
|
69
|
+
var import_subagent_engine_sdk9 = require("@zhushanwen/subagent-engine-sdk");
|
|
70
|
+
|
|
71
|
+
// src/journal-io.ts
|
|
72
|
+
var import_subagent_engine_sdk = require("@zhushanwen/subagent-engine-sdk");
|
|
73
|
+
var import_subagent_engine_sdk2 = require("@zhushanwen/subagent-engine-sdk");
|
|
74
|
+
function replayJournalToSessionView(handle, engineId) {
|
|
75
|
+
const journalPath = handle.data.journalPath;
|
|
76
|
+
if (journalPath === void 0) return void 0;
|
|
77
|
+
const events = (0, import_subagent_engine_sdk2.replayJournal)(journalPath);
|
|
78
|
+
if (events.length === 0) return void 0;
|
|
79
|
+
return (0, import_subagent_engine_sdk2.eventsToSessionView)(events, engineId, (0, import_subagent_engine_sdk2.sessionIdFromHandle)(handle.data));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/db-path.ts
|
|
83
|
+
var os = __toESM(require("os"), 1);
|
|
84
|
+
var path = __toESM(require("path"), 1);
|
|
85
|
+
function zcodeSessionDbPath(engineDataDir) {
|
|
86
|
+
return path.join(engineDataDir, "engines", "zcode", "session-db", "db.sqlite");
|
|
87
|
+
}
|
|
88
|
+
function hostZcodeDbPath() {
|
|
89
|
+
return path.join(os.homedir(), ...ZCODE_HOST_DB_SUFFIX);
|
|
90
|
+
}
|
|
91
|
+
function zcodeDbPathAllowlist(dataDir) {
|
|
92
|
+
return [zcodeSessionDbPath(dataDir), hostZcodeDbPath()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/parser.ts
|
|
96
|
+
function finiteOr(v, fallback) {
|
|
97
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
98
|
+
return Number.isFinite(n) ? n : fallback;
|
|
99
|
+
}
|
|
100
|
+
function mapZcodeUsage(raw) {
|
|
101
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
102
|
+
const r = raw;
|
|
103
|
+
if (r.inputTokens === void 0 && r.outputTokens === void 0) return void 0;
|
|
104
|
+
return {
|
|
105
|
+
input: finiteOr(r.inputTokens, 0),
|
|
106
|
+
output: finiteOr(r.outputTokens, 0),
|
|
107
|
+
cacheRead: finiteOr(r.cacheReadTokens, 0),
|
|
108
|
+
cacheWrite: finiteOr(r.cacheWriteTokens, 0)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function mapZcodeOutcomeUsage(rawUsage, projection) {
|
|
112
|
+
const base = mapZcodeUsage(rawUsage);
|
|
113
|
+
if (base === void 0) return void 0;
|
|
114
|
+
const p = typeof projection === "object" && projection !== null ? projection : {};
|
|
115
|
+
const r = typeof rawUsage === "object" && rawUsage !== null ? rawUsage : {};
|
|
116
|
+
const contextTokens = firstFinite(p["contextUsed"], r.totalTokens, 0);
|
|
117
|
+
const turns = firstFinite(p["turnCount"], 1);
|
|
118
|
+
return { ...base, cost: 0, contextTokens, turns };
|
|
119
|
+
}
|
|
120
|
+
function firstFinite(...vals) {
|
|
121
|
+
for (const v of vals) {
|
|
122
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
123
|
+
if (Number.isFinite(n)) return n;
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function synthesizeCoarseEvents(response, usage) {
|
|
128
|
+
return [
|
|
129
|
+
// usage 给不出完整形状时显式缺省整个字段,不给残缺对象(不变量 2)
|
|
130
|
+
{ type: "message_end", ...usage !== void 0 ? { usage } : {} },
|
|
131
|
+
{ type: "turn_end" }
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/preparer.ts
|
|
136
|
+
var fs = __toESM(require("fs"), 1);
|
|
137
|
+
var os2 = __toESM(require("os"), 1);
|
|
138
|
+
var path2 = __toESM(require("path"), 1);
|
|
139
|
+
var ZcodePrepareError = class extends Error {
|
|
140
|
+
code;
|
|
141
|
+
constructor(code, message) {
|
|
142
|
+
super(`[${code}] ${message}`);
|
|
143
|
+
this.name = "ZcodePrepareError";
|
|
144
|
+
this.code = code;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function isRecord(v) {
|
|
148
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
149
|
+
}
|
|
150
|
+
function isProviderEntry(v) {
|
|
151
|
+
return isRecord(v);
|
|
152
|
+
}
|
|
153
|
+
function readSourceConfig(absPath) {
|
|
154
|
+
const empty = { providers: /* @__PURE__ */ new Map() };
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
158
|
+
} catch {
|
|
159
|
+
return empty;
|
|
160
|
+
}
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(raw);
|
|
164
|
+
} catch {
|
|
165
|
+
return empty;
|
|
166
|
+
}
|
|
167
|
+
if (!isRecord(parsed)) return empty;
|
|
168
|
+
const conf = { providers: /* @__PURE__ */ new Map() };
|
|
169
|
+
const provider = parsed["provider"];
|
|
170
|
+
if (isRecord(provider)) {
|
|
171
|
+
for (const [id, entry] of Object.entries(provider)) {
|
|
172
|
+
if (isProviderEntry(entry)) conf.providers.set(id, entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return conf;
|
|
176
|
+
}
|
|
177
|
+
function modelShort(ref) {
|
|
178
|
+
return ref.slice(ref.lastIndexOf("/") + 1);
|
|
179
|
+
}
|
|
180
|
+
function providerOf(ref) {
|
|
181
|
+
return ref.slice(0, ref.lastIndexOf("/"));
|
|
182
|
+
}
|
|
183
|
+
function splitZcodeModelRef(modelRef) {
|
|
184
|
+
return { providerId: providerOf(modelRef), modelId: modelShort(modelRef) };
|
|
185
|
+
}
|
|
186
|
+
function hasApiKey(entry) {
|
|
187
|
+
const key = entry.options?.apiKey;
|
|
188
|
+
return typeof key === "string" && key !== "";
|
|
189
|
+
}
|
|
190
|
+
function defaultV2ConfigPath() {
|
|
191
|
+
return path2.join(os2.homedir(), ...ZCODE_V2_CONFIG_PATH_SUFFIX);
|
|
192
|
+
}
|
|
193
|
+
var DEFAULT_PROVIDER_ID = "builtin:bigmodel-coding-plan";
|
|
194
|
+
function defaultProviderForShortName(merged, withKey) {
|
|
195
|
+
if (merged.has(DEFAULT_PROVIDER_ID)) return DEFAULT_PROVIDER_ID;
|
|
196
|
+
return withKey[0][0];
|
|
197
|
+
}
|
|
198
|
+
function resolveZcodeModelRef(requested, sources) {
|
|
199
|
+
const v2 = readSourceConfig(sources?.v2ConfigPath ?? defaultV2ConfigPath());
|
|
200
|
+
const merged = v2.providers;
|
|
201
|
+
const withKey = [...merged.entries()].filter(([, e]) => hasApiKey(e));
|
|
202
|
+
const wanted = requested?.trim() || void 0;
|
|
203
|
+
const target = wanted ?? ZCODE_FALLBACK_DEFAULT_MODEL;
|
|
204
|
+
if (withKey.length === 0) {
|
|
205
|
+
const hint = wanted ? `model="${wanted}" \u4E0D\u80FD\u6821\u9A8C` : `\u9ED8\u8BA4\u6A21\u578B "${target}" \u4E0D\u80FD\u6821\u9A8C`;
|
|
206
|
+
throw new ZcodePrepareError(
|
|
207
|
+
"engine_credential_missing",
|
|
208
|
+
`zcode \u5F15\u64CE\u627E\u4E0D\u5230\u4EFB\u4F55\u5E26 apiKey \u7684 provider\uFF08${hint}\uFF09\u3002\u5DF2\u8BFB\u6E90\uFF1A${sources?.v2ConfigPath ?? defaultV2ConfigPath()}\u3002\u6062\u590D\u6307\u5F15\uFF1A\u5148\u5728 ZCode \u684C\u9762\u7AEF\u767B\u5F55\u5E76\u914D\u7F6E provider\uFF08v2 config \u5185\u5B58\u5728\u542B apiKey \u7684\u6761\u76EE\uFF09\u540E\u91CD\u8BD5\uFF1B\u51ED\u636E\u914D\u7F6E\u8BF4\u660E\u89C1 docs/research/agent-engine-zcode.md\u3002`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const provider = target.includes("/") ? providerOf(target) : defaultProviderForShortName(merged, withKey);
|
|
212
|
+
const short = modelShort(target);
|
|
213
|
+
const entry = merged.get(provider);
|
|
214
|
+
if (!entry) {
|
|
215
|
+
const known = withKey.map(([id]) => id).join(", ");
|
|
216
|
+
throw new ZcodePrepareError(
|
|
217
|
+
"model_not_available",
|
|
218
|
+
`\u672A\u77E5 provider "${provider}"\uFF08\u5E26\u51ED\u636E\u7684 provider: ${known}\uFF09\u3002\u6062\u590D\u6307\u5F15\uFF1A\u6539\u7528\u4E0A\u8FF0 provider \u4E4B\u4E00\uFF08\u5168\u540D provider/model\uFF09\uFF0C\u6216\u5148\u5728 ZCode \u684C\u9762\u7AEF\u914D\u7F6E\u8BE5 provider \u540E\u91CD\u8BD5\u3002`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!hasApiKey(entry)) {
|
|
222
|
+
throw new ZcodePrepareError(
|
|
223
|
+
"engine_credential_missing",
|
|
224
|
+
`provider "${provider}" \u5B58\u5728\u4F46\u672A\u914D\u7F6E apiKey\u3002\u6062\u590D\u6307\u5F15\uFF1A\u5728 ZCode \u684C\u9762\u7AEF\u4E3A\u8BE5 provider \u767B\u5F55\u914D\u7F6E\u51ED\u636E\uFF0C\u6216\u6539\u7528\u5DF2\u914D\u51ED\u636E\u7684 provider\uFF08${withKey.map(([id]) => id).join(", ")}\uFF09\u540E\u91CD\u8BD5\u3002`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const models = Object.keys(entry.models ?? {});
|
|
228
|
+
if (models.length > 0 && !models.includes(short)) {
|
|
229
|
+
throw new ZcodePrepareError(
|
|
230
|
+
"model_not_available",
|
|
231
|
+
`\u672A\u77E5\u6A21\u578B "${short}"\uFF08provider ${provider} \u4E0B\u53EF\u7528: ${models.join(", ")}\uFF09\u3002\u6062\u590D\u6307\u5F15\uFF1A\u6539\u7528\u8BE5 provider \u4E0B\u7684\u6A21\u578B\uFF0C\u6216\u5148\u5728 ZCode \u684C\u9762\u7AEF\u542F\u7528\u76EE\u6807\u6A21\u578B\u540E\u91CD\u8BD5\u3002`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return `${provider}/${short}`;
|
|
235
|
+
}
|
|
236
|
+
function listZcodeModels(sources) {
|
|
237
|
+
const v2 = readSourceConfig(sources?.v2ConfigPath ?? defaultV2ConfigPath());
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const [pid, entry] of v2.providers) {
|
|
240
|
+
if (!hasApiKey(entry)) continue;
|
|
241
|
+
const providerName = typeof entry["name"] === "string" && entry["name"].trim() !== "" ? entry["name"].trim() : void 0;
|
|
242
|
+
const models = Object.keys(entry.models ?? {});
|
|
243
|
+
for (const model of models) {
|
|
244
|
+
out.push({
|
|
245
|
+
id: `${pid}/${model}`,
|
|
246
|
+
...providerName !== void 0 ? { name: `${providerName} \xB7 ${model}` } : {}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/appserver-launcher.ts
|
|
254
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
255
|
+
var path3 = __toESM(require("path"), 1);
|
|
256
|
+
|
|
257
|
+
// src/error-message.ts
|
|
258
|
+
var import_subagent_engine_sdk3 = require("@zhushanwen/subagent-engine-sdk");
|
|
259
|
+
|
|
260
|
+
// src/appserver-launcher.ts
|
|
261
|
+
var ZCODE_APPSERVER_LAUNCHER_NAME = "appserver-launcher.cjs";
|
|
262
|
+
var APPSERVER_LAUNCHER_SOURCE = `'use strict';
|
|
263
|
+
// zcode app-server fs \u62E6\u622A wrapper\uFF08\u7531 zcode-subagent-cli appserver-launcher \u843D\u76D8\u751F\u6210\uFF09
|
|
264
|
+
const fs = require('node:fs');
|
|
265
|
+
const path = require('node:path');
|
|
266
|
+
const os = require('node:os');
|
|
267
|
+
|
|
268
|
+
const CLI_PATH = process.env.ZCODE_ENG_CLI_PATH;
|
|
269
|
+
const V2_PATH = process.env.ZCODE_ENG_V2_CONFIG || path.join(os.homedir(), '.zcode', 'v2', 'config.json');
|
|
270
|
+
const CONFIG_PATH = path.join(os.homedir(), '.zcode', 'cli', 'config.json');
|
|
271
|
+
// \u4E0E constants.ts ZCODE_FALLBACK_DEFAULT_MODEL \u53CC\u6E90\uFF08\u5185\u5D4C\u5B57\u7B26\u4E32\u65E0\u6CD5 import\uFF09\u2014\u2014\u6539\u90A3\u8FB9\u987B\u540C\u6B65\u6B64\u5904
|
|
272
|
+
const FALLBACK_MODEL_MAIN = 'builtin:bigmodel-coding-plan/GLM-5.3';
|
|
273
|
+
|
|
274
|
+
// \u5408\u5E76\u5F62\u6001\uFF1A\u771F\u5B9E cli config \u539F\u6837\uFF08model/plugins/mcp/subagents \u7B49 worker \u7EE7\u627F\u9762\u4E0D\u53D8\uFF09
|
|
275
|
+
// + v2 \u7684 provider \u5B57\u5178\u6CE8\u5165\uFF0C\u540C id \u65F6 v2 \u6761\u76EE\u6574\u6761\u4F18\u5148\u2014\u2014GUI \u76F4\u4F20 modelConfig \u65F6
|
|
276
|
+
// v2 \u5373\u6743\u5A01\u6E90\uFF08\u8FDB\u7A0B\u5916\u7B49\u4EF7\u590D\u523B\uFF09\uFF1Bcli config \u4E0D\u5728 GUI \u7BA1\u7406\u9762\u3001\u53EF\u80FD\u6B8B\u7559\u5386\u53F2\u9A8C\u8BC1
|
|
277
|
+
// \u914D\u7F6E\uFF082026-08-25 \u4E8B\u6545\uFF1A\u6B8B\u7559\u65E7 key \u538B\u8FC7\u65B0\u51ED\u636E\u81F4 turn 0 401\uFF09\uFF0C\u6545\u4E0D\u53D6 real \u4F18\u5148\u3002
|
|
278
|
+
// \u8FD4\u56DE null = \u65E0\u53EF\u6CE8\u5165\u51ED\u636E\uFF08v2 \u65E0 provider \u6761\u76EE\uFF09\u2014\u2014\u4E0D patch\uFF0C\u8BA9\u539F\u751F\u62A5\u9519\u900F\u51FA\u3002
|
|
279
|
+
// \u8BFB\u6E90\u7ECF readOrig \u95F4\u63A5\u53D6\uFF1Apatch \u524D\u9996\u8C03 = fs.readFileSync \u672C\u4F53\uFF1Bpatch \u540E = \u539F\u51FD\u6570
|
|
280
|
+
// \uFF08\u9632 patch \u540E\u7684\u9012\u5F52\u81EA\u8C03\u7528\uFF09\u3002
|
|
281
|
+
function injectedConfigText() {
|
|
282
|
+
const readOrig = fs.__origReadFileSync || fs.readFileSync;
|
|
283
|
+
let real = {};
|
|
284
|
+
try { real = JSON.parse(readOrig(CONFIG_PATH, 'utf8')); } catch { /* \u65E0/\u635F\u574F \u2192 \u7A7A */ }
|
|
285
|
+
let v2 = {};
|
|
286
|
+
try { v2 = JSON.parse(readOrig(V2_PATH, 'utf8')); }
|
|
287
|
+
catch (err) {
|
|
288
|
+
// ENOENT = \u684C\u9762\u7AEF\u672A\u767B\u5F55\uFF08\u6B63\u5E38 no-patch\uFF0C\u9759\u9ED8\uFF09\uFF1B\u5176\u4ED6\u5931\u8D25\uFF08JSON \u635F\u574F/\u6743\u9650\uFF09
|
|
289
|
+
// \u51FA\u58F0\u2014\u2014\u5426\u5219\u6392\u969C\u4F1A\u88AB\u539F\u751F\u300C\u8BF7\u767B\u5F55\u300D\u62A5\u9519\u8BEF\u5BFC\uFF0C\u4E0E\u771F\u56E0\uFF08\u6587\u4EF6\u635F\u574F\uFF09\u4E0D\u7B26
|
|
290
|
+
if (!err || err.code !== 'ENOENT') {
|
|
291
|
+
process.stderr.write('[zcode-launcher] v2 config \u8BFB\u53D6\u5931\u8D25(' + ((err && err.code) || (err && err.message) || 'unknown') + '): ' + V2_PATH + '\u2014\u2014\u51ED\u636E\u6CE8\u5165\u672A\u751F\u6548\\n');
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const v2Providers = v2.provider && typeof v2.provider === 'object' ? v2.provider : {};
|
|
296
|
+
const hasKey = (entry) => entry && typeof entry === 'object'
|
|
297
|
+
&& entry.options && typeof entry.options.apiKey === 'string' && entry.options.apiKey !== '';
|
|
298
|
+
const injectable = {};
|
|
299
|
+
for (const [id, entry] of Object.entries(v2Providers)) {
|
|
300
|
+
if (hasKey(entry)) injectable[id] = entry;
|
|
301
|
+
}
|
|
302
|
+
if (Object.keys(injectable).length === 0) return null;
|
|
303
|
+
const merged = { ...real };
|
|
304
|
+
merged.provider = { ...(real.provider && typeof real.provider === 'object' ? real.provider : {}), ...injectable };
|
|
305
|
+
if (!merged.model || typeof merged.model !== 'object' || !merged.model.main) {
|
|
306
|
+
const main = (v2.model && v2.model.main) || FALLBACK_MODEL_MAIN;
|
|
307
|
+
merged.model = { ...(merged.model || {}), main };
|
|
308
|
+
}
|
|
309
|
+
return JSON.stringify(merged);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!CLI_PATH) {
|
|
313
|
+
process.stderr.write('[zcode-launcher] ZCODE_ENG_CLI_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8 app-server\uFF08\u8BE5 env \u7531\u5F15\u64CE\u81EA\u52A8\u8BBE\u7F6E\uFF1B\u624B\u5DE5\u8FD0\u884C wrapper \u9700\u663E\u5F0F\u4F20\u5165\uFF09\\n');
|
|
314
|
+
process.exit(2);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const text = injectedConfigText();
|
|
318
|
+
if (text !== null) {
|
|
319
|
+
// encoding \u5F52\u4E00\uFF08Node \u522B\u540D\u9762\uFF09\uFF1A'utf8'/'utf-8'/\u5927\u5C0F\u5199\u53D8\u4F53\u7EDF\u4E00\u6309 string \u8FD4\u56DE\u2014\u2014
|
|
320
|
+
// \u771F\u5B9E bundle \u7684 config \u8BFB\u53D6\u6070\u7528 'utf-8' \u5F62\u6001\uFF0C\u4EC5\u5339\u914D 'utf8' \u4F1A\u9519\u56DE Buffer
|
|
321
|
+
// \uFF08\u5F53\u4E0B\u6E38\u4E0D\u53EA JSON.parse \u800C\u505A\u5B57\u7B26\u4E32\u8FD0\u7B97\u65F6\u5373 TypeError\uFF09
|
|
322
|
+
const shape = (args, value) => {
|
|
323
|
+
const enc = args[0];
|
|
324
|
+
const e = typeof enc === 'string' ? enc
|
|
325
|
+
: (enc && typeof enc === 'object' && typeof enc.encoding === 'string' ? enc.encoding : undefined);
|
|
326
|
+
if (e !== undefined && String(e).toLowerCase().replace(/-/g, '') === 'utf8') return value;
|
|
327
|
+
return Buffer.from(value);
|
|
328
|
+
};
|
|
329
|
+
fs.__origReadFileSync = fs.readFileSync;
|
|
330
|
+
fs.readFileSync = function (p, ...rest) {
|
|
331
|
+
if (typeof p === 'string' && p === CONFIG_PATH) {
|
|
332
|
+
const cur = injectedConfigText();
|
|
333
|
+
if (cur !== null) return shape(rest, cur);
|
|
334
|
+
}
|
|
335
|
+
return fs.__origReadFileSync.call(fs, p, ...rest);
|
|
336
|
+
};
|
|
337
|
+
fs.promises.__origReadFile = fs.promises.readFile;
|
|
338
|
+
fs.promises.readFile = function (p, ...rest) {
|
|
339
|
+
if (typeof p === 'string' && p === CONFIG_PATH) {
|
|
340
|
+
const cur = injectedConfigText();
|
|
341
|
+
if (cur !== null) return Promise.resolve(shape(rest, cur));
|
|
342
|
+
}
|
|
343
|
+
return fs.promises.__origReadFile.call(fs.promises, p, ...rest);
|
|
344
|
+
};
|
|
345
|
+
// existsSync \u62E6\u622A\uFF1A\u771F\u5B9E bundle \u7684\u7528\u6237 config \u8BFB\u53D6\uFF08loadFileConfig\uFF09\u4EE5 existsSync
|
|
346
|
+
// \u4E3A\u77ED\u8DEF\u95E8\u2014\u2014\u6587\u4EF6\u4E0D\u5B58\u5728\u65F6\u8FD4\u56DE\u7A7A config \u4E14 readFileSync \u4E0D\u88AB\u8C03\u7528\uFF0CGUI-only
|
|
347
|
+
// \u767B\u5F55\u5BBF\u4E3B\uFF08cli/config.json \u4ECE\u672A\u88AB CLI \u4FA7\u5199\u8FC7\uFF09\u4E0A\u6CE8\u5165\u53EA\u6302\u5728\u8BFB\u53D6\u9762 = \u4E0D\u53EF\u89C1
|
|
348
|
+
fs.__origExistsSync = fs.existsSync;
|
|
349
|
+
fs.existsSync = function (p) {
|
|
350
|
+
if (typeof p === 'string' && p === CONFIG_PATH) {
|
|
351
|
+
const cur = injectedConfigText();
|
|
352
|
+
if (cur !== null) return true;
|
|
353
|
+
}
|
|
354
|
+
return fs.__origExistsSync.call(fs, p);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// \u4EE5\u539F argv \u5F62\u6001\u542F\u52A8 zcode.cjs\uFF08argv[0]=node, argv[1]=\u672C wrapper\uFF0Cargv[2]=app-server\u2014\u2014
|
|
359
|
+
// zcode.cjs \u7684 includes('app-server') \u5224\u5B9A\u4E0D\u53D7\u5F71\u54CD\uFF09\u3002import() \u517C\u5BB9 CJS/ESM \u5165\u53E3\u3002
|
|
360
|
+
import(CLI_PATH).catch((err) => {
|
|
361
|
+
process.stderr.write('[zcode-launcher] zcode.cjs \u52A0\u8F7D\u5931\u8D25: ' + (err && err.message || err) + '\\n');
|
|
362
|
+
process.exit(3);
|
|
363
|
+
});
|
|
364
|
+
`;
|
|
365
|
+
function ensureAppServerLauncher(engineDataDir) {
|
|
366
|
+
const dir = path3.join(engineDataDir, "engines", "zcode");
|
|
367
|
+
const file = path3.join(dir, ZCODE_APPSERVER_LAUNCHER_NAME);
|
|
368
|
+
const existing = fs2.existsSync(file) && fs2.statSync(file).isFile() ? fs2.readFileSync(file, "utf8") : void 0;
|
|
369
|
+
if (existing === APPSERVER_LAUNCHER_SOURCE) return file;
|
|
370
|
+
try {
|
|
371
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
372
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
373
|
+
fs2.writeFileSync(tmp, APPSERVER_LAUNCHER_SOURCE);
|
|
374
|
+
fs2.renameSync(tmp, file);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
const reason = (0, import_subagent_engine_sdk3.toErrorMessage)(err);
|
|
377
|
+
throw new Error(
|
|
378
|
+
`zcode app-server launcher \u843D\u76D8\u5931\u8D25: ${file}\uFF08${reason}\uFF09\u3002\u6062\u590D\uFF1A\u68C0\u67E5\u5F15\u64CE\u6570\u636E\u76EE\u5F55 \u5199\u6743\u9650\u4E0E\u78C1\u76D8\u7A7A\u95F4\uFF08engineDataDir \u7531\u5BBF\u4E3B\u4F20\u5165\uFF0C\u8DEF\u5F84\u89C1\u4E0A\uFF09\u540E\u91CD\u8BD5\u3002`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return file;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/reader.ts
|
|
385
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
386
|
+
var ZcodeReaderError = class extends Error {
|
|
387
|
+
code;
|
|
388
|
+
/** 原始失败细节(缺文件/表漂移/运行时不支持)。 */
|
|
389
|
+
detail;
|
|
390
|
+
constructor(detail, hint) {
|
|
391
|
+
super(
|
|
392
|
+
`[engine_session_read_failed] zcode \u539F\u751F session \u8BFB\u53D6\u5931\u8D25\uFF1A${detail}\u3002` + (hint ?? "\u8C03\u7528\u65B9\u5E94\u964D\u7EA7\u5230\u7B2C\u2461\u7EA7\uFF08\u5BBF\u4E3B event journal\uFF09\u6216\u7B2C\u2462\u7EA7\uFF08outcome-only\uFF09\u3002")
|
|
393
|
+
);
|
|
394
|
+
this.name = "ZcodeReaderError";
|
|
395
|
+
this.code = "engine_session_read_failed";
|
|
396
|
+
this.detail = detail;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function parseJsonField(raw, ctx) {
|
|
400
|
+
try {
|
|
401
|
+
const v = JSON.parse(raw);
|
|
402
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw new ZcodeReaderError(
|
|
405
|
+
`${ctx} \u884C data \u4E0D\u662F\u5408\u6CD5 JSON\uFF08${err instanceof Error ? err.message : String(err)}\uFF09`,
|
|
406
|
+
"db \u5185\u5BB9\u635F\u574F\u6216\u7248\u672C\u4E0D\u517C\u5BB9\u2014\u2014\u8C03\u7528\u65B9\u5E94\u964D\u7EA7\u5230\u7B2C\u2461\u7EA7\uFF08\u5BBF\u4E3B event journal\uFF09\u3002"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function finiteOr2(v, fallback) {
|
|
411
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
412
|
+
return Number.isFinite(n) ? n : fallback;
|
|
413
|
+
}
|
|
414
|
+
function usageFromStepFinish(part) {
|
|
415
|
+
const tokens = part.tokens;
|
|
416
|
+
if (typeof tokens !== "object" || tokens === null) return void 0;
|
|
417
|
+
const t = tokens;
|
|
418
|
+
const cache = typeof t.cache === "object" && t.cache !== null ? t.cache : {};
|
|
419
|
+
if (t.input === void 0 && t.output === void 0) return void 0;
|
|
420
|
+
return {
|
|
421
|
+
input: finiteOr2(t.input, 0),
|
|
422
|
+
output: finiteOr2(t.output, 0),
|
|
423
|
+
cacheRead: finiteOr2(cache.read, 0),
|
|
424
|
+
cacheWrite: finiteOr2(cache.write, 0)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function toolFromPart(part) {
|
|
428
|
+
const toolName = typeof part.tool === "string" ? part.tool : "unknown";
|
|
429
|
+
let state;
|
|
430
|
+
if (typeof part.state === "string") {
|
|
431
|
+
try {
|
|
432
|
+
const v = JSON.parse(part.state);
|
|
433
|
+
state = typeof v === "object" && v !== null ? v : void 0;
|
|
434
|
+
} catch {
|
|
435
|
+
state = void 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const status = typeof state?.status === "string" ? state.status : "unknown";
|
|
439
|
+
const output = state?.output;
|
|
440
|
+
return {
|
|
441
|
+
toolName,
|
|
442
|
+
...state?.input !== void 0 ? { args: state.input } : {},
|
|
443
|
+
// 输出形态(实测):string(内容)或 object(结构化)——分别映射到 pi ToolCallResult
|
|
444
|
+
// 的 content[] / details,不发明第二形状
|
|
445
|
+
...typeof output === "string" ? { result: { content: [output] } } : {},
|
|
446
|
+
...typeof output === "object" && output !== null ? { result: { details: output } } : {},
|
|
447
|
+
...status !== "completed" ? { isError: true } : {}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
var TotalsAcc = class {
|
|
451
|
+
input = 0;
|
|
452
|
+
output = 0;
|
|
453
|
+
cacheRead = 0;
|
|
454
|
+
cacheWrite = 0;
|
|
455
|
+
cost = 0;
|
|
456
|
+
has = false;
|
|
457
|
+
add(u) {
|
|
458
|
+
this.has = true;
|
|
459
|
+
this.input += u.input;
|
|
460
|
+
this.output += u.output;
|
|
461
|
+
this.cacheRead += u.cacheRead;
|
|
462
|
+
this.cacheWrite += u.cacheWrite;
|
|
463
|
+
this.cost += u.cost ?? 0;
|
|
464
|
+
}
|
|
465
|
+
toTotal() {
|
|
466
|
+
if (!this.has) return void 0;
|
|
467
|
+
return {
|
|
468
|
+
input: this.input,
|
|
469
|
+
output: this.output,
|
|
470
|
+
cacheRead: this.cacheRead,
|
|
471
|
+
cacheWrite: this.cacheWrite,
|
|
472
|
+
cost: this.cost,
|
|
473
|
+
total: this.input + this.output + this.cacheRead + this.cacheWrite
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
function closeTurn(acc, turns, totals) {
|
|
478
|
+
if (acc === null) return;
|
|
479
|
+
turns.push({ text: acc.text, thinking: acc.thinking, toolCalls: acc.toolCalls, closed: true });
|
|
480
|
+
if (acc.usageDelta !== void 0) totals.add(acc.usageDelta);
|
|
481
|
+
}
|
|
482
|
+
function rowToMessageRow(v) {
|
|
483
|
+
if (typeof v === "object" && v !== null && "id" in v && "data" in v) {
|
|
484
|
+
const r = v;
|
|
485
|
+
if (typeof r.id === "string" && typeof r.data === "string") return { id: r.id, data: r.data };
|
|
486
|
+
}
|
|
487
|
+
throw new ZcodeReaderError("message \u884C\u5F62\u72B6\u5F02\u5E38\uFF08id/data \u5217\u7F3A\u5931\u6216\u7C7B\u578B\u6F02\u79FB\uFF09");
|
|
488
|
+
}
|
|
489
|
+
function rowToPartRow(v) {
|
|
490
|
+
if (typeof v === "object" && v !== null && "data" in v) {
|
|
491
|
+
const r = v;
|
|
492
|
+
if (typeof r.data === "string") return { data: r.data };
|
|
493
|
+
}
|
|
494
|
+
throw new ZcodeReaderError("part \u884C\u5F62\u72B6\u5F02\u5E38\uFF08data \u5217\u7F3A\u5931\u6216\u7C7B\u578B\u6F02\u79FB\uFF09");
|
|
495
|
+
}
|
|
496
|
+
function resolveSessionId(db, sessionId) {
|
|
497
|
+
if (sessionId !== void 0) {
|
|
498
|
+
const hit = db.prepare("SELECT id FROM session WHERE id = ?").get(sessionId);
|
|
499
|
+
if (hit === void 0) {
|
|
500
|
+
throw new ZcodeReaderError(
|
|
501
|
+
`session \u4E0D\u5B58\u5728\uFF1A${sessionId}`,
|
|
502
|
+
"sessionId \u53EF\u80FD\u6765\u81EA\u5DF2\u6E05\u7406\u7684\u6C60\u2014\u2014\u964D\u7EA7\u5230 outcome-only\u3002"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return sessionId;
|
|
506
|
+
}
|
|
507
|
+
const latest = db.prepare("SELECT id FROM session ORDER BY time_created DESC LIMIT 1").get();
|
|
508
|
+
if (latest === void 0 || typeof latest.id !== "string") {
|
|
509
|
+
throw new ZcodeReaderError("db \u5185\u65E0\u4EFB\u4F55 session \u884C");
|
|
510
|
+
}
|
|
511
|
+
return latest.id;
|
|
512
|
+
}
|
|
513
|
+
function loadGroupedParts(db, sessionId) {
|
|
514
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const raw of db.prepare(
|
|
516
|
+
"SELECT part.message_id AS mid, part.data AS data FROM part JOIN message ON part.message_id = message.id WHERE part.session_id = ? ORDER BY message.sequence, part.sequence"
|
|
517
|
+
).all(sessionId)) {
|
|
518
|
+
if (typeof raw !== "object" || raw === null || !("mid" in raw)) continue;
|
|
519
|
+
const mid = String(raw.mid);
|
|
520
|
+
const arr = grouped.get(mid) ?? [];
|
|
521
|
+
arr.push(rowToPartRow(raw));
|
|
522
|
+
grouped.set(mid, arr);
|
|
523
|
+
}
|
|
524
|
+
return grouped;
|
|
525
|
+
}
|
|
526
|
+
function appendPartText(current, part) {
|
|
527
|
+
return current === "" ? String(part.text ?? "") : current + "\n" + String(part.text ?? "");
|
|
528
|
+
}
|
|
529
|
+
function applyPartToTurn(part, acc, turns, totals) {
|
|
530
|
+
switch (part.type) {
|
|
531
|
+
case "text":
|
|
532
|
+
acc = acc ?? { text: "", thinking: "", toolCalls: [] };
|
|
533
|
+
acc.text = appendPartText(acc.text, part);
|
|
534
|
+
return acc;
|
|
535
|
+
case "reasoning":
|
|
536
|
+
acc = acc ?? { text: "", thinking: "", toolCalls: [] };
|
|
537
|
+
acc.thinking = appendPartText(acc.thinking, part);
|
|
538
|
+
return acc;
|
|
539
|
+
case "tool":
|
|
540
|
+
acc = acc ?? { text: "", thinking: "", toolCalls: [] };
|
|
541
|
+
acc.toolCalls.push(toolFromPart(part));
|
|
542
|
+
return acc;
|
|
543
|
+
case "step-finish": {
|
|
544
|
+
const u = usageFromStepFinish(part);
|
|
545
|
+
const cost = finiteOr2(part.cost, 0);
|
|
546
|
+
if (acc === null) acc = { text: "", thinking: "", toolCalls: [] };
|
|
547
|
+
if (u !== void 0) acc.usageDelta = cost > 0 ? { ...u, cost } : u;
|
|
548
|
+
closeTurn(acc, turns, totals);
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
default:
|
|
552
|
+
return acc;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function collectTurns(messages, grouped, totals) {
|
|
556
|
+
const turns = [];
|
|
557
|
+
for (const msg of messages) {
|
|
558
|
+
const parsedMsg = parseJsonField(msg.data, "message");
|
|
559
|
+
if (parsedMsg?.role !== "assistant") continue;
|
|
560
|
+
let acc = null;
|
|
561
|
+
for (const partRow of grouped.get(msg.id) ?? []) {
|
|
562
|
+
const part = parseJsonField(partRow.data, "part");
|
|
563
|
+
if (part === void 0) continue;
|
|
564
|
+
acc = applyPartToTurn(part, acc, turns, totals);
|
|
565
|
+
}
|
|
566
|
+
closeTurn(acc, turns, totals);
|
|
567
|
+
}
|
|
568
|
+
return turns;
|
|
569
|
+
}
|
|
570
|
+
function buildView(db, sessionId) {
|
|
571
|
+
const messages = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY sequence").all(sessionId).map(rowToMessageRow);
|
|
572
|
+
const totals = new TotalsAcc();
|
|
573
|
+
const turns = collectTurns(messages, loadGroupedParts(db, sessionId), totals);
|
|
574
|
+
const usageTotal = totals.toTotal();
|
|
575
|
+
return {
|
|
576
|
+
engineId: ZCODE_ENGINE_ID,
|
|
577
|
+
sessionId,
|
|
578
|
+
...usageTotal !== void 0 ? { usage: usageTotal } : {},
|
|
579
|
+
turns,
|
|
580
|
+
source: "native"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function readZcodeSessionView(dbPath, sessionId) {
|
|
584
|
+
if (!fs3.existsSync(dbPath)) {
|
|
585
|
+
throw new ZcodeReaderError(`db \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${dbPath}`);
|
|
586
|
+
}
|
|
587
|
+
const sqliteModuleId = "node:sqlite";
|
|
588
|
+
const sqliteMod = await import(sqliteModuleId).catch(() => void 0);
|
|
589
|
+
const DatabaseSyncCtor = sqliteMod?.DatabaseSync;
|
|
590
|
+
if (typeof DatabaseSyncCtor !== "function") {
|
|
591
|
+
throw new ZcodeReaderError(
|
|
592
|
+
"\u5F53\u524D node \u8FD0\u884C\u65F6\u4E0D\u652F\u6301 node:sqlite\uFF08\u9700 >=22.13\uFF09",
|
|
593
|
+
"\u5347\u7EA7 node \u6216\u6539\u7528\u7B2C\u2461\u7EA7\uFF08\u5BBF\u4E3B event journal\uFF09\u8BFB\u53D6\u3002"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const open = DatabaseSyncCtor;
|
|
597
|
+
let db;
|
|
598
|
+
try {
|
|
599
|
+
db = new open(dbPath, { readOnly: true });
|
|
600
|
+
return buildView(db, resolveSessionId(db, sessionId));
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (err instanceof ZcodeReaderError) throw err;
|
|
603
|
+
throw new ZcodeReaderError(
|
|
604
|
+
`sqlite \u67E5\u8BE2\u5931\u8D25\uFF08${err instanceof Error ? err.message : String(err)}\uFF09`,
|
|
605
|
+
"\u7591\u4F3C\u8868\u7ED3\u6784\u6F02\u79FB\uFF08zcode schema_migration \u5347\u7EA7\uFF09\u2014\u2014\u8C03\u7528\u65B9\u5E94\u964D\u7EA7\u5230\u7B2C\u2461\u7EA7\uFF1B\u5E76\u628A\u65B0 schema \u6837\u672C\u8865\u5F55\u8FDB golden \u5E93\u540E\u66F4\u65B0 reader\u3002"
|
|
606
|
+
);
|
|
607
|
+
} finally {
|
|
608
|
+
try {
|
|
609
|
+
db?.close();
|
|
610
|
+
} catch (err) {
|
|
611
|
+
void err;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/connection.ts
|
|
617
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
618
|
+
var import_node_path = require("path");
|
|
619
|
+
var import_node_os = require("os");
|
|
620
|
+
var import_subagent_engine_sdk6 = require("@zhushanwen/subagent-engine-sdk");
|
|
621
|
+
|
|
622
|
+
// src/logs/stderr-rotation.ts
|
|
623
|
+
var import_subagent_engine_sdk4 = require("@zhushanwen/subagent-engine-sdk");
|
|
624
|
+
var import_subagent_engine_sdk5 = require("@zhushanwen/subagent-engine-sdk");
|
|
625
|
+
var ZCODE_STDERR_LOG_PREFIX = "zcode-appserver-stderr-";
|
|
626
|
+
function stderrLogPathFor(engineDataDir, pid) {
|
|
627
|
+
return (0, import_subagent_engine_sdk4.stderrLogPathFor)(engineDataDir, pid, ZCODE_STDERR_LOG_PREFIX);
|
|
628
|
+
}
|
|
629
|
+
function cleanupSiblingStderrLogs(logPath, env, now) {
|
|
630
|
+
return (0, import_subagent_engine_sdk4.cleanupSiblingStderrLogs)(logPath, env, ZCODE_STDERR_LOG_PREFIX, now);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/connection.ts
|
|
634
|
+
var logger = (0, import_subagent_engine_sdk6.getLogger)("subagents");
|
|
635
|
+
var RAW_FRAME_LOG_CHARS = 200;
|
|
636
|
+
var RUNTIME_PREFERENCES = Object.freeze({
|
|
637
|
+
nativeSearchEnhancementsEnabled: true,
|
|
638
|
+
memoryEnabled: false,
|
|
639
|
+
askUserQuestionAutoResolutionEnabled: true,
|
|
640
|
+
modelContextBudgetStrategy: "preflight-v1"
|
|
641
|
+
});
|
|
642
|
+
var DEFAULT_REVERSE_HANDLERS = {
|
|
643
|
+
"session/requestRuntimePreferences": () => RUNTIME_PREFERENCES
|
|
644
|
+
};
|
|
645
|
+
function isAppServerRpcError(err) {
|
|
646
|
+
return err instanceof Error && typeof err.code === "number";
|
|
647
|
+
}
|
|
648
|
+
function isRecord2(v) {
|
|
649
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
650
|
+
}
|
|
651
|
+
function frameIdOf(frame) {
|
|
652
|
+
const id = frame.id;
|
|
653
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
654
|
+
}
|
|
655
|
+
function errMessage(err) {
|
|
656
|
+
return (0, import_subagent_engine_sdk3.toErrorMessage)(err);
|
|
657
|
+
}
|
|
658
|
+
function buildAppServerEnv(baseEnv = process.env) {
|
|
659
|
+
return {
|
|
660
|
+
...(0, import_subagent_engine_sdk6.buildNestedSpawnEnv)(baseEnv),
|
|
661
|
+
ZCODE_MODEL_TELEMETRY_ENABLED: "false"
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
var AppServerConnection = class {
|
|
665
|
+
cliPath;
|
|
666
|
+
dataDir;
|
|
667
|
+
cwd;
|
|
668
|
+
env;
|
|
669
|
+
stderrLogPath;
|
|
670
|
+
stderrLogPathResolver;
|
|
671
|
+
/** 当前代 tee 流实际写入的路径(append/轮转/cleanup 共用)。 */
|
|
672
|
+
activeStderrLogPath = null;
|
|
673
|
+
nodeBin;
|
|
674
|
+
requestTimeoutMs;
|
|
675
|
+
reverseHandlers;
|
|
676
|
+
onSpawned;
|
|
677
|
+
launcherScript;
|
|
678
|
+
/** 当前代子进程;null = 无活进程(未启动或已死,下次使用重建)。 */
|
|
679
|
+
child = null;
|
|
680
|
+
pending = /* @__PURE__ */ new Map();
|
|
681
|
+
/** 请求 id 自增序(每代重置——新进程无历史 pending,id 空间互不冲突)。 */
|
|
682
|
+
reqSeq = 0;
|
|
683
|
+
/** stderr 滚动缓冲(代级:崩溃诊断用)。 */
|
|
684
|
+
stderrTail = "";
|
|
685
|
+
stderrStream = null;
|
|
686
|
+
stderrStreamFailed = false;
|
|
687
|
+
/**
|
|
688
|
+
* 本代我方杀链 promise(close/shutdown 共用)。入口顺序不破「shutdown resolve 于
|
|
689
|
+
* 进程退出」契约:close 先行发起时 shutdown await 同一 promise 而非直接 return。
|
|
690
|
+
* killChain 永不 reject(内部 raceTimeout 吞 reject)——close 的 fire-and-forget
|
|
691
|
+
* 无 unhandled rejection 面。
|
|
692
|
+
*/
|
|
693
|
+
killChainPromise;
|
|
694
|
+
generation = 0;
|
|
695
|
+
pushHandlers = /* @__PURE__ */ new Map();
|
|
696
|
+
/** [R4] 连接级崩溃通知面(onClose 订阅者集合;与 pushHandlers 同构的 refCount 形态)。 */
|
|
697
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
698
|
+
capturedProtocolInfo = null;
|
|
699
|
+
constructor(opts) {
|
|
700
|
+
this.cliPath = opts.cliPath;
|
|
701
|
+
this.launcherScript = opts.launcherScript;
|
|
702
|
+
this.dataDir = opts.dataDir;
|
|
703
|
+
this.cwd = opts.cwd;
|
|
704
|
+
this.env = opts.env;
|
|
705
|
+
this.stderrLogPath = opts.stderrLogPath;
|
|
706
|
+
this.stderrLogPathResolver = opts.stderrLogPathResolver ?? (() => this.stderrLogPath);
|
|
707
|
+
this.nodeBin = opts.nodeBin ?? "node";
|
|
708
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? ZCODE_APPSERVER_REQUEST_TIMEOUT_MS;
|
|
709
|
+
this.reverseHandlers = opts.reverseHandlers ?? DEFAULT_REVERSE_HANDLERS;
|
|
710
|
+
this.onSpawned = opts.onSpawned;
|
|
711
|
+
}
|
|
712
|
+
/** 当前代进程 pid(未启动/已死为 undefined)。 */
|
|
713
|
+
get pid() {
|
|
714
|
+
return this.child?.pid;
|
|
715
|
+
}
|
|
716
|
+
/** 当前代是否有活进程(关闭原语与重建测试的观测面)。 */
|
|
717
|
+
get alive() {
|
|
718
|
+
return this.child !== null;
|
|
719
|
+
}
|
|
720
|
+
/** 协议自报信息(A.1 首帧 {protocol:{...}} 或 {method:'protocol'} 推送形态;未收到为 null)。 */
|
|
721
|
+
get protocolInfo() {
|
|
722
|
+
return this.capturedProtocolInfo;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* 订阅服务端推送(按 method)。handler 异常由本模块吞掉并 warn(不拖死帧泵)。
|
|
726
|
+
*
|
|
727
|
+
* @returns 退订函数
|
|
728
|
+
*/
|
|
729
|
+
onNotification(method, handler) {
|
|
730
|
+
let set = this.pushHandlers.get(method);
|
|
731
|
+
if (!set) {
|
|
732
|
+
set = /* @__PURE__ */ new Set();
|
|
733
|
+
this.pushHandlers.set(method, set);
|
|
734
|
+
}
|
|
735
|
+
set.add(handler);
|
|
736
|
+
return () => {
|
|
737
|
+
set?.delete(handler);
|
|
738
|
+
if (set && set.size === 0) this.pushHandlers.delete(method);
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* [R4] 订阅连接崩溃/关闭通知:当前代进程退出(崩溃、spawn 失败、我方杀链收尾)时
|
|
743
|
+
* 回调一次(reason 含 stderr 尾部——失败路径 2 的错误素材)。时序保证:在途
|
|
744
|
+
* pending 全部 reject **之后**触发(订阅方可安全假定「在途请求已死」)。仅在
|
|
745
|
+
* 「有进程死亡」时触发——从未启动过的连接不通知。
|
|
746
|
+
*
|
|
747
|
+
* @returns 退订函数
|
|
748
|
+
*/
|
|
749
|
+
onClose(handler) {
|
|
750
|
+
this.closeHandlers.add(handler);
|
|
751
|
+
return () => {
|
|
752
|
+
this.closeHandlers.delete(handler);
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* [R4] 发一个客户端请求帧但不等待应答(fire-and-forget)。dispose 编排用:
|
|
757
|
+
* 停机时 session/close 帧「发出即算」不阻塞在应答上(对面进程可能正被并发收割,
|
|
758
|
+
* 等应答会让 dispose 挂到请求超时)。不登记 pending——迟到应答按无匹配 id 忽略。
|
|
759
|
+
*/
|
|
760
|
+
post(method, params) {
|
|
761
|
+
this.ensureStarted();
|
|
762
|
+
return this.writeFrame({ id: ++this.reqSeq, method, params });
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* 发一个客户端请求并等应答。惰性启动(首次调用 spawn);error 应答帧转 reject
|
|
766
|
+
* (code/data 挂到 Error 上,-32602 的 zod 诊断随 data 带出便于逆向 schema)。
|
|
767
|
+
* 出站帧精确键集 {id, method, params}——无 jsonrpc 字段(A.1)。
|
|
768
|
+
*/
|
|
769
|
+
request(method, params, opts) {
|
|
770
|
+
this.ensureStarted();
|
|
771
|
+
if (this.child === null) {
|
|
772
|
+
return Promise.reject(new Error("app-server \u8FDE\u63A5\u4E0D\u53EF\u7528\uFF08\u8FDB\u7A0B\u672A\u542F\u52A8\u6216\u5DF2\u9000\u51FA\uFF09"));
|
|
773
|
+
}
|
|
774
|
+
const timeoutMs = opts?.timeoutMs ?? this.requestTimeoutMs;
|
|
775
|
+
const id = ++this.reqSeq;
|
|
776
|
+
return new Promise((resolve, reject) => {
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
this.pending.delete(id);
|
|
779
|
+
reject(new Error(`\u8BF7\u6C42 ${method} \u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`));
|
|
780
|
+
}, timeoutMs);
|
|
781
|
+
this.pending.set(id, {
|
|
782
|
+
// 泛型 T 是调用方对线上 unknown 结果的视图声明,连接层不做运行时校验
|
|
783
|
+
// (协议层保持透明,结构校验归会话层/消费方)。
|
|
784
|
+
resolve,
|
|
785
|
+
reject,
|
|
786
|
+
timer,
|
|
787
|
+
method
|
|
788
|
+
});
|
|
789
|
+
if (!this.writeFrame({ id, method, params })) {
|
|
790
|
+
this.pending.delete(id);
|
|
791
|
+
clearTimeout(timer);
|
|
792
|
+
reject(new Error(`\u8BF7\u6C42 ${method} \u5199\u5165\u5931\u8D25\uFF08app-server \u8FDB\u7A0B\u4E0D\u53EF\u5199\uFF09`));
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* 同步关闭面(R4 dispose 编排的原语):立即发 SIGTERM(同步系统调用,返回前信号
|
|
798
|
+
* 已发出),grace→SIGKILL 升级由杀链 promise 跟进(fire-and-forget)。幂等;对未启动
|
|
799
|
+
* 连接是零成本 no-op。close 之后调 shutdown 会 await 同一杀链再 resolve(见
|
|
800
|
+
* killChainPromise 字段注释)。
|
|
801
|
+
*/
|
|
802
|
+
close() {
|
|
803
|
+
const child = this.child;
|
|
804
|
+
if (child === null || this.killChainPromise !== void 0) return;
|
|
805
|
+
this.killChainPromise = (0, import_subagent_engine_sdk6.killChain)(child, { graceMs: ZCODE_KILL_GRACE_MS });
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* 异步完整关闭面:SIGTERM → grace → SIGKILL 杀链走完(含收尸),resolve 于进程
|
|
809
|
+
* 退出。幂等:与 close 共享同一杀链 promise——close 先行时 await 它再 resolve,
|
|
810
|
+
* 不另起杀链(graceMs 取首次发起值)。之后首个 request() 自动重建(与崩溃重建
|
|
811
|
+
* 同路径,§3.4 不变量 4)。
|
|
812
|
+
*/
|
|
813
|
+
async shutdown(opts) {
|
|
814
|
+
if (this.killChainPromise === void 0) {
|
|
815
|
+
const child = this.child;
|
|
816
|
+
if (child === null) return;
|
|
817
|
+
this.killChainPromise = (0, import_subagent_engine_sdk6.killChain)(child, { graceMs: opts?.graceMs ?? ZCODE_KILL_GRACE_MS });
|
|
818
|
+
}
|
|
819
|
+
await this.killChainPromise;
|
|
820
|
+
}
|
|
821
|
+
// ============================================================
|
|
822
|
+
// 进程生命周期(代管理)
|
|
823
|
+
// ============================================================
|
|
824
|
+
ensureStarted() {
|
|
825
|
+
if (this.child !== null) return;
|
|
826
|
+
this.spawnGeneration();
|
|
827
|
+
}
|
|
828
|
+
spawnGeneration() {
|
|
829
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
830
|
+
this.reqSeq = 0;
|
|
831
|
+
this.stderrTail = "";
|
|
832
|
+
this.killChainPromise = void 0;
|
|
833
|
+
this.generation += 1;
|
|
834
|
+
const gen = this.generation;
|
|
835
|
+
const script = this.launcherScript ?? this.cliPath;
|
|
836
|
+
const child = (0, import_subagent_engine_sdk6.spawnEngineChild)({
|
|
837
|
+
command: this.nodeBin,
|
|
838
|
+
args: [script, "app-server", "--cwd", this.cwd],
|
|
839
|
+
env: (0, import_subagent_engine_sdk6.buildEngineChildEnv)(this.env, {
|
|
840
|
+
// L0 XYZ_AGENT_DATA_DIR 缺省链:显式传参 > 宿主注入 env > tmpdir 占位
|
|
841
|
+
//(app-server 不消费该键,占位仅满足 L0 必注入契约;生产调用方
|
|
842
|
+
// zcode-engine.ensureAppServerRuntime 恒传 engineDataDir)。
|
|
843
|
+
dataDir: this.dataDir ?? process.env["XYZ_AGENT_DATA_DIR"] ?? (0, import_node_os.tmpdir)(),
|
|
844
|
+
envPrefixes: ["ZCODE_"]
|
|
845
|
+
}),
|
|
846
|
+
stdout: "pipe",
|
|
847
|
+
stderr: "pipe"
|
|
848
|
+
});
|
|
849
|
+
this.child = child;
|
|
850
|
+
if (this.onSpawned !== void 0) {
|
|
851
|
+
try {
|
|
852
|
+
this.onSpawned(child);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
logger.warn(`onSpawned \u56DE\u8C03\u5F02\u5E38\uFF08\u5FFD\u7565\uFF09: ${errMessage(err)}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
let finalized = false;
|
|
858
|
+
const finalize = (reason) => {
|
|
859
|
+
if (finalized || this.generation !== gen) return;
|
|
860
|
+
finalized = true;
|
|
861
|
+
this.closeStderrStream();
|
|
862
|
+
this.child = null;
|
|
863
|
+
const err = new Error(`app-server ${reason}`);
|
|
864
|
+
for (const entry of this.pending.values()) {
|
|
865
|
+
clearTimeout(entry.timer);
|
|
866
|
+
entry.reject(err);
|
|
867
|
+
}
|
|
868
|
+
this.pending.clear();
|
|
869
|
+
for (const fn of [...this.closeHandlers]) {
|
|
870
|
+
try {
|
|
871
|
+
fn(reason);
|
|
872
|
+
} catch (err2) {
|
|
873
|
+
logger.warn(`close handler \u5F02\u5E38: ${errMessage(err2)}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
(0, import_subagent_engine_sdk6.pumpNdjsonLines)(child.stdout, (line) => this.handleLine(line));
|
|
878
|
+
child.stderr?.setEncoding("utf8");
|
|
879
|
+
child.stderr?.on("data", (chunk) => {
|
|
880
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-ZCODE_APPSERVER_STDERR_TAIL_BUFFER_CHARS);
|
|
881
|
+
this.appendStderrLog(chunk);
|
|
882
|
+
});
|
|
883
|
+
child.stdin?.on("error", () => {
|
|
884
|
+
});
|
|
885
|
+
child.once("close", (code, signal) => {
|
|
886
|
+
finalize(
|
|
887
|
+
`\u8FDB\u7A0B\u9000\u51FA\uFF08code=${code} signal=${signal ?? "none"}\uFF09stderr \u5C3E\u90E8: ${this.stderrTail.slice(-ZCODE_APPSERVER_STDERR_TAIL_CHARS)}`
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
child.once("error", (err) => finalize(`spawn \u5931\u8D25: ${err.message}`));
|
|
891
|
+
}
|
|
892
|
+
// ============================================================
|
|
893
|
+
// 帧分发(A.1 四帧型;宽容解析,坏行跳过不断流)
|
|
894
|
+
// ============================================================
|
|
895
|
+
handleLine(line) {
|
|
896
|
+
const text = line.trim();
|
|
897
|
+
if (!text) return;
|
|
898
|
+
let frame;
|
|
899
|
+
try {
|
|
900
|
+
frame = JSON.parse(text);
|
|
901
|
+
} catch {
|
|
902
|
+
logger.warn(`\u65E0\u6CD5\u89E3\u6790\u7684\u534F\u8BAE\u884C\uFF08\u5FFD\u7565\uFF09: ${text.slice(0, RAW_FRAME_LOG_CHARS)}`);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.handleFrame(frame, text);
|
|
906
|
+
}
|
|
907
|
+
handleFrame(frame, rawText) {
|
|
908
|
+
if (!isRecord2(frame)) {
|
|
909
|
+
logger.warn(`\u975E\u5BF9\u8C61\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (isRecord2(frame.protocol)) this.capturedProtocolInfo = frame.protocol;
|
|
913
|
+
if (typeof frame.method === "string") {
|
|
914
|
+
const reverseId = frameIdOf(frame);
|
|
915
|
+
if (reverseId !== void 0) {
|
|
916
|
+
this.answerReverse(reverseId, frame.method, frame.params);
|
|
917
|
+
} else {
|
|
918
|
+
this.dispatchPush(frame.method, frame.params);
|
|
919
|
+
}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (frame.id !== void 0 && frame.id !== null) {
|
|
923
|
+
if (typeof frame.id !== "number") {
|
|
924
|
+
logger.warn(`\u5E94\u7B54 id \u975E\u6570\u5B57\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.settlePending(frame.id, frame);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (isRecord2(frame.protocol)) return;
|
|
931
|
+
logger.warn(`\u65E0\u6CD5\u5F52\u7C7B\u7684\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
|
|
932
|
+
}
|
|
933
|
+
settlePending(id, frame) {
|
|
934
|
+
const entry = this.pending.get(id);
|
|
935
|
+
if (!entry) {
|
|
936
|
+
logger.warn(`\u54CD\u5E94\u65E0\u5339\u914D\u8BF7\u6C42 id=${id}\uFF08\u5FFD\u7565\uFF09`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
this.pending.delete(id);
|
|
940
|
+
clearTimeout(entry.timer);
|
|
941
|
+
if (frame.error !== void 0) {
|
|
942
|
+
const e = isRecord2(frame.error) ? frame.error : {};
|
|
943
|
+
const code = typeof e.code === "number" ? e.code : void 0;
|
|
944
|
+
const message = typeof e.message === "string" ? e.message : JSON.stringify(e.message ?? e);
|
|
945
|
+
const dataSlice = e.data === void 0 ? "" : `\uFF1Bdata: ${JSON.stringify(e.data).slice(0, ZCODE_APPSERVER_STDERR_TAIL_CHARS)}`;
|
|
946
|
+
const err = new Error(`${entry.method} \u5931\u8D25: [${code ?? "?"}] ${message}${dataSlice}`);
|
|
947
|
+
err.code = code;
|
|
948
|
+
err.data = e.data;
|
|
949
|
+
entry.reject(err);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
entry.resolve(frame.result);
|
|
953
|
+
}
|
|
954
|
+
dispatchPush(method, params) {
|
|
955
|
+
if (method === "protocol") this.capturedProtocolInfo = isRecord2(params) ? params : null;
|
|
956
|
+
const set = this.pushHandlers.get(method);
|
|
957
|
+
if (!set) return;
|
|
958
|
+
for (const fn of set) {
|
|
959
|
+
try {
|
|
960
|
+
fn(params);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
logger.warn(`push handler \u5F02\u5E38\uFF08${method}\uFF09: ${errMessage(err)}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// ============================================================
|
|
967
|
+
// 反向请求应答(D9)
|
|
968
|
+
// ============================================================
|
|
969
|
+
answerReverse(id, method, params) {
|
|
970
|
+
const handler = this.reverseHandlers[method];
|
|
971
|
+
if (!handler) {
|
|
972
|
+
logger.warn(`\u672A\u77E5\u53CD\u5411\u8BF7\u6C42 ${method}\uFF08id=${id}\uFF09\uFF1A\u56DE\u7A7A result\uFF08\u4E0D\u7B54\u4F1A 15s \u8D85\u65F6\u65AD\u8FDE\uFF0C\u65E7\u5B9E\u6D4B -32022\uFF09`);
|
|
973
|
+
this.writeFrame({ id, result: {} });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
Promise.resolve().then(() => handler(params)).then((result) => this.writeFrame({ id, result: result ?? {} })).catch((err) => {
|
|
977
|
+
logger.warn(`\u53CD\u5411\u8BF7\u6C42 ${method} handler \u5F02\u5E38: ${errMessage(err)}`);
|
|
978
|
+
this.writeFrame({ id, error: { code: -32e3, message: errMessage(err) } });
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
// ============================================================
|
|
982
|
+
// 写帧 / stderr tee
|
|
983
|
+
// ============================================================
|
|
984
|
+
/** 写一帧(JSON + 换行)。返回 false = 进程不可写(调用方决定如何失败)。 */
|
|
985
|
+
writeFrame(frame) {
|
|
986
|
+
const child = this.child;
|
|
987
|
+
if (child === null || child.stdin === null) return false;
|
|
988
|
+
try {
|
|
989
|
+
child.stdin.write(`${JSON.stringify(frame)}
|
|
990
|
+
`);
|
|
991
|
+
return true;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
logger.warn(`\u5199\u5165 app-server \u5931\u8D25: ${errMessage(err)}`);
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/** stderr 实时 append 落盘(懒打开 + 尺寸轮转 + 过期清理;失败静默——取证面不
|
|
998
|
+
* 能拖垮主通道)。 */
|
|
999
|
+
appendStderrLog(chunk) {
|
|
1000
|
+
if (this.stderrStreamFailed) return;
|
|
1001
|
+
if (this.stderrStream === null) {
|
|
1002
|
+
const target = this.stderrLogPathResolver(this.child?.pid);
|
|
1003
|
+
try {
|
|
1004
|
+
fs4.mkdirSync((0, import_node_path.dirname)(target), { recursive: true });
|
|
1005
|
+
this.activeStderrLogPath = target;
|
|
1006
|
+
this.stderrStream = fs4.createWriteStream(target, { flags: "a" });
|
|
1007
|
+
this.stderrStream.on("error", () => {
|
|
1008
|
+
this.stderrStreamFailed = true;
|
|
1009
|
+
});
|
|
1010
|
+
cleanupSiblingStderrLogs(target, process.env);
|
|
1011
|
+
} catch {
|
|
1012
|
+
this.stderrStreamFailed = true;
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
this.stderrStream.write(chunk);
|
|
1017
|
+
if (this.activeStderrLogPath !== null) {
|
|
1018
|
+
const params = (0, import_subagent_engine_sdk5.stderrRotationParams)(process.env);
|
|
1019
|
+
if ((0, import_subagent_engine_sdk5.rotateStderrLogIfNeeded)(this.activeStderrLogPath, params)) {
|
|
1020
|
+
this.closeStderrStream();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/** 代收尾时关闭 tee 流(下一代懒重开;W11 起路径按新一代 pid 解析)。 */
|
|
1025
|
+
closeStderrStream() {
|
|
1026
|
+
if (this.stderrStream !== null) {
|
|
1027
|
+
const stream = this.stderrStream;
|
|
1028
|
+
this.stderrStream = null;
|
|
1029
|
+
this.activeStderrLogPath = null;
|
|
1030
|
+
stream.end();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// src/session-channel.ts
|
|
1036
|
+
var import_node_crypto = require("crypto");
|
|
1037
|
+
var import_subagent_engine_sdk7 = require("@zhushanwen/subagent-engine-sdk");
|
|
1038
|
+
var logger2 = (0, import_subagent_engine_sdk7.getLogger)("subagents");
|
|
1039
|
+
var SUBSCRIBE_DELIVERY_KIND = "desktop-continuous";
|
|
1040
|
+
var WORKSPACE_KEY_HASH_CHARS = 16;
|
|
1041
|
+
var CREATE_REPLY_LOG_CHARS = 300;
|
|
1042
|
+
var DELTA_LOG_CHARS = 120;
|
|
1043
|
+
function isRecord3(v) {
|
|
1044
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1045
|
+
}
|
|
1046
|
+
function errMessage2(err) {
|
|
1047
|
+
return (0, import_subagent_engine_sdk3.toErrorMessage)(err);
|
|
1048
|
+
}
|
|
1049
|
+
function stableWorkspaceKey(workspacePath) {
|
|
1050
|
+
return "ws-" + (0, import_node_crypto.createHash)("sha256").update(workspacePath).digest("hex").slice(0, WORKSPACE_KEY_HASH_CHARS);
|
|
1051
|
+
}
|
|
1052
|
+
function extractCreatedSessionId(created) {
|
|
1053
|
+
if (!isRecord3(created)) return void 0;
|
|
1054
|
+
const session = isRecord3(created.session) ? created.session : void 0;
|
|
1055
|
+
if (session !== void 0 && typeof session.sessionId === "string" && session.sessionId !== "") {
|
|
1056
|
+
return session.sessionId;
|
|
1057
|
+
}
|
|
1058
|
+
if (session !== void 0 && typeof session.id === "string" && session.id !== "") {
|
|
1059
|
+
return session.id;
|
|
1060
|
+
}
|
|
1061
|
+
if (typeof created.sessionId === "string" && created.sessionId !== "") {
|
|
1062
|
+
return created.sessionId;
|
|
1063
|
+
}
|
|
1064
|
+
return void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function extractPushSessionId(params) {
|
|
1067
|
+
if (!isRecord3(params)) return void 0;
|
|
1068
|
+
if (typeof params.sessionId === "string" && params.sessionId !== "")
|
|
1069
|
+
return params.sessionId;
|
|
1070
|
+
if (isRecord3(params.session) && typeof params.session.id === "string" && params.session.id !== "") {
|
|
1071
|
+
return params.session.id;
|
|
1072
|
+
}
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function messageRole(m) {
|
|
1076
|
+
if (typeof m.role === "string") return m.role;
|
|
1077
|
+
if (isRecord3(m.info) && typeof m.info.role === "string") return m.info.role;
|
|
1078
|
+
return void 0;
|
|
1079
|
+
}
|
|
1080
|
+
function partsToText(parts) {
|
|
1081
|
+
if (!Array.isArray(parts)) return void 0;
|
|
1082
|
+
const texts = [];
|
|
1083
|
+
for (const p of parts) {
|
|
1084
|
+
if (isRecord3(p) && p.type === "text" && typeof p.text === "string")
|
|
1085
|
+
texts.push(p.text);
|
|
1086
|
+
}
|
|
1087
|
+
return texts.length > 0 ? texts.join("") : void 0;
|
|
1088
|
+
}
|
|
1089
|
+
function contentToText(content) {
|
|
1090
|
+
if (typeof content === "string") return content;
|
|
1091
|
+
if (isRecord3(content) && typeof content.text === "string")
|
|
1092
|
+
return content.text;
|
|
1093
|
+
if (Array.isArray(content)) {
|
|
1094
|
+
const texts = content.map((b) => {
|
|
1095
|
+
if (typeof b === "string") return b;
|
|
1096
|
+
return isRecord3(b) && typeof b.text === "string" ? b.text : "";
|
|
1097
|
+
});
|
|
1098
|
+
const joined = texts.join("");
|
|
1099
|
+
return joined !== "" ? joined : void 0;
|
|
1100
|
+
}
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
function extractAssistantText(readResult) {
|
|
1104
|
+
if (typeof readResult === "string") return readResult;
|
|
1105
|
+
if (!isRecord3(readResult)) return void 0;
|
|
1106
|
+
if (Array.isArray(readResult.messages)) {
|
|
1107
|
+
const last = [...readResult.messages].reverse().find(
|
|
1108
|
+
(m) => isRecord3(m) && (messageRole(m) === "assistant" || messageRole(m) === void 0)
|
|
1109
|
+
);
|
|
1110
|
+
if (last !== void 0) {
|
|
1111
|
+
const viaContent = contentToText(last.content);
|
|
1112
|
+
if (viaContent !== void 0) return viaContent;
|
|
1113
|
+
const viaParts = partsToText(last.parts);
|
|
1114
|
+
if (viaParts !== void 0) return viaParts;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
for (const k of ["text", "response", "content", "message"]) {
|
|
1118
|
+
if (typeof readResult[k] === "string") return readResult[k];
|
|
1119
|
+
}
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
function extractReadUsage(readResult) {
|
|
1123
|
+
if (!isRecord3(readResult)) return void 0;
|
|
1124
|
+
const messages = Array.isArray(readResult.messages) ? readResult.messages : [];
|
|
1125
|
+
for (const m of [...messages].reverse()) {
|
|
1126
|
+
if (!isRecord3(m) || messageRole(m) !== "assistant" || !Array.isArray(m.parts))
|
|
1127
|
+
continue;
|
|
1128
|
+
const finish = [...m.parts].reverse().find(
|
|
1129
|
+
(p) => isRecord3(p) && p.type === "step-finish" && isRecord3(p.tokens)
|
|
1130
|
+
);
|
|
1131
|
+
if (finish !== void 0 && isRecord3(finish.tokens)) return finish.tokens;
|
|
1132
|
+
}
|
|
1133
|
+
return isRecord3(readResult.usage) ? readResult.usage : void 0;
|
|
1134
|
+
}
|
|
1135
|
+
var TurnTimeoutError = class extends Error {
|
|
1136
|
+
kind;
|
|
1137
|
+
elapsed;
|
|
1138
|
+
lastEventAt;
|
|
1139
|
+
thresholdMs;
|
|
1140
|
+
constructor(kind, parts) {
|
|
1141
|
+
super(
|
|
1142
|
+
kind === "idle" ? `zcode turn idle \u5224\u6B7B\uFF1A\u8FDE\u7EED ${parts.thresholdMs}ms \u672A\u89C2\u5BDF\u5230\u672C turn \u4EFB\u4F55\u4E8B\u4EF6\uFF08session/event \u4E0E v4/telemetry/event \u5747\u9759\u9ED8\uFF09\uFF0C\u7EC8\u6001\u672A\u5230\u8FBE\u3002` + (parts.lastEventAt === void 0 ? "\u672C\u8F6E\u81EA send \u8D77\u672A\u89C2\u5BDF\u5230\u4EFB\u4F55\u4E8B\u4EF6\uFF08\u8FDB\u7A0B\u5047\u6B7B/\u534F\u8BAE\u9759\u9ED8\u5F62\u6001\uFF09\u3002" : `\u6700\u540E\u4E8B\u4EF6\u65F6\u523B ${new Date(parts.lastEventAt).toISOString()}\u3002`) + "\u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF1B\u82E5\u6301\u7EED\u51FA\u73B0\uFF0C\u68C0\u67E5 ZCode \u684C\u9762\u7AEF\u6A21\u578B\u8FDE\u901A\u6027\u6216\u6539\u7528 engine: pi\u3002" : `zcode turn \u603B\u4E0A\u754C\u5224\u6B7B\uFF1A${parts.thresholdMs}ms \u5185\u672A\u89C2\u5BDF\u5230\u7EC8\u6001\uFF08turn.terminal \u4E0E\u6536\u5C3E\u5E27\u5747\u672A\u5230\u8FBE\uFF1Bidle \u5224\u5B9A\u672A\u89E6\u53D1\u2014\u2014\u4E8B\u4EF6\u6D41\u4ECD\u6D3B\u8DC3\uFF0Cchatty-wedge \u5F62\u6001\uFF09\u3002` + (parts.lastEventAt === void 0 ? "" : `\u6700\u540E\u4E8B\u4EF6\u65F6\u523B ${new Date(parts.lastEventAt).toISOString()}\u3002`) + `\u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF1B\u82E5\u672C\u4EFB\u52A1\u5C5E\u5408\u6CD5\u8D85\u957F\u4EFB\u52A1\uFF08\u9884\u671F\u8D85\u8FC7 ${parts.thresholdMs}ms \u603B\u4E0A\u754C\uFF09\uFF0C\u91CD\u8DD1\u524D\u8BBE ${ZCODE_TURN_MAX_TIMEOUT_ENV} \u4E3A\u66F4\u5927\u6BEB\u79D2\u503C\uFF0C\u6216\u8BBE 0 \u5173\u95ED\u603B\u4E0A\u754C\uFF08\u5173\u95ED\u540E chatty \u5F62\u6001\u4E0D\u518D\u81EA\u52A8\u56DE\u6536\uFF0C\u9759\u9ED8 wedged \u4ECD\u7531 idle \u5C42\u515C\u5E95\u2014\u2014\u81EA\u884C\u6743\u8861\uFF09\u3002`
|
|
1143
|
+
);
|
|
1144
|
+
this.name = "TurnTimeoutError";
|
|
1145
|
+
this.kind = kind;
|
|
1146
|
+
this.elapsed = parts.elapsed;
|
|
1147
|
+
this.lastEventAt = parts.lastEventAt;
|
|
1148
|
+
this.thresholdMs = parts.thresholdMs;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function clearTurnTimers(turn) {
|
|
1152
|
+
if (turn.idleTimer !== void 0) {
|
|
1153
|
+
clearTimeout(turn.idleTimer);
|
|
1154
|
+
turn.idleTimer = void 0;
|
|
1155
|
+
}
|
|
1156
|
+
if (turn.ceilingTimer !== void 0) {
|
|
1157
|
+
clearTimeout(turn.ceilingTimer);
|
|
1158
|
+
turn.ceilingTimer = void 0;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function armTurnTimer(onFire, ms) {
|
|
1162
|
+
const timer = setTimeout(onFire, ms);
|
|
1163
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1164
|
+
return timer;
|
|
1165
|
+
}
|
|
1166
|
+
function resolveTurnTimerMs(parts) {
|
|
1167
|
+
let value;
|
|
1168
|
+
let source;
|
|
1169
|
+
if (parts.explicit !== void 0) {
|
|
1170
|
+
value = parts.explicit;
|
|
1171
|
+
source = "\u663E\u5F0F\u4F20\u53C2";
|
|
1172
|
+
} else {
|
|
1173
|
+
const raw = process.env[parts.envName];
|
|
1174
|
+
const parsed = parseZcodeTurnTimeoutEnv(raw);
|
|
1175
|
+
if (parsed.state === "valid") {
|
|
1176
|
+
value = parsed.ms;
|
|
1177
|
+
source = `env ${parts.envName}=${raw}`;
|
|
1178
|
+
} else {
|
|
1179
|
+
if (parsed.state === "invalid") {
|
|
1180
|
+
logger2.warn(
|
|
1181
|
+
`[session-channel] ${parts.envName}="${raw}" \u975E\u6CD5\uFF08\u5E94\u4E3A\u6BEB\u79D2\u6570\u5B57\uFF09\u2014\u2014\u56DE\u843D\u9ED8\u8BA4 ${parts.fallbackMs}ms\uFF08${parts.label}\uFF09\u3002\u8BBE\u7F6E\u6B63\u6BEB\u79D2\u503C\u8986\u76D6\uFF0C\u6216 0 \u663E\u5F0F\u5173\u95ED`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
value = parts.fallbackMs;
|
|
1185
|
+
source = "\u9ED8\u8BA4\u503C";
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (value <= 0) {
|
|
1189
|
+
logger2.warn(
|
|
1190
|
+
`[session-channel] zcode turn ${parts.label}\u5DF2\u5173\u95ED\uFF08${source}\uFF09\u2014\u2014${parts.offConsequence}\u3002\u8BBE\u6B63\u6BEB\u79D2\u503C\uFF08env ${parts.envName} \u6216\u663E\u5F0F\u4F20\u53C2\uFF09\u6062\u590D\u56DE\u6536\u5C42`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
return value;
|
|
1194
|
+
}
|
|
1195
|
+
var SessionChannel = class {
|
|
1196
|
+
conn;
|
|
1197
|
+
/** sessionId → 在途 turn(终态落定后仍保留到 runTurn 收尾——收尾帧数据吸收窗口)。 */
|
|
1198
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
1199
|
+
offHandlers;
|
|
1200
|
+
/**
|
|
1201
|
+
* [P0-1 U5/D7] dispose 标志:dispose 收割后 closeSession 短路——收割形态下连接
|
|
1202
|
+
* 可能仍 alive(close 事件迟到,finalize 未跑、child 非 null),此时发 close
|
|
1203
|
+
* request 会写死进程(请求挂到控制面超时)或触发惰性重建(凭空 spawn 无人使用的
|
|
1204
|
+
* 进程,违背 dispose 防泄漏语义);进程随即被引擎杀链回收,会话驻留随之消亡。
|
|
1205
|
+
*/
|
|
1206
|
+
disposed = false;
|
|
1207
|
+
constructor(conn) {
|
|
1208
|
+
this.conn = conn;
|
|
1209
|
+
this.offHandlers = [
|
|
1210
|
+
conn.onNotification(
|
|
1211
|
+
"session/event",
|
|
1212
|
+
(params) => this.handleSessionEvent(params)
|
|
1213
|
+
),
|
|
1214
|
+
conn.onNotification(
|
|
1215
|
+
"v4/telemetry/event",
|
|
1216
|
+
(params) => this.handleTelemetry(params)
|
|
1217
|
+
),
|
|
1218
|
+
// 连接崩溃收割:进程死亡(崩溃/我方杀链)立即 fail 全部在途 turn——
|
|
1219
|
+
// onClose 契约保证触发时连接层在途 request 已全部 reject,此处补齐
|
|
1220
|
+
// 「无在途 request 的 turn」的收割(否则挂到 turn idle/总上界预算耗尽)
|
|
1221
|
+
conn.onClose((reason) => this.failAllTurns(`app-server ${reason}`))
|
|
1222
|
+
];
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* [P0-1 U5/D7] dispose 前收割 + 退订:「退订 = 不会再有事件」在结构上蕴含「在途
|
|
1226
|
+
* turn 不应再等」——close 缺失/被吞没形态(onClose 收割主路径未触发)下,在途
|
|
1227
|
+
* turn 于退订前经既有 failAllTurns 收敛为明确失败,不再挂满自身 idle/总上界预算
|
|
1228
|
+
* (设计 §3.4 退化路径闭合;race 窗口在引擎 shutdownRuntimeAndDisposeChannel,
|
|
1229
|
+
* 量级与 awaitConnFinalized 同源 ZCODE_APPSERVER_HARVEST_GRACE_MS)。幂等:与
|
|
1230
|
+
* onClose 收割先到者赢(turn.fail 的 settled 守卫——已落定 turn 不改写),
|
|
1231
|
+
* activeTurns 已空则 no-op。不影响连接自身生命周期(R4)。
|
|
1232
|
+
*/
|
|
1233
|
+
dispose() {
|
|
1234
|
+
this.disposed = true;
|
|
1235
|
+
this.failAllTurns(
|
|
1236
|
+
"zcode session channel \u5DF2 dispose\uFF1A\u8FDE\u63A5 close \u4E8B\u4EF6\u672A\u5728\u6536\u5272\u7A97\u53E3\u5185\u5230\u8FBE\uFF0C\u5728\u9014 turn \u4E8E\u9000\u8BA2\u524D\u6536\u5272\u4E3A\u660E\u786E\u5931\u8D25\uFF08\u4E0D\u518D\u6302\u5230 turn \u81EA\u8EAB\u9884\u7B97\u8017\u5C3D\uFF09\u3002\u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\u3002"
|
|
1237
|
+
);
|
|
1238
|
+
for (const off of this.offHandlers.splice(0)) off();
|
|
1239
|
+
}
|
|
1240
|
+
/** 连接死亡收割:全部在途 turn 立即 reject(reason 即连接层崩溃错误)。 */
|
|
1241
|
+
failAllTurns(reason) {
|
|
1242
|
+
const err = new Error(reason);
|
|
1243
|
+
for (const turn of [...this.activeTurns.values()]) {
|
|
1244
|
+
turn.fail(err);
|
|
1245
|
+
this.activeTurns.delete(turn.sessionId);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* session/create(A.2 ① 键集逐字:对面 zod strict 拒未知键,出站必须精确)。
|
|
1250
|
+
* persistence:"immediate" 固定携带(D4:SQLite 持久化,上游不可覆盖);空工具
|
|
1251
|
+
* 清单不设键(防上游带空数组)。sessionId 从 result.session.sessionId 提取
|
|
1252
|
+
* (projection.sessionId 恒 "unknown" 勿用)。
|
|
1253
|
+
*/
|
|
1254
|
+
async createSession(params) {
|
|
1255
|
+
const frame = {
|
|
1256
|
+
workspace: {
|
|
1257
|
+
workspacePath: params.workspacePath,
|
|
1258
|
+
workspaceKey: stableWorkspaceKey(params.workspacePath)
|
|
1259
|
+
},
|
|
1260
|
+
mode: params.mode,
|
|
1261
|
+
...params.model !== void 0 ? { model: params.model } : {},
|
|
1262
|
+
...params.thoughtLevel !== void 0 ? { thoughtLevel: params.thoughtLevel } : {},
|
|
1263
|
+
...params.toolAllowlist !== void 0 && params.toolAllowlist.length > 0 ? { toolAllowlist: params.toolAllowlist } : {},
|
|
1264
|
+
...params.toolDenylist !== void 0 && params.toolDenylist.length > 0 ? { toolDenylist: params.toolDenylist } : {},
|
|
1265
|
+
persistence: "immediate"
|
|
1266
|
+
};
|
|
1267
|
+
const created = await this.conn.request("session/create", frame);
|
|
1268
|
+
const sessionId = extractCreatedSessionId(created);
|
|
1269
|
+
if (sessionId === void 0) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`session/create \u672A\u8FD4\u56DE sessionId: ${JSON.stringify(created).slice(
|
|
1272
|
+
0,
|
|
1273
|
+
CREATE_REPLY_LOG_CHARS
|
|
1274
|
+
)}`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
return sessionId;
|
|
1278
|
+
}
|
|
1279
|
+
/** session/subscribe(A.2 ②:deliveryKind 必填——缺失则终态事件不达、会话假死)。 */
|
|
1280
|
+
async subscribe(sessionId) {
|
|
1281
|
+
await this.conn.request("session/subscribe", {
|
|
1282
|
+
sessionId,
|
|
1283
|
+
deliveryKind: SUBSCRIBE_DELIVERY_KIND
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
/** session/send(A.2 ③:字段是 content 不是 text)→ {accepted:true}。 */
|
|
1287
|
+
async send(sessionId, content) {
|
|
1288
|
+
const r = await this.conn.request("session/send", { sessionId, content });
|
|
1289
|
+
if (isRecord3(r) && r.accepted === false) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
"session/send \u8FD4\u56DE accepted:false\uFF08\u6295\u9012\u672A\u88AB\u63A5\u53D7\u2014\u2014\u4F1A\u8BDD\u53EF\u80FD\u5DF2\u5931\u6548\u6216\u62D2\u7EDD\u6295\u9012\uFF09"
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* session/close(A.2 ⑦,D4 用后即毁——回收引擎驻留内存,SQLite 持久化保留)。
|
|
1297
|
+
* best-effort:失败只 warn 不抛(会话已终态,close 失败只泄漏一个驻留会话,
|
|
1298
|
+
* 不应炸掉已成功/已失败的任务结论)。连接已死时跳过——close 是 request 语义,
|
|
1299
|
+
* 对死连接发请求会触发惰性重建(凭空 spawn 一代无人使用的进程)。
|
|
1300
|
+
*/
|
|
1301
|
+
async closeSession(sessionId) {
|
|
1302
|
+
if (this.disposed) return;
|
|
1303
|
+
if (!this.conn.alive) return;
|
|
1304
|
+
try {
|
|
1305
|
+
await this.conn.request(
|
|
1306
|
+
"session/close",
|
|
1307
|
+
{ sessionId },
|
|
1308
|
+
{ timeoutMs: ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS }
|
|
1309
|
+
);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
logger2.warn(
|
|
1312
|
+
`session/close \u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0Cbest-effort \u5FFD\u7565\uFF09: ${errMessage2(
|
|
1313
|
+
err
|
|
1314
|
+
)}`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* 单任务自包含入口(§2.4 ①-⑦):create → subscribe → send → 事件流
|
|
1320
|
+
* (onTextDelta)→ 终态 → read 兜底 → resolve {response, usage, sessionId}
|
|
1321
|
+
* → close。resolve 严格晚于全部事件回调(不变量 2);close 走 finally——成功/
|
|
1322
|
+
* 失败路径都回收会话(D4)。RPC 错误(含 -32601/-32602 漂移类)原样透传(R5 消费)。
|
|
1323
|
+
*/
|
|
1324
|
+
async runTurn(params, content, opts = {}) {
|
|
1325
|
+
if (content === "")
|
|
1326
|
+
throw new Error("runTurn: content \u5FC5\u586B\uFF08session/send \u7A7A\u6295\u9012\u65E0\u610F\u4E49\uFF09");
|
|
1327
|
+
const sessionId = await this.createSession(params);
|
|
1328
|
+
opts.onSessionCreated?.(sessionId);
|
|
1329
|
+
const opened = this.openTurn(sessionId, opts);
|
|
1330
|
+
try {
|
|
1331
|
+
await this.subscribe(sessionId);
|
|
1332
|
+
try {
|
|
1333
|
+
await this.send(sessionId, content);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
opened.turn.fail(err instanceof Error ? err : new Error(String(err)));
|
|
1336
|
+
void opened.done.catch(() => {
|
|
1337
|
+
});
|
|
1338
|
+
throw err;
|
|
1339
|
+
}
|
|
1340
|
+
const terminal = await opened.done;
|
|
1341
|
+
const readResult = await this.readBestEffort(sessionId);
|
|
1342
|
+
const readText = extractAssistantText(readResult);
|
|
1343
|
+
const response = readText ?? opened.turn.finalText ?? opened.turn.deltas.join("");
|
|
1344
|
+
const usage = opened.turn.finalUsage ?? extractReadUsage(readResult);
|
|
1345
|
+
return {
|
|
1346
|
+
sessionId,
|
|
1347
|
+
response,
|
|
1348
|
+
...usage !== void 0 ? { usage } : {},
|
|
1349
|
+
terminal,
|
|
1350
|
+
...opened.turn.lastTerminalStatus !== void 0 ? { lastTerminalStatus: opened.turn.lastTerminalStatus } : {},
|
|
1351
|
+
...opened.turn.lastTerminalError !== void 0 ? { lastTerminalError: opened.turn.lastTerminalError } : {}
|
|
1352
|
+
};
|
|
1353
|
+
} finally {
|
|
1354
|
+
opened.stopTimers();
|
|
1355
|
+
this.activeTurns.delete(sessionId);
|
|
1356
|
+
await this.closeSession(sessionId);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// ============================================================
|
|
1360
|
+
// 内部:turn 生命周期与推送泵
|
|
1361
|
+
// ============================================================
|
|
1362
|
+
openTurn(sessionId, opts) {
|
|
1363
|
+
let resolveDone;
|
|
1364
|
+
let rejectDone;
|
|
1365
|
+
const done = new Promise((res, rej) => {
|
|
1366
|
+
resolveDone = res;
|
|
1367
|
+
rejectDone = rej;
|
|
1368
|
+
});
|
|
1369
|
+
const turn = {
|
|
1370
|
+
sessionId,
|
|
1371
|
+
callbacks: { onTextDelta: opts.onTextDelta, onThinkingDelta: opts.onThinkingDelta },
|
|
1372
|
+
settled: false,
|
|
1373
|
+
deltas: [],
|
|
1374
|
+
finalText: void 0,
|
|
1375
|
+
finalUsage: void 0,
|
|
1376
|
+
terminal: void 0,
|
|
1377
|
+
lastTerminalStatus: void 0,
|
|
1378
|
+
lastTerminalError: void 0,
|
|
1379
|
+
idleMs: 0,
|
|
1380
|
+
ceilingMs: 0,
|
|
1381
|
+
startedAt: Date.now(),
|
|
1382
|
+
lastEventAt: void 0,
|
|
1383
|
+
idleTimer: void 0,
|
|
1384
|
+
ceilingTimer: void 0,
|
|
1385
|
+
// 装配占位:下方 timer 创建后重绑(settle/fail 需要清理两 timer)
|
|
1386
|
+
settle: () => {
|
|
1387
|
+
},
|
|
1388
|
+
fail: () => {
|
|
1389
|
+
},
|
|
1390
|
+
fireTimeout: () => {
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
turn.idleMs = resolveTurnTimerMs({
|
|
1394
|
+
explicit: opts.idleTimeoutMs,
|
|
1395
|
+
envName: ZCODE_TURN_IDLE_TIMEOUT_ENV,
|
|
1396
|
+
fallbackMs: ZCODE_TURN_IDLE_TIMEOUT_MS,
|
|
1397
|
+
label: "idle \u4E3B\u5224\u5B9A",
|
|
1398
|
+
offConsequence: "\u9759\u9ED8 wedged\uFF08\u65E0\u4E8B\u4EF6\uFF09\u5F62\u6001\u5C06\u65E0\u81EA\u52A8\u56DE\u6536\uFF0C\u4EFB\u52A1\u53EF\u80FD\u6302\u5230\u5BBF\u4E3B\u8FDB\u7A0B\u9000\u51FA"
|
|
1399
|
+
});
|
|
1400
|
+
turn.ceilingMs = resolveTurnTimerMs({
|
|
1401
|
+
explicit: opts.turnTimeoutMs,
|
|
1402
|
+
envName: ZCODE_TURN_MAX_TIMEOUT_ENV,
|
|
1403
|
+
fallbackMs: ZCODE_TURN_MAX_TIMEOUT_MS,
|
|
1404
|
+
label: "\u603B\u4E0A\u754C",
|
|
1405
|
+
offConsequence: "chatty-wedge\uFF08\u6709\u4E8B\u4EF6\u65E0\u7EC8\u6001\uFF09\u5F62\u6001\u5C06\u65E0\u81EA\u52A8\u56DE\u6536\uFF0C\u4EC5\u5269 idle \u9759\u9ED8\u5224\u5B9A\u515C\u5E95"
|
|
1406
|
+
});
|
|
1407
|
+
const fireTimeout = (kind) => {
|
|
1408
|
+
if (turn.settled) return;
|
|
1409
|
+
turn.settled = true;
|
|
1410
|
+
clearTurnTimers(turn);
|
|
1411
|
+
this.activeTurns.delete(sessionId);
|
|
1412
|
+
rejectDone(
|
|
1413
|
+
new TurnTimeoutError(kind, {
|
|
1414
|
+
thresholdMs: kind === "idle" ? turn.idleMs : turn.ceilingMs,
|
|
1415
|
+
elapsed: Date.now() - turn.startedAt,
|
|
1416
|
+
lastEventAt: turn.lastEventAt
|
|
1417
|
+
})
|
|
1418
|
+
);
|
|
1419
|
+
};
|
|
1420
|
+
turn.fireTimeout = fireTimeout;
|
|
1421
|
+
if (turn.idleMs > 0) {
|
|
1422
|
+
turn.idleTimer = armTurnTimer(() => turn.fireTimeout("idle"), turn.idleMs);
|
|
1423
|
+
}
|
|
1424
|
+
if (turn.ceilingMs > 0) {
|
|
1425
|
+
turn.ceilingTimer = armTurnTimer(
|
|
1426
|
+
() => turn.fireTimeout("ceiling"),
|
|
1427
|
+
turn.ceilingMs
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
turn.settle = (t) => {
|
|
1431
|
+
if (turn.settled) return;
|
|
1432
|
+
turn.settled = true;
|
|
1433
|
+
turn.terminal = t;
|
|
1434
|
+
clearTurnTimers(turn);
|
|
1435
|
+
resolveDone(t);
|
|
1436
|
+
};
|
|
1437
|
+
turn.fail = (err) => {
|
|
1438
|
+
if (turn.settled) return;
|
|
1439
|
+
turn.settled = true;
|
|
1440
|
+
clearTurnTimers(turn);
|
|
1441
|
+
rejectDone(err);
|
|
1442
|
+
};
|
|
1443
|
+
this.activeTurns.set(sessionId, turn);
|
|
1444
|
+
return { turn, done, stopTimers: () => clearTurnTimers(turn) };
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* idle 主判定的事件刷新(P0-1 D1):本 turn 任何事件到达即重置 idle 倒数——
|
|
1448
|
+
* 活跃事件流零误杀的结构保证。总上界不受影响(固定倒数)。已落定 turn 无
|
|
1449
|
+
* timer 可刷新(只剩 lastEventAt 记账)。
|
|
1450
|
+
*/
|
|
1451
|
+
refreshIdle(turn) {
|
|
1452
|
+
turn.lastEventAt = Date.now();
|
|
1453
|
+
if (turn.settled || turn.idleTimer === void 0) return;
|
|
1454
|
+
clearTimeout(turn.idleTimer);
|
|
1455
|
+
turn.idleTimer = armTurnTimer(() => turn.fireTimeout("idle"), turn.idleMs);
|
|
1456
|
+
}
|
|
1457
|
+
lookupTurn(sessionId) {
|
|
1458
|
+
if (sessionId !== void 0) return this.activeTurns.get(sessionId);
|
|
1459
|
+
const pending = [];
|
|
1460
|
+
for (const t of this.activeTurns.values()) if (!t.settled) pending.push(t);
|
|
1461
|
+
return pending.length === 1 ? pending[0] : void 0;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* turn.terminal 专用归因(P0-1 U1/S5 归因放宽):在 `lookupTurn` 基础上放宽
|
|
1465
|
+
* 「已落定 turn 不再受理」——权威终态晚于 final-frame 宽松终态到达(runTurn
|
|
1466
|
+
* 收尾窗口内 turn 仍在册)时仍归因,**只记录 status 不改写落定结果**(D5①)。
|
|
1467
|
+
* 带 sid 精确归因(落定与否均可);无 sid 仍守宁丢勿错:唯一未落定优先,
|
|
1468
|
+
* 全部落定且在册仅剩一个(单任务收尾窗口的典型形态)才归因,多在途丢弃。
|
|
1469
|
+
*/
|
|
1470
|
+
lookupTurnForTerminal(sessionId) {
|
|
1471
|
+
if (sessionId !== void 0) return this.activeTurns.get(sessionId);
|
|
1472
|
+
const pending = [];
|
|
1473
|
+
for (const t of this.activeTurns.values()) if (!t.settled) pending.push(t);
|
|
1474
|
+
if (pending.length === 1) return pending[0];
|
|
1475
|
+
if (pending.length === 0 && this.activeTurns.size === 1) {
|
|
1476
|
+
return [...this.activeTurns.values()][0];
|
|
1477
|
+
}
|
|
1478
|
+
return void 0;
|
|
1479
|
+
}
|
|
1480
|
+
handleSessionEvent(params) {
|
|
1481
|
+
const turn = this.lookupTurn(extractPushSessionId(params));
|
|
1482
|
+
if (turn === void 0) return;
|
|
1483
|
+
const payload = isRecord3(params) && isRecord3(params.payload) ? params.payload : void 0;
|
|
1484
|
+
if (payload === void 0) return;
|
|
1485
|
+
this.refreshIdle(turn);
|
|
1486
|
+
if (this.applyFinalFrame(turn, payload)) return;
|
|
1487
|
+
this.applyStreamDelta(turn, payload);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* 收尾帧判定与吸收(A.2 ⑤ 两种形态:payload.response / stopReason+content 变体)。
|
|
1491
|
+
* 命中任一形态则落 finalText/finalUsage 并以 final-frame 宽松终态收口,返回 true。
|
|
1492
|
+
*/
|
|
1493
|
+
applyFinalFrame(turn, payload) {
|
|
1494
|
+
if (typeof payload.response === "string" && payload.response !== "") {
|
|
1495
|
+
turn.finalText = payload.response;
|
|
1496
|
+
if (isRecord3(payload.usage)) turn.finalUsage = payload.usage;
|
|
1497
|
+
turn.settle({ status: "success", source: "final-frame" });
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
if (payload.stopReason === "stop" && typeof payload.content === "string" && payload.content !== "") {
|
|
1501
|
+
turn.finalText = payload.content;
|
|
1502
|
+
if (isRecord3(payload.usage)) turn.finalUsage = payload.usage;
|
|
1503
|
+
turn.settle({ status: "success", source: "final-frame" });
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
/** 增量 delta 分发:非空 delta 入账 + 回调;终态后迟到丢弃(不变量 2)。
|
|
1509
|
+
* kind 区分两通道(F2):reasoning_delta 只走 thinking 回调(不入 deltas 账本
|
|
1510
|
+
* ——deltas 是 response 兜底聚合,推理文本不是答案正文);text_delta / kind
|
|
1511
|
+
* 缺席走 text 路径。 */
|
|
1512
|
+
applyStreamDelta(turn, payload) {
|
|
1513
|
+
if (typeof payload.delta !== "string" || payload.delta === "") return;
|
|
1514
|
+
if (turn.settled) {
|
|
1515
|
+
logger2.warn(
|
|
1516
|
+
`\u7EC8\u6001\u540E\u8FDF\u5230\u7684 delta \u4E22\u5F03\uFF08\u4F1A\u8BDD ${turn.sessionId}\uFF0C\u4E0D\u53D8\u91CF 2\uFF1Aresolve \u540E\u4E0D\u518D\u53D1\u4E8B\u4EF6\uFF09: ${payload.delta.slice(
|
|
1517
|
+
0,
|
|
1518
|
+
DELTA_LOG_CHARS
|
|
1519
|
+
)}`
|
|
1520
|
+
);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (payload.kind === "reasoning_delta") {
|
|
1524
|
+
turn.callbacks.onThinkingDelta?.(payload.delta);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
turn.deltas.push(payload.delta);
|
|
1528
|
+
turn.callbacks.onTextDelta?.(payload.delta);
|
|
1529
|
+
}
|
|
1530
|
+
handleTelemetry(params) {
|
|
1531
|
+
if (!isRecord3(params)) return;
|
|
1532
|
+
if (params.kind === "turn.terminal") {
|
|
1533
|
+
this.applyTerminalTelemetry(params);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (params.kind === "stream.chunk") {
|
|
1537
|
+
this.applyStreamChunkTelemetry(params);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
/** turn.terminal 遥测的吸收(含已落定 turn 的只记录路径,见 D5①)。 */
|
|
1541
|
+
applyTerminalTelemetry(params) {
|
|
1542
|
+
const turn = this.lookupTurnForTerminal(extractPushSessionId(params));
|
|
1543
|
+
if (turn === void 0) return;
|
|
1544
|
+
const status = typeof params.status === "string" ? params.status : "unknown";
|
|
1545
|
+
turn.lastTerminalStatus = status;
|
|
1546
|
+
this.recordTerminalError(turn, params);
|
|
1547
|
+
if (turn.settled) {
|
|
1548
|
+
logger2.warn(
|
|
1549
|
+
`\u6743\u5A01\u7EC8\u6001\u665A\u4E8E\u843D\u5B9A\u7ED3\u679C\u5230\u8FBE\uFF08\u4F1A\u8BDD ${turn.sessionId}\uFF0C\u5DF2\u843D\u5B9A source=${turn.terminal?.source}\uFF09\uFF1Aturn.terminal status="${status}" \u4EC5\u8BB0\u5F55\u4E0D\u6539\u5199\uFF08P0-1 D5\u2460\uFF09`
|
|
1550
|
+
);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
turn.settle({ status, source: "turn.terminal" });
|
|
1554
|
+
}
|
|
1555
|
+
/** terminal 帧专属错误信息入账(⛔P-Z2 实证:真实 failed 终态的 errorCode/errorMessage
|
|
1556
|
+
* 只在 terminal 帧携带,read/delta 拿不到——先到/迟到都记录,消费层合成失败
|
|
1557
|
+
* 文案时优先采信)。二者均缺失时不挂 lastTerminalError 字段。 */
|
|
1558
|
+
recordTerminalError(turn, params) {
|
|
1559
|
+
const errorCode = typeof params.errorCode === "string" ? params.errorCode : void 0;
|
|
1560
|
+
const errorMessage = typeof params.errorMessage === "string" ? params.errorMessage : void 0;
|
|
1561
|
+
if (errorCode === void 0 && errorMessage === void 0) return;
|
|
1562
|
+
turn.lastTerminalError = {
|
|
1563
|
+
...errorCode !== void 0 ? { code: errorCode } : {},
|
|
1564
|
+
...errorMessage !== void 0 ? { message: errorMessage } : {}
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/** stream.chunk 遥测的吸收(无文本遥测 + 文本字段漂移兜底)。 */
|
|
1568
|
+
applyStreamChunkTelemetry(params) {
|
|
1569
|
+
const turn = this.lookupTurn(extractPushSessionId(params));
|
|
1570
|
+
if (turn === void 0 || turn.settled) return;
|
|
1571
|
+
this.refreshIdle(turn);
|
|
1572
|
+
for (const key of ["chunk", "text", "content"]) {
|
|
1573
|
+
const v = params[key];
|
|
1574
|
+
if (typeof v === "string" && v !== "") {
|
|
1575
|
+
turn.deltas.push(v);
|
|
1576
|
+
turn.callbacks.onTextDelta?.(v);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/** 终态后 read(A.2 ⑥)。失败/超时不抛——降级链由 runTurn 收口(read 只兜底不致命)。
|
|
1582
|
+
* 连接已死时跳过(与 closeSession 同判——对死连接发请求会凭空重建进程)。 */
|
|
1583
|
+
async readBestEffort(sessionId) {
|
|
1584
|
+
if (!this.conn.alive) return void 0;
|
|
1585
|
+
try {
|
|
1586
|
+
return await this.conn.request(
|
|
1587
|
+
"session/read",
|
|
1588
|
+
{ sessionId },
|
|
1589
|
+
{
|
|
1590
|
+
timeoutMs: ZCODE_APPSERVER_TURN_READ_TIMEOUT_MS
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
logger2.warn(
|
|
1595
|
+
`session/read \u515C\u5E95\u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0C\u964D\u7EA7\u6536\u5C3E\u5E27/delta \u805A\u5408\uFF09: ${errMessage2(
|
|
1596
|
+
err
|
|
1597
|
+
)}`
|
|
1598
|
+
);
|
|
1599
|
+
return void 0;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
// src/zcode-engine.ts
|
|
1605
|
+
var logger3 = (0, import_subagent_engine_sdk8.getLogger)("subagents");
|
|
1606
|
+
var PROBE_VERSION_TIMEOUT_MS = 15e3;
|
|
1607
|
+
var COMMON_THOUGHT_LEVELS = ["low", "high", "max"];
|
|
1608
|
+
var ZcodeEngine = class {
|
|
1609
|
+
id = ZCODE_ENGINE_ID;
|
|
1610
|
+
deps;
|
|
1611
|
+
probeCache;
|
|
1612
|
+
appserverRuntime;
|
|
1613
|
+
/**
|
|
1614
|
+
* [P0-1 U4] 引擎停机标志(dispose 置位,不重置——dispose 后首个 run 走重建路径
|
|
1615
|
+
* 不受影响):瞬时重试判定据此排除 dispose 收割引发的崩溃形态——停机后的重试轮
|
|
1616
|
+
* 会经 ensureAppServerRuntime 惰性重建进程(复活已停机引擎),违背 dispose 防泄漏
|
|
1617
|
+
* 语义。
|
|
1618
|
+
*/
|
|
1619
|
+
disposed = false;
|
|
1620
|
+
constructor(deps) {
|
|
1621
|
+
this.deps = deps;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* zcode 链路实际接通的能力(D3 链路口径。声明升级必须先改链路再改声明(C4 原则)。
|
|
1625
|
+
*/
|
|
1626
|
+
capabilities() {
|
|
1627
|
+
return {
|
|
1628
|
+
// 无 --json-schema 类通道;公共 schema 仿真层(prompt 约定 + 容错提取 + ajv)
|
|
1629
|
+
schemaEnforcement: "emulated",
|
|
1630
|
+
// send-while-running 恒 -32010 硬错误(旧实测)——app-server 常驻化不改变此判据
|
|
1631
|
+
steer: "unsupported",
|
|
1632
|
+
// 无同进程 idle 复用(D4:每任务自包含 create→run→close)
|
|
1633
|
+
conversation: "unsupported",
|
|
1634
|
+
// 无 --append-system-prompt flag(实测拒收)——persona 只能拼进 prompt
|
|
1635
|
+
personaInjection: "prompt",
|
|
1636
|
+
// app-server 推送流实时流出(session/event payload.delta → text_delta)
|
|
1637
|
+
eventGranularity: "stream",
|
|
1638
|
+
// 首期未接 worktree 隔离(公共层 worktree-manager 接入后升 emulated)
|
|
1639
|
+
sandbox: "none",
|
|
1640
|
+
// sqlite 三级 JOIN 完整重建 turns(reader 实测)
|
|
1641
|
+
sessionRead: "full",
|
|
1642
|
+
// --resume 冷启动可用(实测)
|
|
1643
|
+
resume: "cold",
|
|
1644
|
+
// abort 走 D3 链(stop→grace→killChain)但声明维持 kill-only 不升级(改链路
|
|
1645
|
+
// 先于改声明;stop 链路经 conformance 真机验证后再评估升 native)
|
|
1646
|
+
interrupt: "kill-only",
|
|
1647
|
+
// --mode build/edit/plan/yolo 原生权限档位
|
|
1648
|
+
permissionMode: "native",
|
|
1649
|
+
// [D3-④] 无 turn_end 语义,轮数上限不可兑现(预检 gate 据此同步拒绝)
|
|
1650
|
+
maxTurns: false
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/** 探针(D7):二进制存在 + 版本解析(zcode 无公开契约,版本漂移的入口信号)。 */
|
|
1654
|
+
async probe(opts) {
|
|
1655
|
+
if (!opts?.force && this.probeCache) return this.probeCache;
|
|
1656
|
+
const cliPath = this.deps.cliPath ?? ZCODE_CLI_DEFAULT_PATH;
|
|
1657
|
+
const binary = this.probeBinaryCheck(cliPath);
|
|
1658
|
+
const checks = [binary];
|
|
1659
|
+
let engineVersion = "";
|
|
1660
|
+
if (binary.ok) {
|
|
1661
|
+
const version = await this.probeVersionCheck(cliPath);
|
|
1662
|
+
checks.push(version.check);
|
|
1663
|
+
engineVersion = version.engineVersion;
|
|
1664
|
+
}
|
|
1665
|
+
const ok = checks.every((c) => c.ok);
|
|
1666
|
+
const report = {
|
|
1667
|
+
ok,
|
|
1668
|
+
engineVersion,
|
|
1669
|
+
checks,
|
|
1670
|
+
...ok ? {} : { error: this.probeFailureRecovery(cliPath) }
|
|
1671
|
+
};
|
|
1672
|
+
this.probeCache = report;
|
|
1673
|
+
return report;
|
|
1674
|
+
}
|
|
1675
|
+
/** check 1:二进制存在性(isFile 才算——同名目录不是可执行入口)。 */
|
|
1676
|
+
probeBinaryCheck(cliPath) {
|
|
1677
|
+
const binaryOk = fs5.existsSync(cliPath) && fs5.statSync(cliPath).isFile();
|
|
1678
|
+
return {
|
|
1679
|
+
name: "binary",
|
|
1680
|
+
ok: binaryOk,
|
|
1681
|
+
detail: binaryOk ? cliPath : `zcode CLI \u4E0D\u5B58\u5728\uFF1A${cliPath}`
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/** check 2:`--version` 解析(probeVersion 可注入——测试 fake 防真实子进程)。 */
|
|
1685
|
+
async probeVersionCheck(cliPath) {
|
|
1686
|
+
const runVersion = this.deps.probeVersion ?? ((cliPath2) => defaultProbeVersion(cliPath2, this.deps.engineDataDir()));
|
|
1687
|
+
const version = await runVersion(cliPath);
|
|
1688
|
+
const versionOk = version !== void 0 && version.length > 0;
|
|
1689
|
+
return {
|
|
1690
|
+
check: {
|
|
1691
|
+
name: "version",
|
|
1692
|
+
ok: versionOk,
|
|
1693
|
+
detail: versionOk ? version : "zcode --version \u8FD4\u56DE\u7A7A\u6216\u5931\u8D25"
|
|
1694
|
+
},
|
|
1695
|
+
engineVersion: version ?? ""
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
/** 探针失败的恢复指引(§3.3.3 终态四:版本确认命令 + 探针重跑 + 调研文档路径)。 */
|
|
1699
|
+
probeFailureRecovery(cliPath) {
|
|
1700
|
+
return {
|
|
1701
|
+
code: "engine_probe_failed",
|
|
1702
|
+
recovery: `Run \`node ${cliPath} --version\` \u786E\u8BA4 zcode CLI \u53EF\u7528\u4E14\u7248\u672C\u672A\u6F02\u79FB\uFF0C\u7136\u540E\u91CD\u8DD1\u63A2\u9488\uFF08\u91CD\u65B0\u521D\u59CB\u5316\u5F15\u64CE\u6216 probe({force:true})\uFF09\u3002\u82E5 app-server \u534F\u8BAE\u5DF2\u6F02\u79FB\uFF08RPC \u9519\u8BEF\uFF09\uFF0C\u91CD\u542F ZCode \u6216\u56FA\u5B9A zcode \u7248\u672C\u540E\u91CD\u8BD5\u3002\u53C2\u7167 docs/research/agent-engine-zcode.md\u3002`
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/** D1 主语义:唯一通道 = app-server 常驻连接(spawn 降级链已删除)。 */
|
|
1706
|
+
async run(task, ctx) {
|
|
1707
|
+
return this.runViaAppServer(task, ctx);
|
|
1708
|
+
}
|
|
1709
|
+
// ============================================================
|
|
1710
|
+
// app-server 常驻路径(D1/D3/D4)
|
|
1711
|
+
// ============================================================
|
|
1712
|
+
/**
|
|
1713
|
+
* 常驻路径主编排:模型解析(v2 单源校验)→ 惰性连接 + runTurn(事件时序前移:
|
|
1714
|
+
* text_delta 流式、终态后 message_end/turn_end)→ schema 仿真重试 → outcome/handle。
|
|
1715
|
+
* poolKey 固定 'shared'(共享宿主 HOME,无池),onPoolResolved 在 prepare 期、
|
|
1716
|
+
* onHandleReady 在 create 应答后(§3.4 不变量 3)。
|
|
1717
|
+
*/
|
|
1718
|
+
async runViaAppServer(task, ctx) {
|
|
1719
|
+
const startedAt = Date.now();
|
|
1720
|
+
if (ctx.signal?.aborted === true) {
|
|
1721
|
+
return this.abortedAppServerRun(task, ctx, startedAt);
|
|
1722
|
+
}
|
|
1723
|
+
const modelRef = resolveZcodeModelRef(task.model, this.deps.sources);
|
|
1724
|
+
this.warnIgnoredCtxModel(task, ctx, modelRef);
|
|
1725
|
+
this.warnThoughtLevelUncommon(task, ctx);
|
|
1726
|
+
ctx.onPoolResolved?.(ZCODE_SHARED_POOL_KEY);
|
|
1727
|
+
const cwd = task.cwd ?? process.cwd();
|
|
1728
|
+
const schema = isPlainObject(task.schema) ? task.schema : void 0;
|
|
1729
|
+
const basePrompt = this.buildPrompt(task, schema);
|
|
1730
|
+
const usageAcc = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, has: false };
|
|
1731
|
+
const final = await this.runAppServerAttemptsWithRetry(task, ctx, modelRef, cwd, basePrompt, schema, usageAcc);
|
|
1732
|
+
const outcome = this.finalizeOutcome(task, ctx, final, usageAcc, startedAt);
|
|
1733
|
+
return { handle: this.appServerHandle(outcome), outcome };
|
|
1734
|
+
}
|
|
1735
|
+
/** pre-aborted 短路收口:合成中止 outcome + 'shared' 锚定 handle。 */
|
|
1736
|
+
abortedAppServerRun(task, ctx, startedAt) {
|
|
1737
|
+
ctx.onPoolResolved?.(ZCODE_SHARED_POOL_KEY);
|
|
1738
|
+
const outcome = this.finalizeOutcome(
|
|
1739
|
+
task,
|
|
1740
|
+
ctx,
|
|
1741
|
+
abortedAppServerAttempt(ctx),
|
|
1742
|
+
{ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, has: false },
|
|
1743
|
+
startedAt
|
|
1744
|
+
);
|
|
1745
|
+
return { handle: this.appServerHandle(outcome), outcome };
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* 首轮执行 + 双重试编排(常驻路径):
|
|
1749
|
+
* - **schema 仿真重试**(既有语义):parsed 但校验失败时重试一次(强化 JSON 输出
|
|
1750
|
+
* 指令——与 structured-output 的重试语义对齐)。
|
|
1751
|
+
* - **瞬时失败自动重试一次**([P0-1 U4/D6]):末次 attempt 为 timeout 类(idle/
|
|
1752
|
+
* ceiling)或连接崩溃类失败且非用户 abort → 用新会话重跑一次(attempt 本就每次
|
|
1753
|
+
* 新建会话)。重试轮 prompt 用 basePrompt 原样重跑(失败形态非 schema),文案补
|
|
1754
|
+
* 「已自动重试一次」句(retried 标记仅对真实发生的重试生效)。
|
|
1755
|
+
*
|
|
1756
|
+
* 两次重试一次封顶各自独立(D6 被否①:多次重试/指数退避不做——重跑一轮=整任务
|
|
1757
|
+
* 重算,一次封顶)。组合序:瞬时重试在前、schema 重试在后——瞬时重试轮 parsed 且
|
|
1758
|
+
* 校验失败时仍进 schema 重试(末次 attempt 语义,schema 重试编排保持现状不动)。
|
|
1759
|
+
*
|
|
1760
|
+
* 重试轮是独立会话的独立 LLM 调用:token 计入 outcome.usage 总量;事件面
|
|
1761
|
+
* text_delta 按实际流出(含失败轮——journal 记录真实流水),message_end/turn_end
|
|
1762
|
+
* 只在最终轮终态后合成(不变量 2/5)。
|
|
1763
|
+
*/
|
|
1764
|
+
async runAppServerAttemptsWithRetry(task, ctx, modelRef, cwd, basePrompt, schema, usageAcc) {
|
|
1765
|
+
const attemptStartedAt = Date.now();
|
|
1766
|
+
let final = await this.attemptAppServerTurn(task, ctx, modelRef, cwd, basePrompt);
|
|
1767
|
+
accumulateUsage(usageAcc, final);
|
|
1768
|
+
if (final.kind === "run-failed" && final.transient !== void 0 && ctx.signal?.aborted !== true && !this.disposed) {
|
|
1769
|
+
const budget = resolveTransientRetryBudget(explicitTurnBudgetMs(), Date.now() - attemptStartedAt);
|
|
1770
|
+
if (budget.state === "depleted") {
|
|
1771
|
+
logger3.warn(
|
|
1772
|
+
`[zcode-engine] \u672B\u6B21 attempt \u77AC\u65F6\u5931\u8D25\uFF08${final.transient}\uFF09\u2014\u2014\u663E\u5F0F\u603B\u4E0A\u754C\u9884\u7B97\u5269\u4F59\u4E0D\u8DB3 ${ZCODE_TURN_RETRY_MIN_BUDGET_MS}ms\uFF0C\u4E0D\u91CD\u8BD5\u76F4\u63A5\u7EC8\u6001\u5316\uFF08\u9884\u7B97\u7EE7\u627F\uFF1A\u91CD\u8BD5\u4E0D\u91CD\u7F6E\u603B\u9884\u7B97\uFF09`
|
|
1773
|
+
);
|
|
1774
|
+
} else {
|
|
1775
|
+
logger3.warn(
|
|
1776
|
+
`[zcode-engine] \u672B\u6B21 attempt \u77AC\u65F6\u5931\u8D25\uFF08${final.transient}\uFF09\u2014\u2014\u6B62\u635F\u94FE\u5DF2\u7EC8\u5C40\uFF0C\u65B0\u4F1A\u8BDD\u81EA\u52A8\u91CD\u8BD5\u4E00\u6B21` + (budget.state === "inherit" ? `\uFF08\u9884\u7B97\u7EE7\u627F\uFF1A\u91CD\u8BD5\u8F6E\u603B\u4E0A\u754C=\u5269\u4F59 ${budget.remainingMs}ms\uFF0C\u4E0D\u91CD\u7F6E\u603B\u9884\u7B97\uFF09` : "\uFF08\u65E0\u663E\u5F0F\u603B\u4E0A\u754C\u9884\u7B97\uFF0C\u91CD\u8BD5\u8F6E\u8D70 env/\u9ED8\u8BA4\u4E0A\u754C\uFF09")
|
|
1777
|
+
);
|
|
1778
|
+
const retry = await this.attemptAppServerTurn(task, ctx, modelRef, cwd, basePrompt, {
|
|
1779
|
+
// 预算继承传递点(D2 内部传参面):显式预算 → 剩余值;无显式预算 → 缺省
|
|
1780
|
+
// (channel 侧走同一 env/默认,与首轮行为一致)
|
|
1781
|
+
...budget.state === "inherit" ? { turnTimeoutMs: budget.remainingMs } : {},
|
|
1782
|
+
// 重试事实进文案:「已自动重试一次」句仅对真实发生的重试生效(未重试形态
|
|
1783
|
+
// 不含——与行为一致,§5.2 F-1/F-4)
|
|
1784
|
+
retried: true
|
|
1785
|
+
});
|
|
1786
|
+
accumulateUsage(usageAcc, retry);
|
|
1787
|
+
final = retry;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (final.kind === "parsed" && final.schemaResult !== void 0 && !final.schemaResult.ok && schema !== void 0) {
|
|
1791
|
+
const retryPrompt = appendSchemaRetryDirective(basePrompt, final.schemaResult.error);
|
|
1792
|
+
const retry = await this.attemptAppServerTurn(task, ctx, modelRef, cwd, retryPrompt);
|
|
1793
|
+
accumulateUsage(usageAcc, retry);
|
|
1794
|
+
final = retry;
|
|
1795
|
+
}
|
|
1796
|
+
return final;
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* 常驻路径的 handle 合成(poolKey 固定 'shared';dbPath = 隔离会话库绝对路径——
|
|
1800
|
+
* 2026-09 会话库隔离,与 spawn env 同源 zcodeSessionDbPath(engineDataDir),设计 D1)。
|
|
1801
|
+
*/
|
|
1802
|
+
appServerHandle(outcome) {
|
|
1803
|
+
return {
|
|
1804
|
+
data: {
|
|
1805
|
+
v: 1,
|
|
1806
|
+
engineId: ZCODE_ENGINE_ID,
|
|
1807
|
+
sessionRef: {
|
|
1808
|
+
dbPath: zcodeSessionDbPath(this.deps.engineDataDir()),
|
|
1809
|
+
...outcome.sessionId !== void 0 ? { sessionId: outcome.sessionId } : {}
|
|
1810
|
+
},
|
|
1811
|
+
poolKey: ZCODE_SHARED_POOL_KEY,
|
|
1812
|
+
...this.probeCache?.engineVersion !== void 0 && this.probeCache.engineVersion !== "" ? { engineVersion: this.probeCache.engineVersion } : {},
|
|
1813
|
+
adapterVersion: ZCODE_ADAPTER_VERSION
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* 单轮常驻执行:runTurn 组合面 + D3 abort 链 + 事件前移(text_delta 实时流出;
|
|
1819
|
+
* 终态数据经 read 兜底收口后才 resolve——不变量 1/2)。
|
|
1820
|
+
*
|
|
1821
|
+
* @param opts turnTimeoutMs:显式总上界传参面(D2 内部传参点)——U4/D6 预算继承
|
|
1822
|
+
* 向重试轮传剩余值;缺省不传(channel 走 env→默认,首轮行为)。
|
|
1823
|
+
* retried:瞬时重试轮标记——失败文案补「已自动重试一次」句(F-1/F-4)。
|
|
1824
|
+
*/
|
|
1825
|
+
async attemptAppServerTurn(task, ctx, modelRef, cwd, prompt, opts = {}) {
|
|
1826
|
+
const rt = this.ensureAppServerRuntime();
|
|
1827
|
+
const { providerId, modelId } = splitZcodeModelRef(modelRef);
|
|
1828
|
+
const createParams = buildAppServerCreateParams(task, providerId, modelId, cwd);
|
|
1829
|
+
let currentSessionId;
|
|
1830
|
+
let signalSessionCreated;
|
|
1831
|
+
const sessionCreated = new Promise((resolve) => {
|
|
1832
|
+
signalSessionCreated = resolve;
|
|
1833
|
+
});
|
|
1834
|
+
const turn = rt.channel.runTurn(createParams, prompt, {
|
|
1835
|
+
// 事件时序前移:payload.delta → text_delta 实时流出(stream 粒度,D5);
|
|
1836
|
+
// reasoning_delta → thinking_delta 分流(F2 口径:answer 通道仅 text_delta,
|
|
1837
|
+
// 不变量 3a 的拼接比对不含 reasoning)
|
|
1838
|
+
onTextDelta: (delta) => ctx.onEvent?.({ type: "text_delta", delta }),
|
|
1839
|
+
onThinkingDelta: (delta) => ctx.onEvent?.({ type: "thinking_delta", delta }),
|
|
1840
|
+
onSessionCreated: (sessionId) => {
|
|
1841
|
+
currentSessionId = sessionId;
|
|
1842
|
+
rt.activeSessions.add(sessionId);
|
|
1843
|
+
signalSessionCreated?.();
|
|
1844
|
+
ctx.onHandleReady?.({
|
|
1845
|
+
sessionRef: { dbPath: zcodeSessionDbPath(this.deps.engineDataDir()), sessionId },
|
|
1846
|
+
poolKey: ZCODE_SHARED_POOL_KEY
|
|
1847
|
+
});
|
|
1848
|
+
},
|
|
1849
|
+
// 显式总上界传参(D2/U4:缺省缺席——channel 侧 resolveTurnTimerMs 走 env→默认)
|
|
1850
|
+
...opts.turnTimeoutMs !== void 0 ? { turnTimeoutMs: opts.turnTimeoutMs } : {}
|
|
1851
|
+
});
|
|
1852
|
+
const onAbort = () => {
|
|
1853
|
+
void this.appServerAbortChain(rt, turn, () => currentSessionId, sessionCreated, { escalateOn: "turn-settled" });
|
|
1854
|
+
};
|
|
1855
|
+
if (ctx.signal !== void 0) {
|
|
1856
|
+
if (ctx.signal.aborted) onAbort();
|
|
1857
|
+
else ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
const r = await turn;
|
|
1861
|
+
if (ctx.signal?.aborted === true) return abortedAppServerAttempt(ctx);
|
|
1862
|
+
return parsedAppServerAttempt(task, r);
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
if (ctx.signal?.aborted === true) return abortedAppServerAttempt(ctx);
|
|
1865
|
+
if (err instanceof TurnTimeoutError) {
|
|
1866
|
+
const stopPath = await this.appServerAbortChain(
|
|
1867
|
+
rt,
|
|
1868
|
+
turn,
|
|
1869
|
+
() => currentSessionId,
|
|
1870
|
+
sessionCreated,
|
|
1871
|
+
{ escalateOn: "stop-outcome" }
|
|
1872
|
+
);
|
|
1873
|
+
return timeoutAppServerAttempt(err, currentSessionId, stopPath, { retried: opts.retried });
|
|
1874
|
+
}
|
|
1875
|
+
if (!isAppServerRpcError(err) && !rt.conn.alive) {
|
|
1876
|
+
return failedAppServerAttempt(err, currentSessionId, { retried: opts.retried, transient: "conn-closed" });
|
|
1877
|
+
}
|
|
1878
|
+
return failedAppServerAttempt(err, currentSessionId, { retried: opts.retried });
|
|
1879
|
+
} finally {
|
|
1880
|
+
if (currentSessionId !== void 0) rt.activeSessions.delete(currentSessionId);
|
|
1881
|
+
if (ctx.signal !== void 0) ctx.signal.removeEventListener("abort", onAbort);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* killChain 后等待连接 finalize 实际完成(child 置空 + onClose 广播)再宣告链终局:
|
|
1886
|
+
* shutdown resolve 于 `exit` 事件,而 finalize 挂 `close`(stdio 排空)——两者之间的
|
|
1887
|
+
* 事件窗口内 conn.child 仍非 null,紧接的下一任务 request 会复用垂死进程(写入成功
|
|
1888
|
+
* 但必败,走崩溃路径)而非触发重建。与 shutdownRuntimeAndDisposeChannel 的
|
|
1889
|
+
* HARVEST_GRACE 同款 race 形态(close 永不到达不挂死)。超时入口的 await 链终局
|
|
1890
|
+
* 语义(D6 重试时序依据)因此是「进程收割确认完成」而非「SIGTERM 已发出」。
|
|
1891
|
+
*/
|
|
1892
|
+
async awaitConnFinalized(rt) {
|
|
1893
|
+
if (!rt.conn.alive) return;
|
|
1894
|
+
await new Promise((resolve) => {
|
|
1895
|
+
const timer = setTimeout(() => finish(), ZCODE_APPSERVER_HARVEST_GRACE_MS);
|
|
1896
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1897
|
+
const off = rt.conn.onClose(() => finish());
|
|
1898
|
+
function finish() {
|
|
1899
|
+
clearTimeout(timer);
|
|
1900
|
+
off();
|
|
1901
|
+
resolve();
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* D3 abort 链执行体(双入口分岔,P0-1 U2 参数化——设计
|
|
1907
|
+
* timeout-zcode-turn-and-settled-watchdog.md §6 D3 v1.1):
|
|
1908
|
+
* - **用户取消入口**(`escalateOn: "turn-settled"`,现状语义零改动;fire-and-forget——
|
|
1909
|
+
* 与 turn promise 并行推进):stop 帧(超时 ZCODE_APPSERVER_STOP_TIMEOUT_MS)→
|
|
1910
|
+
* grace 窗口内 turn 落定即止(不杀共享进程)→ 超窗 killChain(conn.shutdown 全序:
|
|
1911
|
+
* SIGTERM→grace→SIGKILL)。turn 的最终落定由 attempt 主路径 await 收口,本链不
|
|
1912
|
+
* 直接产出终态。abort 与 create 竞态(signal 先到、session 未建立):等会话建立
|
|
1913
|
+
* (带上限)再发 stop——否则 stop 永远发不出,直接连坐杀共享进程。
|
|
1914
|
+
* - **超时入口**(`escalateOn: "stop-outcome"`,channel 判死后由 catch await 链终局):
|
|
1915
|
+
* turn 已 reject,对它 race 恒真不可用(v1 击穿点)——升级判据改挂在 **stop 应答
|
|
1916
|
+
* 三态**:①成功应答 → 服务端接受停 turn,止损确认,链终止;②协议性 error 应答
|
|
1917
|
+
* (有 error 帧即控制面活的证据,多因 runTurn finally 的 closeSession 先行关会话,
|
|
1918
|
+
* 健康形态竞态)→ 链终止**不升级**(止损由 close 回收 + 服务端自治承担;把
|
|
1919
|
+
* 「stop 报错」一律升级会误杀健康共享进程并连坐并发任务);③超时/写入失败/进程
|
|
1920
|
+
* 死等连接级失败(控制面死)→ killChain 升级。判据实现依据:error 应答帧 reject
|
|
1921
|
+
* 携带 number code(isAppServerRpcError);连接级失败是无 code 的新 Error
|
|
1922
|
+
* (connection.request 三态 reject 形态)。返回值即止损路径(超时入口的 outcome
|
|
1923
|
+
* 文案素材——D3 强制可观测面)。
|
|
1924
|
+
*/
|
|
1925
|
+
async appServerAbortChain(rt, turn, getSessionId, sessionCreated, entry) {
|
|
1926
|
+
const graceRaceThenKill = async () => {
|
|
1927
|
+
const settled = await Promise.race([
|
|
1928
|
+
turn.then(
|
|
1929
|
+
() => true,
|
|
1930
|
+
() => true
|
|
1931
|
+
),
|
|
1932
|
+
delayResolved(ZCODE_APPSERVER_ABORT_GRACE_MS, false)
|
|
1933
|
+
]);
|
|
1934
|
+
if (settled) return "settled-in-grace";
|
|
1935
|
+
logger3.warn(
|
|
1936
|
+
`[zcode-engine] abort grace \u7A97\u53E3\u5185\u672A\u89C1\u7EC8\u6001\u2014\u2014killChain \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF08\u63A5\u53D7\u8FDE\u5750\uFF0C\u5728\u9014\u4EFB\u52A1\u8D70\u5D29\u6E83\u8DEF\u5F84\uFF09`
|
|
1937
|
+
);
|
|
1938
|
+
await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
|
|
1939
|
+
await this.awaitConnFinalized(rt);
|
|
1940
|
+
return "escalated-kill";
|
|
1941
|
+
};
|
|
1942
|
+
let sessionId = getSessionId();
|
|
1943
|
+
if (sessionId === void 0) {
|
|
1944
|
+
await Promise.race([sessionCreated, delayResolved(ZCODE_APPSERVER_STOP_TIMEOUT_MS, void 0)]);
|
|
1945
|
+
sessionId = getSessionId();
|
|
1946
|
+
if (sessionId === void 0) {
|
|
1947
|
+
if (entry.escalateOn === "stop-outcome") return "no-session";
|
|
1948
|
+
return graceRaceThenKill();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (!rt.conn.alive) {
|
|
1952
|
+
if (entry.escalateOn === "stop-outcome") return "stop-unreachable-killed";
|
|
1953
|
+
return graceRaceThenKill();
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
await rt.conn.request("session/stop", { sessionId }, { timeoutMs: ZCODE_APPSERVER_STOP_TIMEOUT_MS });
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
if (entry.escalateOn === "stop-outcome") {
|
|
1959
|
+
if (isAppServerRpcError(err)) {
|
|
1960
|
+
logger3.debug(
|
|
1961
|
+
`[zcode-engine] session/stop \u62A5\u534F\u8BAE\u6027 error\uFF08${errMessage3(err)}\uFF09\u2014\u2014\u4F1A\u8BDD\u5DF2\u56DE\u6536\uFF0C\u63A7\u5236\u9762\u5B58\u6D3B\uFF0C\u4E0D\u5347\u7EA7\u6740\u94FE`
|
|
1962
|
+
);
|
|
1963
|
+
return "stop-rejected";
|
|
1964
|
+
}
|
|
1965
|
+
logger3.warn(
|
|
1966
|
+
`[zcode-engine] session/stop \u65E0\u5E94\u7B54\uFF08${errMessage3(err)}\uFF09\u2014\u2014\u5347\u7EA7 killChain \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF08\u8D85\u65F6\u5165\u53E3\uFF0C\u63A5\u53D7\u8FDE\u5750\uFF09`
|
|
1967
|
+
);
|
|
1968
|
+
await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
|
|
1969
|
+
await this.awaitConnFinalized(rt);
|
|
1970
|
+
return "stop-unreachable-killed";
|
|
1971
|
+
}
|
|
1972
|
+
logger3.debug(
|
|
1973
|
+
`[zcode-engine] session/stop \u5931\u8D25\uFF08${errMessage3(err)}\uFF09\u2014\u2014grace \u540E\u8D70 killChain \u515C\u5E95`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
if (entry.escalateOn === "stop-outcome") return "stop-acked";
|
|
1977
|
+
return graceRaceThenKill();
|
|
1978
|
+
}
|
|
1979
|
+
// ── 常驻运行时管理(D1/D6)──────────────────────
|
|
1980
|
+
/**
|
|
1981
|
+
* 惰性获取常驻运行时(D1:每引擎实例一条连接,全任务共享;连接自身的崩溃重建在
|
|
1982
|
+
* connection 层内部完成——同一条代码路径,§3.4 不变量 4)。常驻进程不进宿主
|
|
1983
|
+
* spawnedChildren、不调 onChildSpawned(D6——生命周期归 dispose)。进程级 --cwd
|
|
1984
|
+
* 用引擎数据目录(连接跨任务共享的中性位置,工作区由 create 的
|
|
1985
|
+
* workspace.workspacePath 按任务传递——D10 基线不预设任务级进程 cwd)。
|
|
1986
|
+
* spawn 经 fs 拦截 wrapper(appserver-launcher:cli config 读取重定向为
|
|
1987
|
+
* 「真实文件 + v2 provider 注入」——CLI 形态 app-server 在共享宿主 HOME 下的
|
|
1988
|
+
* 唯一凭据供数通路,机制与漂移面见该文件头注)。
|
|
1989
|
+
*/
|
|
1990
|
+
ensureAppServerRuntime() {
|
|
1991
|
+
if (this.appserverRuntime !== void 0) return this.appserverRuntime;
|
|
1992
|
+
const cliPath = this.deps.cliPath ?? ZCODE_CLI_DEFAULT_PATH;
|
|
1993
|
+
const engineDataDir = this.deps.engineDataDir();
|
|
1994
|
+
const launcherScript = ensureAppServerLauncher(engineDataDir);
|
|
1995
|
+
const env = buildAppServerEnv(this.deps.processEnv ?? process.env);
|
|
1996
|
+
env.ZCODE_ENG_CLI_PATH = cliPath;
|
|
1997
|
+
env.ZCODE_ENG_V2_CONFIG = this.deps.sources?.v2ConfigPath ?? defaultV2ConfigPath();
|
|
1998
|
+
const sessionDbPath = zcodeSessionDbPath(engineDataDir);
|
|
1999
|
+
env.ZCODE_SESSION_DB_PATH = sessionDbPath;
|
|
2000
|
+
delete env.ZCODE_SESSION_DB;
|
|
2001
|
+
fs5.mkdirSync(path4.dirname(sessionDbPath), { recursive: true });
|
|
2002
|
+
const conn = new AppServerConnection({
|
|
2003
|
+
cliPath,
|
|
2004
|
+
dataDir: engineDataDir,
|
|
2005
|
+
cwd: engineDataDir,
|
|
2006
|
+
env,
|
|
2007
|
+
launcherScript,
|
|
2008
|
+
stderrLogPath: path4.join(engineDataDir, "logs", "zcode-appserver-stderr.log"),
|
|
2009
|
+
// [W11] 实例维度 tee 路径(设计 §3.9):文件名带当前代 app-server pid——
|
|
2010
|
+
// 双实例(pi 宿主 + runtime 各一个引擎 CLI)并发 append/轮转互不干扰。
|
|
2011
|
+
stderrLogPathResolver: (pid) => typeof pid === "number" ? stderrLogPathFor(engineDataDir, pid) : path4.join(engineDataDir, "logs", "zcode-appserver-stderr.log")
|
|
2012
|
+
});
|
|
2013
|
+
const rt = {
|
|
2014
|
+
conn,
|
|
2015
|
+
channel: new SessionChannel(conn),
|
|
2016
|
+
activeSessions: /* @__PURE__ */ new Set()
|
|
2017
|
+
};
|
|
2018
|
+
this.appserverRuntime = rt;
|
|
2019
|
+
return rt;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* [R5 修复 R4 既有竞态] shutdown → 等崩溃收割实际发生 → channel 退订。killChain 在
|
|
2023
|
+
* `exit` 事件 resolve,而连接 finalize(onClose → channel 的 failAllTurns)挂
|
|
2024
|
+
* `close` 事件——两者之间有一个事件循环窗口:shutdown resolve 后立即退订,在途
|
|
2025
|
+
* turn 会错过收割、挂到 turnTimeoutMs(300s)。退订前等 onClose 触发(本方法先于
|
|
2026
|
+
* shutdown 订阅;channel 的订阅在构造期更早——其 failAllTurns 先于本 promise
|
|
2027
|
+
* resolve 执行);ZCODE_APPSERVER_HARVEST_GRACE_MS 兜底防 `close` 永不到达时挂死。
|
|
2028
|
+
* [P0-1 U5/D7] grace race 输掉(close 迟到/永不到达——stdio 被孙进程持有排空不
|
|
2029
|
+
* 尽等病态形态)时,channel.dispose() 内置的 dispose 收割(failAllTurns 先于退订,
|
|
2030
|
+
* SessionChannel.dispose)兜底在途 turn——退化终点从「挂满 turn 自身 idle/总上界
|
|
2031
|
+
* 预算」收敛为「grace 窗口内明确失败」(设计 §3.4 退化路径闭合);race 窗口与
|
|
2032
|
+
* awaitConnFinalized 同源同量级(ZCODE_APPSERVER_HARVEST_GRACE_MS)。正常 close
|
|
2033
|
+
* 先到时 onClose 收割先行,dispose 收割幂等 no-op(零回归)。
|
|
2034
|
+
*/
|
|
2035
|
+
async shutdownRuntimeAndDisposeChannel(rt) {
|
|
2036
|
+
const harvested = new Promise((resolve) => {
|
|
2037
|
+
const off = rt.conn.onClose(() => {
|
|
2038
|
+
off();
|
|
2039
|
+
resolve();
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
|
|
2043
|
+
await Promise.race([harvested, delayResolved(ZCODE_APPSERVER_HARVEST_GRACE_MS, void 0)]);
|
|
2044
|
+
rt.channel.dispose();
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* [R1 D6 主体] 引擎停机面:①fire 全部在途会话的 session/close 帧(不等待
|
|
2048
|
+
* 应答——D6① 顺序规定:close 帧必须先于 SIGTERM,否则对面来不及处理即被杀)→
|
|
2049
|
+
* ②同步 SIGTERM(conn.shutdown 调用内 killChain 前缀同步执行——同步面在返回
|
|
2050
|
+
* Promise 前完成)→ ③grace → SIGKILL(异步面,Promise resolve 于进程退出)。
|
|
2051
|
+
* 幂等:运行时字段取走即置空,二次调用零副作用;dispose 后首个 run 经
|
|
2052
|
+
* ensureAppServerRuntime 自动重建(与崩溃重建同一代码路径,不变量 4)。
|
|
2053
|
+
*/
|
|
2054
|
+
async dispose() {
|
|
2055
|
+
this.disposed = true;
|
|
2056
|
+
const rt = this.appserverRuntime;
|
|
2057
|
+
if (rt === void 0) return;
|
|
2058
|
+
this.appserverRuntime = void 0;
|
|
2059
|
+
if (rt.conn.alive) {
|
|
2060
|
+
for (const sessionId of [...rt.activeSessions]) {
|
|
2061
|
+
rt.conn.post("session/close", { sessionId });
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
await this.shutdownRuntimeAndDisposeChannel(rt);
|
|
2065
|
+
}
|
|
2066
|
+
/** 终态合成(extension-conventions 函数 80 行上限,从 run 提取):aborted / run-failed / parsed 三分支。 */
|
|
2067
|
+
finalizeOutcome(task, ctx, final, usageAcc, startedAt) {
|
|
2068
|
+
const outcome = {
|
|
2069
|
+
engineId: ZCODE_ENGINE_ID,
|
|
2070
|
+
content: "",
|
|
2071
|
+
durationMs: Date.now() - startedAt,
|
|
2072
|
+
...typeof task.worktree === "object" && task.worktree !== null ? { worktreePath: task.worktree.path } : {},
|
|
2073
|
+
// D9① fallback 留痕:路由层经 RunContext 投影(zcode 无 record 通路,outcome 是
|
|
2074
|
+
// 唯一留痕面;pi 引擎另有 record 投影)
|
|
2075
|
+
...ctx.engineFallback !== void 0 ? { engineFallback: ctx.engineFallback } : {}
|
|
2076
|
+
};
|
|
2077
|
+
const emit = (event) => {
|
|
2078
|
+
ctx.onEvent?.(event);
|
|
2079
|
+
};
|
|
2080
|
+
if (final.kind === "aborted") {
|
|
2081
|
+
this.applyAbortedOutcome(outcome, task, ctx, final, emit);
|
|
2082
|
+
} else if (final.kind === "run-failed") {
|
|
2083
|
+
this.applyRunFailedOutcome(outcome, final, emit);
|
|
2084
|
+
} else {
|
|
2085
|
+
this.applyParsedOutcome(outcome, final, usageAcc, emit);
|
|
2086
|
+
}
|
|
2087
|
+
return outcome;
|
|
2088
|
+
}
|
|
2089
|
+
/** abort 合成终态:exitCode=null(record 正常收尾,不留僵尸)。 */
|
|
2090
|
+
applyAbortedOutcome(outcome, task, ctx, final, emit) {
|
|
2091
|
+
outcome.exitCode = null;
|
|
2092
|
+
outcome.error = isHostTimeoutAbort(ctx) ? (0, import_subagent_engine_sdk9.synthesizeTimeoutOutcome)(task, final.output.stdoutText, ZCODE_ENGINE_ID).error ?? (0, import_subagent_engine_sdk9.engineTimeoutDetail)(final.output.stdoutText) : final.abortMessage ?? `engine_run_failed: zcode \u4EFB\u52A1\u88AB\u4E2D\u6B62\uFF08app-server abort \u94FE\u6536\u53E3\uFF0C\u5BBF\u4E3B\u5408\u6210\u7EC8\u6001\uFF09\u3002\u8F93\u51FA\u5C3E\u90E8: ${final.output.stdoutText.slice(-ZCODE_ERROR_TAIL_CHARS)}`;
|
|
2093
|
+
emit({ type: "error", message: outcome.error });
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* run-failed 合成终态:错误信息由 buildAppServerRunFailedMessage 产出(已含恢复
|
|
2097
|
+
* 指引)直接透传;附带的会话 id 落 outcome.sessionId(错误规格表 -32004 行「含会话
|
|
2098
|
+
* id」——appServerHandle 据此写 handle.sessionRef,run-failed 不再恒缺)。
|
|
2099
|
+
*/
|
|
2100
|
+
applyRunFailedOutcome(outcome, final, emit) {
|
|
2101
|
+
outcome.exitCode = final.output.exitCode;
|
|
2102
|
+
if (final.sessionId !== void 0) outcome.sessionId = final.sessionId;
|
|
2103
|
+
outcome.error = final.message;
|
|
2104
|
+
emit({ type: "error", message: outcome.error });
|
|
2105
|
+
}
|
|
2106
|
+
/** parsed 合成终态:content/sessionId/usage 落位 + schema 校验分流 + coarse 事件。 */
|
|
2107
|
+
applyParsedOutcome(outcome, final, usageAcc, emit) {
|
|
2108
|
+
const payload = final.payload;
|
|
2109
|
+
outcome.content = payload.response;
|
|
2110
|
+
outcome.exitCode = final.output.exitCode;
|
|
2111
|
+
if (payload.sessionId !== void 0) outcome.sessionId = payload.sessionId;
|
|
2112
|
+
if (usageAcc.has) {
|
|
2113
|
+
const last = payload.outcomeUsage;
|
|
2114
|
+
outcome.usage = {
|
|
2115
|
+
input: usageAcc.input,
|
|
2116
|
+
output: usageAcc.output,
|
|
2117
|
+
cacheRead: usageAcc.cacheRead,
|
|
2118
|
+
cacheWrite: usageAcc.cacheWrite,
|
|
2119
|
+
cost: 0,
|
|
2120
|
+
contextTokens: last?.contextTokens ?? usageAcc.input + usageAcc.output + usageAcc.cacheRead + usageAcc.cacheWrite,
|
|
2121
|
+
turns: last?.turns ?? 1
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
if (final.schemaResult !== void 0) {
|
|
2125
|
+
if (final.schemaResult.ok) {
|
|
2126
|
+
outcome.parsedOutput = final.schemaResult.parsed;
|
|
2127
|
+
} else {
|
|
2128
|
+
outcome.error = `schema_emulation_failed: zcode \u8F93\u51FA\u7ECF\u4E24\u8F6E\uFF08\u542B\u5F3A\u5316 prompt \u91CD\u8BD5\uFF09\u4ECD\u672A\u901A\u8FC7 schema \u6821\u9A8C\u3002\u672B\u8F6E\u5931\u8D25\u539F\u56E0: ${final.schemaResult.error}\u3002\u539F\u59CB\u8F93\u51FA\u5C3E\u90E8: ${final.schemaResult.tail}\u3002\u6062\u590D\u6307\u5F15\uFF1A\u7B80\u5316 schema \u6216\u62C6\u5C0F\u4EFB\u52A1\u540E\u91CD\u6D3E\uFF1B\u9700\u8981\u5F3A schema \u7EA6\u675F\u65F6\u6539\u7528 engine: pi\uFF08native schema \u6CE8\u5165\uFF09\u3002`;
|
|
2129
|
+
emit({ type: "error", message: outcome.error });
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
for (const ev of synthesizeCoarseEvents(payload.response, payload.usage)) emit(ev);
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* D1 可选面:zcode 首期不支持 conversation(capabilities 声明)——同步拒绝、
|
|
2136
|
+
* 不创建进程,文案给可操作建议(A11)。
|
|
2137
|
+
*/
|
|
2138
|
+
async interact(_handle, _action) {
|
|
2139
|
+
return {
|
|
2140
|
+
ok: false,
|
|
2141
|
+
code: "engine_capability_unsupported",
|
|
2142
|
+
message: "zcode \u5F15\u64CE\u4E0D\u652F\u6301 conversation \u4EA4\u4E92\u63A7\u5236\u9762\uFF08capabilities.conversation = 'unsupported'\uFF0C\u6BCF\u4EFB\u52A1\u81EA\u5305\u542B\u4F1A\u8BDD\uFF0C\u65E0\u540C\u8FDB\u7A0B idle \u590D\u7528\uFF09\u3002\u6062\u590D\u6307\u5F15\uFF1A\u6539\u7528\u5355\u6B21 subagent \u8C03\u7528\u91CD\u65B0\u6D3E\u53D1\u4EFB\u52A1\uFF0C\u6216\u4F7F\u7528 engine: 'pi'\uFF08chatMode idle \u590D\u7528\uFF0C\u652F\u6301 message/close/cancel\uFF09\u3002"
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
/** [U7] 模型可发现性:v2 桌面登录态聚合(带凭据 provider × models),失败安全返回清单本身可能为空。 */
|
|
2146
|
+
listModels() {
|
|
2147
|
+
return listZcodeModels(this.deps.sources);
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* [u-h2 D2-2] 派发同步期 model 校验:委托 resolveZcodeModelRef(与 run prepare 期
|
|
2151
|
+
* 同一函数——canonicalRef 归一化、短名缺省 provider、凭据与清单校验单一权威,无双实现)。
|
|
2152
|
+
* modelRef undefined = 返回引擎缺省模型 canonical 全名(ZCODE_FALLBACK_DEFAULT_MODEL,
|
|
2153
|
+
* D2-1 ctxModel 不透传的承接面)。校验失败原样抛 ZcodePrepareError,由编排层
|
|
2154
|
+
* (engine/model-validation.ts)包装成「引擎与模型不配套」文案。
|
|
2155
|
+
*/
|
|
2156
|
+
validateModel(modelRef) {
|
|
2157
|
+
return { canonicalRef: resolveZcodeModelRef(modelRef, this.deps.sources) };
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* D6 read 三级降级:①sqlite 原生读取 → ②宿主 event journal 重放(对齐点①接线:
|
|
2161
|
+
* replayJournalToSessionView 复用 live reducer,重放等价性见 §3.3.6)→ ③outcome-only。
|
|
2162
|
+
* sessionId 缺失(解析失败的 run 无法定位 session)跳过①级;②级依赖
|
|
2163
|
+
* handle.journalPath(宿主 run 后回填)。dbPath:新 handle 恒为隔离库绝对路径
|
|
2164
|
+
* (zcodeSessionDbPath(engineDataDir),tier1 白名单集合见方法体——宿主路径仅
|
|
2165
|
+
* 「共享 HOME 时代」存量兼容);旧 records(池时代)的相对路径仍按 poolKey
|
|
2166
|
+
* 锚定解析(read 兼容旧数据,池目录不存在时自然落②级 journal 降级)。
|
|
2167
|
+
*/
|
|
2168
|
+
async read(handle) {
|
|
2169
|
+
if (handle.data.engineId !== ZCODE_ENGINE_ID) {
|
|
2170
|
+
return { engineId: ZCODE_ENGINE_ID, turns: [], source: "outcome-only" };
|
|
2171
|
+
}
|
|
2172
|
+
const sessionId = handle.data.sessionRef["sessionId"];
|
|
2173
|
+
const dbPathRaw = handle.data.sessionRef["dbPath"];
|
|
2174
|
+
if (typeof sessionId === "string" && typeof dbPathRaw === "string") {
|
|
2175
|
+
let dbPath;
|
|
2176
|
+
if (path4.isAbsolute(dbPathRaw)) {
|
|
2177
|
+
if (zcodeDbPathAllowlist(this.deps.engineDataDir()).includes(dbPathRaw)) {
|
|
2178
|
+
dbPath = dbPathRaw;
|
|
2179
|
+
} else {
|
|
2180
|
+
logger3.warn("[zcode-engine] record dbPath \u975E\u767D\u540D\u5355 db \u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u62D2\u7EDD \u2460\u7EA7\u8BFB\u53D6\u964D journal", {
|
|
2181
|
+
dbPath: dbPathRaw
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
} else {
|
|
2185
|
+
dbPath = path4.join(
|
|
2186
|
+
(0, import_subagent_engine_sdk9.resolvePoolDir)(this.deps.engineDataDir(), ZCODE_ENGINE_ID, handle.data.poolKey),
|
|
2187
|
+
dbPathRaw
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
if (dbPath !== void 0) {
|
|
2191
|
+
try {
|
|
2192
|
+
return await readZcodeSessionView(dbPath, sessionId);
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
logger3.warn("[zcode-engine] native session read failed, degrade to journal replay", {
|
|
2195
|
+
dbPath,
|
|
2196
|
+
sessionId,
|
|
2197
|
+
reason: (0, import_subagent_engine_sdk3.toErrorMessage)(err)
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
const journaled = replayJournalToSessionView(handle, ZCODE_ENGINE_ID);
|
|
2203
|
+
if (journaled !== void 0) return journaled;
|
|
2204
|
+
return { engineId: ZCODE_ENGINE_ID, turns: [], source: "outcome-only" };
|
|
2205
|
+
}
|
|
2206
|
+
// ── 内部 ──
|
|
2207
|
+
/**
|
|
2208
|
+
* [RX2-F1] appserver 路径的非常见档位提示:thinkingLevel → thoughtLevel 恒等透传
|
|
2209
|
+
* (F15a),全 7 档放行不拦截——但部分档位(off/minimal/medium/xhigh 等)不在部分
|
|
2210
|
+
* 模型的合法值域内(如 GLM-5.3 仅接受 low/high/max),app-server 侧对不支持的档位
|
|
2211
|
+
* warn-skip(会话照常但档位静默失效),调用方无从察觉。此处仅对
|
|
2212
|
+
* COMMON_THOUGHT_LEVELS 之外的档位出声一行提示(措辞是「若不支持将被忽略/回落」的
|
|
2213
|
+
* 或然警告,非无效断言);是否真不支持由目标模型决定,core 不做权威校验(引擎层
|
|
2214
|
+
* 不掌握各模型值域)。
|
|
2215
|
+
*/
|
|
2216
|
+
warnThoughtLevelUncommon(task, ctx) {
|
|
2217
|
+
const thoughtLevel = task.thinkingLevel?.trim();
|
|
2218
|
+
if (thoughtLevel === void 0 || thoughtLevel === "") return;
|
|
2219
|
+
if (COMMON_THOUGHT_LEVELS.includes(thoughtLevel)) return;
|
|
2220
|
+
logger3.warn(
|
|
2221
|
+
`[zcode-engine] thinkingLevel=${thoughtLevel} \u5DF2\u900F\u4F20\u4E3A thoughtLevel\uFF08\u975E\u5E38\u89C1\u6863\u4F4D\uFF09\uFF1A\u82E5\u76EE\u6807\u6A21\u578B\u4E0D\u652F\u6301\u8BE5\u6863\u4F4D\u5C06\u88AB\u5FFD\u7565/\u56DE\u843D\u5230\u6A21\u578B\u7F3A\u7701\u63A8\u7406\u6863\u4F4D\uFF08\u5E38\u89C1\u6863\u4F4D\uFF1A${COMMON_THOUGHT_LEVELS.join("/")}\uFF09\uFF1B\u6863\u4F4D\u662F\u5426\u751F\u6548\u4EE5\u6A21\u578B\u5B9E\u9645\u884C\u4E3A\u4E3A\u51C6`,
|
|
2222
|
+
{ taskId: ctx.taskId }
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* [F16b] ctxModel 忽略留痕:ctxModel 是 pi 链路的第三层兜底(port.ts 契约——
|
|
2227
|
+
* 依赖 pi resolveModel 链的引擎才消费它),zcode 自带 provider 体系与缺省模型
|
|
2228
|
+
* (resolveZcodeModelRef:requested > ZCODE_FALLBACK_DEFAULT_MODEL),不消费
|
|
2229
|
+
* ctxModel。「调用方给了 ctxModel 但 task.model 未显式指定」时出声一行,说明
|
|
2230
|
+
* 实际落引擎缺省模型(含实际 model id)——防静默降档无据可查。只在「ctx 有模型
|
|
2231
|
+
* 但被忽略」场景输出:显式 task.model 走正常解析链、ctx 本就无模型属预期缺省,
|
|
2232
|
+
* 均不出声(避免噪音)。
|
|
2233
|
+
*/
|
|
2234
|
+
warnIgnoredCtxModel(task, ctx, modelRef) {
|
|
2235
|
+
if (ctx.ctxModel === void 0) return;
|
|
2236
|
+
const requested = task.model?.trim();
|
|
2237
|
+
if (requested !== void 0 && requested !== "") return;
|
|
2238
|
+
logger3.warn(
|
|
2239
|
+
`[zcode-engine] ctx.ctxModel\uFF08${ctx.ctxModel.id}\uFF09\u88AB\u5FFD\u7565\u2014\u2014ctxModel \u662F pi \u94FE\u8DEF\u515C\u5E95\uFF0Czcode \u4E0D\u6D88\u8D39\uFF1Btask.model \u672A\u663E\u5F0F\u6307\u5B9A\uFF0C\u5B9E\u9645\u4F7F\u7528\u5F15\u64CE\u7F3A\u7701\u6A21\u578B ${modelRef}`,
|
|
2240
|
+
{ taskId: ctx.taskId }
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* persona 拼接后的完整 prompt(personaInjection: 'prompt'——zcode 无 flag 通道):
|
|
2245
|
+
* appendSystemPrompt 段在前(人设/约束语境——D6 合流后 persona≡skillPath+
|
|
2246
|
+
* appendSystemPrompt 平铺,由上游解析进 appendSystemPrompt),task 正文居中,
|
|
2247
|
+
* schema 仿真段尾置(common/schema-emulation 公共层产出)。
|
|
2248
|
+
*/
|
|
2249
|
+
buildPrompt(task, schema) {
|
|
2250
|
+
const segments = [...task.appendSystemPrompt ?? []];
|
|
2251
|
+
segments.push(task.prompt);
|
|
2252
|
+
if (schema !== void 0) segments.push((0, import_subagent_engine_sdk9.buildSchemaEmulationSegment)(schema));
|
|
2253
|
+
return segments.join("\n\n");
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
var ZCODE_TURN_RETRY_MIN_BUDGET_MS = 3e5;
|
|
2257
|
+
function resolveTransientRetryBudget(totalBudgetMs, consumedMs) {
|
|
2258
|
+
if (totalBudgetMs === void 0) return { state: "unbounded" };
|
|
2259
|
+
const remainingMs = totalBudgetMs - consumedMs;
|
|
2260
|
+
return remainingMs >= ZCODE_TURN_RETRY_MIN_BUDGET_MS ? { state: "inherit", remainingMs } : { state: "depleted" };
|
|
2261
|
+
}
|
|
2262
|
+
function explicitTurnBudgetMs() {
|
|
2263
|
+
const parsed = parseZcodeTurnTimeoutEnv(process.env[ZCODE_TURN_MAX_TIMEOUT_ENV]);
|
|
2264
|
+
return parsed.state === "valid" && parsed.ms > 0 ? parsed.ms : void 0;
|
|
2265
|
+
}
|
|
2266
|
+
function isPlainObject(v) {
|
|
2267
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2268
|
+
}
|
|
2269
|
+
function errMessage3(err) {
|
|
2270
|
+
return (0, import_subagent_engine_sdk3.toErrorMessage)(err);
|
|
2271
|
+
}
|
|
2272
|
+
function delayResolved(ms, value) {
|
|
2273
|
+
return new Promise((resolve) => {
|
|
2274
|
+
const t = setTimeout(() => resolve(value), ms);
|
|
2275
|
+
if (typeof t.unref === "function") t.unref();
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
function syntheticAppServerOutput(exitCode) {
|
|
2279
|
+
return { exitCode, stdoutText: "", stderrTail: "" };
|
|
2280
|
+
}
|
|
2281
|
+
function abortedAppServerAttempt(ctx) {
|
|
2282
|
+
void ctx;
|
|
2283
|
+
return {
|
|
2284
|
+
kind: "aborted",
|
|
2285
|
+
output: syntheticAppServerOutput(null),
|
|
2286
|
+
abortMessage: `engine_run_failed: zcode \u4EFB\u52A1\u88AB\u4E2D\u6B62\uFF08app-server abort \u94FE\uFF1Asession/stop \u2192 ${ZCODE_APPSERVER_ABORT_GRACE_MS}ms grace \u786E\u8BA4 \u2192 \u8D85\u65F6 killChain SIGTERM\u2192${ZCODE_KILL_GRACE_MS}ms\u2192SIGKILL \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF0C\u5728\u9014\u4EFB\u52A1\u8D70\u5D29\u6E83\u8DEF\u5F84\uFF09\u3002`
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function turnResultToPayload(r) {
|
|
2290
|
+
return {
|
|
2291
|
+
response: r.response,
|
|
2292
|
+
sessionId: r.sessionId,
|
|
2293
|
+
...mapZcodeUsage(r.usage) !== void 0 ? { usage: mapZcodeUsage(r.usage) } : {},
|
|
2294
|
+
...mapZcodeOutcomeUsage(r.usage, void 0) !== void 0 ? { outcomeUsage: mapZcodeOutcomeUsage(r.usage, void 0) } : {}
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
function buildAppServerRunFailedMessage(err, sessionId, retried = false) {
|
|
2298
|
+
if (isAppServerRpcError(err) && err.code === ZCODE_APPSERVER_ERR_MODEL_CONFIG_MISSING && /Model config is missing/.test(err.message)) {
|
|
2299
|
+
return `engine_credential_missing: app-server \u62A5 "Model config is missing"\uFF08\u5BBF\u4E3B HOME \u7684 zcode \u914D\u7F6E\u65E0\u53EF\u7528\u6A21\u578B\uFF09\u3002\u6062\u590D\u6307\u5F15\uFF1A\u5728 ZCode \u684C\u9762\u7AEF\u767B\u5F55\u5E76\u914D\u7F6E provider \u51ED\u636E\u540E\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF08\u5E38\u9A7B\u8FDE\u63A5\u5728\u5F15\u64CE\u8FDB\u7A0B\u91CD\u542F\u540E\u751F\u6548\u65B0\u51ED\u636E\uFF09\u3002`;
|
|
2300
|
+
}
|
|
2301
|
+
if (isAppServerRpcError(err) && err.code === ZCODE_APPSERVER_ERR_BUSY_SESSION) {
|
|
2302
|
+
const sid2 = sessionId !== void 0 ? `\uFF08\u4F1A\u8BDD id: ${sessionId}\uFF09` : "";
|
|
2303
|
+
return `engine_run_failed: app-server \u62A5 -32010${sid2}\uFF08send \u65F6\u8BE5\u4F1A\u8BDD\u5DF2\u6709\u8F6E\u5728\u8DD1\uFF0Cbusy \u4E0D\u6392\u961F\u4E0D\u6253\u65AD\uFF09\u3002\u5355\u4F1A\u8BDD\u4E00\u4EFB\u52A1\u662F\u7ED3\u6784\u4FDD\u8BC1\uFF0C\u51FA\u73B0\u5373 bug\uFF1B\u8BF7\u9644\u5E26 sessionId \u4E0E state \u6D41\u6C34\uFF08\u8FDE\u63A5/\u4F1A\u8BDD\u4E8B\u4EF6\u65E5\u5FD7\uFF09\u4E0A\u62A5\u95EE\u9898\u3002`;
|
|
2304
|
+
}
|
|
2305
|
+
const code = isAppServerRpcError(err) && err.code !== void 0 ? `\uFF08code ${err.code}\uFF09` : "";
|
|
2306
|
+
const sid = sessionId !== void 0 ? `\uFF08\u4F1A\u8BDD ${sessionId}\uFF09` : "";
|
|
2307
|
+
const retryNote = retried ? `\u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF08\u77AC\u65F6\u6545\u969C\u5DF2\u81EA\u52A8\u91CD\u8BD5\u4E00\u6B21\u4ECD\u5931\u8D25\uFF1B\u91CD\u8BD5\u7528\u7684\u662F\u5D29\u6E83\u540E\u81EA\u52A8\u91CD\u5EFA\u7684\u65B0\u4F1A\u8BDD\uFF09\u3002` : `\u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF08\u8FDE\u63A5\u5D29\u6E83\u540E\u81EA\u52A8\u91CD\u5EFA\u8FDB\u7A0B\uFF09\uFF1B`;
|
|
2308
|
+
return `engine_run_failed: app-server \u4F1A\u8BDD\u6267\u884C\u5931\u8D25${code}${sid}: ${errMessage3(err).slice(-ZCODE_ERROR_TAIL_CHARS)}\u3002${retryNote}\u82E5\u6301\u7EED\u5931\u8D25\uFF08\u7591\u4F3C zcode \u5347\u7EA7\u540E\u534F\u8BAE\u6F02\u79FB\u2014\u2014-32601/-32602 \u7C7B\u9519\u8BEF\uFF09\uFF0C\u91CD\u542F ZCode \u6216\u56FA\u5B9A zcode \u7248\u672C\u540E\u91CD\u8BD5\uFF0C\u6216\u6539\u7528 engine: pi\u3002`;
|
|
2309
|
+
}
|
|
2310
|
+
function accumulateUsage(acc, r) {
|
|
2311
|
+
if (r.kind !== "parsed") return;
|
|
2312
|
+
const u = r.payload.usage;
|
|
2313
|
+
if (u === void 0) return;
|
|
2314
|
+
acc.has = true;
|
|
2315
|
+
acc.input += u.input;
|
|
2316
|
+
acc.output += u.output;
|
|
2317
|
+
acc.cacheRead += u.cacheRead;
|
|
2318
|
+
acc.cacheWrite += u.cacheWrite;
|
|
2319
|
+
}
|
|
2320
|
+
function appendSchemaRetryDirective(basePrompt, validationError) {
|
|
2321
|
+
return basePrompt + `
|
|
2322
|
+
|
|
2323
|
+
## Retry: Structured Output Failed
|
|
2324
|
+
Your previous answer failed schema validation: ${validationError}
|
|
2325
|
+
Answer again. Output ONLY the JSON value conforming to the schema above \u2014 no prose, no markdown fences, no extra text.`;
|
|
2326
|
+
}
|
|
2327
|
+
function buildAppServerCreateParams(task, providerId, modelId, cwd) {
|
|
2328
|
+
const denyTools = (task.denyTools ?? []).filter((t) => typeof t === "string" && t.trim() !== "");
|
|
2329
|
+
const thoughtLevel = task.thinkingLevel?.trim();
|
|
2330
|
+
return {
|
|
2331
|
+
workspacePath: cwd,
|
|
2332
|
+
mode: "yolo",
|
|
2333
|
+
// per-session model(G3):create 参数透传(A.2 ① strict 对象)——同进程任务
|
|
2334
|
+
// 各用各的模型,互不干扰
|
|
2335
|
+
model: { providerId, modelId },
|
|
2336
|
+
...thoughtLevel !== void 0 && thoughtLevel !== "" ? { thoughtLevel } : {},
|
|
2337
|
+
...denyTools.length > 0 ? { toolDenylist: denyTools } : {}
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
function authoritativeTerminalStatus(r) {
|
|
2341
|
+
if (r.lastTerminalStatus !== void 0) return r.lastTerminalStatus;
|
|
2342
|
+
return r.terminal.source === "turn.terminal" ? r.terminal.status : void 0;
|
|
2343
|
+
}
|
|
2344
|
+
function isFailedTerminalStatus(status) {
|
|
2345
|
+
return status === "failed" || status === "error";
|
|
2346
|
+
}
|
|
2347
|
+
function parsedAppServerAttempt(task, r) {
|
|
2348
|
+
if (isFailedTerminalStatus(authoritativeTerminalStatus(r))) {
|
|
2349
|
+
return failedTerminalAppServerAttempt(r);
|
|
2350
|
+
}
|
|
2351
|
+
const schema = isPlainObject(task.schema) ? task.schema : void 0;
|
|
2352
|
+
return {
|
|
2353
|
+
kind: "parsed",
|
|
2354
|
+
output: syntheticAppServerOutput(0),
|
|
2355
|
+
payload: turnResultToPayload(r),
|
|
2356
|
+
...schema !== void 0 ? { schemaResult: (0, import_subagent_engine_sdk9.extractAndValidateStructuredOutput)(r.response, schema) } : {}
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function failedTerminalAppServerAttempt(r) {
|
|
2360
|
+
const status = authoritativeTerminalStatus(r);
|
|
2361
|
+
const detail = r.lastTerminalError;
|
|
2362
|
+
const detailParts = [];
|
|
2363
|
+
if (detail?.code !== void 0) detailParts.push(`errorCode: ${detail.code}`);
|
|
2364
|
+
if (detail?.message !== void 0) detailParts.push(detail.message);
|
|
2365
|
+
let body;
|
|
2366
|
+
if (detailParts.length > 0) {
|
|
2367
|
+
body = `\u670D\u52A1\u7AEF\u9519\u8BEF\uFF1A${detailParts.join("\uFF1A")}\u3002`;
|
|
2368
|
+
} else {
|
|
2369
|
+
const tail = r.response.trim();
|
|
2370
|
+
body = tail !== "" ? `\u670D\u52A1\u7AEF\u8FD4\u56DE\u5C3E\u90E8\uFF1A${tail.slice(-ZCODE_ERROR_TAIL_CHARS)}\u3002` : "\u670D\u52A1\u7AEF\u65E0\u8FD4\u56DE\u5185\u5BB9\uFF08read \u515C\u5E95/delta \u805A\u5408\u5747\u4E3A\u7A7A\uFF09\u3002";
|
|
2371
|
+
}
|
|
2372
|
+
return {
|
|
2373
|
+
kind: "run-failed",
|
|
2374
|
+
output: syntheticAppServerOutput(null),
|
|
2375
|
+
message: `engine_run_failed: app-server \u7EC8\u6001 status=${status}\uFF08\u4F1A\u8BDD ${r.sessionId}\uFF09\u3002${body}
|
|
2376
|
+
\u{1F449} \u6062\u590D\u6307\u5F15\uFF1A\u9519\u8BEF\u5185\u5BB9\u6765\u81EA\u6A21\u578B/\u670D\u52A1\u7AEF\uFF1B\u76F4\u63A5\u91CD\u8DD1\uFF0C\u82E5\u6301\u7EED\u51FA\u73B0\u6838\u5BF9 ZCode \u684C\u9762\u7AEF\u51ED\u636E\u4E0E\u6A21\u578B\u914D\u7F6E\uFF08engine_credential_missing \u540C\u65CF\u6392\u67E5\uFF09\u3002`,
|
|
2377
|
+
sessionId: r.sessionId
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
function failedAppServerAttempt(err, currentSessionId, opts = {}) {
|
|
2381
|
+
return {
|
|
2382
|
+
kind: "run-failed",
|
|
2383
|
+
output: syntheticAppServerOutput(null),
|
|
2384
|
+
message: buildAppServerRunFailedMessage(err, currentSessionId, opts.retried === true),
|
|
2385
|
+
// [P0-1 U4/D6] 瞬时失败形态标记(重试判定判据——timeout 形态走
|
|
2386
|
+
// timeoutAppServerAttempt,此处只承载 conn-closed)
|
|
2387
|
+
...opts.transient !== void 0 ? { transient: opts.transient } : {},
|
|
2388
|
+
// 错误规格表 -32004 行「按任务失败上报(含会话 id)」:create 成功后运行中失败
|
|
2389
|
+
// (-32004/-32010 等)时留痕会话 id——经 applyRunFailedOutcome 落 outcome.sessionId
|
|
2390
|
+
// 与 handle.sessionRef(create 阶段失败无会话,缺省不带)
|
|
2391
|
+
...currentSessionId !== void 0 ? { sessionId: currentSessionId } : {}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function timeoutAppServerAttempt(err, currentSessionId, stopPath, opts = {}) {
|
|
2395
|
+
return {
|
|
2396
|
+
kind: "run-failed",
|
|
2397
|
+
output: syntheticAppServerOutput(null),
|
|
2398
|
+
message: buildAppServerTimeoutMessage(err, currentSessionId, stopPath, opts.retried === true),
|
|
2399
|
+
transient: "timeout",
|
|
2400
|
+
...currentSessionId !== void 0 ? { sessionId: currentSessionId } : {}
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function stopPathText(stopPath) {
|
|
2404
|
+
switch (stopPath) {
|
|
2405
|
+
case "stop-acked":
|
|
2406
|
+
return "session/stop \u5DF2\u9001\u8FBE\uFF08\u670D\u52A1\u7AEF\u63A5\u53D7\u505C turn\uFF09";
|
|
2407
|
+
case "stop-rejected":
|
|
2408
|
+
return "session/stop \u62A5\u534F\u8BAE\u6027 error\uFF08\u4F1A\u8BDD\u5DF2\u88AB\u56DE\u6536\uFF0C\u63A7\u5236\u9762\u5B58\u6D3B\u2014\u2014\u6B62\u635F\u7531\u4F1A\u8BDD\u56DE\u6536\u627F\u62C5\uFF0C\u4E0D\u5347\u7EA7\u6740\u94FE\uFF09";
|
|
2409
|
+
case "stop-unreachable-killed":
|
|
2410
|
+
return `session/stop \u65E0\u5E94\u7B54\u5DF2\u5347\u7EA7\u6740\u94FE\uFF08SIGTERM\u2192${ZCODE_KILL_GRACE_MS}ms\u2192SIGKILL \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF09`;
|
|
2411
|
+
case "no-session":
|
|
2412
|
+
return "\u4F1A\u8BDD\u672A\u5EFA\u7ACB\uFF08\u4EFB\u52A1\u672A\u5F00\u59CB\u6267\u884C\uFF0C\u65E0\u5728\u9014\u6D88\u8017\uFF09";
|
|
2413
|
+
default:
|
|
2414
|
+
return "grace \u7A97\u53E3\u5185\u7EC8\u6001\u843D\u5B9A\u6216\u5DF2\u8D70\u6740\u94FE";
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function buildAppServerTimeoutMessage(err, sessionId, stopPath, retried = false) {
|
|
2418
|
+
const sid = sessionId !== void 0 ? `\uFF08\u4F1A\u8BDD ${sessionId}\uFF09` : "";
|
|
2419
|
+
const lastEventText = err.lastEventAt !== void 0 ? `\uFF0C\u6700\u540E\u4E8B\u4EF6 ${new Date(err.lastEventAt).toISOString()}` : "\uFF0C\u6574\u8F6E\u672A\u89C2\u5BDF\u5230\u4EFB\u4F55\u4E8B\u4EF6\uFF08\u8FDB\u7A0B\u5047\u6B7B/\u534F\u8BAE\u9759\u9ED8\u5F62\u6001\uFF09";
|
|
2420
|
+
const head = err.kind === "idle" ? `engine_timeout: zcode turn \u8FDE\u7EED\u9759\u9ED8 ${err.thresholdMs}ms\uFF08idle \u5224\u5B9A${lastEventText}\uFF0C\u603B\u8017\u65F6 ${err.elapsed}ms\uFF09${sid}\u3002` : `engine_timeout: zcode turn \u603B\u4E0A\u754C ${err.thresholdMs}ms \u5185\u672A\u89C2\u5BDF\u5230\u7EC8\u6001\uFF08chatty-wedge \u5224\u5B9A\u2014\u2014\u4E8B\u4EF6\u6D41\u4ECD\u6D3B\u8DC3\u800C\u7EC8\u6001\u672A\u5230\u8FBE\uFF0C\u603B\u8017\u65F6 ${err.elapsed}ms\uFF09${sid}\u3002`;
|
|
2421
|
+
const selfHelp = err.kind === "ceiling" ? `\u82E5\u672C\u4EFB\u52A1\u5C5E\u5408\u6CD5\u8D85\u957F\u4EFB\u52A1\uFF08\u9884\u671F\u8D85\u8FC7 ${err.thresholdMs}ms\uFF09\uFF0C\u91CD\u8DD1\u524D\u8BBE ${ZCODE_TURN_MAX_TIMEOUT_ENV} \u4E3A\u66F4\u5927\u6BEB\u79D2\u503C\u6216 0 \u5173\u95ED\u603B\u4E0A\u754C\uFF08\u5173\u95ED\u540E chatty \u5F62\u6001\u4E0D\u518D\u81EA\u52A8\u56DE\u6536\uFF0C\u9759\u9ED8 wedged \u4ECD\u7531 idle \u5C42\u515C\u5E95\u2014\u2014\u81EA\u884C\u6743\u8861\uFF09\u3002` : "";
|
|
2422
|
+
const rerunGuide = retried ? `\u{1F449} \u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF08\u77AC\u65F6\u6545\u969C\u5DF2\u81EA\u52A8\u91CD\u8BD5\u4E00\u6B21\u4ECD\u8D85\u65F6\uFF1B\u91CD\u8BD5\u5728\u6B62\u635F\u94FE\u7EC8\u5C40\u540E\u542F\u52A8\uFF0C\u65E0\u65B0\u65E7\u4EFB\u52A1\u53CC\u8DD1\u7A97\uFF09\uFF1B` : `\u{1F449} \u6062\u590D\u6307\u5F15\uFF1A\u76F4\u63A5\u91CD\u8DD1\u672C\u4EFB\u52A1\uFF1B`;
|
|
2423
|
+
return `${head}\u6B62\u635F\u8DEF\u5F84\uFF1A${stopPathText(stopPath)}\u3002
|
|
2424
|
+
${rerunGuide}${selfHelp}\u82E5\u6301\u7EED\u51FA\u73B0\uFF0C\u68C0\u67E5 ZCode \u684C\u9762\u7AEF\u6A21\u578B\u8FDE\u901A\u6027\u6216\u6539\u7528 engine: pi\u3002`;
|
|
2425
|
+
}
|
|
2426
|
+
function isHostTimeoutAbort(ctx) {
|
|
2427
|
+
return ctx.signal?.aborted === true && ctx.signal.reason === import_subagent_engine_sdk9.HOST_TIMEOUT_ABORT_REASON;
|
|
2428
|
+
}
|
|
2429
|
+
var PROBE_STDOUT_CAP_CHARS = 4096;
|
|
2430
|
+
async function defaultProbeVersion(cliPath, dataDir) {
|
|
2431
|
+
try {
|
|
2432
|
+
return await new Promise((resolve, reject) => {
|
|
2433
|
+
const child = (0, import_subagent_engine_sdk9.spawnEngineChild)({
|
|
2434
|
+
command: "node",
|
|
2435
|
+
args: [cliPath, "--version"],
|
|
2436
|
+
env: (0, import_subagent_engine_sdk9.buildEngineChildEnv)(process.env, { dataDir }),
|
|
2437
|
+
stdout: "pipe",
|
|
2438
|
+
stderr: "ignore",
|
|
2439
|
+
signal: AbortSignal.timeout(PROBE_VERSION_TIMEOUT_MS)
|
|
2440
|
+
});
|
|
2441
|
+
let out = "";
|
|
2442
|
+
child.stdout?.setEncoding("utf8");
|
|
2443
|
+
child.stdout?.on("data", (chunk) => {
|
|
2444
|
+
if (out.length < PROBE_STDOUT_CAP_CHARS) out += chunk;
|
|
2445
|
+
});
|
|
2446
|
+
child.once("error", reject);
|
|
2447
|
+
child.once("close", (code) => {
|
|
2448
|
+
if (code !== 0) reject(new Error(`zcode --version exited with code ${code}`));
|
|
2449
|
+
else resolve(out.trim().split("\n")[0]?.trim() || void 0);
|
|
2450
|
+
});
|
|
2451
|
+
});
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
logger3.debug(
|
|
2454
|
+
`[zcode-engine] probe version check failed (best-effort continue): ${(0, import_subagent_engine_sdk3.toErrorMessage)(err)}`
|
|
2455
|
+
);
|
|
2456
|
+
return void 0;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// src/registration.ts
|
|
2461
|
+
function defaultEngineDataDir() {
|
|
2462
|
+
return (0, import_subagent_engine_sdk10.resolveEngineDataDir)(process.env);
|
|
2463
|
+
}
|
|
2464
|
+
function createZcodeEngine(deps) {
|
|
2465
|
+
return new ZcodeEngine(deps);
|
|
2466
|
+
}
|
|
2467
|
+
function createDefaultZcodeEngine() {
|
|
2468
|
+
return createZcodeEngine({
|
|
2469
|
+
engineDataDir: defaultEngineDataDir,
|
|
2470
|
+
...process.env["XYZ_ZCODE_CLI"] !== void 0 ? { cliPath: process.env["XYZ_ZCODE_CLI"] } : {}
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/port-types.ts
|
|
2475
|
+
var import_subagent_engine_sdk11 = require("@zhushanwen/subagent-engine-sdk");
|
|
2476
|
+
|
|
2477
|
+
// src/server.ts
|
|
2478
|
+
var REVERSE_TIMEOUT_DEFAULT_MS = 6e4;
|
|
2479
|
+
var EngineProtocolServer = class {
|
|
2480
|
+
write;
|
|
2481
|
+
engine;
|
|
2482
|
+
reverseClock;
|
|
2483
|
+
reverseTimeoutMs;
|
|
2484
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
2485
|
+
reversePending = /* @__PURE__ */ new Map();
|
|
2486
|
+
revSeq = 0;
|
|
2487
|
+
initialized = false;
|
|
2488
|
+
constructor(opts) {
|
|
2489
|
+
this.write = opts.write;
|
|
2490
|
+
this.engine = opts.engine ?? createDefaultZcodeEngine();
|
|
2491
|
+
this.reverseClock = opts.reverseClock;
|
|
2492
|
+
this.reverseTimeoutMs = opts.reverseTimeoutMs ?? REVERSE_TIMEOUT_DEFAULT_MS;
|
|
2493
|
+
}
|
|
2494
|
+
/** 入站帧消费(请求帧 + 反向请求应答帧;main.ts 的行解析器拆行后喂入)。 */
|
|
2495
|
+
handleFrame(frame) {
|
|
2496
|
+
if ((0, import_subagent_engine_sdk12.isResponseFrame)(frame)) {
|
|
2497
|
+
this.settleReverse(frame.id, frame);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if ((0, import_subagent_engine_sdk12.isReverseRequestFrame)(frame)) {
|
|
2501
|
+
this.write({
|
|
2502
|
+
id: 0,
|
|
2503
|
+
error: {
|
|
2504
|
+
code: "engine_protocol_bad_frame",
|
|
2505
|
+
message: `unexpected reverse request frame from host: ${String(frame.method)}`,
|
|
2506
|
+
recovery: "The engine protocol v1 only carries host/* requests engine\u2192host."
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
if (typeof frame === "object" && frame !== null && "id" in frame && "method" in frame && typeof frame.method === "string") {
|
|
2512
|
+
const { id, method, params } = frame;
|
|
2513
|
+
if (typeof id === "number") {
|
|
2514
|
+
void this.dispatch(id, method, params).then(
|
|
2515
|
+
(result) => this.write({ id, result }),
|
|
2516
|
+
(err) => this.write({ id, error: toProtocolError(err) })
|
|
2517
|
+
);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
/** 10 正向方法分发(表驱动:method → 处理器;未知方法 → engine_protocol_unknown_method)。 */
|
|
2523
|
+
async dispatch(id, method, params) {
|
|
2524
|
+
const handler = this.methodHandlers[method];
|
|
2525
|
+
if (handler === void 0) {
|
|
2526
|
+
throw new import_subagent_engine_sdk12.EngineSdkError(
|
|
2527
|
+
"engine_protocol_unknown_method",
|
|
2528
|
+
`unknown protocol method: ${method} (request id ${id})`,
|
|
2529
|
+
"The engine speaks protocol v1; check the installed engine package version vs the host."
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
return handler(params);
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* 10 正向方法 → 处理器映射(每个处理器消费原始 params 并自行收敛类型——
|
|
2536
|
+
* 与原 switch case 表达式一一对应)。
|
|
2537
|
+
*/
|
|
2538
|
+
methodHandlers = {
|
|
2539
|
+
initialize: (p) => this.initialize(p),
|
|
2540
|
+
probe: (p) => this.engine.probe(probeParamsOf(p)),
|
|
2541
|
+
run: (p) => this.run(p),
|
|
2542
|
+
cancel: (p) => this.cancel(p),
|
|
2543
|
+
interact: (p) => this.interact(p),
|
|
2544
|
+
read: (p) => this.read(p),
|
|
2545
|
+
listModels: () => ({ models: this.engine.listModels?.() ?? null }),
|
|
2546
|
+
validateModel: (p) => this.validateModel(p),
|
|
2547
|
+
dispose: async () => {
|
|
2548
|
+
await this.engine.dispose?.();
|
|
2549
|
+
return { ok: true };
|
|
2550
|
+
},
|
|
2551
|
+
ping: () => ({ pong: true })
|
|
2552
|
+
};
|
|
2553
|
+
// ── initialize:版本协商(越界 → engine_protocol_mismatch)+ 能力应答 ──
|
|
2554
|
+
initialize(params) {
|
|
2555
|
+
if (params?.protocolVersion !== import_subagent_engine_sdk12.ENGINE_PROTOCOL_VERSION) {
|
|
2556
|
+
throw new import_subagent_engine_sdk12.EngineSdkError(
|
|
2557
|
+
"engine_protocol_mismatch",
|
|
2558
|
+
`host protocol version ${String(params?.protocolVersion)} is not compatible with engine protocol v${import_subagent_engine_sdk12.ENGINE_PROTOCOL_VERSION}`,
|
|
2559
|
+
`Upgrade the engine package or the host so both speak protocol v${import_subagent_engine_sdk12.ENGINE_PROTOCOL_VERSION}.`
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
this.initialized = true;
|
|
2563
|
+
const models = this.engine.listModels?.() ?? null;
|
|
2564
|
+
return {
|
|
2565
|
+
protocolVersion: import_subagent_engine_sdk12.ENGINE_PROTOCOL_VERSION,
|
|
2566
|
+
engineId: this.engine.id,
|
|
2567
|
+
engineVersion: ZCODE_ADAPTER_VERSION,
|
|
2568
|
+
adapterVersion: ZCODE_ADAPTER_VERSION,
|
|
2569
|
+
capabilities: this.engine.capabilities(),
|
|
2570
|
+
...models !== null ? { models: models.map((m) => ({ id: m.id })) } : {}
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
// ── run:协议载荷 → 本地 AgentCallOpts/RunContext;事件 → 通知/host 通道 ──
|
|
2574
|
+
async run(params) {
|
|
2575
|
+
if (!this.initialized) {
|
|
2576
|
+
throw new import_subagent_engine_sdk12.EngineSdkError(
|
|
2577
|
+
"engine_protocol_not_initialized",
|
|
2578
|
+
"run before initialize is a protocol violation",
|
|
2579
|
+
"The host must complete the initialize handshake before dispatching runs."
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
const { runId, task, ctx } = params;
|
|
2583
|
+
const controller = new AbortController();
|
|
2584
|
+
const active = { controller, seq: 0 };
|
|
2585
|
+
this.activeRuns.set(runId, active);
|
|
2586
|
+
const fullTask = { ...task, ...ctx.model !== void 0 ? { model: ctx.model } : {} };
|
|
2587
|
+
const ctxModel = (0, import_subagent_engine_sdk11.parseCtxModel)(ctx.ctxModel);
|
|
2588
|
+
const stream = ctx.streamMode === "stream" ? { onDelta: (delta) => {
|
|
2589
|
+
void this.reverseRequestInternal("host/streamDelta", { runId, delta });
|
|
2590
|
+
} } : void 0;
|
|
2591
|
+
const runCtx = {
|
|
2592
|
+
taskId: runId,
|
|
2593
|
+
poolKey: ctx.poolKey,
|
|
2594
|
+
signal: controller.signal,
|
|
2595
|
+
onEvent: (event) => this.emitEvent(runId, event),
|
|
2596
|
+
...ctxModel !== void 0 ? { ctxModel } : {},
|
|
2597
|
+
...stream !== void 0 ? { stream } : {},
|
|
2598
|
+
...ctx.schemaEnv !== void 0 ? { schemaEnv: ctx.schemaEnv } : {},
|
|
2599
|
+
...ctx.engineFallback !== void 0 ? { engineFallback: ctx.engineFallback } : {},
|
|
2600
|
+
onPoolResolved: (poolKey) => {
|
|
2601
|
+
void this.reverseRequestInternal("host/poolResolved", { runId, poolKey });
|
|
2602
|
+
},
|
|
2603
|
+
onHandleReady: (partial) => {
|
|
2604
|
+
void this.reverseRequestInternal("host/handleReady", { runId, sessionRef: partial.sessionRef, poolKey: partial.poolKey });
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
try {
|
|
2608
|
+
const r = await this.engine.run(fullTask, runCtx);
|
|
2609
|
+
return { handle: r.handle.data, outcome: r.outcome };
|
|
2610
|
+
} finally {
|
|
2611
|
+
this.activeRuns.delete(runId);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
cancel(params) {
|
|
2615
|
+
const active = this.activeRuns.get(params.runId);
|
|
2616
|
+
if (active !== void 0) active.controller.abort(new Error(`cancelled by host: ${params.reason}`));
|
|
2617
|
+
return { ok: true };
|
|
2618
|
+
}
|
|
2619
|
+
async interact(params) {
|
|
2620
|
+
return this.engine.interact({ data: params.handle }, params.action);
|
|
2621
|
+
}
|
|
2622
|
+
read(params) {
|
|
2623
|
+
return this.engine.read({ data: params.handle });
|
|
2624
|
+
}
|
|
2625
|
+
validateModel(params) {
|
|
2626
|
+
if (this.engine.validateModel === void 0) {
|
|
2627
|
+
throw new import_subagent_engine_sdk12.EngineSdkError(
|
|
2628
|
+
"engine_capability_unsupported",
|
|
2629
|
+
"engine does not implement validateModel",
|
|
2630
|
+
"Manifest omits modelCatalog; model validation falls back to run-time engine checks."
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
return this.engine.validateModel(params.modelRef);
|
|
2634
|
+
}
|
|
2635
|
+
// ── 出站:事件通知 + 反向请求客户端 ──
|
|
2636
|
+
emitEvent(runId, event) {
|
|
2637
|
+
const active = this.activeRuns.get(runId);
|
|
2638
|
+
const seq = active !== void 0 ? ++active.seq : 0;
|
|
2639
|
+
this.write({ method: "event", params: { runId, seq, event } });
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* 发反向请求并等应答(帧④必须应答)。计时面登记:发出 → clock.started;收到
|
|
2643
|
+
* ack 形态应答 → clock.acked(两阶段第一段,等待不计超时);终结 → clock.settled。
|
|
2644
|
+
* 应答超时兜底 reject——数据面通道(host/log 等)core 10s 应答,reject 由调用方
|
|
2645
|
+
* 吞掉留痕(数据面丢失不拖死主链路)。
|
|
2646
|
+
*/
|
|
2647
|
+
/** 反向请求发送(公开面:main.ts 的 host/log 桥接消费;内部 run 通道同路)。 */
|
|
2648
|
+
reverseRequest(method, params) {
|
|
2649
|
+
return this.reverseRequestInternal(method, params);
|
|
2650
|
+
}
|
|
2651
|
+
reverseRequestInternal(method, params) {
|
|
2652
|
+
const id = `rev-${++this.revSeq}`;
|
|
2653
|
+
return new Promise((resolve, reject) => {
|
|
2654
|
+
const timer = setTimeout(() => {
|
|
2655
|
+
this.reversePending.delete(id);
|
|
2656
|
+
this.reverseClock?.settled(id);
|
|
2657
|
+
reject(new Error(`reverse request ${method} (${id}) timed out after ${this.reverseTimeoutMs}ms`));
|
|
2658
|
+
}, this.reverseTimeoutMs);
|
|
2659
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
2660
|
+
this.reversePending.set(id, { resolve, reject, timer });
|
|
2661
|
+
this.reverseClock?.started(id);
|
|
2662
|
+
this.write({ id, method, params });
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
/** 反向请求应答落位(ack 两阶段:{ack:true}/{unsupported:true}/{ok:true} 均先 ack 计时面)。 */
|
|
2666
|
+
settleReverse(id, frame) {
|
|
2667
|
+
const pending = this.reversePending.get(String(id));
|
|
2668
|
+
if (pending === void 0) return;
|
|
2669
|
+
this.reversePending.delete(String(id));
|
|
2670
|
+
clearTimeout(pending.timer);
|
|
2671
|
+
this.reverseClock?.acked(String(id));
|
|
2672
|
+
this.reverseClock?.settled(String(id));
|
|
2673
|
+
if (frame.error !== void 0) pending.reject(new Error(`reverse request ${String(id)} rejected: ${(0, import_subagent_engine_sdk3.toErrorMessage)(frame.error)}`));
|
|
2674
|
+
else pending.resolve(frame.result);
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
function probeParamsOf(params) {
|
|
2678
|
+
return typeof params === "object" && params !== null ? params : void 0;
|
|
2679
|
+
}
|
|
2680
|
+
function toProtocolError(err) {
|
|
2681
|
+
if (err instanceof import_subagent_engine_sdk12.EngineSdkError) return err.toStructured();
|
|
2682
|
+
return {
|
|
2683
|
+
code: "engine_run_failed",
|
|
2684
|
+
message: (0, import_subagent_engine_sdk3.toErrorMessage)(err),
|
|
2685
|
+
recovery: "Check the engine process logs (engineDataDir/logs/) and rerun; if persistent, reinstall or upgrade the engine package."
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/main.ts
|
|
2690
|
+
var clock = (0, import_subagent_engine_sdk13.armEngineSelfDestruct)({ stdin: process.stdin });
|
|
2691
|
+
(0, import_subagent_engine_sdk13.runEngineCliEntry)({
|
|
2692
|
+
component: "zcode-engine-cli",
|
|
2693
|
+
server: new EngineProtocolServer({
|
|
2694
|
+
write: (frame) => {
|
|
2695
|
+
process.stdout.write(`${JSON.stringify(frame)}
|
|
2696
|
+
`);
|
|
2697
|
+
},
|
|
2698
|
+
reverseClock: clock
|
|
2699
|
+
})
|
|
2700
|
+
});
|