@kiturone/kapibala 0.0.3 → 0.0.4
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/dist/index.cjs +2678 -1095
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +615 -62
- package/dist/index.d.ts +615 -62
- package/dist/index.js +2282 -703
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
AbortError: () => AbortError,
|
|
34
34
|
AgentLoop: () => AgentLoop,
|
|
35
35
|
AgentSession: () => AgentSession,
|
|
36
|
+
AnthropicProvider: () => AnthropicProvider,
|
|
36
37
|
ContextBudgetError: () => ContextBudgetError,
|
|
37
38
|
ContextManager: () => ContextManager,
|
|
38
39
|
ContextOverflowError: () => ContextOverflowError,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
KapibalaError: () => KapibalaError,
|
|
46
47
|
ModelError: () => ModelError,
|
|
47
48
|
OpenAICompatibleProvider: () => OpenAICompatibleProvider,
|
|
49
|
+
OpenAIResponsesProvider: () => OpenAIResponsesProvider,
|
|
48
50
|
PathSandbox: () => PathSandbox,
|
|
49
51
|
PermissionPolicy: () => PermissionPolicy,
|
|
50
52
|
PromptAssembler: () => PromptAssembler,
|
|
@@ -64,6 +66,7 @@ __export(index_exports, {
|
|
|
64
66
|
ToolRegistry: () => ToolRegistry,
|
|
65
67
|
TransportError: () => TransportError,
|
|
66
68
|
UnicodeTokenEstimator: () => UnicodeTokenEstimator,
|
|
69
|
+
assembleCanonicalToolResults: () => assembleCanonicalToolResults,
|
|
67
70
|
builtinTools: () => builtinTools,
|
|
68
71
|
classifyBashOutput: () => classifyBashOutput,
|
|
69
72
|
cleanupCommandResults: () => cleanupCommandResults,
|
|
@@ -103,6 +106,7 @@ __export(index_exports, {
|
|
|
103
106
|
supportsSessionState: () => supportsSessionState,
|
|
104
107
|
toToolDefinition: () => toToolDefinition,
|
|
105
108
|
truncateToolOutput: () => truncateToolOutput,
|
|
109
|
+
validateModelBinding: () => validateModelBinding,
|
|
106
110
|
validatePermissionRules: () => validatePermissionRules,
|
|
107
111
|
validateSummary: () => validateSummary,
|
|
108
112
|
visibleTools: () => visibleTools,
|
|
@@ -125,10 +129,341 @@ function isThinkingBlock(block) {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
// src/context/history.ts
|
|
128
|
-
var
|
|
132
|
+
var import_node_crypto3 = require("crypto");
|
|
129
133
|
|
|
130
|
-
// src/
|
|
134
|
+
// src/models/protocol-state.ts
|
|
131
135
|
var import_node_crypto = require("crypto");
|
|
136
|
+
|
|
137
|
+
// src/errors/index.ts
|
|
138
|
+
function sanitizeModelErrorMessage(value) {
|
|
139
|
+
return value.replace(
|
|
140
|
+
/\b(?:Authorization|Proxy-Authorization)\s*:\s*(?:Bearer|Basic)\s+[^\s,;"']+/gi,
|
|
141
|
+
"Authorization: [redacted]"
|
|
142
|
+
).replace(/\b(?:Cookie|Set-Cookie)\s*:[^\r\n]*/gi, "Cookie: [redacted]").replace(/\p{Cc}/gu, " ").replace(/\bBearer\s+[^\s,;"']+/gi, "Bearer [redacted]").replace(/\b(?:sk|api|key)-[A-Za-z0-9_-]{5,}/g, "[redacted]").replace(
|
|
143
|
+
/((?:api[_-]?key|access[_-]?token|token|password|secret|authorization|cookie)["']?\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;&]+)/gi,
|
|
144
|
+
"$1[redacted]"
|
|
145
|
+
).replace(/https?:\/\/[^\s<>"']+/gi, (raw) => {
|
|
146
|
+
try {
|
|
147
|
+
const url = new URL(raw);
|
|
148
|
+
if (url.username || url.password) {
|
|
149
|
+
url.username = "redacted";
|
|
150
|
+
url.password = "redacted";
|
|
151
|
+
}
|
|
152
|
+
if (url.search) url.search = "?redacted";
|
|
153
|
+
if (url.hash) url.hash = "#redacted";
|
|
154
|
+
return url.toString();
|
|
155
|
+
} catch {
|
|
156
|
+
return "[redacted URL]";
|
|
157
|
+
}
|
|
158
|
+
}).slice(0, 600);
|
|
159
|
+
}
|
|
160
|
+
function safeErrorIdentifier(value) {
|
|
161
|
+
return value && /^[A-Za-z0-9_.:-]{1,80}$/.test(value) && !/^(?:sk|api|key)-/i.test(value) ? value : void 0;
|
|
162
|
+
}
|
|
163
|
+
function classifyModelError(options) {
|
|
164
|
+
const codes = [options.providerCode, options.providerType, options.code];
|
|
165
|
+
if (codes.some(
|
|
166
|
+
(c) => [
|
|
167
|
+
"context_length_exceeded",
|
|
168
|
+
"context_window_exceeded",
|
|
169
|
+
"max_context_length_exceeded",
|
|
170
|
+
"MODEL_CONTEXT_EXCEEDED"
|
|
171
|
+
].includes(c ?? "")
|
|
172
|
+
))
|
|
173
|
+
return "context";
|
|
174
|
+
if (codes.some(
|
|
175
|
+
(c) => [
|
|
176
|
+
"insufficient_quota",
|
|
177
|
+
"insufficient_balance",
|
|
178
|
+
"quota_exceeded",
|
|
179
|
+
"billing_hard_limit_reached"
|
|
180
|
+
].includes(c ?? "")
|
|
181
|
+
) || options.status === 402)
|
|
182
|
+
return "quota";
|
|
183
|
+
if (codes.some(
|
|
184
|
+
(c) => ["invalid_api_key", "authentication_error", "permission_error"].includes(c ?? "")
|
|
185
|
+
) || options.status === 401 || options.status === 403)
|
|
186
|
+
return "authentication";
|
|
187
|
+
if (codes.some((c) => ["rate_limit_exceeded", "rate_limit_error"].includes(c ?? "")) || options.status === 429)
|
|
188
|
+
return "rate_limit";
|
|
189
|
+
if (options.status === 408 || /TIMEOUT|ETIMEDOUT/.test(options.transportCode ?? ""))
|
|
190
|
+
return "timeout";
|
|
191
|
+
if (options.transportCode || options.code === "MODEL_STREAM_INTERRUPTED" || options.stage === "connect")
|
|
192
|
+
return "network";
|
|
193
|
+
if (codes.some(
|
|
194
|
+
(c) => ["server_error", "overloaded_error", "api_error", "internal_server_error"].includes(c ?? "")
|
|
195
|
+
) || options.status && options.status >= 500)
|
|
196
|
+
return "service";
|
|
197
|
+
if (codes.some((c) => ["invalid_request_error", "invalid_argument"].includes(c ?? "")))
|
|
198
|
+
return "invalid_request";
|
|
199
|
+
if (options.status && options.status >= 400) return "invalid_request";
|
|
200
|
+
if (options.code === "MODEL_STREAM_INCOMPLETE" || options.code === "MODEL_INVALID_RESPONSE")
|
|
201
|
+
return "invalid_response";
|
|
202
|
+
return "unknown";
|
|
203
|
+
}
|
|
204
|
+
var MODEL_ERROR_ADVICE = {
|
|
205
|
+
authentication: "\u68C0\u67E5 API Key\u3001\u6A21\u578B\u6743\u9650\u53CA\u7AEF\u70B9\u914D\u7F6E\u540E\u91CD\u8BD5\u3002",
|
|
206
|
+
quota: "\u68C0\u67E5\u8D26\u6237\u4F59\u989D\u6216\u914D\u989D\u540E\u91CD\u8BD5\u3002",
|
|
207
|
+
rate_limit: "\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u964D\u4F4E\u8BF7\u6C42\u9891\u7387\u3002",
|
|
208
|
+
context: "\u9009\u62E9\u66F4\u5927\u4E0A\u4E0B\u6587\u7A97\u53E3\u3001\u538B\u7F29\u5386\u53F2\u6216\u62C6\u5206\u4EFB\u52A1\u540E\u91CD\u8BD5\u3002",
|
|
209
|
+
service: "\u670D\u52A1\u6682\u65F6\u5F02\u5E38\uFF0C\u53EF\u7A0D\u540E\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u3002",
|
|
210
|
+
network: "\u68C0\u67E5\u7F51\u7EDC\u3001\u4EE3\u7406\u548C\u7AEF\u70B9\uFF1B\u8FDE\u63A5\u6062\u590D\u540E\u53EF\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u3002",
|
|
211
|
+
timeout: "\u68C0\u67E5\u7F51\u7EDC\u53CA\u670D\u52A1\u54CD\u5E94\u60C5\u51B5\uFF0C\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
212
|
+
invalid_request: "\u68C0\u67E5\u6A21\u578B\u4E0E\u7AEF\u70B9\u7684\u534F\u8BAE\u517C\u5BB9\u6027\u53CA\u8BF7\u6C42\u914D\u7F6E\u3002",
|
|
213
|
+
invalid_response: "\u54CD\u5E94\u4E0D\u5B8C\u6574\u6216\u683C\u5F0F\u5F02\u5E38\uFF0C\u53EF\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u5E76\u68C0\u67E5\u7AEF\u70B9\u534F\u8BAE\u517C\u5BB9\u6027\u3002",
|
|
214
|
+
unknown: "\u539F\u56E0\u672A\u786E\u8BA4\uFF0C\u8BF7\u7ED3\u5408\u9519\u8BEF\u7801\u548C\u8FD0\u884C\u65E5\u5FD7\u68C0\u67E5\u3002"
|
|
215
|
+
};
|
|
216
|
+
function describeModelError(error, context = {}) {
|
|
217
|
+
return {
|
|
218
|
+
category: error.category,
|
|
219
|
+
code: error.code,
|
|
220
|
+
message: sanitizeModelErrorMessage(error.message),
|
|
221
|
+
status: error.status,
|
|
222
|
+
providerCode: error.providerCode,
|
|
223
|
+
providerType: error.providerType,
|
|
224
|
+
transportCode: error.transportCode,
|
|
225
|
+
stage: error.stage,
|
|
226
|
+
retryPolicy: error.retryPolicy,
|
|
227
|
+
suggestion: MODEL_ERROR_ADVICE[error.category],
|
|
228
|
+
modelId: context.modelId ? sanitizeModelErrorMessage(context.modelId) : void 0,
|
|
229
|
+
provider: context.provider ? sanitizeModelErrorMessage(context.provider) : void 0,
|
|
230
|
+
operation: context.operation,
|
|
231
|
+
modelRole: context.modelRole
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
var KapibalaError = class extends Error {
|
|
235
|
+
code;
|
|
236
|
+
retryPolicy;
|
|
237
|
+
safeMessage;
|
|
238
|
+
constructor(message, options) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = "KapibalaError";
|
|
241
|
+
this.code = options?.code ?? "INTERNAL_ERROR";
|
|
242
|
+
this.retryPolicy = options?.retryPolicy ?? "never";
|
|
243
|
+
this.safeMessage = options?.safeMessage ?? "Operation failed";
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
var ModelError = class extends KapibalaError {
|
|
247
|
+
status;
|
|
248
|
+
/** 属于可退避重试的瞬态类别(限流/服务/超时/网络);不等于自动重试授权。 */
|
|
249
|
+
retryable;
|
|
250
|
+
/** 可选的受控底层连接错误码,不包含原始传输异常文本。 */
|
|
251
|
+
transportCode;
|
|
252
|
+
providerCode;
|
|
253
|
+
providerType;
|
|
254
|
+
stage;
|
|
255
|
+
category;
|
|
256
|
+
/** 构建模型错误,清理服务端原因并按明确状态/错误码分类;重试策略仅为建议,不授权重放。 */
|
|
257
|
+
constructor(message, options = {}) {
|
|
258
|
+
const category = classifyModelError(options);
|
|
259
|
+
const actionRequired = ["authentication", "quota", "context", "invalid_request"].includes(
|
|
260
|
+
category
|
|
261
|
+
);
|
|
262
|
+
const retryable = options.retryable ?? ["rate_limit", "service", "timeout", "network"].includes(category);
|
|
263
|
+
super(sanitizeModelErrorMessage(message), {
|
|
264
|
+
code: safeErrorIdentifier(options.code) ?? (category === "unknown" ? "MODEL_ERROR" : `MODEL_${category.toUpperCase()}`),
|
|
265
|
+
retryPolicy: actionRequired ? "after_user_action" : retryable ? "backoff" : "never",
|
|
266
|
+
safeMessage: sanitizeModelErrorMessage(message)
|
|
267
|
+
});
|
|
268
|
+
this.name = "ModelError";
|
|
269
|
+
this.status = options?.status;
|
|
270
|
+
this.transportCode = safeErrorIdentifier(options.transportCode);
|
|
271
|
+
this.providerCode = safeErrorIdentifier(options.providerCode);
|
|
272
|
+
this.providerType = safeErrorIdentifier(options.providerType);
|
|
273
|
+
this.stage = options.stage;
|
|
274
|
+
this.category = category;
|
|
275
|
+
this.retryable = retryable;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
var ContextOverflowError = class extends ModelError {
|
|
279
|
+
/** 保留明确上下文错误的脱敏服务端原因;只允许尚无输出的请求进入一次预算处理。 */
|
|
280
|
+
constructor(status, details = {}) {
|
|
281
|
+
super(details.message ?? "Model context window exceeded", {
|
|
282
|
+
...details,
|
|
283
|
+
status,
|
|
284
|
+
retryable: false,
|
|
285
|
+
code: "MODEL_CONTEXT_EXCEEDED"
|
|
286
|
+
});
|
|
287
|
+
this.name = "ContextOverflowError";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var TransportError = class extends KapibalaError {
|
|
291
|
+
retryable;
|
|
292
|
+
constructor(message, retryable = true) {
|
|
293
|
+
super(message);
|
|
294
|
+
this.name = "TransportError";
|
|
295
|
+
this.retryable = retryable;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var ToolError = class extends KapibalaError {
|
|
299
|
+
constructor(message) {
|
|
300
|
+
super(message, {
|
|
301
|
+
code: "TOOL_ERROR",
|
|
302
|
+
retryPolicy: "never",
|
|
303
|
+
safeMessage: "Tool execution failed"
|
|
304
|
+
});
|
|
305
|
+
this.name = "ToolError";
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var ToolNotFound = class extends KapibalaError {
|
|
309
|
+
toolName;
|
|
310
|
+
availableTools;
|
|
311
|
+
constructor(toolName, availableTools = []) {
|
|
312
|
+
super(`Tool '${toolName}' not found. Available tools: ${availableTools.join(", ") || "none"}`, {
|
|
313
|
+
code: "TOOL_NOT_FOUND",
|
|
314
|
+
retryPolicy: "never",
|
|
315
|
+
safeMessage: "Requested tool is unavailable"
|
|
316
|
+
});
|
|
317
|
+
this.name = "ToolNotFound";
|
|
318
|
+
this.toolName = toolName;
|
|
319
|
+
this.availableTools = availableTools;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var AbortError = class extends KapibalaError {
|
|
323
|
+
constructor(message = "Execution aborted by user") {
|
|
324
|
+
super(message, {
|
|
325
|
+
code: "ABORTED",
|
|
326
|
+
retryPolicy: "after_user_action",
|
|
327
|
+
safeMessage: "Operation cancelled"
|
|
328
|
+
});
|
|
329
|
+
this.name = "AbortError";
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
var FatalError = class extends KapibalaError {
|
|
333
|
+
constructor(message) {
|
|
334
|
+
super(message);
|
|
335
|
+
this.name = "FatalError";
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var SessionBusyError = class extends KapibalaError {
|
|
339
|
+
constructor(operation) {
|
|
340
|
+
super(`Session is already running; cannot ${operation}`);
|
|
341
|
+
this.name = "SessionBusyError";
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var ContextBudgetError = class extends KapibalaError {
|
|
345
|
+
constructor(usedTokens, inputBudget) {
|
|
346
|
+
super(
|
|
347
|
+
`Context input requires approximately ${usedTokens} tokens; budget is ${inputBudget}. Choose a larger context window or start a smaller task.`,
|
|
348
|
+
{
|
|
349
|
+
code: "CONTEXT_BUDGET_EXCEEDED",
|
|
350
|
+
retryPolicy: "after_user_action",
|
|
351
|
+
safeMessage: "\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u8DB3\uFF0C\u8BF7\u9009\u62E9\u66F4\u5927\u7A97\u53E3\u6216\u62C6\u5206\u4EFB\u52A1\u3002"
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
this.usedTokens = usedTokens;
|
|
355
|
+
this.inputBudget = inputBudget;
|
|
356
|
+
this.name = "ContextBudgetError";
|
|
357
|
+
}
|
|
358
|
+
usedTokens;
|
|
359
|
+
inputBudget;
|
|
360
|
+
};
|
|
361
|
+
var ContextStorageError = class extends KapibalaError {
|
|
362
|
+
constructor(message) {
|
|
363
|
+
super(message, {
|
|
364
|
+
code: "CONTEXT_STORAGE_ERROR",
|
|
365
|
+
retryPolicy: "after_user_action",
|
|
366
|
+
safeMessage: "\u4E0A\u4E0B\u6587\u72B6\u6001\u4FDD\u5B58\u5931\u8D25\uFF0C\u5019\u9009\u672A\u751F\u6548\u3002"
|
|
367
|
+
});
|
|
368
|
+
this.name = "ContextStorageError";
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/models/protocol-state.ts
|
|
373
|
+
function invalidState() {
|
|
374
|
+
throw new ModelError("Invalid protocol state or provenance", {
|
|
375
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
376
|
+
stage: "request",
|
|
377
|
+
retryable: false
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function endpointScope(target) {
|
|
381
|
+
return (0, import_node_crypto.createHash)("sha256").update(`${target.baseURL.replace(/\/+$/, "")}\0${target.workspaceId ?? ""}`).digest("hex");
|
|
382
|
+
}
|
|
383
|
+
function createProtocolOrigin(target, context) {
|
|
384
|
+
return {
|
|
385
|
+
version: 1,
|
|
386
|
+
protocol: target.protocol,
|
|
387
|
+
modelName: target.modelName,
|
|
388
|
+
modelId: context?.modelId,
|
|
389
|
+
runId: context?.runId ?? `unmanaged-${(0, import_node_crypto.randomUUID)()}`,
|
|
390
|
+
endpointScope: endpointScope(target)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function validOrigin(value) {
|
|
394
|
+
if (!value || typeof value !== "object") return false;
|
|
395
|
+
const origin = value;
|
|
396
|
+
return origin.version === 1 && ["anthropic", "openai-responses"].includes(origin.protocol) && typeof origin.modelName === "string" && !!origin.modelName && typeof origin.runId === "string" && !!origin.runId && typeof origin.endpointScope === "string" && /^[a-f0-9]{64}$/.test(origin.endpointScope) && (origin.modelId === void 0 || typeof origin.modelId === "string");
|
|
397
|
+
}
|
|
398
|
+
function sameProtocolModel(origin, target) {
|
|
399
|
+
return validOrigin(origin) && origin.protocol === target.protocol && origin.modelName === target.modelName && origin.endpointScope === endpointScope(target);
|
|
400
|
+
}
|
|
401
|
+
function privateReplayAllowed(origin, target, context) {
|
|
402
|
+
return !!origin && !!context?.runId && sameProtocolModel(origin, target) && origin.runId === context.runId && origin.modelId === context.modelId;
|
|
403
|
+
}
|
|
404
|
+
var validatedBlocks = /* @__PURE__ */ new WeakMap();
|
|
405
|
+
var validatedContents = /* @__PURE__ */ new WeakMap();
|
|
406
|
+
function validateProtocolContent(content) {
|
|
407
|
+
if (validatedContents.get(content)) return;
|
|
408
|
+
for (const block of content) {
|
|
409
|
+
if (!block || typeof block !== "object") invalidState();
|
|
410
|
+
if (validatedBlocks.get(block)) continue;
|
|
411
|
+
if ("origin" in block && block.origin !== void 0 && !validOrigin(block.origin))
|
|
412
|
+
invalidState();
|
|
413
|
+
if ("protocolMeta" in block && block.protocolMeta !== void 0) {
|
|
414
|
+
const meta = block.protocolMeta;
|
|
415
|
+
if (!meta || !validOrigin(meta.origin) || !Number.isSafeInteger(meta.itemIndex) || meta.itemIndex < 0 || meta.itemId !== void 0 && (typeof meta.itemId !== "string" || !meta.itemId) || meta.contentIndex !== void 0 && (!Number.isSafeInteger(meta.contentIndex) || meta.contentIndex < 0) || meta.phase !== void 0 && !["commentary", "final_answer"].includes(meta.phase) || meta.contentType !== void 0 && !["output_text", "refusal"].includes(meta.contentType) || meta.arguments !== void 0 && typeof meta.arguments !== "string")
|
|
416
|
+
invalidState();
|
|
417
|
+
}
|
|
418
|
+
if (block.type === "provider_state") {
|
|
419
|
+
const item = block.item;
|
|
420
|
+
if (!validOrigin(block.origin) || block.origin.protocol !== "openai-responses" || !item || item.type !== "reasoning" || typeof item.id !== "string" || !item.id || !Array.isArray(item.summary) || item.summary.some((s) => !s || s.type !== "summary_text" || typeof s.text !== "string") || item.encrypted_content !== void 0 && typeof item.encrypted_content !== "string" || item.status !== void 0 && !["completed", "in_progress", "incomplete"].includes(item.status))
|
|
421
|
+
invalidState();
|
|
422
|
+
try {
|
|
423
|
+
if (JSON.stringify(item).length > 16 * 1024 * 1024) invalidState();
|
|
424
|
+
} catch {
|
|
425
|
+
invalidState();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
validatedBlocks.set(block, true);
|
|
429
|
+
}
|
|
430
|
+
validatedContents.set(content, true);
|
|
431
|
+
}
|
|
432
|
+
function projectProtocolHistory(messages, target, context) {
|
|
433
|
+
return messages.map((message) => {
|
|
434
|
+
validateProtocolContent(message.content);
|
|
435
|
+
const copy = structuredClone(message);
|
|
436
|
+
copy.content = copy.content.flatMap((block) => {
|
|
437
|
+
if (block.type === "provider_state")
|
|
438
|
+
return privateReplayAllowed(block.origin, target, context) && (block.item.status === void 0 || block.item.status === "completed") ? [block] : [];
|
|
439
|
+
if (block.type === "redacted_thinking")
|
|
440
|
+
return privateReplayAllowed(block.origin, target, context) ? [block] : [];
|
|
441
|
+
if (block.type === "thinking") {
|
|
442
|
+
if (privateReplayAllowed(block.origin, target, context)) return [block];
|
|
443
|
+
if (!block.origin && target.protocol === "openai-compatible") return [block];
|
|
444
|
+
return block.thinking ? [{ type: "text", text: block.thinking }] : [];
|
|
445
|
+
}
|
|
446
|
+
if ((block.type === "text" || block.type === "tool_use") && block.protocolMeta && !sameProtocolModel(block.protocolMeta.origin, target))
|
|
447
|
+
block.protocolMeta = void 0;
|
|
448
|
+
return [block];
|
|
449
|
+
});
|
|
450
|
+
return copy;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function summarySafeMessage(message) {
|
|
454
|
+
const copy = structuredClone(message);
|
|
455
|
+
copy.content = copy.content.flatMap((block) => {
|
|
456
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking") return [];
|
|
457
|
+
if (block.type === "thinking")
|
|
458
|
+
return block.thinking ? [{ type: "text", text: block.thinking }] : [];
|
|
459
|
+
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
460
|
+
return [block];
|
|
461
|
+
});
|
|
462
|
+
return copy;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/types/identity.ts
|
|
466
|
+
var import_node_crypto2 = require("crypto");
|
|
132
467
|
var lastTime = 0;
|
|
133
468
|
var sequence = 0n;
|
|
134
469
|
var sequenceMask = (1n << 74n) - 1n;
|
|
@@ -136,7 +471,7 @@ function createMessageId() {
|
|
|
136
471
|
const now = Date.now();
|
|
137
472
|
if (now > lastTime) {
|
|
138
473
|
lastTime = now;
|
|
139
|
-
sequence = BigInt(`0x${(0,
|
|
474
|
+
sequence = BigInt(`0x${(0, import_node_crypto2.randomBytes)(10).toString("hex")}`) & sequenceMask;
|
|
140
475
|
} else if (sequence === sequenceMask) {
|
|
141
476
|
lastTime++;
|
|
142
477
|
sequence = 0n;
|
|
@@ -149,9 +484,10 @@ function createMessageId() {
|
|
|
149
484
|
|
|
150
485
|
// src/context/history.ts
|
|
151
486
|
function identifyHistory(messages, source) {
|
|
487
|
+
for (const message of messages) validateProtocolContent(message.content);
|
|
152
488
|
return messages.map((message, position) => ({
|
|
153
489
|
...structuredClone(message),
|
|
154
|
-
id: message.id ?? `legacy-${(0,
|
|
490
|
+
id: message.id ?? `legacy-${(0, import_node_crypto3.createHash)("sha256").update(`${source}\0${position}\0${JSON.stringify(message)}`).digest("hex")}`
|
|
155
491
|
}));
|
|
156
492
|
}
|
|
157
493
|
function normalizeHistory(input) {
|
|
@@ -160,7 +496,7 @@ function normalizeHistory(input) {
|
|
|
160
496
|
const calls = /* @__PURE__ */ new Set();
|
|
161
497
|
const push = (message, sources) => {
|
|
162
498
|
const last = result.messages.at(-1);
|
|
163
|
-
const pure = (m) => m.content.every((b) => b.type === "text");
|
|
499
|
+
const pure = (m) => m.content.every((b) => b.type === "text" && !b.protocolMeta);
|
|
164
500
|
if (last && !last.contextSummary && !message.contextSummary && last.role === message.role && ["user", "assistant"].includes(message.role) && pure(last) && pure(message)) {
|
|
165
501
|
last.content.push(...message.content);
|
|
166
502
|
result.sources.at(-1).push(...sources);
|
|
@@ -251,8 +587,10 @@ function indexInteractions(input, terminals = []) {
|
|
|
251
587
|
const last = group.at(-1);
|
|
252
588
|
const normalized = normalizeHistory(group);
|
|
253
589
|
if (last.role === "assistant" && (!last.state || last.state === "completed") && last.content.every(
|
|
254
|
-
(b) => b.type === "text" || b.type === "thinking" || b.type === "redacted_thinking"
|
|
255
|
-
) && last.content.some(
|
|
590
|
+
(b) => b.type === "text" || b.type === "thinking" || b.type === "redacted_thinking" || b.type === "provider_state"
|
|
591
|
+
) && last.content.some(
|
|
592
|
+
(b) => b.type === "text" && b.text.trim() && b.protocolMeta?.phase !== "commentary"
|
|
593
|
+
) && !normalized.repairs.length)
|
|
256
594
|
boundary.status = "completed";
|
|
257
595
|
}
|
|
258
596
|
const successIndex = interactions.map((i) => i.status).lastIndexOf("completed");
|
|
@@ -376,6 +714,7 @@ var SessionStore = class _SessionStore {
|
|
|
376
714
|
}
|
|
377
715
|
/** 消息级持久化,补稳定身份并保留所有终态字段,成功后方可进入下一次写入。 */
|
|
378
716
|
async append(message) {
|
|
717
|
+
validateProtocolContent(message.content);
|
|
379
718
|
const payload = { ...structuredClone(message), id: message.id ?? createMessageId() };
|
|
380
719
|
await this.enqueue("message", payload);
|
|
381
720
|
message.id ??= payload.id;
|
|
@@ -549,6 +888,7 @@ var SessionStore = class _SessionStore {
|
|
|
549
888
|
if (messages.has(record.payload.id))
|
|
550
889
|
throw new Error(`Duplicate message ID: ${record.payload.id}`);
|
|
551
890
|
messages.add(record.payload.id);
|
|
891
|
+
validateProtocolContent(record.payload.content);
|
|
552
892
|
}
|
|
553
893
|
if (record.type === "header" && records.length)
|
|
554
894
|
throw new Error("Session corrupt: repeated header");
|
|
@@ -570,7 +910,7 @@ var SessionStore = class _SessionStore {
|
|
|
570
910
|
};
|
|
571
911
|
|
|
572
912
|
// src/context/session-lock.ts
|
|
573
|
-
var
|
|
913
|
+
var import_node_crypto4 = require("crypto");
|
|
574
914
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
575
915
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
576
916
|
var SessionLock = class _SessionLock {
|
|
@@ -587,7 +927,7 @@ var SessionLock = class _SessionLock {
|
|
|
587
927
|
import_node_fs3.default.mkdirSync(import_node_path2.default.dirname(lockPath), { recursive: true });
|
|
588
928
|
const recovery = `${lockPath}.recovery`;
|
|
589
929
|
_SessionLock.ensureNoLiveRecovery(recovery);
|
|
590
|
-
const owner = { nonce: (0,
|
|
930
|
+
const owner = { nonce: (0, import_node_crypto4.randomUUID)(), pid: process.pid, createdAt: Date.now() };
|
|
591
931
|
let createdDir = false;
|
|
592
932
|
try {
|
|
593
933
|
import_node_fs3.default.mkdirSync(lockPath);
|
|
@@ -610,7 +950,7 @@ var SessionLock = class _SessionLock {
|
|
|
610
950
|
if (current?.nonce !== stale.nonce || current.pid !== stale.pid || !_SessionLock.isDead(current.pid)) {
|
|
611
951
|
throw new Error("Session locked: owner changed");
|
|
612
952
|
}
|
|
613
|
-
const quarantine = `${lockPath}.stale-${(0,
|
|
953
|
+
const quarantine = `${lockPath}.stale-${(0, import_node_crypto4.randomUUID)()}`;
|
|
614
954
|
import_node_fs3.default.renameSync(lockPath, quarantine);
|
|
615
955
|
let freshDir = false;
|
|
616
956
|
try {
|
|
@@ -708,7 +1048,7 @@ var SessionLock = class _SessionLock {
|
|
|
708
1048
|
|
|
709
1049
|
// src/context/session-manager.ts
|
|
710
1050
|
var import_node_child_process = require("child_process");
|
|
711
|
-
var
|
|
1051
|
+
var import_node_crypto5 = require("crypto");
|
|
712
1052
|
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
713
1053
|
var import_node_os = __toESM(require("os"), 1);
|
|
714
1054
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -738,7 +1078,7 @@ function resolveSessionProject(cwd) {
|
|
|
738
1078
|
return {
|
|
739
1079
|
projectRoot: root,
|
|
740
1080
|
cwd: realCwd,
|
|
741
|
-
projectKey: (0,
|
|
1081
|
+
projectKey: (0, import_node_crypto5.createHash)("sha256").update(normalized).digest("hex"),
|
|
742
1082
|
gitBranch,
|
|
743
1083
|
gitWorktree
|
|
744
1084
|
};
|
|
@@ -890,7 +1230,7 @@ var SessionManager = class {
|
|
|
890
1230
|
if (!import_node_fs4.default.existsSync(source)) continue;
|
|
891
1231
|
const before = import_node_fs4.default.statSync(source);
|
|
892
1232
|
const bytes = import_node_fs4.default.readFileSync(source);
|
|
893
|
-
const identity = (0,
|
|
1233
|
+
const identity = (0, import_node_crypto5.createHash)("sha256").update(import_node_fs4.default.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
|
|
894
1234
|
const importLock = await SessionLock.acquire(
|
|
895
1235
|
import_node_path3.default.join(this.directory, `import-${identity}.lock`)
|
|
896
1236
|
);
|
|
@@ -1118,389 +1458,189 @@ var SessionManager = class {
|
|
|
1118
1458
|
const id = import_node_path3.default.basename(file, ".jsonl").split("_").at(-1);
|
|
1119
1459
|
return import_node_path3.default.join(import_node_path3.default.dirname(file), `${id}.meta.json`);
|
|
1120
1460
|
}
|
|
1121
|
-
lockPath(id) {
|
|
1122
|
-
return import_node_path3.default.join(this.directory, `${id}.lock`);
|
|
1123
|
-
}
|
|
1124
|
-
report(message) {
|
|
1125
|
-
try {
|
|
1126
|
-
this.diagnostic?.(message);
|
|
1127
|
-
} catch {
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
|
|
1132
|
-
// src/context/budget.ts
|
|
1133
|
-
var import_node_crypto5 = require("crypto");
|
|
1134
|
-
|
|
1135
|
-
// src/models/router.ts
|
|
1136
|
-
var DEFAULT_CONTEXT_WINDOW_TOKENS = 1e6;
|
|
1137
|
-
function resolveContextWindow(value) {
|
|
1138
|
-
if (value === void 0) {
|
|
1139
|
-
return { tokens: DEFAULT_CONTEXT_WINDOW_TOKENS, estimated: true };
|
|
1140
|
-
}
|
|
1141
|
-
let tokens;
|
|
1142
|
-
if (typeof value === "number") {
|
|
1143
|
-
tokens = value;
|
|
1144
|
-
} else {
|
|
1145
|
-
const match = /^([0-9]+(?:\.[0-9]+)?)([km])$/i.exec(value.trim());
|
|
1146
|
-
const unit = match?.[2]?.toLowerCase();
|
|
1147
|
-
const fractionDigits = match?.[1]?.split(".")[1]?.length ?? 0;
|
|
1148
|
-
const maxFractionDigits = unit === "m" ? 6 : 3;
|
|
1149
|
-
if (!match || fractionDigits > maxFractionDigits) {
|
|
1150
|
-
throw new RangeError(
|
|
1151
|
-
`Invalid contextWindow '${value}': expected a positive integer or K/M value`
|
|
1152
|
-
);
|
|
1153
|
-
}
|
|
1154
|
-
const multiplier = unit === "m" ? 1e6 : 1e3;
|
|
1155
|
-
tokens = Number(match[1]) * multiplier;
|
|
1156
|
-
}
|
|
1157
|
-
if (!Number.isSafeInteger(tokens) || tokens <= 0) {
|
|
1158
|
-
throw new RangeError(
|
|
1159
|
-
`Invalid contextWindow '${value}': token count must be a positive integer`
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
return { tokens, estimated: false };
|
|
1163
|
-
}
|
|
1164
|
-
var SimpleModelRouter = class {
|
|
1165
|
-
providers = /* @__PURE__ */ new Map();
|
|
1166
|
-
profiles = /* @__PURE__ */ new Map();
|
|
1167
|
-
defaultRole = "default";
|
|
1168
|
-
constructor(defaultProfile, defaultProvider) {
|
|
1169
|
-
this.setRole("default", defaultProfile, defaultProvider);
|
|
1170
|
-
}
|
|
1171
|
-
setRole(role, profile, provider) {
|
|
1172
|
-
this.profiles.set(role, profile);
|
|
1173
|
-
this.providers.set(role, provider);
|
|
1174
|
-
}
|
|
1175
|
-
resolve(role = "default") {
|
|
1176
|
-
return this.providers.get(role) ?? this.providers.get("default");
|
|
1177
|
-
}
|
|
1178
|
-
getProfile(role = "default") {
|
|
1179
|
-
return this.profiles.get(role) ?? this.profiles.get("default");
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
// src/context/budget.ts
|
|
1184
|
-
function createContextBudget(window) {
|
|
1185
|
-
const resolved = resolveContextWindow(window);
|
|
1186
|
-
const contextWindow = resolved.estimated ? 32e3 : resolved.tokens;
|
|
1187
|
-
const outputReserve = Math.min(4096, Math.floor(contextWindow * 0.125));
|
|
1188
|
-
const safetyReserve = Math.max(128, Math.ceil(contextWindow * 0.05));
|
|
1189
|
-
const inputBudget = contextWindow - outputReserve - safetyReserve;
|
|
1190
|
-
const trigger = Math.floor(inputBudget * 0.85);
|
|
1191
|
-
const target = Math.floor(inputBudget * 0.65);
|
|
1192
|
-
if (!(0 < target && target < trigger && trigger < inputBudget && outputReserve > 0))
|
|
1193
|
-
throw new Error("Invalid context budget: model window is too small");
|
|
1194
|
-
return {
|
|
1195
|
-
contextWindow,
|
|
1196
|
-
estimatedWindow: resolved.estimated,
|
|
1197
|
-
outputReserve,
|
|
1198
|
-
safetyReserve,
|
|
1199
|
-
inputBudget,
|
|
1200
|
-
trigger,
|
|
1201
|
-
target
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
function stableSerialize(value) {
|
|
1205
|
-
const visit = (v) => {
|
|
1206
|
-
if (Array.isArray(v)) return v.map(visit);
|
|
1207
|
-
if (v && typeof v === "object")
|
|
1208
|
-
return Object.fromEntries(
|
|
1209
|
-
Object.entries(v).filter(([, val]) => val !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, val]) => [key, visit(val)])
|
|
1210
|
-
);
|
|
1211
|
-
return v;
|
|
1212
|
-
};
|
|
1213
|
-
return JSON.stringify(visit(value));
|
|
1214
|
-
}
|
|
1215
|
-
function requestFingerprint(request, modelFingerprint = "") {
|
|
1216
|
-
return (0, import_node_crypto5.createHash)("sha256").update(
|
|
1217
|
-
stableSerialize({
|
|
1218
|
-
modelFingerprint,
|
|
1219
|
-
systemPrompt: request.systemPrompt,
|
|
1220
|
-
messages: request.messages,
|
|
1221
|
-
tools: request.tools,
|
|
1222
|
-
maxTokens: request.maxTokens,
|
|
1223
|
-
temperature: request.temperature
|
|
1224
|
-
})
|
|
1225
|
-
).digest("hex");
|
|
1226
|
-
}
|
|
1227
|
-
var UnicodeTokenEstimator = class {
|
|
1228
|
-
calibration = /* @__PURE__ */ new Map();
|
|
1229
|
-
/** 计算系统、工具、历史和协议开销,返回估算/校准来源,保持请求不变。 */
|
|
1230
|
-
estimate(request, modelFingerprint = "") {
|
|
1231
|
-
const count = (text) => {
|
|
1232
|
-
let ascii = 0;
|
|
1233
|
-
let unicode = 0;
|
|
1234
|
-
for (const char of text) {
|
|
1235
|
-
if (char.codePointAt(0) < 128) ascii++;
|
|
1236
|
-
else unicode++;
|
|
1237
|
-
}
|
|
1238
|
-
return Math.ceil(ascii / 4) + unicode;
|
|
1239
|
-
};
|
|
1240
|
-
const system = count(request.systemPrompt ?? "");
|
|
1241
|
-
const tools = request.tools?.length ? count(stableSerialize(request.tools)) + request.tools.length * 8 : 0;
|
|
1242
|
-
const history = request.messages.reduce(
|
|
1243
|
-
(total, message) => total + count(stableSerialize(message.content)) + 6,
|
|
1244
|
-
0
|
|
1245
|
-
);
|
|
1246
|
-
const overhead = 12;
|
|
1247
|
-
const factor = this.calibration.get(requestFingerprint(request, modelFingerprint)) ?? 1;
|
|
1248
|
-
return {
|
|
1249
|
-
total: Math.ceil((system + tools + history + overhead) * factor),
|
|
1250
|
-
system: Math.ceil(system * factor),
|
|
1251
|
-
tools: Math.ceil(tools * factor),
|
|
1252
|
-
history: Math.ceil(history * factor),
|
|
1253
|
-
overhead: Math.ceil(overhead * factor),
|
|
1254
|
-
source: factor === 1 ? "estimated" : "calibrated"
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
/** 仅接受真实正整数 prompt usage,校准不会降低原保守估算;缓存最多 64 个最终请求。 */
|
|
1258
|
-
observe(request, actualPromptTokens, modelFingerprint = "") {
|
|
1259
|
-
if (!Number.isSafeInteger(actualPromptTokens) || actualPromptTokens <= 0) return;
|
|
1260
|
-
const key = requestFingerprint(request, modelFingerprint);
|
|
1261
|
-
this.calibration.delete(key);
|
|
1262
|
-
const baseline = this.estimate(request, modelFingerprint).total;
|
|
1263
|
-
this.calibration.set(key, Math.max(1, actualPromptTokens / baseline));
|
|
1264
|
-
if (this.calibration.size > 64) this.calibration.delete(this.calibration.keys().next().value);
|
|
1461
|
+
lockPath(id) {
|
|
1462
|
+
return import_node_path3.default.join(this.directory, `${id}.lock`);
|
|
1463
|
+
}
|
|
1464
|
+
report(message) {
|
|
1465
|
+
try {
|
|
1466
|
+
this.diagnostic?.(message);
|
|
1467
|
+
} catch {
|
|
1468
|
+
}
|
|
1265
1469
|
}
|
|
1266
1470
|
};
|
|
1267
1471
|
|
|
1268
|
-
// src/context/
|
|
1472
|
+
// src/context/budget.ts
|
|
1269
1473
|
var import_node_crypto6 = require("crypto");
|
|
1270
1474
|
|
|
1271
|
-
// src/
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
} catch {
|
|
1290
|
-
return "[redacted URL]";
|
|
1475
|
+
// src/models/router.ts
|
|
1476
|
+
var DEFAULT_CONTEXT_WINDOW_TOKENS = 1e6;
|
|
1477
|
+
function resolveContextWindow(value) {
|
|
1478
|
+
if (value === void 0) {
|
|
1479
|
+
return { tokens: DEFAULT_CONTEXT_WINDOW_TOKENS, estimated: true };
|
|
1480
|
+
}
|
|
1481
|
+
let tokens;
|
|
1482
|
+
if (typeof value === "number") {
|
|
1483
|
+
tokens = value;
|
|
1484
|
+
} else {
|
|
1485
|
+
const match = /^([0-9]+(?:\.[0-9]+)?)([km])$/i.exec(value.trim());
|
|
1486
|
+
const unit = match?.[2]?.toLowerCase();
|
|
1487
|
+
const fractionDigits = match?.[1]?.split(".")[1]?.length ?? 0;
|
|
1488
|
+
const maxFractionDigits = unit === "m" ? 6 : 3;
|
|
1489
|
+
if (!match || fractionDigits > maxFractionDigits) {
|
|
1490
|
+
throw new RangeError(
|
|
1491
|
+
`Invalid contextWindow '${value}': expected a positive integer or K/M value`
|
|
1492
|
+
);
|
|
1291
1493
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
function safeErrorIdentifier(value) {
|
|
1295
|
-
return value && /^[A-Za-z0-9_.:-]{1,80}$/.test(value) && !/^(?:sk|api|key)-/i.test(value) ? value : void 0;
|
|
1296
|
-
}
|
|
1297
|
-
function classifyModelError(options) {
|
|
1298
|
-
const codes = [options.providerCode, options.providerType, options.code];
|
|
1299
|
-
if (codes.some(
|
|
1300
|
-
(c) => [
|
|
1301
|
-
"context_length_exceeded",
|
|
1302
|
-
"context_window_exceeded",
|
|
1303
|
-
"max_context_length_exceeded",
|
|
1304
|
-
"MODEL_CONTEXT_EXCEEDED"
|
|
1305
|
-
].includes(c ?? "")
|
|
1306
|
-
))
|
|
1307
|
-
return "context";
|
|
1308
|
-
if (codes.some(
|
|
1309
|
-
(c) => [
|
|
1310
|
-
"insufficient_quota",
|
|
1311
|
-
"insufficient_balance",
|
|
1312
|
-
"quota_exceeded",
|
|
1313
|
-
"billing_hard_limit_reached"
|
|
1314
|
-
].includes(c ?? "")
|
|
1315
|
-
) || options.status === 402)
|
|
1316
|
-
return "quota";
|
|
1317
|
-
if (codes.some(
|
|
1318
|
-
(c) => ["invalid_api_key", "authentication_error", "permission_error"].includes(c ?? "")
|
|
1319
|
-
) || options.status === 401 || options.status === 403)
|
|
1320
|
-
return "authentication";
|
|
1321
|
-
if (codes.some((c) => ["rate_limit_exceeded", "rate_limit_error"].includes(c ?? "")) || options.status === 429)
|
|
1322
|
-
return "rate_limit";
|
|
1323
|
-
if (options.status === 408 || /TIMEOUT|ETIMEDOUT/.test(options.transportCode ?? ""))
|
|
1324
|
-
return "timeout";
|
|
1325
|
-
if (options.transportCode || options.code === "MODEL_STREAM_INTERRUPTED" || options.stage === "connect")
|
|
1326
|
-
return "network";
|
|
1327
|
-
if (codes.some(
|
|
1328
|
-
(c) => ["server_error", "overloaded_error", "api_error", "internal_server_error"].includes(c ?? "")
|
|
1329
|
-
) || options.status && options.status >= 500)
|
|
1330
|
-
return "service";
|
|
1331
|
-
if (codes.some((c) => ["invalid_request_error", "invalid_argument"].includes(c ?? "")))
|
|
1332
|
-
return "invalid_request";
|
|
1333
|
-
if (options.status && options.status >= 400) return "invalid_request";
|
|
1334
|
-
if (options.code === "MODEL_STREAM_INCOMPLETE" || options.code === "MODEL_INVALID_RESPONSE")
|
|
1335
|
-
return "invalid_response";
|
|
1336
|
-
return "unknown";
|
|
1337
|
-
}
|
|
1338
|
-
var MODEL_ERROR_ADVICE = {
|
|
1339
|
-
authentication: "\u68C0\u67E5 API Key\u3001\u6A21\u578B\u6743\u9650\u53CA\u7AEF\u70B9\u914D\u7F6E\u540E\u91CD\u8BD5\u3002",
|
|
1340
|
-
quota: "\u68C0\u67E5\u8D26\u6237\u4F59\u989D\u6216\u914D\u989D\u540E\u91CD\u8BD5\u3002",
|
|
1341
|
-
rate_limit: "\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u964D\u4F4E\u8BF7\u6C42\u9891\u7387\u3002",
|
|
1342
|
-
context: "\u9009\u62E9\u66F4\u5927\u4E0A\u4E0B\u6587\u7A97\u53E3\u3001\u538B\u7F29\u5386\u53F2\u6216\u62C6\u5206\u4EFB\u52A1\u540E\u91CD\u8BD5\u3002",
|
|
1343
|
-
service: "\u670D\u52A1\u6682\u65F6\u5F02\u5E38\uFF0C\u53EF\u7A0D\u540E\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u3002",
|
|
1344
|
-
network: "\u68C0\u67E5\u7F51\u7EDC\u3001\u4EE3\u7406\u548C\u7AEF\u70B9\uFF1B\u8FDE\u63A5\u6062\u590D\u540E\u53EF\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u3002",
|
|
1345
|
-
timeout: "\u68C0\u67E5\u7F51\u7EDC\u53CA\u670D\u52A1\u54CD\u5E94\u60C5\u51B5\uFF0C\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
1346
|
-
invalid_request: "\u68C0\u67E5\u6A21\u578B\u4E0E\u7AEF\u70B9\u7684\u534F\u8BAE\u517C\u5BB9\u6027\u53CA\u8BF7\u6C42\u914D\u7F6E\u3002",
|
|
1347
|
-
invalid_response: "\u54CD\u5E94\u4E0D\u5B8C\u6574\u6216\u683C\u5F0F\u5F02\u5E38\uFF0C\u53EF\u5728\u5F53\u524D\u4F1A\u8BDD\u91CD\u8BD5\u5E76\u68C0\u67E5\u7AEF\u70B9\u534F\u8BAE\u517C\u5BB9\u6027\u3002",
|
|
1348
|
-
unknown: "\u539F\u56E0\u672A\u786E\u8BA4\uFF0C\u8BF7\u7ED3\u5408\u9519\u8BEF\u7801\u548C\u8FD0\u884C\u65E5\u5FD7\u68C0\u67E5\u3002"
|
|
1349
|
-
};
|
|
1350
|
-
function describeModelError(error, context = {}) {
|
|
1351
|
-
return {
|
|
1352
|
-
category: error.category,
|
|
1353
|
-
code: error.code,
|
|
1354
|
-
message: sanitizeModelErrorMessage(error.message),
|
|
1355
|
-
status: error.status,
|
|
1356
|
-
providerCode: error.providerCode,
|
|
1357
|
-
providerType: error.providerType,
|
|
1358
|
-
transportCode: error.transportCode,
|
|
1359
|
-
stage: error.stage,
|
|
1360
|
-
retryPolicy: error.retryPolicy,
|
|
1361
|
-
suggestion: MODEL_ERROR_ADVICE[error.category],
|
|
1362
|
-
modelId: context.modelId ? sanitizeModelErrorMessage(context.modelId) : void 0,
|
|
1363
|
-
provider: context.provider ? sanitizeModelErrorMessage(context.provider) : void 0,
|
|
1364
|
-
operation: context.operation
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
var KapibalaError = class extends Error {
|
|
1368
|
-
code;
|
|
1369
|
-
retryPolicy;
|
|
1370
|
-
safeMessage;
|
|
1371
|
-
constructor(message, options) {
|
|
1372
|
-
super(message);
|
|
1373
|
-
this.name = "KapibalaError";
|
|
1374
|
-
this.code = options?.code ?? "INTERNAL_ERROR";
|
|
1375
|
-
this.retryPolicy = options?.retryPolicy ?? "never";
|
|
1376
|
-
this.safeMessage = options?.safeMessage ?? "Operation failed";
|
|
1494
|
+
const multiplier = unit === "m" ? 1e6 : 1e3;
|
|
1495
|
+
tokens = Number(match[1]) * multiplier;
|
|
1377
1496
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
retryable;
|
|
1382
|
-
/** 可选的受控底层连接错误码,不包含原始传输异常文本。 */
|
|
1383
|
-
transportCode;
|
|
1384
|
-
providerCode;
|
|
1385
|
-
providerType;
|
|
1386
|
-
stage;
|
|
1387
|
-
category;
|
|
1388
|
-
/** 构建模型错误,清理服务端原因并按明确状态/错误码分类;重试策略仅为建议,不授权重放。 */
|
|
1389
|
-
constructor(message, options = {}) {
|
|
1390
|
-
const category = classifyModelError(options);
|
|
1391
|
-
const actionRequired = ["authentication", "quota", "context", "invalid_request"].includes(
|
|
1392
|
-
category
|
|
1497
|
+
if (!Number.isSafeInteger(tokens) || tokens <= 0) {
|
|
1498
|
+
throw new RangeError(
|
|
1499
|
+
`Invalid contextWindow '${value}': token count must be a positive integer`
|
|
1393
1500
|
);
|
|
1394
|
-
const retryable = options.retryable ?? ["rate_limit", "service", "timeout", "network"].includes(category);
|
|
1395
|
-
super(sanitizeModelErrorMessage(message), {
|
|
1396
|
-
code: safeErrorIdentifier(options.code) ?? (category === "unknown" ? "MODEL_ERROR" : `MODEL_${category.toUpperCase()}`),
|
|
1397
|
-
retryPolicy: actionRequired ? "after_user_action" : retryable ? "backoff" : "never",
|
|
1398
|
-
safeMessage: sanitizeModelErrorMessage(message)
|
|
1399
|
-
});
|
|
1400
|
-
this.name = "ModelError";
|
|
1401
|
-
this.status = options?.status;
|
|
1402
|
-
this.transportCode = safeErrorIdentifier(options.transportCode);
|
|
1403
|
-
this.providerCode = safeErrorIdentifier(options.providerCode);
|
|
1404
|
-
this.providerType = safeErrorIdentifier(options.providerType);
|
|
1405
|
-
this.stage = options.stage;
|
|
1406
|
-
this.category = category;
|
|
1407
|
-
this.retryable = retryable;
|
|
1408
1501
|
}
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
retryable: false
|
|
1417
|
-
code: "MODEL_CONTEXT_EXCEEDED"
|
|
1502
|
+
return { tokens, estimated: false };
|
|
1503
|
+
}
|
|
1504
|
+
function validateModelBinding(profile, provider) {
|
|
1505
|
+
const binding = provider.binding;
|
|
1506
|
+
if (binding && (binding.protocol !== profile.provider || binding.modelName !== profile.modelName || binding.baseURL.replace(/\/+$/, "") !== profile.baseURL.replace(/\/+$/, "") || binding.workspaceId !== profile.anthropicWorkspaceId)) {
|
|
1507
|
+
throw new ModelError("Model profile and provider binding do not match", {
|
|
1508
|
+
stage: "request",
|
|
1509
|
+
retryable: false
|
|
1418
1510
|
});
|
|
1419
|
-
this.name = "ContextOverflowError";
|
|
1420
1511
|
}
|
|
1421
|
-
}
|
|
1422
|
-
var
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
this.
|
|
1512
|
+
}
|
|
1513
|
+
var SimpleModelRouter = class {
|
|
1514
|
+
providers = /* @__PURE__ */ new Map();
|
|
1515
|
+
profiles = /* @__PURE__ */ new Map();
|
|
1516
|
+
defaultRole = "default";
|
|
1517
|
+
constructor(defaultProfile, defaultProvider) {
|
|
1518
|
+
this.setRole("default", defaultProfile, defaultProvider);
|
|
1428
1519
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
retryPolicy: "never",
|
|
1435
|
-
safeMessage: "Tool execution failed"
|
|
1436
|
-
});
|
|
1437
|
-
this.name = "ToolError";
|
|
1520
|
+
/** 校验配对后同时替换两者并保存配置副本,校验失败不污染既有绑定。 */
|
|
1521
|
+
setRole(role, profile, provider) {
|
|
1522
|
+
validateModelBinding(profile, provider);
|
|
1523
|
+
this.profiles.set(role, structuredClone(profile));
|
|
1524
|
+
this.providers.set(role, provider);
|
|
1438
1525
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
this.
|
|
1450
|
-
|
|
1451
|
-
|
|
1526
|
+
/** 显式场景要求存在绑定;只有普通查询和摘要允许既有默认回退。 */
|
|
1527
|
+
getBinding(role = "default", strict = false) {
|
|
1528
|
+
if (strict && (!this.profiles.has(role) || !this.providers.has(role)))
|
|
1529
|
+
throw new ModelError(`Model role '${role}' is not configured`, {
|
|
1530
|
+
stage: "request",
|
|
1531
|
+
retryable: false
|
|
1532
|
+
});
|
|
1533
|
+
return { profile: structuredClone(this.getProfile(role)), provider: this.resolve(role) };
|
|
1534
|
+
}
|
|
1535
|
+
resolve(role = "default") {
|
|
1536
|
+
return this.providers.get(role) ?? this.providers.get("default");
|
|
1537
|
+
}
|
|
1538
|
+
/** 返回指定角色或默认模型的独立副本,调用方不能改写在途绑定。 */
|
|
1539
|
+
getProfile(role = "default") {
|
|
1540
|
+
return structuredClone(this.profiles.get(role) ?? this.profiles.get("default"));
|
|
1452
1541
|
}
|
|
1453
1542
|
};
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1543
|
+
|
|
1544
|
+
// src/types/serialization.ts
|
|
1545
|
+
function stableSerialize(value) {
|
|
1546
|
+
const visit = (v) => {
|
|
1547
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
1548
|
+
if (v && typeof v === "object")
|
|
1549
|
+
return Object.fromEntries(
|
|
1550
|
+
Object.entries(v).filter(([, val]) => val !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, val]) => [key, visit(val)])
|
|
1551
|
+
);
|
|
1552
|
+
return v;
|
|
1553
|
+
};
|
|
1554
|
+
return JSON.stringify(visit(value));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/context/budget.ts
|
|
1558
|
+
function createContextBudget(window, maxOutputTokens = 4096) {
|
|
1559
|
+
if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0)
|
|
1560
|
+
throw new ModelError("Invalid output token budget: expected a positive safe integer", {
|
|
1561
|
+
code: "MODEL_INVALID_REQUEST",
|
|
1562
|
+
stage: "request",
|
|
1563
|
+
retryable: false
|
|
1564
|
+
});
|
|
1565
|
+
const resolved = resolveContextWindow(window);
|
|
1566
|
+
const contextWindow = resolved.estimated ? 32e3 : resolved.tokens;
|
|
1567
|
+
const outputReserve = Math.min(maxOutputTokens, Math.floor(contextWindow * 0.125));
|
|
1568
|
+
const safetyReserve = Math.max(128, Math.ceil(contextWindow * 0.05));
|
|
1569
|
+
const inputBudget = contextWindow - outputReserve - safetyReserve;
|
|
1570
|
+
const trigger = Math.floor(inputBudget * 0.85);
|
|
1571
|
+
const target = Math.floor(inputBudget * 0.65);
|
|
1572
|
+
if (!(0 < target && target < trigger && trigger < inputBudget && outputReserve > 0))
|
|
1573
|
+
throw new ModelError("Invalid context budget: model window is too small", {
|
|
1574
|
+
code: "MODEL_INVALID_REQUEST",
|
|
1575
|
+
stage: "request",
|
|
1576
|
+
retryable: false
|
|
1460
1577
|
});
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
};
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1578
|
+
return {
|
|
1579
|
+
contextWindow,
|
|
1580
|
+
estimatedWindow: resolved.estimated,
|
|
1581
|
+
outputReserve,
|
|
1582
|
+
safetyReserve,
|
|
1583
|
+
inputBudget,
|
|
1584
|
+
trigger,
|
|
1585
|
+
target
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function requestFingerprint(request, modelFingerprint = "") {
|
|
1589
|
+
return (0, import_node_crypto6.createHash)("sha256").update(
|
|
1590
|
+
stableSerialize({
|
|
1591
|
+
modelFingerprint,
|
|
1592
|
+
systemPrompt: request.systemPrompt,
|
|
1593
|
+
messages: request.messages,
|
|
1594
|
+
tools: request.tools,
|
|
1595
|
+
maxTokens: request.maxTokens,
|
|
1596
|
+
temperature: request.temperature
|
|
1597
|
+
})
|
|
1598
|
+
).digest("hex");
|
|
1599
|
+
}
|
|
1600
|
+
var UnicodeTokenEstimator = class {
|
|
1601
|
+
calibration = /* @__PURE__ */ new Map();
|
|
1602
|
+
/** 计算系统、工具、历史和协议开销,返回估算/校准来源,保持请求不变。 */
|
|
1603
|
+
estimate(request, modelFingerprint = "") {
|
|
1604
|
+
const count = (text) => {
|
|
1605
|
+
let ascii = 0;
|
|
1606
|
+
let unicode = 0;
|
|
1607
|
+
for (const char of text) {
|
|
1608
|
+
if (char.codePointAt(0) < 128) ascii++;
|
|
1609
|
+
else unicode++;
|
|
1484
1610
|
}
|
|
1611
|
+
return Math.ceil(ascii / 4) + unicode;
|
|
1612
|
+
};
|
|
1613
|
+
const system = count(request.systemPrompt ?? "");
|
|
1614
|
+
const tools = request.tools?.length ? count(stableSerialize(request.tools)) + request.tools.length * 8 : 0;
|
|
1615
|
+
const history = request.messages.reduce(
|
|
1616
|
+
(total, message) => total + count(stableSerialize(message.content)) + 6,
|
|
1617
|
+
0
|
|
1485
1618
|
);
|
|
1486
|
-
|
|
1487
|
-
this.
|
|
1488
|
-
|
|
1619
|
+
const overhead = 12;
|
|
1620
|
+
const factor = this.calibration.get(requestFingerprint(request, modelFingerprint)) ?? 1;
|
|
1621
|
+
return {
|
|
1622
|
+
total: Math.ceil((system + tools + history + overhead) * factor),
|
|
1623
|
+
system: Math.ceil(system * factor),
|
|
1624
|
+
tools: Math.ceil(tools * factor),
|
|
1625
|
+
history: Math.ceil(history * factor),
|
|
1626
|
+
overhead: Math.ceil(overhead * factor),
|
|
1627
|
+
source: factor === 1 ? "estimated" : "calibrated"
|
|
1628
|
+
};
|
|
1489
1629
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
safeMessage: "\u4E0A\u4E0B\u6587\u72B6\u6001\u4FDD\u5B58\u5931\u8D25\uFF0C\u5019\u9009\u672A\u751F\u6548\u3002"
|
|
1499
|
-
});
|
|
1500
|
-
this.name = "ContextStorageError";
|
|
1630
|
+
/** 仅接受真实正整数 prompt usage,校准不会降低原保守估算;缓存最多 64 个最终请求。 */
|
|
1631
|
+
observe(request, actualPromptTokens, modelFingerprint = "") {
|
|
1632
|
+
if (!Number.isSafeInteger(actualPromptTokens) || actualPromptTokens <= 0) return;
|
|
1633
|
+
const key = requestFingerprint(request, modelFingerprint);
|
|
1634
|
+
this.calibration.delete(key);
|
|
1635
|
+
const baseline = this.estimate(request, modelFingerprint).total;
|
|
1636
|
+
this.calibration.set(key, Math.max(1, actualPromptTokens / baseline));
|
|
1637
|
+
if (this.calibration.size > 64) this.calibration.delete(this.calibration.keys().next().value);
|
|
1501
1638
|
}
|
|
1502
1639
|
};
|
|
1503
1640
|
|
|
1641
|
+
// src/context/manager.ts
|
|
1642
|
+
var import_node_crypto7 = require("crypto");
|
|
1643
|
+
|
|
1504
1644
|
// src/context/summary.ts
|
|
1505
1645
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
1506
1646
|
function truncateToolOutput(text, maximum = 2e3) {
|
|
@@ -1618,6 +1758,7 @@ var SummaryService = class {
|
|
|
1618
1758
|
/**
|
|
1619
1759
|
* 按完整用户交互分批,以旧摘要和新增原始前缀取材;中间结果只是局部候选,不激活。
|
|
1620
1760
|
* 单个不可拆分输入超预算、无终态、截断或非法来源均失败,取消先关闭 Provider 迭代。
|
|
1761
|
+
* 摘要输出独立限制为 4096,并受 Profile 更低覆盖及窗口约束;非法 Profile 预算先失败。
|
|
1621
1762
|
*/
|
|
1622
1763
|
async generate(history, previous, signal) {
|
|
1623
1764
|
if (!history.length) throw new Error("No eligible summary prefix");
|
|
@@ -1630,7 +1771,11 @@ var SummaryService = class {
|
|
|
1630
1771
|
try {
|
|
1631
1772
|
const profile = this.options.router.getProfile("summary");
|
|
1632
1773
|
const provider = this.options.router.resolve("summary");
|
|
1633
|
-
const
|
|
1774
|
+
const profileBudget = createContextBudget(profile.contextWindow, profile.maxOutputTokens);
|
|
1775
|
+
const budget = createContextBudget(
|
|
1776
|
+
profile.contextWindow,
|
|
1777
|
+
Math.min(4096, profileBudget.outputReserve)
|
|
1778
|
+
);
|
|
1634
1779
|
const allSources = /* @__PURE__ */ new Set([
|
|
1635
1780
|
...history.map((m) => m.id),
|
|
1636
1781
|
...previous?.summary.references ?? []
|
|
@@ -1640,7 +1785,7 @@ var SummaryService = class {
|
|
|
1640
1785
|
for (const message of history) {
|
|
1641
1786
|
if (!units.length || message.role === "user" && (!message.interactionId || message.interactionId !== units.at(-1)[0].interactionId))
|
|
1642
1787
|
units.push([]);
|
|
1643
|
-
units.at(-1).push(
|
|
1788
|
+
units.at(-1).push(summarySafeMessage(message));
|
|
1644
1789
|
}
|
|
1645
1790
|
let rolling = previous?.summary;
|
|
1646
1791
|
let calls = 0;
|
|
@@ -1672,7 +1817,8 @@ var SummaryService = class {
|
|
|
1672
1817
|
tools: [],
|
|
1673
1818
|
maxTokens: budget.outputReserve,
|
|
1674
1819
|
signal: controller.signal,
|
|
1675
|
-
|
|
1820
|
+
// 原生模型使用自己的采样默认,摘要目的不强制沿用旧 Chat 的采样参数。
|
|
1821
|
+
...profile.provider === "openai-compatible" ? { temperature: 0 } : {}
|
|
1676
1822
|
});
|
|
1677
1823
|
while (position < units.length) {
|
|
1678
1824
|
if (controller.signal.aborted) throw new AbortError();
|
|
@@ -1706,6 +1852,11 @@ var SummaryService = class {
|
|
|
1706
1852
|
stopped = true;
|
|
1707
1853
|
finishReason = event.finishReason;
|
|
1708
1854
|
attemptUsage = event.usage;
|
|
1855
|
+
if (event.finalContent?.some((block) => block.type === "tool_use"))
|
|
1856
|
+
throw new Error("Summary unexpectedly attempted a tool call");
|
|
1857
|
+
if (event.refusal) throw new Error("Summary model refused the request");
|
|
1858
|
+
if (event.finalContent)
|
|
1859
|
+
text = event.finalContent.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1709
1860
|
}
|
|
1710
1861
|
}
|
|
1711
1862
|
} finally {
|
|
@@ -1746,7 +1897,13 @@ var SummaryService = class {
|
|
|
1746
1897
|
|
|
1747
1898
|
// src/context/manager.ts
|
|
1748
1899
|
var POLICY_VERSION = 1;
|
|
1749
|
-
var digest = (value) => (0,
|
|
1900
|
+
var digest = (value) => (0, import_node_crypto7.createHash)("sha256").update(stableSerialize(value)).digest("hex");
|
|
1901
|
+
function createRequestBudget(profile, request) {
|
|
1902
|
+
const profileBudget = createContextBudget(profile.contextWindow, profile.maxOutputTokens);
|
|
1903
|
+
if (request.maxTokens === void 0) return profileBudget;
|
|
1904
|
+
const requestBudget = createContextBudget(profile.contextWindow, request.maxTokens);
|
|
1905
|
+
return requestBudget.outputReserve < profileBudget.outputReserve ? requestBudget : profileBudget;
|
|
1906
|
+
}
|
|
1750
1907
|
function summaryFailureCode(error) {
|
|
1751
1908
|
if (error instanceof ModelError) return error.code;
|
|
1752
1909
|
if (error instanceof SyntaxError) return "SUMMARY_INVALID_JSON";
|
|
@@ -1853,28 +2010,39 @@ var ContextManager = class {
|
|
|
1853
2010
|
this.terminals.push(terminal);
|
|
1854
2011
|
}
|
|
1855
2012
|
/**
|
|
1856
|
-
* 最终 Hook
|
|
2013
|
+
* 最终 Hook 之后统一预算;Profile 与请求的更低覆盖同时约束输出预留和实际 maxTokens。
|
|
2014
|
+
* 非法输出配置或输入超限在 Provider 调用前失败;不使用固定 4096 覆盖原生模型上限。
|
|
2015
|
+
* 签名或加密续答可禁止压缩;此时仍估算并拒绝超预算请求,不剪裁历史或调用摘要网络。
|
|
2016
|
+
* @param allowCompaction 是否允许当前请求剪裁/摘要;冻结私有续答前缀时传 false。
|
|
1857
2017
|
* @returns 语义事件及准备完成的请求,不改变持久历史。
|
|
1858
2018
|
*/
|
|
1859
|
-
async *prepare(history, request, profile, runId, reason = "threshold") {
|
|
1860
|
-
const budget =
|
|
1861
|
-
if (request.maxTokens !== void 0 && (!Number.isSafeInteger(request.maxTokens) || request.maxTokens <= 0))
|
|
1862
|
-
throw new Error("Invalid output token budget");
|
|
2019
|
+
async *prepare(history, request, profile, runId, reason = "threshold", allowCompaction = true) {
|
|
2020
|
+
const budget = createRequestBudget(profile, request);
|
|
1863
2021
|
let prepared = {
|
|
1864
2022
|
...request,
|
|
1865
|
-
maxTokens:
|
|
2023
|
+
maxTokens: budget.outputReserve
|
|
1866
2024
|
};
|
|
1867
2025
|
const estimate = (req) => this.estimator.estimate(req, `${profile.id}:${profile.modelName}`);
|
|
1868
|
-
const base =
|
|
1869
|
-
|
|
1870
|
-
|
|
2026
|
+
const base = projectProtocolHistory(
|
|
2027
|
+
this.project(history),
|
|
2028
|
+
{
|
|
2029
|
+
protocol: profile.provider,
|
|
2030
|
+
modelName: profile.modelName,
|
|
2031
|
+
baseURL: profile.baseURL,
|
|
2032
|
+
workspaceId: profile.anthropicWorkspaceId
|
|
2033
|
+
},
|
|
2034
|
+
request.context
|
|
2035
|
+
);
|
|
2036
|
+
const index2 = indexInteractions(history, this.terminals);
|
|
2037
|
+
const protectedIds = new Set(index2.protectedMessageIds);
|
|
1871
2038
|
const coveredPosition = this.checkpoint ? history.findIndex((m) => m.id === this.checkpoint.coveredEndMessageId) : -1;
|
|
1872
2039
|
const eligibleIds = /* @__PURE__ */ new Set();
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
2040
|
+
const normalizedBase = normalizeHistory(history.slice(coveredPosition + 1));
|
|
2041
|
+
const baseSources = normalizedBase.sources;
|
|
2042
|
+
const baseRaw = normalizedBase.messages;
|
|
1875
2043
|
const knownOrder = prepared.messages.filter((m) => base.some((original) => original.id === m.id)).map((m) => m.id);
|
|
1876
2044
|
const orderValid = stableSerialize(knownOrder) === stableSerialize(base.map((m) => m.id));
|
|
1877
|
-
for (const interaction of
|
|
2045
|
+
for (const interaction of index2.interactions) {
|
|
1878
2046
|
if (interaction.messageIds.every(
|
|
1879
2047
|
(id) => history.findIndex((m) => m.id === id) <= coveredPosition
|
|
1880
2048
|
))
|
|
@@ -1903,7 +2071,7 @@ var ContextManager = class {
|
|
|
1903
2071
|
messages: prepared.messages.filter((m) => !m.id || !eligibleViewIds.has(m.id))
|
|
1904
2072
|
};
|
|
1905
2073
|
const protectedEstimate = estimate(protectedRequest);
|
|
1906
|
-
this.publishSnapshot(history, prepared, profile, budget,
|
|
2074
|
+
this.publishSnapshot(history, prepared, profile, budget, index2.protectedMessageIds);
|
|
1907
2075
|
if (protectedEstimate.total > budget.inputBudget) {
|
|
1908
2076
|
yield {
|
|
1909
2077
|
type: "context_budget_exceeded",
|
|
@@ -1923,7 +2091,8 @@ var ContextManager = class {
|
|
|
1923
2091
|
modelName: summaryProfile.modelName,
|
|
1924
2092
|
provider: summaryProfile.provider,
|
|
1925
2093
|
baseURL: summaryProfile.baseURL,
|
|
1926
|
-
contextWindow: summaryProfile.contextWindow
|
|
2094
|
+
contextWindow: summaryProfile.contextWindow,
|
|
2095
|
+
maxOutputTokens: summaryProfile.maxOutputTokens
|
|
1927
2096
|
},
|
|
1928
2097
|
policy: POLICY_VERSION
|
|
1929
2098
|
});
|
|
@@ -1935,7 +2104,7 @@ var ContextManager = class {
|
|
|
1935
2104
|
persistence: this.persistence === "disk" ? "disk" : "memory"
|
|
1936
2105
|
};
|
|
1937
2106
|
const prefix = history.filter((m) => eligibleIds.has(m.id));
|
|
1938
|
-
if (prefix.length && this.persistence !== "unsupported" && (reason !== "threshold" || estimate(prepared).total >= budget.trigger)) {
|
|
2107
|
+
if (allowCompaction && prefix.length && this.persistence !== "unsupported" && (reason !== "threshold" || estimate(prepared).total >= budget.trigger)) {
|
|
1939
2108
|
if (reason === "threshold") {
|
|
1940
2109
|
const targets = this.pruningTargets(history, eligibleIds, prepared);
|
|
1941
2110
|
if (targets.length) {
|
|
@@ -1959,7 +2128,7 @@ var ContextManager = class {
|
|
|
1959
2128
|
await this.commit("context_pruned", { ...record });
|
|
1960
2129
|
this.pruning.push(record);
|
|
1961
2130
|
prepared = candidate;
|
|
1962
|
-
this.publishSnapshot(history, prepared, profile, budget,
|
|
2131
|
+
this.publishSnapshot(history, prepared, profile, budget, index2.protectedMessageIds);
|
|
1963
2132
|
yield {
|
|
1964
2133
|
type: "compaction_finish",
|
|
1965
2134
|
...eventBase,
|
|
@@ -2038,7 +2207,7 @@ var ContextManager = class {
|
|
|
2038
2207
|
this.failure = { epoch, consecutiveFailures: 0 };
|
|
2039
2208
|
this.summaryCount++;
|
|
2040
2209
|
prepared = candidate;
|
|
2041
|
-
this.publishSnapshot(history, prepared, profile, budget,
|
|
2210
|
+
this.publishSnapshot(history, prepared, profile, budget, index2.protectedMessageIds);
|
|
2042
2211
|
yield {
|
|
2043
2212
|
type: "compaction_finish",
|
|
2044
2213
|
...eventBase,
|
|
@@ -2060,7 +2229,7 @@ var ContextManager = class {
|
|
|
2060
2229
|
};
|
|
2061
2230
|
await this.commit("compaction_state", { ...failed });
|
|
2062
2231
|
this.failure = failed;
|
|
2063
|
-
this.publishSnapshot(history, prepared, profile, budget,
|
|
2232
|
+
this.publishSnapshot(history, prepared, profile, budget, index2.protectedMessageIds);
|
|
2064
2233
|
yield {
|
|
2065
2234
|
type: "compaction_failed",
|
|
2066
2235
|
...eventBase,
|
|
@@ -2080,7 +2249,7 @@ var ContextManager = class {
|
|
|
2080
2249
|
}
|
|
2081
2250
|
}
|
|
2082
2251
|
const final = estimate(prepared);
|
|
2083
|
-
this.publishSnapshot(history, prepared, profile, budget,
|
|
2252
|
+
this.publishSnapshot(history, prepared, profile, budget, index2.protectedMessageIds);
|
|
2084
2253
|
if (final.total > budget.inputBudget) {
|
|
2085
2254
|
yield {
|
|
2086
2255
|
type: "context_budget_exceeded",
|
|
@@ -2105,13 +2274,16 @@ var ContextManager = class {
|
|
|
2105
2274
|
getSnapshot() {
|
|
2106
2275
|
return this.snapshot ? structuredClone(this.snapshot) : void 0;
|
|
2107
2276
|
}
|
|
2108
|
-
/**
|
|
2277
|
+
/**
|
|
2278
|
+
* 从当前静态投影估算,不执行 Hook/Provider 或更新状态;恢复后即可查看检查点。
|
|
2279
|
+
* 输出预留与准备请求使用同一 Profile/请求覆盖规则;非法覆盖也在只读检查时失败。
|
|
2280
|
+
*/
|
|
2109
2281
|
inspect(history, request, profile) {
|
|
2110
|
-
const budget =
|
|
2282
|
+
const budget = createRequestBudget(profile, request);
|
|
2111
2283
|
return {
|
|
2112
2284
|
...this.buildSnapshot(
|
|
2113
2285
|
history,
|
|
2114
|
-
request,
|
|
2286
|
+
{ ...request, maxTokens: budget.outputReserve },
|
|
2115
2287
|
profile,
|
|
2116
2288
|
budget,
|
|
2117
2289
|
indexInteractions(history, this.terminals).protectedMessageIds
|
|
@@ -2192,14 +2364,14 @@ ${renderFileDetails(checkpoint.details)}`
|
|
|
2192
2364
|
const previousEnd = this.checkpoint ? history.findIndex((m) => m.id === this.checkpoint.coveredEndMessageId) : -1;
|
|
2193
2365
|
if (checkpoint.policyVersion !== POLICY_VERSION || typeof checkpoint.id !== "string" || !checkpoint.id || checkpoint.previousCheckpointId !== (this.checkpoint?.id ?? null) || end <= previousEnd || history[end + 1]?.id !== checkpoint.firstKeptMessageId || digest(history.slice(0, end + 1)) !== checkpoint.sourceDigest)
|
|
2194
2366
|
return false;
|
|
2195
|
-
const
|
|
2196
|
-
if (history.slice(0, end + 1).some((m) =>
|
|
2367
|
+
const index2 = indexInteractions(history, this.terminals);
|
|
2368
|
+
if (history.slice(0, end + 1).some((m) => index2.protectedMessageIds.includes(m.id)))
|
|
2197
2369
|
return false;
|
|
2198
|
-
if (!
|
|
2370
|
+
if (!index2.interactions.some(
|
|
2199
2371
|
(i) => this.isClosedInteraction(history, i) && i.messageIds.at(-1) === checkpoint.coveredEndMessageId
|
|
2200
2372
|
))
|
|
2201
2373
|
return false;
|
|
2202
|
-
if (
|
|
2374
|
+
if (index2.interactions.filter(
|
|
2203
2375
|
(interaction) => interaction.messageIds.some(
|
|
2204
2376
|
(id) => history.findIndex((message) => message.id === id) <= end
|
|
2205
2377
|
)
|
|
@@ -2274,15 +2446,15 @@ ${renderFileDetails(checkpoint.details)}`
|
|
|
2274
2446
|
try {
|
|
2275
2447
|
if (pruning.policyVersion !== POLICY_VERSION || typeof pruning.id !== "string" || !pruning.id || !Array.isArray(pruning.targets) || pruning.baseCheckpointId !== (this.checkpoint?.id ?? null) || pruning.previousPruningId !== (this.pruning.at(-1)?.id ?? null))
|
|
2276
2448
|
return false;
|
|
2277
|
-
const
|
|
2278
|
-
const protectedIds = new Set(
|
|
2449
|
+
const index2 = indexInteractions(history, this.terminals);
|
|
2450
|
+
const protectedIds = new Set(index2.protectedMessageIds);
|
|
2279
2451
|
return pruning.targets.length > 0 && pruning.targets.every((target) => {
|
|
2280
2452
|
const message = history.find((m) => m.id === target.messageId);
|
|
2281
2453
|
const result = message?.content.find(
|
|
2282
2454
|
(b) => b.type === "tool_result" && b.toolUseId === target.toolCallId
|
|
2283
2455
|
);
|
|
2284
2456
|
const call = history.flatMap((m) => m.role === "assistant" ? m.content : []).find((b) => b.type === "tool_use" && b.id === target.toolCallId);
|
|
2285
|
-
return !!message && !protectedIds.has(message.id) &&
|
|
2457
|
+
return !!message && !protectedIds.has(message.id) && index2.interactions.some(
|
|
2286
2458
|
(i) => i.status === "completed" && i.messageIds.includes(message.id)
|
|
2287
2459
|
) && !!result && result.type === "tool_result" && !result.isError && !result.errorCode && !!call && call.type === "tool_use" && call.source === "builtin" && ["read_file", "glob", "grep"].includes(call.name) && this.options.builtInToolNames?.().has(call.name) && typeof target.description === "string" && Array.from(target.description).length <= 1e3 && digest({ call, result }) === target.sourceDigest;
|
|
2288
2460
|
});
|
|
@@ -2310,7 +2482,7 @@ ${renderFileDetails(checkpoint.details)}`
|
|
|
2310
2482
|
};
|
|
2311
2483
|
|
|
2312
2484
|
// src/extensibility/logging/index.ts
|
|
2313
|
-
var
|
|
2485
|
+
var import_node_crypto8 = require("crypto");
|
|
2314
2486
|
var SAFE_FIELDS = /* @__PURE__ */ new Set([
|
|
2315
2487
|
"mode",
|
|
2316
2488
|
"toolName",
|
|
@@ -2374,7 +2546,7 @@ function projectSafeFields(fields) {
|
|
|
2374
2546
|
return safe;
|
|
2375
2547
|
}
|
|
2376
2548
|
function createLogId() {
|
|
2377
|
-
return (0,
|
|
2549
|
+
return (0, import_node_crypto8.randomUUID)();
|
|
2378
2550
|
}
|
|
2379
2551
|
function commandAuditSummary(command) {
|
|
2380
2552
|
const parts = command.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2392,14 +2564,14 @@ function commandAuditSummary(command) {
|
|
|
2392
2564
|
"get-content"
|
|
2393
2565
|
])).has(first) ? first : void 0;
|
|
2394
2566
|
return {
|
|
2395
|
-
commandDigest: (0,
|
|
2567
|
+
commandDigest: (0, import_node_crypto8.createHash)("sha256").update(command, "utf8").digest("hex"),
|
|
2396
2568
|
commandPreview: category ? `${category} (${Math.max(0, parts.length - 1)} args)` : "[redacted]",
|
|
2397
2569
|
commandLength: Buffer.byteLength(command, "utf8"),
|
|
2398
2570
|
argumentCount: Math.max(0, parts.length - 1)
|
|
2399
2571
|
};
|
|
2400
2572
|
}
|
|
2401
2573
|
function executionFingerprint(input) {
|
|
2402
|
-
return (0,
|
|
2574
|
+
return (0, import_node_crypto8.createHash)("sha256").update(
|
|
2403
2575
|
JSON.stringify([
|
|
2404
2576
|
input.command,
|
|
2405
2577
|
input.shell,
|
|
@@ -2496,6 +2668,7 @@ var FileLogSink = class {
|
|
|
2496
2668
|
if (!import_node_fs5.default.existsSync(this.directory)) return 0;
|
|
2497
2669
|
return import_node_fs5.default.readdirSync(this.directory).filter((name) => this.isOwnedFile(name)).reduce((total, name) => total + import_node_fs5.default.statSync(import_node_path5.default.join(this.directory, name)).size, 0);
|
|
2498
2670
|
}
|
|
2671
|
+
/** 优先写入当天未满的最后一个序号文件,超限或无同名文件时开下一个序号。 */
|
|
2499
2672
|
chooseFile(date, bytes) {
|
|
2500
2673
|
const names = import_node_fs5.default.readdirSync(this.directory).filter((name) => name.startsWith(`${this.prefix}-${date}-`) && this.isOwnedFile(name)).sort();
|
|
2501
2674
|
const lastName = names.at(-1);
|
|
@@ -2509,6 +2682,7 @@ var FileLogSink = class {
|
|
|
2509
2682
|
`${this.prefix}-${date}-${String(next).padStart(3, "0")}.jsonl`
|
|
2510
2683
|
);
|
|
2511
2684
|
}
|
|
2685
|
+
/** 按文件名中的日期删除早于保留期的自有文件;删除依据文件名而非文件 mtime。 */
|
|
2512
2686
|
cleanupExpired(currentDate) {
|
|
2513
2687
|
const current = Date.parse(`${currentDate}T00:00:00.000Z`);
|
|
2514
2688
|
const cutoff = current - (this.retentionDays ?? 0) * 864e5;
|
|
@@ -2598,9 +2772,19 @@ var import_node_path6 = __toESM(require("path"), 1);
|
|
|
2598
2772
|
var MAX_FILE_BYTES = 32 * 1024;
|
|
2599
2773
|
var MAX_TOTAL_BYTES = 128 * 1024;
|
|
2600
2774
|
var MAX_LEVELS = 16;
|
|
2775
|
+
function canonicalizeOptionalFile(file) {
|
|
2776
|
+
try {
|
|
2777
|
+
return import_node_fs6.default.realpathSync.native(file);
|
|
2778
|
+
} catch {
|
|
2779
|
+
return import_node_path6.default.resolve(file);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2601
2782
|
function loadInstructions(options) {
|
|
2602
|
-
const root = import_node_fs6.default.realpathSync(options.projectRoot);
|
|
2603
|
-
const cwd = import_node_fs6.default.realpathSync(options.cwd);
|
|
2783
|
+
const root = import_node_fs6.default.realpathSync.native(options.projectRoot);
|
|
2784
|
+
const cwd = import_node_fs6.default.realpathSync.native(options.cwd);
|
|
2785
|
+
const userFile = canonicalizeOptionalFile(
|
|
2786
|
+
options.userFile ?? import_node_path6.default.join(import_node_os3.default.homedir(), ".kapibala", "AGENTS.md")
|
|
2787
|
+
);
|
|
2604
2788
|
const relative = import_node_path6.default.relative(root, cwd);
|
|
2605
2789
|
if (relative === ".." || relative.startsWith(`..${import_node_path6.default.sep}`) || import_node_path6.default.isAbsolute(relative)) {
|
|
2606
2790
|
throw new Error("Instruction cwd is outside project root");
|
|
@@ -2608,7 +2792,6 @@ function loadInstructions(options) {
|
|
|
2608
2792
|
const segments = relative ? relative.split(import_node_path6.default.sep) : [];
|
|
2609
2793
|
if (segments.length + 1 > MAX_LEVELS)
|
|
2610
2794
|
throw new Error(`Instruction directory chain exceeds ${MAX_LEVELS} levels`);
|
|
2611
|
-
const userFile = options.userFile ?? import_node_path6.default.join(import_node_os3.default.homedir(), ".kapibala", "AGENTS.md");
|
|
2612
2795
|
const candidates = [userFile];
|
|
2613
2796
|
let directory = root;
|
|
2614
2797
|
candidates.push(import_node_path6.default.join(directory, "AGENTS.md"));
|
|
@@ -2664,7 +2847,7 @@ function loadInstructions(options) {
|
|
|
2664
2847
|
|
|
2665
2848
|
// src/capabilities/shell/detect.ts
|
|
2666
2849
|
var import_node_child_process2 = require("child_process");
|
|
2667
|
-
var
|
|
2850
|
+
var import_node_crypto9 = require("crypto");
|
|
2668
2851
|
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
2669
2852
|
var import_node_os4 = __toESM(require("os"), 1);
|
|
2670
2853
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
@@ -2722,7 +2905,7 @@ function buildEnvironment(executable, kind, cwd) {
|
|
|
2722
2905
|
kind,
|
|
2723
2906
|
executable,
|
|
2724
2907
|
cwd,
|
|
2725
|
-
executableDigest: (0,
|
|
2908
|
+
executableDigest: (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify([executable, stat.size, stat.mtimeMs])).digest("hex")
|
|
2726
2909
|
};
|
|
2727
2910
|
}
|
|
2728
2911
|
function candidatesFor(kind, platform, pathEnv) {
|
|
@@ -2792,7 +2975,7 @@ function detectShell(options) {
|
|
|
2792
2975
|
|
|
2793
2976
|
// src/capabilities/shell/tool.ts
|
|
2794
2977
|
var import_node_child_process3 = require("child_process");
|
|
2795
|
-
var
|
|
2978
|
+
var import_node_crypto10 = require("crypto");
|
|
2796
2979
|
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
2797
2980
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
2798
2981
|
var import_node_string_decoder = require("string_decoder");
|
|
@@ -2882,7 +3065,7 @@ function directScriptDigest(command, kind, cwd) {
|
|
|
2882
3065
|
if (relative.startsWith("..") || import_node_path8.default.isAbsolute(relative)) return void 0;
|
|
2883
3066
|
const stat = import_node_fs8.default.statSync(script);
|
|
2884
3067
|
if (!stat.isFile() || stat.size > 1024 * 1024) return void 0;
|
|
2885
|
-
return (0,
|
|
3068
|
+
return (0, import_node_crypto10.createHash)("sha256").update(script).update("\0").update(import_node_fs8.default.readFileSync(script)).digest("hex");
|
|
2886
3069
|
} catch {
|
|
2887
3070
|
return void 0;
|
|
2888
3071
|
}
|
|
@@ -2912,7 +3095,7 @@ function createNativeBashGroup(environment) {
|
|
|
2912
3095
|
let accepted = false;
|
|
2913
3096
|
const group = {
|
|
2914
3097
|
environment,
|
|
2915
|
-
token: (0,
|
|
3098
|
+
token: (0, import_node_crypto10.randomUUID)(),
|
|
2916
3099
|
ready: new Promise((resolve) => {
|
|
2917
3100
|
resolveReady = resolve;
|
|
2918
3101
|
}),
|
|
@@ -3061,7 +3244,7 @@ function cleanupCommandResults(rootDir, now = Date.now()) {
|
|
|
3061
3244
|
if (stat.mtimeMs < cutoff) import_node_fs8.default.unlinkSync(file);
|
|
3062
3245
|
else retained.push(line);
|
|
3063
3246
|
}
|
|
3064
|
-
const replacement = import_node_path8.default.join(directory, `.managed-results-${(0,
|
|
3247
|
+
const replacement = import_node_path8.default.join(directory, `.managed-results-${(0, import_node_crypto10.randomUUID)()}.tmp`);
|
|
3065
3248
|
import_node_fs8.default.writeFileSync(replacement, retained.length ? `${retained.join("\n")}
|
|
3066
3249
|
` : "", { mode: 384 });
|
|
3067
3250
|
import_node_fs8.default.renameSync(replacement, manifest);
|
|
@@ -3103,8 +3286,8 @@ function createRunCommandTool(environment) {
|
|
|
3103
3286
|
const timeoutMs = effectiveTimeout(input);
|
|
3104
3287
|
const scriptDigest = directScriptDigest(input.command, environment.kind, cwd);
|
|
3105
3288
|
const stat = import_node_fs8.default.statSync(environment.executable);
|
|
3106
|
-
const executableDigest = (0,
|
|
3107
|
-
const environmentDigest = (0,
|
|
3289
|
+
const executableDigest = (0, import_node_crypto10.createHash)("sha256").update(JSON.stringify([environment.executable, stat.size, stat.mtimeMs])).digest("hex");
|
|
3290
|
+
const environmentDigest = (0, import_node_crypto10.createHash)("sha256").update(JSON.stringify(limitedEnvironment(environment.kind))).digest("hex");
|
|
3108
3291
|
return {
|
|
3109
3292
|
command: input.command,
|
|
3110
3293
|
interpreter: environment.kind,
|
|
@@ -3167,7 +3350,7 @@ ${input.command}` : input.command;
|
|
|
3167
3350
|
if (outputBytes > INLINE_LIMIT && descriptor === void 0) {
|
|
3168
3351
|
const directory = resultDirectory(ctx.rootDir);
|
|
3169
3352
|
import_node_fs8.default.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
3170
|
-
outputFile = import_node_path8.default.join(directory, `run-${(0,
|
|
3353
|
+
outputFile = import_node_path8.default.join(directory, `run-${(0, import_node_crypto10.randomUUID)()}.txt`);
|
|
3171
3354
|
descriptor = import_node_fs8.default.openSync(outputFile, "wx", 384);
|
|
3172
3355
|
recordManagedResult(directory, outputFile, descriptor);
|
|
3173
3356
|
const prefix = Buffer.concat(inline);
|
|
@@ -3439,8 +3622,8 @@ var CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
3439
3622
|
"agent:spawn"
|
|
3440
3623
|
]);
|
|
3441
3624
|
function validatePermissionRules(rules) {
|
|
3442
|
-
for (const [
|
|
3443
|
-
const ruleNumber =
|
|
3625
|
+
for (const [index2, rule] of rules.entries()) {
|
|
3626
|
+
const ruleNumber = index2 + 1;
|
|
3444
3627
|
if (!["allow", "ask", "deny"].includes(rule.action)) {
|
|
3445
3628
|
throw new Error(`Permission rule ${ruleNumber}: invalid action`);
|
|
3446
3629
|
}
|
|
@@ -3469,6 +3652,11 @@ function samePath(a, b) {
|
|
|
3469
3652
|
return process.platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
3470
3653
|
}
|
|
3471
3654
|
var PermissionPolicy = class {
|
|
3655
|
+
/**
|
|
3656
|
+
* 裁决顺序固定:硬限制 → Plan 收窄 → deny 规则 → 会话缓存拒绝 → ask 规则 →
|
|
3657
|
+
* 会话缓存允许 → allow 规则(run_command 必须带 shell 范围,工具级 allow 不能放行命令)
|
|
3658
|
+
* → 模式默认。同一输入总是得到同一裁决,不读取任何外部状态。
|
|
3659
|
+
*/
|
|
3472
3660
|
decide(request) {
|
|
3473
3661
|
if (request.hardDenied) return { decision: "deny", source: "hard_limit" };
|
|
3474
3662
|
if (request.mode === "Plan" && (request.dangerous || request.capabilities.length === 0 || request.capabilities.some((capability) => capability !== "fs:read"))) {
|
|
@@ -3523,7 +3711,7 @@ function visibleTools(tools, mode, rules = []) {
|
|
|
3523
3711
|
}
|
|
3524
3712
|
|
|
3525
3713
|
// src/capabilities/security/approval.ts
|
|
3526
|
-
var
|
|
3714
|
+
var import_node_crypto11 = require("crypto");
|
|
3527
3715
|
function stableJsonStringify(value) {
|
|
3528
3716
|
if (value === null || typeof value !== "object") {
|
|
3529
3717
|
return JSON.stringify(value);
|
|
@@ -3538,7 +3726,7 @@ function stableJsonStringify(value) {
|
|
|
3538
3726
|
var SessionApprovalCache = class {
|
|
3539
3727
|
choices = /* @__PURE__ */ new Map();
|
|
3540
3728
|
key(request) {
|
|
3541
|
-
return (0,
|
|
3729
|
+
return (0, import_node_crypto11.createHash)("sha256").update(
|
|
3542
3730
|
stableJsonStringify([
|
|
3543
3731
|
request.toolName,
|
|
3544
3732
|
[...request.capabilities].sort(),
|
|
@@ -3597,6 +3785,7 @@ var ToolRegistry = class {
|
|
|
3597
3785
|
get(name) {
|
|
3598
3786
|
return this.tools.get(name);
|
|
3599
3787
|
}
|
|
3788
|
+
/** 未命中时抛 ToolNotFound,并携带当前全部可用名称辅助模型自纠。 */
|
|
3600
3789
|
resolve(name) {
|
|
3601
3790
|
const tool = this.tools.get(name);
|
|
3602
3791
|
if (!tool) {
|
|
@@ -3995,15 +4184,21 @@ var builtinTools = [readFileTool, writeFileTool, editFileTool, globTool, grepToo
|
|
|
3995
4184
|
// src/extensibility/hooks/registry.ts
|
|
3996
4185
|
var HookRegistry = class {
|
|
3997
4186
|
hooks = {};
|
|
4187
|
+
/** 注册到指定切点;不查重,触发顺序即注册顺序。 */
|
|
3998
4188
|
on(point, handler) {
|
|
3999
4189
|
if (!this.hooks[point]) {
|
|
4000
4190
|
this.hooks[point] = [];
|
|
4001
4191
|
}
|
|
4002
4192
|
this.hooks[point].push(handler);
|
|
4003
4193
|
}
|
|
4194
|
+
/** 返回该切点的处理器列表(无注册时为空数组);数组为内部引用,调用方不应修改。 */
|
|
4004
4195
|
get(point) {
|
|
4005
4196
|
return this.hooks[point] ?? [];
|
|
4006
4197
|
}
|
|
4198
|
+
/**
|
|
4199
|
+
* 串行 await 通知型切点的全部处理器;任一抛错立即中断并向调用方传播。
|
|
4200
|
+
* 需要返回值的 model:before / tool:before 不经 emit,由执行方自行遍历。
|
|
4201
|
+
*/
|
|
4007
4202
|
async emit(point, ctx, ...args) {
|
|
4008
4203
|
const handlers = this.get(point);
|
|
4009
4204
|
for (const handler of handlers) {
|
|
@@ -4013,7 +4208,7 @@ var HookRegistry = class {
|
|
|
4013
4208
|
};
|
|
4014
4209
|
|
|
4015
4210
|
// src/runtime/executor/index.ts
|
|
4016
|
-
var
|
|
4211
|
+
var import_node_crypto12 = require("crypto");
|
|
4017
4212
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
4018
4213
|
var DEFAULT_TOOL_TIMEOUT_MS = 12e4;
|
|
4019
4214
|
var ToolExecutor = class {
|
|
@@ -4052,6 +4247,7 @@ var ToolExecutor = class {
|
|
|
4052
4247
|
cancel() {
|
|
4053
4248
|
this.cancellation.abort();
|
|
4054
4249
|
}
|
|
4250
|
+
/** 串行执行全部调用并返回与入参顺序一致的结果;某个结果要求用户介入后,其余调用直接拒绝。 */
|
|
4055
4251
|
async runAll(calls, onProgress) {
|
|
4056
4252
|
const results = [];
|
|
4057
4253
|
let requiresUserAction = false;
|
|
@@ -4102,8 +4298,168 @@ var ToolExecutor = class {
|
|
|
4102
4298
|
}
|
|
4103
4299
|
if (forcedDeny) {
|
|
4104
4300
|
try {
|
|
4105
|
-
await audit("tool.decided", { decision: "deny", decisionSource: "prior_user_action" });
|
|
4106
|
-
await audit("tool.finished", { status: "skipped", errorCode: "SKIPPED_AFTER_USER_ACTION" });
|
|
4301
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "prior_user_action" });
|
|
4302
|
+
await audit("tool.finished", { status: "skipped", errorCode: "SKIPPED_AFTER_USER_ACTION" });
|
|
4303
|
+
} catch {
|
|
4304
|
+
return failure(
|
|
4305
|
+
"Audit unavailable; tool was not started",
|
|
4306
|
+
"AUDIT_UNAVAILABLE",
|
|
4307
|
+
"after_user_action"
|
|
4308
|
+
);
|
|
4309
|
+
}
|
|
4310
|
+
return failure(
|
|
4311
|
+
"Earlier tool call requires user action",
|
|
4312
|
+
"SKIPPED_AFTER_USER_ACTION",
|
|
4313
|
+
"after_user_action"
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
4316
|
+
const hookCtx = { signal: this.signal, logger: this.logger };
|
|
4317
|
+
let currentInput = call.input;
|
|
4318
|
+
try {
|
|
4319
|
+
for (const hook of this.hooks.get("tool:before")) {
|
|
4320
|
+
const decision = await hook(hookCtx, { id: call.id, name: call.name, input: currentInput });
|
|
4321
|
+
if (decision.action === "modify") currentInput = decision.input;
|
|
4322
|
+
if (decision.action === "skip") {
|
|
4323
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
|
|
4324
|
+
await audit("tool.finished", { status: "skipped" });
|
|
4325
|
+
return {
|
|
4326
|
+
type: "tool_result",
|
|
4327
|
+
toolUseId: call.id,
|
|
4328
|
+
content: decision.result,
|
|
4329
|
+
isError: decision.isError ?? false,
|
|
4330
|
+
durationMs: Math.max(0, Date.now() - startedAt)
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
} catch {
|
|
4335
|
+
try {
|
|
4336
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
|
|
4337
|
+
await audit("tool.finished", { status: "failed" });
|
|
4338
|
+
} catch {
|
|
4339
|
+
}
|
|
4340
|
+
return failure("Tool preparation failed", "HOOK_ERROR");
|
|
4341
|
+
}
|
|
4342
|
+
let tool;
|
|
4343
|
+
try {
|
|
4344
|
+
tool = this.tools.resolve(call.name);
|
|
4345
|
+
} catch (error) {
|
|
4346
|
+
const known = error instanceof KapibalaError ? error : void 0;
|
|
4347
|
+
try {
|
|
4348
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "unavailable" });
|
|
4349
|
+
await audit("tool.finished", { status: "failed" });
|
|
4350
|
+
} catch {
|
|
4351
|
+
}
|
|
4352
|
+
return failure(
|
|
4353
|
+
known?.safeMessage ?? "Requested tool is unavailable",
|
|
4354
|
+
known?.code ?? "TOOL_NOT_FOUND"
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
try {
|
|
4358
|
+
return await this.executeResolved(
|
|
4359
|
+
call,
|
|
4360
|
+
currentInput,
|
|
4361
|
+
tool,
|
|
4362
|
+
startedAt,
|
|
4363
|
+
audit,
|
|
4364
|
+
failure,
|
|
4365
|
+
hookCtx,
|
|
4366
|
+
onProgress
|
|
4367
|
+
);
|
|
4368
|
+
} catch {
|
|
4369
|
+
try {
|
|
4370
|
+
await audit("tool.finished", { status: "unknown", errorCode: "OUTCOME_UNKNOWN" });
|
|
4371
|
+
} catch {
|
|
4372
|
+
}
|
|
4373
|
+
return failure(
|
|
4374
|
+
"Tool outcome is unknown after an executor failure",
|
|
4375
|
+
"OUTCOME_UNKNOWN",
|
|
4376
|
+
"after_user_action"
|
|
4377
|
+
);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
async executeResolved(call, currentInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
|
|
4381
|
+
let shell;
|
|
4382
|
+
try {
|
|
4383
|
+
shell = tool.approvalScope?.(currentInput, this.rootDir);
|
|
4384
|
+
} catch {
|
|
4385
|
+
try {
|
|
4386
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "invalid_target" });
|
|
4387
|
+
await audit("tool.finished", { status: "failed", errorCode: "INVALID_TARGET" });
|
|
4388
|
+
} catch {
|
|
4389
|
+
}
|
|
4390
|
+
return failure("Invalid tool execution target", "INVALID_TARGET");
|
|
4391
|
+
}
|
|
4392
|
+
let auditFields = {};
|
|
4393
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4394
|
+
const request = {
|
|
4395
|
+
toolName: call.name,
|
|
4396
|
+
capabilities: tool.metadata?.permissions ?? [],
|
|
4397
|
+
input: currentInput,
|
|
4398
|
+
rootDir: this.rootDir,
|
|
4399
|
+
...shell ? { shell } : {},
|
|
4400
|
+
// 解释器内建查询与内容已绑定摘要的工作区直引脚本可确认不受外部脚本变更影响,
|
|
4401
|
+
// 允许会话级批准;其余 shell 命令仅允许单次批准。
|
|
4402
|
+
sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
|
|
4403
|
+
};
|
|
4404
|
+
const repeatKey = (0, import_node_crypto12.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex");
|
|
4405
|
+
if (this.neverFailures.has(repeatKey)) {
|
|
4406
|
+
try {
|
|
4407
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
4408
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
4409
|
+
} catch {
|
|
4410
|
+
return failure(
|
|
4411
|
+
"Audit unavailable; tool was not started",
|
|
4412
|
+
"AUDIT_UNAVAILABLE",
|
|
4413
|
+
"after_user_action"
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
return failure(
|
|
4417
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
4418
|
+
"REPEAT_BLOCKED"
|
|
4419
|
+
);
|
|
4420
|
+
}
|
|
4421
|
+
const cached = this.approvalCache.get(request);
|
|
4422
|
+
const decision = this.policy.decide({
|
|
4423
|
+
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
4424
|
+
toolName: call.name,
|
|
4425
|
+
capabilities: request.capabilities,
|
|
4426
|
+
dangerous: tool.metadata?.dangerous,
|
|
4427
|
+
cachedApproval: cached === "allow",
|
|
4428
|
+
cachedDenial: cached === "deny",
|
|
4429
|
+
rules: this.permissionRules,
|
|
4430
|
+
...shell ? { shell } : {}
|
|
4431
|
+
});
|
|
4432
|
+
let permitted = decision.decision === "allow";
|
|
4433
|
+
let source = decision.source;
|
|
4434
|
+
let approvalChoice;
|
|
4435
|
+
if (decision.decision === "ask") {
|
|
4436
|
+
let choice = "deny_once";
|
|
4437
|
+
try {
|
|
4438
|
+
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
4439
|
+
} catch {
|
|
4440
|
+
choice = "cancel";
|
|
4441
|
+
}
|
|
4442
|
+
approvalChoice = choice;
|
|
4443
|
+
if (choice === "allow_session" && request.sessionAllowed)
|
|
4444
|
+
this.approvalCache.set(request, "allow");
|
|
4445
|
+
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
4446
|
+
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
4447
|
+
source = this.approvalChannel ? "human" : "non_interactive";
|
|
4448
|
+
}
|
|
4449
|
+
auditFields = shell ? {
|
|
4450
|
+
shell: shell.interpreter,
|
|
4451
|
+
cwdDigest: (0, import_node_crypto12.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
4452
|
+
executableDigest: shell.executableDigest,
|
|
4453
|
+
executionFingerprint: shell.executionFingerprint,
|
|
4454
|
+
...commandAuditSummary(shell.command)
|
|
4455
|
+
} : {};
|
|
4456
|
+
try {
|
|
4457
|
+
await audit("tool.decided", {
|
|
4458
|
+
decision: permitted ? "allow" : "deny",
|
|
4459
|
+
decisionSource: source,
|
|
4460
|
+
...approvalChoice ? { approvalChoice } : {},
|
|
4461
|
+
...auditFields
|
|
4462
|
+
});
|
|
4107
4463
|
} catch {
|
|
4108
4464
|
return failure(
|
|
4109
4465
|
"Audit unavailable; tool was not started",
|
|
@@ -4111,716 +4467,1669 @@ var ToolExecutor = class {
|
|
|
4111
4467
|
"after_user_action"
|
|
4112
4468
|
);
|
|
4113
4469
|
}
|
|
4470
|
+
if (!permitted || this.signal?.aborted) {
|
|
4471
|
+
const code = this.signal?.aborted ? "ABORTED" : "PERMISSION_DENIED";
|
|
4472
|
+
try {
|
|
4473
|
+
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
4474
|
+
} catch {
|
|
4475
|
+
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action");
|
|
4476
|
+
}
|
|
4477
|
+
return failure(
|
|
4478
|
+
this.signal?.aborted ? "Operation cancelled" : "Tool permission denied",
|
|
4479
|
+
code,
|
|
4480
|
+
"after_user_action"
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
if (shell) {
|
|
4484
|
+
try {
|
|
4485
|
+
const next = tool.approvalScope?.(currentInput, this.rootDir);
|
|
4486
|
+
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
4487
|
+
if (attempt === 0 && next) {
|
|
4488
|
+
try {
|
|
4489
|
+
await audit("tool.target_changed", { status: "redeciding" });
|
|
4490
|
+
} catch {
|
|
4491
|
+
return failure(
|
|
4492
|
+
"Audit unavailable; tool was not started",
|
|
4493
|
+
"AUDIT_UNAVAILABLE",
|
|
4494
|
+
"after_user_action"
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
shell = next;
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
4501
|
+
return failure(
|
|
4502
|
+
"Execution target changed after approval",
|
|
4503
|
+
"TARGET_CHANGED",
|
|
4504
|
+
"after_user_action"
|
|
4505
|
+
);
|
|
4506
|
+
}
|
|
4507
|
+
} catch {
|
|
4508
|
+
return failure(
|
|
4509
|
+
"Execution target changed after approval",
|
|
4510
|
+
"TARGET_CHANGED",
|
|
4511
|
+
"after_user_action"
|
|
4512
|
+
);
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
break;
|
|
4516
|
+
}
|
|
4517
|
+
try {
|
|
4518
|
+
await audit("tool.started", { status: "started", ...auditFields });
|
|
4519
|
+
} catch {
|
|
4114
4520
|
return failure(
|
|
4115
|
-
"
|
|
4116
|
-
"
|
|
4521
|
+
"Audit unavailable; tool was not started",
|
|
4522
|
+
"AUDIT_UNAVAILABLE",
|
|
4117
4523
|
"after_user_action"
|
|
4118
4524
|
);
|
|
4119
4525
|
}
|
|
4120
|
-
|
|
4121
|
-
let
|
|
4526
|
+
let outputStr;
|
|
4527
|
+
let isError = false;
|
|
4528
|
+
let errorCode;
|
|
4529
|
+
let retryPolicy;
|
|
4122
4530
|
try {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
return {
|
|
4130
|
-
type: "tool_result",
|
|
4131
|
-
toolUseId: call.id,
|
|
4132
|
-
content: decision.result,
|
|
4133
|
-
isError: decision.isError ?? false,
|
|
4134
|
-
durationMs: Math.max(0, Date.now() - startedAt)
|
|
4135
|
-
};
|
|
4136
|
-
}
|
|
4531
|
+
const executionController = new AbortController();
|
|
4532
|
+
const abortFromSession = () => executionController.abort();
|
|
4533
|
+
if (this.signal?.aborted) {
|
|
4534
|
+
executionController.abort();
|
|
4535
|
+
} else {
|
|
4536
|
+
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
4137
4537
|
}
|
|
4138
|
-
|
|
4538
|
+
const toolCtx = {
|
|
4539
|
+
rootDir: this.rootDir,
|
|
4540
|
+
signal: executionController.signal,
|
|
4541
|
+
logger: this.logger,
|
|
4542
|
+
onProgress: (progress) => onProgress?.({ id: call.id, name: call.name, ...progress }),
|
|
4543
|
+
...shell ? {
|
|
4544
|
+
onProcessSpawned: () => audit("process.spawned", { status: "spawned", ...auditFields })
|
|
4545
|
+
} : {}
|
|
4546
|
+
};
|
|
4139
4547
|
try {
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4548
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(currentInput, toolCtx) : await this.withTimeout(
|
|
4549
|
+
tool.execute(currentInput, toolCtx),
|
|
4550
|
+
call.name,
|
|
4551
|
+
executionController
|
|
4552
|
+
);
|
|
4553
|
+
outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
4554
|
+
if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
|
|
4555
|
+
isError = true;
|
|
4556
|
+
errorCode = "COMMAND_EXIT_NONZERO";
|
|
4557
|
+
retryPolicy = "never";
|
|
4558
|
+
}
|
|
4559
|
+
} finally {
|
|
4560
|
+
this.signal?.removeEventListener("abort", abortFromSession);
|
|
4143
4561
|
}
|
|
4144
|
-
|
|
4562
|
+
} catch (err) {
|
|
4563
|
+
isError = true;
|
|
4564
|
+
const known = err instanceof KapibalaError ? err : void 0;
|
|
4565
|
+
outputStr = known?.safeMessage ?? "Tool execution failed";
|
|
4566
|
+
errorCode = known?.code ?? "TOOL_FAILED";
|
|
4567
|
+
retryPolicy = known?.retryPolicy ?? "never";
|
|
4568
|
+
}
|
|
4569
|
+
const resultBlock = {
|
|
4570
|
+
type: "tool_result",
|
|
4571
|
+
toolUseId: call.id,
|
|
4572
|
+
content: outputStr,
|
|
4573
|
+
isError,
|
|
4574
|
+
...errorCode ? { errorCode, retryPolicy } : {}
|
|
4575
|
+
};
|
|
4576
|
+
if (resultBlock.isError && resultBlock.errorCode) {
|
|
4577
|
+
resultBlock.content = JSON.stringify({
|
|
4578
|
+
code: resultBlock.errorCode,
|
|
4579
|
+
retryPolicy: resultBlock.retryPolicy,
|
|
4580
|
+
message: outputStr
|
|
4581
|
+
});
|
|
4145
4582
|
}
|
|
4146
|
-
let tool;
|
|
4147
4583
|
try {
|
|
4148
|
-
|
|
4584
|
+
for (const hook of this.hooks.get("tool:after")) {
|
|
4585
|
+
await hook(
|
|
4586
|
+
hookCtx,
|
|
4587
|
+
{ id: call.id, name: call.name, input: currentInput },
|
|
4588
|
+
{ output: outputStr, isError }
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4591
|
+
} catch {
|
|
4592
|
+
resultBlock.content = JSON.stringify({
|
|
4593
|
+
code: "HOOK_ERROR",
|
|
4594
|
+
retryPolicy: "after_user_action",
|
|
4595
|
+
message: "Tool finished but result processing failed"
|
|
4596
|
+
});
|
|
4597
|
+
resultBlock.isError = true;
|
|
4598
|
+
resultBlock.errorCode = "HOOK_ERROR";
|
|
4599
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
4600
|
+
}
|
|
4601
|
+
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
4602
|
+
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
4603
|
+
this.neverFailures.add(
|
|
4604
|
+
(0, import_node_crypto12.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex")
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4607
|
+
try {
|
|
4608
|
+
await audit("tool.finished", {
|
|
4609
|
+
status: resultBlock.isError ? "failed" : "completed",
|
|
4610
|
+
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
4611
|
+
durationMs: resultBlock.durationMs
|
|
4612
|
+
});
|
|
4613
|
+
} catch {
|
|
4614
|
+
return failure(
|
|
4615
|
+
"Tool outcome is unknown because the audit result could not be written",
|
|
4616
|
+
"OUTCOME_UNKNOWN",
|
|
4617
|
+
"after_user_action"
|
|
4618
|
+
);
|
|
4619
|
+
}
|
|
4620
|
+
return resultBlock;
|
|
4621
|
+
}
|
|
4622
|
+
/**
|
|
4623
|
+
* 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
|
|
4624
|
+
* 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
|
|
4625
|
+
*
|
|
4626
|
+
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
4627
|
+
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
4628
|
+
*/
|
|
4629
|
+
withTimeout(promise, toolName, executionController) {
|
|
4630
|
+
let timer;
|
|
4631
|
+
const timeout = new Promise((_, reject) => {
|
|
4632
|
+
timer = setTimeout(() => {
|
|
4633
|
+
const error = new KapibalaError(
|
|
4634
|
+
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
4635
|
+
{
|
|
4636
|
+
code: "OUTCOME_UNKNOWN",
|
|
4637
|
+
retryPolicy: "after_user_action",
|
|
4638
|
+
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
4639
|
+
}
|
|
4640
|
+
);
|
|
4641
|
+
reject(error);
|
|
4642
|
+
executionController.abort(error);
|
|
4643
|
+
}, this.toolTimeoutMs);
|
|
4644
|
+
});
|
|
4645
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
|
|
4649
|
+
// src/models/index.ts
|
|
4650
|
+
function assembleCanonicalToolResults(results) {
|
|
4651
|
+
return results.map((result) => ({
|
|
4652
|
+
role: "tool",
|
|
4653
|
+
content: [structuredClone(result)],
|
|
4654
|
+
timestamp: Date.now()
|
|
4655
|
+
}));
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
// src/models/transport/http.ts
|
|
4659
|
+
function safeTransportCode(error) {
|
|
4660
|
+
const codes = /* @__PURE__ */ new Set([
|
|
4661
|
+
"UND_ERR_SOCKET",
|
|
4662
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
4663
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
4664
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
4665
|
+
"ECONNRESET",
|
|
4666
|
+
"ETIMEDOUT",
|
|
4667
|
+
"ECONNREFUSED",
|
|
4668
|
+
"ENOTFOUND",
|
|
4669
|
+
"EAI_AGAIN"
|
|
4670
|
+
]);
|
|
4671
|
+
let current = error;
|
|
4672
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
|
|
4673
|
+
const value = current;
|
|
4674
|
+
if (typeof value.code === "string" && codes.has(value.code)) return value.code;
|
|
4675
|
+
current = value.cause;
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
function isContextOverflow(error, protocol) {
|
|
4679
|
+
if (!error || typeof error !== "object") return false;
|
|
4680
|
+
const detail = error;
|
|
4681
|
+
if (["context_length_exceeded", "context_window_exceeded", "max_context_length_exceeded"].includes(
|
|
4682
|
+
String(detail.code)
|
|
4683
|
+
))
|
|
4684
|
+
return true;
|
|
4685
|
+
return protocol === "anthropic" && detail.type === "invalid_request_error" && typeof detail.message === "string" && detail.message.length <= 160 && /^prompt is too long(?:: [0-9]+ tokens > [0-9]+ maximum)?$/.test(detail.message);
|
|
4686
|
+
}
|
|
4687
|
+
async function openModelResponse(options) {
|
|
4688
|
+
const controller = new AbortController();
|
|
4689
|
+
const abort = () => controller.abort();
|
|
4690
|
+
if (options.signal?.aborted) abort();
|
|
4691
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
4692
|
+
const timeout = options.connectTimeoutMs ?? 6e4;
|
|
4693
|
+
const timer = setTimeout(abort, timeout);
|
|
4694
|
+
const close = () => {
|
|
4695
|
+
clearTimeout(timer);
|
|
4696
|
+
options.signal?.removeEventListener("abort", abort);
|
|
4697
|
+
controller.abort();
|
|
4698
|
+
};
|
|
4699
|
+
try {
|
|
4700
|
+
let response;
|
|
4701
|
+
try {
|
|
4702
|
+
response = await fetch(options.url, {
|
|
4703
|
+
method: "POST",
|
|
4704
|
+
headers: { "Content-Type": "application/json", ...options.headers },
|
|
4705
|
+
body: JSON.stringify(options.payload),
|
|
4706
|
+
signal: controller.signal
|
|
4707
|
+
});
|
|
4149
4708
|
} catch (error) {
|
|
4150
|
-
|
|
4709
|
+
if (options.signal?.aborted) throw error;
|
|
4710
|
+
throw new ModelError(
|
|
4711
|
+
controller.signal.aborted ? "Model connection timed out" : "Model connection failed",
|
|
4712
|
+
{
|
|
4713
|
+
stage: "connect",
|
|
4714
|
+
status: controller.signal.aborted ? 408 : void 0,
|
|
4715
|
+
transportCode: safeTransportCode(error)
|
|
4716
|
+
}
|
|
4717
|
+
);
|
|
4718
|
+
} finally {
|
|
4719
|
+
clearTimeout(timer);
|
|
4720
|
+
}
|
|
4721
|
+
if (!response.ok) {
|
|
4722
|
+
let detail;
|
|
4151
4723
|
try {
|
|
4152
|
-
await
|
|
4153
|
-
await audit("tool.finished", { status: "failed" });
|
|
4724
|
+
detail = JSON.parse(await response.text())?.error;
|
|
4154
4725
|
} catch {
|
|
4155
4726
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4727
|
+
const errorOptions = {
|
|
4728
|
+
status: response.status,
|
|
4729
|
+
stage: "response",
|
|
4730
|
+
providerCode: typeof detail?.code === "string" ? detail.code : void 0,
|
|
4731
|
+
providerType: typeof detail?.type === "string" ? detail.type : void 0
|
|
4732
|
+
};
|
|
4733
|
+
if ([400, 413, 422].includes(response.status) && isContextOverflow(detail, options.protocol))
|
|
4734
|
+
throw new ContextOverflowError(response.status, errorOptions);
|
|
4735
|
+
throw new ModelError(`Model API request failed (${response.status})`, errorOptions);
|
|
4736
|
+
}
|
|
4737
|
+
if (!response.body)
|
|
4738
|
+
throw new ModelError("Model response body is missing", {
|
|
4739
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4740
|
+
stage: "response"
|
|
4741
|
+
});
|
|
4742
|
+
return { response, close };
|
|
4743
|
+
} catch (error) {
|
|
4744
|
+
close();
|
|
4745
|
+
throw error;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
function parseWireEvent(data) {
|
|
4749
|
+
try {
|
|
4750
|
+
const value = JSON.parse(data);
|
|
4751
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
4752
|
+
} catch {
|
|
4753
|
+
}
|
|
4754
|
+
throw new ModelError("Model response contained an invalid JSON event", {
|
|
4755
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4756
|
+
stage: "stream",
|
|
4757
|
+
retryable: false
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
function parseToolArguments(argumentsText) {
|
|
4761
|
+
try {
|
|
4762
|
+
const value = argumentsText.trim() ? JSON.parse(argumentsText) : {};
|
|
4763
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
4764
|
+
} catch {
|
|
4765
|
+
}
|
|
4766
|
+
throw new ModelError("Invalid model tool arguments", {
|
|
4767
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4768
|
+
stage: "stream",
|
|
4769
|
+
retryable: false
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// src/models/transport/sse.ts
|
|
4774
|
+
async function* parseSSEFrames(stream) {
|
|
4775
|
+
const reader = stream.getReader();
|
|
4776
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
4777
|
+
let buffer = "";
|
|
4778
|
+
let data = [];
|
|
4779
|
+
let event = "message";
|
|
4780
|
+
let frameLength = 0;
|
|
4781
|
+
const feed = (line) => {
|
|
4782
|
+
if (!line) {
|
|
4783
|
+
const frame = data.length ? { event, data: data.join("\n") } : void 0;
|
|
4784
|
+
data = [];
|
|
4785
|
+
event = "message";
|
|
4786
|
+
frameLength = 0;
|
|
4787
|
+
return frame;
|
|
4788
|
+
}
|
|
4789
|
+
if (line.startsWith(":")) return;
|
|
4790
|
+
const colon = line.indexOf(":");
|
|
4791
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
4792
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
4793
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
4794
|
+
if (field === "data") {
|
|
4795
|
+
data.push(value);
|
|
4796
|
+
frameLength += value.length;
|
|
4797
|
+
} else if (field === "event") event = value || "message";
|
|
4798
|
+
if (frameLength > 16 * 1024 * 1024)
|
|
4799
|
+
throw new ModelError("Model SSE frame exceeds the supported size", {
|
|
4800
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4801
|
+
stage: "stream",
|
|
4802
|
+
retryable: false
|
|
4803
|
+
});
|
|
4804
|
+
};
|
|
4805
|
+
try {
|
|
4806
|
+
let ended = false;
|
|
4807
|
+
while (!ended) {
|
|
4808
|
+
const next = await reader.read();
|
|
4809
|
+
ended = next.done;
|
|
4810
|
+
buffer += next.done ? decoder.decode() : decoder.decode(next.value, { stream: true });
|
|
4811
|
+
while (true) {
|
|
4812
|
+
const index2 = buffer.search(/[\r\n]/);
|
|
4813
|
+
if (index2 < 0 || !ended && buffer[index2] === "\r" && index2 === buffer.length - 1) break;
|
|
4814
|
+
const line = buffer.slice(0, index2);
|
|
4815
|
+
const width = buffer[index2] === "\r" && buffer[index2 + 1] === "\n" ? 2 : 1;
|
|
4816
|
+
buffer = buffer.slice(index2 + width);
|
|
4817
|
+
const frame = feed(line);
|
|
4818
|
+
if (frame) yield frame;
|
|
4819
|
+
}
|
|
4820
|
+
if (buffer.length > 16 * 1024 * 1024)
|
|
4821
|
+
throw new ModelError("Model SSE line exceeds the supported size", {
|
|
4822
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4823
|
+
stage: "stream",
|
|
4824
|
+
retryable: false
|
|
4825
|
+
});
|
|
4160
4826
|
}
|
|
4827
|
+
if (buffer) feed(buffer);
|
|
4828
|
+
const tail = feed("");
|
|
4829
|
+
if (tail) yield tail;
|
|
4830
|
+
} finally {
|
|
4161
4831
|
try {
|
|
4162
|
-
|
|
4163
|
-
call,
|
|
4164
|
-
currentInput,
|
|
4165
|
-
tool,
|
|
4166
|
-
startedAt,
|
|
4167
|
-
audit,
|
|
4168
|
-
failure,
|
|
4169
|
-
hookCtx,
|
|
4170
|
-
onProgress
|
|
4171
|
-
);
|
|
4832
|
+
await reader.cancel();
|
|
4172
4833
|
} catch {
|
|
4173
|
-
try {
|
|
4174
|
-
await audit("tool.finished", { status: "unknown", errorCode: "OUTCOME_UNKNOWN" });
|
|
4175
|
-
} catch {
|
|
4176
|
-
}
|
|
4177
|
-
return failure(
|
|
4178
|
-
"Tool outcome is unknown after an executor failure",
|
|
4179
|
-
"OUTCOME_UNKNOWN",
|
|
4180
|
-
"after_user_action"
|
|
4181
|
-
);
|
|
4182
4834
|
}
|
|
4835
|
+
reader.releaseLock();
|
|
4183
4836
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
// src/models/openai-compatible/index.ts
|
|
4840
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
|
|
4841
|
+
function invalidResponse(message) {
|
|
4842
|
+
return new ModelError(message, {
|
|
4843
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4844
|
+
stage: "stream",
|
|
4845
|
+
retryable: false
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
var OpenAICompatibleProvider = class {
|
|
4849
|
+
name = "openai-compatible";
|
|
4850
|
+
baseURL;
|
|
4851
|
+
apiKey;
|
|
4852
|
+
/** 未提供凭据时由 Session 在记录用户消息前拒绝请求。 */
|
|
4853
|
+
get credentialsReady() {
|
|
4854
|
+
return Boolean(this.apiKey.trim());
|
|
4855
|
+
}
|
|
4856
|
+
modelName;
|
|
4857
|
+
supportsThinking;
|
|
4858
|
+
replayReasoningContent;
|
|
4859
|
+
connectTimeoutMs;
|
|
4860
|
+
chatCapabilities;
|
|
4861
|
+
/** 暴露实际请求目标供原子绑定校验,不包含凭据。 */
|
|
4862
|
+
get binding() {
|
|
4863
|
+
return {
|
|
4864
|
+
protocol: "openai-compatible",
|
|
4865
|
+
baseURL: this.baseURL,
|
|
4866
|
+
modelName: this.modelName
|
|
4867
|
+
};
|
|
4868
|
+
}
|
|
4869
|
+
/**
|
|
4870
|
+
* 初始化 Chat 协议适配;显式端点能力优先,思考回传独立于模型 supportsThinking 标签。
|
|
4871
|
+
* @param options 请求目标、凭据和能力配置;缺省只有 DeepSeek 官方端点回传可见思考。
|
|
4872
|
+
*/
|
|
4873
|
+
constructor(options) {
|
|
4874
|
+
this.baseURL = options.baseURL.replace(/\/+$/, "");
|
|
4875
|
+
this.apiKey = options.apiKey;
|
|
4876
|
+
this.modelName = options.modelName;
|
|
4877
|
+
this.supportsThinking = options.supportsThinking ?? false;
|
|
4878
|
+
this.chatCapabilities = Object.freeze({ ...options.chatCapabilities });
|
|
4879
|
+
this.replayReasoningContent = this.chatCapabilities.replayReasoningContent ?? options.replayReasoningContent ?? new URL(this.baseURL).hostname === "api.deepseek.com";
|
|
4880
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
4881
|
+
}
|
|
4882
|
+
/**
|
|
4883
|
+
* 发送 Chat 流式请求;默认同时要求合法 finish_reason 和 DONE,只有显式例外允许完整 EOF。
|
|
4884
|
+
* 全部工具的 ID、名称和对象参数在任何完成事件之前校验;截断、过滤或中断不交付工具。
|
|
4885
|
+
* @param req canonical 历史、工具、预算与取消信号;能力覆盖来自端点配置。
|
|
4886
|
+
* @returns 文本、可见思考和工具增量,完整响应最后产生 message_stop。
|
|
4887
|
+
* @throws ModelError 响应缺少合法终态、工具身份或完整对象参数时失败,不自动重放请求。
|
|
4888
|
+
*/
|
|
4889
|
+
async *create(req) {
|
|
4890
|
+
const url = `${this.baseURL}/chat/completions`;
|
|
4891
|
+
const wireMessages = this.translateMessagesToWire(req.messages, req.systemPrompt);
|
|
4892
|
+
const wireTools = req.tools && req.tools.length > 0 ? req.tools.map((t) => ({
|
|
4893
|
+
type: "function",
|
|
4894
|
+
function: {
|
|
4895
|
+
name: t.name,
|
|
4896
|
+
description: t.description,
|
|
4897
|
+
parameters: t.parameters
|
|
4898
|
+
}
|
|
4899
|
+
})) : void 0;
|
|
4900
|
+
const payload = {
|
|
4901
|
+
model: this.modelName,
|
|
4902
|
+
messages: wireMessages,
|
|
4903
|
+
stream: true
|
|
4904
|
+
};
|
|
4905
|
+
if (this.chatCapabilities.supportsStreamingUsage !== false)
|
|
4906
|
+
payload.stream_options = { include_usage: true };
|
|
4907
|
+
if (this.chatCapabilities.supportsTemperature !== false)
|
|
4908
|
+
payload.temperature = req.temperature ?? 0.7;
|
|
4909
|
+
if (wireTools) {
|
|
4910
|
+
payload.tools = wireTools;
|
|
4911
|
+
}
|
|
4912
|
+
if (req.maxTokens) {
|
|
4913
|
+
payload[this.chatCapabilities.maxTokensField ?? "max_tokens"] = req.maxTokens;
|
|
4914
|
+
}
|
|
4915
|
+
let response;
|
|
4916
|
+
const timeoutController = new AbortController();
|
|
4917
|
+
const timeoutTimer = setTimeout(() => timeoutController.abort(), this.connectTimeoutMs);
|
|
4918
|
+
const onOuterAbort = () => timeoutController.abort();
|
|
4919
|
+
if (req.signal?.aborted) {
|
|
4920
|
+
timeoutController.abort();
|
|
4921
|
+
} else {
|
|
4922
|
+
req.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
4923
|
+
}
|
|
4186
4924
|
try {
|
|
4187
|
-
shell = tool.approvalScope?.(currentInput, this.rootDir);
|
|
4188
|
-
} catch {
|
|
4189
4925
|
try {
|
|
4190
|
-
await
|
|
4191
|
-
|
|
4192
|
-
|
|
4926
|
+
response = await fetch(url, {
|
|
4927
|
+
method: "POST",
|
|
4928
|
+
headers: {
|
|
4929
|
+
"Content-Type": "application/json",
|
|
4930
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
4931
|
+
},
|
|
4932
|
+
body: JSON.stringify(payload),
|
|
4933
|
+
signal: timeoutController.signal
|
|
4934
|
+
});
|
|
4935
|
+
} catch (err) {
|
|
4936
|
+
if (req.signal?.aborted) {
|
|
4937
|
+
throw err;
|
|
4938
|
+
}
|
|
4939
|
+
if (timeoutController.signal.aborted) {
|
|
4940
|
+
throw new ModelError(`Connection to ${url} timed out after ${this.connectTimeoutMs}ms`, {
|
|
4941
|
+
status: 408,
|
|
4942
|
+
retryable: true,
|
|
4943
|
+
stage: "connect"
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
throw new ModelError(`Failed to connect to ${url}: ${err.message}`, {
|
|
4947
|
+
transportCode: safeTransportCode(err),
|
|
4948
|
+
stage: "connect"
|
|
4949
|
+
});
|
|
4950
|
+
} finally {
|
|
4951
|
+
clearTimeout(timeoutTimer);
|
|
4193
4952
|
}
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
const request = {
|
|
4199
|
-
toolName: call.name,
|
|
4200
|
-
capabilities: tool.metadata?.permissions ?? [],
|
|
4201
|
-
input: currentInput,
|
|
4202
|
-
rootDir: this.rootDir,
|
|
4203
|
-
...shell ? { shell } : {},
|
|
4204
|
-
// 只有无参数的解释器内建查询能确认不依赖外部脚本;其余命令仅允许单次批准。
|
|
4205
|
-
sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
|
|
4206
|
-
};
|
|
4207
|
-
const repeatKey = (0, import_node_crypto11.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex");
|
|
4208
|
-
if (this.neverFailures.has(repeatKey)) {
|
|
4953
|
+
if (!response.ok) {
|
|
4954
|
+
const errorText = await response.text();
|
|
4955
|
+
let errorDetails;
|
|
4956
|
+
let errorMsg = `API request failed with status ${response.status}: ${response.statusText}`;
|
|
4209
4957
|
try {
|
|
4210
|
-
|
|
4211
|
-
|
|
4958
|
+
const errorJson = JSON.parse(errorText);
|
|
4959
|
+
errorDetails = errorJson.error;
|
|
4960
|
+
if (typeof errorJson.error?.message === "string") {
|
|
4961
|
+
errorMsg = errorJson.error.message;
|
|
4962
|
+
}
|
|
4212
4963
|
} catch {
|
|
4213
|
-
|
|
4214
|
-
"Audit unavailable; tool was not started",
|
|
4215
|
-
"AUDIT_UNAVAILABLE",
|
|
4216
|
-
"after_user_action"
|
|
4217
|
-
);
|
|
4964
|
+
if (errorText) errorMsg += ` - ${errorText.slice(0, 300)}`;
|
|
4218
4965
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4966
|
+
const details = errorDetails;
|
|
4967
|
+
const options = {
|
|
4968
|
+
status: response.status,
|
|
4969
|
+
providerCode: typeof details?.code === "string" ? details.code : void 0,
|
|
4970
|
+
providerType: typeof details?.type === "string" ? details.type : void 0,
|
|
4971
|
+
stage: "response"
|
|
4972
|
+
};
|
|
4973
|
+
if ([400, 413, 422].includes(response.status) && isContextOverflow(errorDetails))
|
|
4974
|
+
throw new ContextOverflowError(response.status, { ...options, message: errorMsg });
|
|
4975
|
+
throw new ModelError(errorMsg, options);
|
|
4223
4976
|
}
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
let
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4977
|
+
if (!response.body) {
|
|
4978
|
+
throw new ModelError("Response body is null", {
|
|
4979
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
4980
|
+
stage: "response"
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
4984
|
+
let usage;
|
|
4985
|
+
let finishReason;
|
|
4986
|
+
let streamDone = false;
|
|
4987
|
+
const requestStartTime = Date.now();
|
|
4988
|
+
let ttftMs;
|
|
4989
|
+
for await (const frame of parseSSEFrames(response.body)) {
|
|
4990
|
+
if (req.signal?.aborted) return;
|
|
4991
|
+
const chunk = frame.data;
|
|
4992
|
+
if (chunk.trim() === "[DONE]") {
|
|
4993
|
+
streamDone = true;
|
|
4994
|
+
break;
|
|
4995
|
+
}
|
|
4996
|
+
if (!chunk) continue;
|
|
4997
|
+
let json;
|
|
4240
4998
|
try {
|
|
4241
|
-
|
|
4999
|
+
json = JSON.parse(chunk);
|
|
4242
5000
|
} catch {
|
|
4243
|
-
|
|
5001
|
+
throw new ModelError("Model response contained invalid JSON", {
|
|
5002
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5003
|
+
stage: "stream",
|
|
5004
|
+
retryable: false
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
5007
|
+
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
5008
|
+
throw new ModelError("Model response contained an invalid event", {
|
|
5009
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5010
|
+
stage: "stream",
|
|
5011
|
+
retryable: false
|
|
5012
|
+
});
|
|
5013
|
+
const wireError = json.error ?? (frame.event === "error" ? json : void 0);
|
|
5014
|
+
if (wireError) {
|
|
5015
|
+
const message = typeof wireError.message === "string" ? wireError.message : "Model API stream error";
|
|
5016
|
+
const details = [wireError.type, wireError.code].filter((value) => typeof value === "string" && value.length > 0).join(", ");
|
|
5017
|
+
const options = {
|
|
5018
|
+
providerCode: typeof wireError.code === "string" ? wireError.code : void 0,
|
|
5019
|
+
providerType: typeof wireError.type === "string" ? wireError.type : void 0,
|
|
5020
|
+
stage: "stream"
|
|
5021
|
+
};
|
|
5022
|
+
if (isContextOverflow(wireError))
|
|
5023
|
+
throw new ContextOverflowError(void 0, { ...options, message });
|
|
5024
|
+
throw new ModelError(details ? `${message} (${details})` : message, options);
|
|
5025
|
+
}
|
|
5026
|
+
if (json.usage) {
|
|
5027
|
+
usage = {
|
|
5028
|
+
promptTokens: json.usage.prompt_tokens ?? 0,
|
|
5029
|
+
completionTokens: json.usage.completion_tokens ?? 0,
|
|
5030
|
+
totalTokens: json.usage.total_tokens ?? 0
|
|
5031
|
+
};
|
|
5032
|
+
const cached = json.usage.prompt_tokens_details?.cached_tokens ?? json.usage.prompt_cache_hit_tokens;
|
|
5033
|
+
if (Number.isSafeInteger(cached) && cached >= 0) usage.cachedPromptTokens = cached;
|
|
5034
|
+
}
|
|
5035
|
+
const choice = json.choices?.[0];
|
|
5036
|
+
if (!choice) continue;
|
|
5037
|
+
if (finishReason !== void 0)
|
|
5038
|
+
throw invalidResponse("Model response contained a choice after its terminal state");
|
|
5039
|
+
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
5040
|
+
if (typeof choice.finish_reason !== "string")
|
|
5041
|
+
throw invalidResponse("Model response contained an invalid finish reason");
|
|
5042
|
+
finishReason = choice.finish_reason;
|
|
5043
|
+
}
|
|
5044
|
+
const delta = choice.delta;
|
|
5045
|
+
if (!delta) continue;
|
|
5046
|
+
if (typeof delta !== "object" || Array.isArray(delta))
|
|
5047
|
+
throw invalidResponse("Model response contained an invalid delta");
|
|
5048
|
+
if (delta.function_call)
|
|
5049
|
+
throw invalidResponse("Model response used an unsupported legacy function call");
|
|
5050
|
+
for (const field of ["content", "reasoning_content"])
|
|
5051
|
+
if (delta[field] != null && typeof delta[field] !== "string")
|
|
5052
|
+
throw invalidResponse("Model response contained an invalid text delta");
|
|
5053
|
+
if (delta.tool_calls != null && !Array.isArray(delta.tool_calls))
|
|
5054
|
+
throw invalidResponse("Model response contained invalid tool calls");
|
|
5055
|
+
if ((delta.content || delta.reasoning_content || delta.tool_calls && delta.tool_calls.length > 0) && ttftMs === void 0) {
|
|
5056
|
+
ttftMs = Date.now() - requestStartTime;
|
|
4244
5057
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
this.approvalCache.set(request, "allow");
|
|
4248
|
-
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
4249
|
-
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
4250
|
-
source = this.approvalChannel ? "human" : "non_interactive";
|
|
4251
|
-
}
|
|
4252
|
-
auditFields = shell ? {
|
|
4253
|
-
shell: shell.interpreter,
|
|
4254
|
-
cwdDigest: (0, import_node_crypto11.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
4255
|
-
executableDigest: shell.executableDigest,
|
|
4256
|
-
executionFingerprint: shell.executionFingerprint,
|
|
4257
|
-
...commandAuditSummary(shell.command)
|
|
4258
|
-
} : {};
|
|
4259
|
-
try {
|
|
4260
|
-
await audit("tool.decided", {
|
|
4261
|
-
decision: permitted ? "allow" : "deny",
|
|
4262
|
-
decisionSource: source,
|
|
4263
|
-
...approvalChoice ? { approvalChoice } : {},
|
|
4264
|
-
...auditFields
|
|
4265
|
-
});
|
|
4266
|
-
} catch {
|
|
4267
|
-
return failure(
|
|
4268
|
-
"Audit unavailable; tool was not started",
|
|
4269
|
-
"AUDIT_UNAVAILABLE",
|
|
4270
|
-
"after_user_action"
|
|
4271
|
-
);
|
|
4272
|
-
}
|
|
4273
|
-
if (!permitted || this.signal?.aborted) {
|
|
4274
|
-
const code = this.signal?.aborted ? "ABORTED" : "PERMISSION_DENIED";
|
|
4275
|
-
try {
|
|
4276
|
-
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
4277
|
-
} catch {
|
|
4278
|
-
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action");
|
|
5058
|
+
if (delta.content) {
|
|
5059
|
+
yield { type: "text_delta", text: delta.content };
|
|
4279
5060
|
}
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
5061
|
+
if (delta.reasoning_content) {
|
|
5062
|
+
yield { type: "thinking_delta", thinking: delta.reasoning_content };
|
|
5063
|
+
}
|
|
5064
|
+
if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
5065
|
+
for (const tc of delta.tool_calls) {
|
|
5066
|
+
if (!tc || typeof tc !== "object" || !Number.isSafeInteger(tc.index) || tc.index < 0)
|
|
5067
|
+
throw invalidResponse("Model response contained an invalid tool index");
|
|
5068
|
+
if (tc.type != null && tc.type !== "function")
|
|
5069
|
+
throw invalidResponse("Model response contained an unsupported tool type");
|
|
5070
|
+
const index2 = tc.index;
|
|
5071
|
+
let entry = pendingToolCalls.get(index2);
|
|
5072
|
+
if (!entry) {
|
|
5073
|
+
entry = {
|
|
5074
|
+
id: "",
|
|
5075
|
+
name: "",
|
|
5076
|
+
argumentChunks: [],
|
|
5077
|
+
emittedChunks: 0,
|
|
5078
|
+
started: false
|
|
5079
|
+
};
|
|
5080
|
+
pendingToolCalls.set(index2, entry);
|
|
5081
|
+
}
|
|
5082
|
+
for (const [field, value] of [
|
|
5083
|
+
["id", tc.id],
|
|
5084
|
+
["name", tc.function?.name]
|
|
5085
|
+
]) {
|
|
5086
|
+
if (value === void 0 || value === null) continue;
|
|
5087
|
+
if (typeof value !== "string" || !value.trim() || entry[field] && entry[field] !== value)
|
|
5088
|
+
throw invalidResponse(
|
|
5089
|
+
"Model response contained an invalid or changed tool identity"
|
|
4298
5090
|
);
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
5091
|
+
entry[field] = value;
|
|
5092
|
+
}
|
|
5093
|
+
if (tc.function?.arguments !== void 0) {
|
|
5094
|
+
if (typeof tc.function.arguments !== "string")
|
|
5095
|
+
throw invalidResponse("Model response contained an invalid tool argument delta");
|
|
5096
|
+
if (tc.function.arguments) entry.argumentChunks.push(tc.function.arguments);
|
|
5097
|
+
}
|
|
5098
|
+
if (entry.id && entry.name && !entry.started) {
|
|
5099
|
+
entry.started = true;
|
|
5100
|
+
yield { type: "tool_call_start", id: entry.id, name: entry.name };
|
|
5101
|
+
}
|
|
5102
|
+
while (entry.started && entry.emittedChunks < entry.argumentChunks.length) {
|
|
5103
|
+
yield {
|
|
5104
|
+
type: "tool_call_delta",
|
|
5105
|
+
id: entry.id,
|
|
5106
|
+
argumentChunk: entry.argumentChunks[entry.emittedChunks++]
|
|
5107
|
+
};
|
|
4302
5108
|
}
|
|
4303
|
-
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
4304
|
-
return failure(
|
|
4305
|
-
"Execution target changed after approval",
|
|
4306
|
-
"TARGET_CHANGED",
|
|
4307
|
-
"after_user_action"
|
|
4308
|
-
);
|
|
4309
5109
|
}
|
|
4310
|
-
} catch {
|
|
4311
|
-
return failure(
|
|
4312
|
-
"Execution target changed after approval",
|
|
4313
|
-
"TARGET_CHANGED",
|
|
4314
|
-
"after_user_action"
|
|
4315
|
-
);
|
|
4316
|
-
}
|
|
4317
|
-
}
|
|
4318
|
-
break;
|
|
4319
|
-
}
|
|
4320
|
-
try {
|
|
4321
|
-
await audit("tool.started", { status: "started", ...auditFields });
|
|
4322
|
-
} catch {
|
|
4323
|
-
return failure(
|
|
4324
|
-
"Audit unavailable; tool was not started",
|
|
4325
|
-
"AUDIT_UNAVAILABLE",
|
|
4326
|
-
"after_user_action"
|
|
4327
|
-
);
|
|
4328
|
-
}
|
|
4329
|
-
let outputStr;
|
|
4330
|
-
let isError = false;
|
|
4331
|
-
let errorCode;
|
|
4332
|
-
let retryPolicy;
|
|
4333
|
-
try {
|
|
4334
|
-
const executionController = new AbortController();
|
|
4335
|
-
const abortFromSession = () => executionController.abort();
|
|
4336
|
-
if (this.signal?.aborted) {
|
|
4337
|
-
executionController.abort();
|
|
4338
|
-
} else {
|
|
4339
|
-
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
4340
|
-
}
|
|
4341
|
-
const toolCtx = {
|
|
4342
|
-
rootDir: this.rootDir,
|
|
4343
|
-
signal: executionController.signal,
|
|
4344
|
-
logger: this.logger,
|
|
4345
|
-
onProgress: (progress) => onProgress?.({ id: call.id, name: call.name, ...progress }),
|
|
4346
|
-
...shell ? {
|
|
4347
|
-
onProcessSpawned: () => audit("process.spawned", { status: "spawned", ...auditFields })
|
|
4348
|
-
} : {}
|
|
4349
|
-
};
|
|
4350
|
-
try {
|
|
4351
|
-
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(currentInput, toolCtx) : await this.withTimeout(
|
|
4352
|
-
tool.execute(currentInput, toolCtx),
|
|
4353
|
-
call.name,
|
|
4354
|
-
executionController
|
|
4355
|
-
);
|
|
4356
|
-
outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
4357
|
-
if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
|
|
4358
|
-
isError = true;
|
|
4359
|
-
errorCode = "COMMAND_EXIT_NONZERO";
|
|
4360
|
-
retryPolicy = "never";
|
|
4361
5110
|
}
|
|
4362
|
-
} finally {
|
|
4363
|
-
this.signal?.removeEventListener("abort", abortFromSession);
|
|
4364
|
-
}
|
|
4365
|
-
} catch (err) {
|
|
4366
|
-
isError = true;
|
|
4367
|
-
const known = err instanceof KapibalaError ? err : void 0;
|
|
4368
|
-
outputStr = known?.safeMessage ?? "Tool execution failed";
|
|
4369
|
-
errorCode = known?.code ?? "TOOL_FAILED";
|
|
4370
|
-
retryPolicy = known?.retryPolicy ?? "never";
|
|
4371
|
-
}
|
|
4372
|
-
const resultBlock = {
|
|
4373
|
-
type: "tool_result",
|
|
4374
|
-
toolUseId: call.id,
|
|
4375
|
-
content: outputStr,
|
|
4376
|
-
isError,
|
|
4377
|
-
...errorCode ? { errorCode, retryPolicy } : {}
|
|
4378
|
-
};
|
|
4379
|
-
if (resultBlock.isError && resultBlock.errorCode) {
|
|
4380
|
-
resultBlock.content = JSON.stringify({
|
|
4381
|
-
code: resultBlock.errorCode,
|
|
4382
|
-
retryPolicy: resultBlock.retryPolicy,
|
|
4383
|
-
message: outputStr
|
|
4384
|
-
});
|
|
4385
|
-
}
|
|
4386
|
-
try {
|
|
4387
|
-
for (const hook of this.hooks.get("tool:after")) {
|
|
4388
|
-
await hook(
|
|
4389
|
-
hookCtx,
|
|
4390
|
-
{ id: call.id, name: call.name, input: currentInput },
|
|
4391
|
-
{ output: outputStr, isError }
|
|
4392
|
-
);
|
|
4393
5111
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
)
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
5112
|
+
if (req.signal?.aborted) return;
|
|
5113
|
+
if (finishReason === void 0 || !streamDone && this.chatCapabilities.requiresDone !== false)
|
|
5114
|
+
throw new ModelError("Model response stream ended without a completion marker", {
|
|
5115
|
+
retryable: false,
|
|
5116
|
+
code: "MODEL_STREAM_INCOMPLETE",
|
|
5117
|
+
stage: "stream"
|
|
5118
|
+
});
|
|
5119
|
+
if (finishReason !== "stop" && finishReason !== "tool_calls" && finishReason !== "length")
|
|
5120
|
+
throw invalidResponse("Model response contained an unsupported or filtered finish reason");
|
|
5121
|
+
if (pendingToolCalls.size > 0 && !["tool_calls", "length"].includes(finishReason))
|
|
5122
|
+
throw invalidResponse("Model response finish reason did not match its tool calls");
|
|
5123
|
+
if (pendingToolCalls.size === 0 && finishReason === "tool_calls")
|
|
5124
|
+
throw invalidResponse("Model response finish reason did not match its tool calls");
|
|
5125
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5126
|
+
const completedTools = [...pendingToolCalls.values()].map((tc) => {
|
|
5127
|
+
if (!tc.id || !tc.name || ids.has(tc.id))
|
|
5128
|
+
throw invalidResponse("Model response contained missing or duplicate tool identities");
|
|
5129
|
+
ids.add(tc.id);
|
|
5130
|
+
return {
|
|
5131
|
+
type: "tool_call_finish",
|
|
5132
|
+
id: tc.id,
|
|
5133
|
+
name: tc.name,
|
|
5134
|
+
input: parseToolArguments(tc.argumentChunks.join(""))
|
|
5135
|
+
};
|
|
4415
5136
|
});
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
5137
|
+
for (const tool of completedTools) yield tool;
|
|
5138
|
+
const durationMs = Date.now() - requestStartTime;
|
|
5139
|
+
yield { type: "message_stop", usage, ttftMs, durationMs, finishReason };
|
|
5140
|
+
} catch (error) {
|
|
5141
|
+
if (error instanceof ModelError || req.signal?.aborted) throw error;
|
|
5142
|
+
const transportCode = safeTransportCode(error);
|
|
5143
|
+
throw new ModelError(
|
|
5144
|
+
`Model response stream failed${transportCode ? ` (${transportCode})` : ""}`,
|
|
5145
|
+
{
|
|
5146
|
+
retryable: false,
|
|
5147
|
+
code: "MODEL_STREAM_INTERRUPTED",
|
|
5148
|
+
transportCode,
|
|
5149
|
+
stage: "stream"
|
|
5150
|
+
}
|
|
4421
5151
|
);
|
|
5152
|
+
} finally {
|
|
5153
|
+
req.signal?.removeEventListener("abort", onOuterAbort);
|
|
5154
|
+
timeoutController.abort();
|
|
4422
5155
|
}
|
|
4423
|
-
return resultBlock;
|
|
4424
5156
|
}
|
|
4425
5157
|
/**
|
|
4426
|
-
*
|
|
4427
|
-
*
|
|
4428
|
-
*
|
|
4429
|
-
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
4430
|
-
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
5158
|
+
* 将工具结果装配为 canonical tool 消息,wire 角色仅在下一请求投影时转换。
|
|
5159
|
+
* @param results 已完成的工具结果,保持调用 ID 与内容,不重放工具。
|
|
5160
|
+
* @returns 逐条克隆的 canonical tool 消息,允许消息级落盘。
|
|
4431
5161
|
*/
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
5162
|
+
assembleToolResults(results) {
|
|
5163
|
+
return assembleCanonicalToolResults(results);
|
|
5164
|
+
}
|
|
5165
|
+
/**
|
|
5166
|
+
* 将历史投影为 Chat 消息;合并连续 user,剔除空 assistant,保持工具调用与结果身份。
|
|
5167
|
+
* 仅显式允许的端点回传可见思考;签名、脱敏思考和原生私有 Item 不适用于 Chat。
|
|
5168
|
+
* @param messages canonical 历史;只投影文本、可选可见思考和工具语义。
|
|
5169
|
+
* @param systemPrompt 可选全局提示词,去除首尾空白后作为首条 system。
|
|
5170
|
+
* @returns 与输入历史独立的 Chat wire 消息数组。
|
|
5171
|
+
*/
|
|
5172
|
+
translateMessagesToWire(messages, systemPrompt) {
|
|
5173
|
+
const wire = [];
|
|
5174
|
+
if (systemPrompt?.trim()) {
|
|
5175
|
+
wire.push({ role: "system", content: systemPrompt.trim() });
|
|
5176
|
+
}
|
|
5177
|
+
for (const msg of messages) {
|
|
5178
|
+
if (msg.role === "system") {
|
|
5179
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
5180
|
+
if (text) wire.push({ role: "system", content: text });
|
|
5181
|
+
} else if (msg.role === "user") {
|
|
5182
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
5183
|
+
const previous = wire[wire.length - 1];
|
|
5184
|
+
if (previous?.role === "user" && typeof previous.content === "string") {
|
|
5185
|
+
previous.content = [previous.content, text].filter(Boolean).join("\n\n");
|
|
5186
|
+
} else {
|
|
5187
|
+
wire.push({ role: "user", content: text });
|
|
5188
|
+
}
|
|
5189
|
+
} else if (msg.role === "assistant") {
|
|
5190
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
5191
|
+
const toolCalls = [];
|
|
5192
|
+
for (const block of msg.content) {
|
|
5193
|
+
if (block.type === "tool_use") {
|
|
5194
|
+
toolCalls.push({
|
|
5195
|
+
id: block.id,
|
|
5196
|
+
type: "function",
|
|
5197
|
+
function: {
|
|
5198
|
+
name: block.name,
|
|
5199
|
+
arguments: JSON.stringify(block.input)
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
4442
5202
|
}
|
|
4443
|
-
);
|
|
4444
|
-
reject(error);
|
|
4445
|
-
executionController.abort(error);
|
|
4446
|
-
}, this.toolTimeoutMs);
|
|
4447
|
-
});
|
|
4448
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
4449
|
-
}
|
|
4450
|
-
};
|
|
4451
|
-
|
|
4452
|
-
// src/models/openai-compatible/sse.ts
|
|
4453
|
-
async function* parseSSEStream(stream, onDone) {
|
|
4454
|
-
const reader = stream.getReader();
|
|
4455
|
-
const decoder = new TextDecoder();
|
|
4456
|
-
let buffer = "";
|
|
4457
|
-
try {
|
|
4458
|
-
while (true) {
|
|
4459
|
-
const { done, value } = await reader.read();
|
|
4460
|
-
if (done) break;
|
|
4461
|
-
buffer += decoder.decode(value, { stream: true });
|
|
4462
|
-
const lines = buffer.split(/\r?\n/);
|
|
4463
|
-
buffer = lines.pop() ?? "";
|
|
4464
|
-
for (const line of lines) {
|
|
4465
|
-
const trimmed = line.trim();
|
|
4466
|
-
if (!trimmed || trimmed.startsWith(":")) {
|
|
4467
|
-
continue;
|
|
4468
5203
|
}
|
|
4469
|
-
if (
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
5204
|
+
if (!text && toolCalls.length === 0) continue;
|
|
5205
|
+
wire.push({
|
|
5206
|
+
role: "assistant",
|
|
5207
|
+
content: text || (this.replayReasoningContent ? "" : null),
|
|
5208
|
+
...this.replayReasoningContent ? {
|
|
5209
|
+
reasoning_content: msg.content.filter((block) => block.type === "thinking").map((block) => block.thinking).join("")
|
|
5210
|
+
} : {},
|
|
5211
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : void 0
|
|
5212
|
+
});
|
|
5213
|
+
} else if (msg.role === "tool") {
|
|
5214
|
+
for (const block of msg.content) {
|
|
5215
|
+
if (block.type === "tool_result") {
|
|
5216
|
+
wire.push({
|
|
5217
|
+
role: "tool",
|
|
5218
|
+
tool_call_id: block.toolUseId,
|
|
5219
|
+
content: block.content
|
|
5220
|
+
});
|
|
4474
5221
|
}
|
|
4475
|
-
yield dataContent;
|
|
4476
5222
|
}
|
|
4477
5223
|
}
|
|
4478
5224
|
}
|
|
4479
|
-
|
|
4480
|
-
const dataContent = buffer.trim().slice(5).trim();
|
|
4481
|
-
if (dataContent !== "[DONE]") {
|
|
4482
|
-
yield dataContent;
|
|
4483
|
-
} else onDone?.();
|
|
4484
|
-
}
|
|
4485
|
-
} finally {
|
|
4486
|
-
try {
|
|
4487
|
-
await reader.cancel();
|
|
4488
|
-
} finally {
|
|
4489
|
-
reader.releaseLock();
|
|
4490
|
-
}
|
|
5225
|
+
return wire;
|
|
4491
5226
|
}
|
|
4492
|
-
}
|
|
5227
|
+
};
|
|
4493
5228
|
|
|
4494
|
-
// src/models/
|
|
4495
|
-
var
|
|
4496
|
-
function
|
|
4497
|
-
|
|
4498
|
-
"
|
|
4499
|
-
"
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
"ECONNRESET",
|
|
4503
|
-
"ETIMEDOUT",
|
|
4504
|
-
"ECONNREFUSED",
|
|
4505
|
-
"ENOTFOUND",
|
|
4506
|
-
"EAI_AGAIN"
|
|
4507
|
-
]);
|
|
4508
|
-
let current = error;
|
|
4509
|
-
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
|
|
4510
|
-
const candidate = current;
|
|
4511
|
-
if (typeof candidate.code === "string" && allowed.has(candidate.code)) return candidate.code;
|
|
4512
|
-
current = candidate.cause;
|
|
4513
|
-
}
|
|
4514
|
-
return void 0;
|
|
5229
|
+
// src/models/anthropic/index.ts
|
|
5230
|
+
var import_node_crypto13 = require("crypto");
|
|
5231
|
+
function invalidResponse2() {
|
|
5232
|
+
throw new ModelError("Invalid Anthropic response state", {
|
|
5233
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5234
|
+
stage: "stream",
|
|
5235
|
+
retryable: false
|
|
5236
|
+
});
|
|
4515
5237
|
}
|
|
4516
|
-
function
|
|
4517
|
-
if (
|
|
4518
|
-
|
|
4519
|
-
|
|
5238
|
+
function wireToolId(id) {
|
|
5239
|
+
if (typeof id !== "string" || !id)
|
|
5240
|
+
throw new ModelError("Invalid tool history identity", {
|
|
5241
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5242
|
+
stage: "request",
|
|
5243
|
+
retryable: false
|
|
5244
|
+
});
|
|
5245
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0, import_node_crypto13.createHash)("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
4520
5246
|
}
|
|
4521
|
-
var
|
|
4522
|
-
name = "
|
|
5247
|
+
var AnthropicProvider = class {
|
|
5248
|
+
name = "anthropic";
|
|
4523
5249
|
baseURL;
|
|
4524
|
-
apiKey;
|
|
4525
5250
|
modelName;
|
|
4526
|
-
|
|
4527
|
-
replayReasoningContent;
|
|
5251
|
+
workspaceId;
|
|
4528
5252
|
connectTimeoutMs;
|
|
4529
|
-
|
|
5253
|
+
apiKey;
|
|
5254
|
+
/** 表明该适配器已装载非空凭据,不暴露凭据内容。 */
|
|
5255
|
+
get credentialsReady() {
|
|
5256
|
+
return Boolean(this.apiKey.trim());
|
|
5257
|
+
}
|
|
5258
|
+
/** 暴露不含凭据的请求目标;用于宿主校验 Profile 与 Provider 的原子绑定。 */
|
|
5259
|
+
get binding() {
|
|
5260
|
+
return {
|
|
5261
|
+
protocol: "anthropic",
|
|
5262
|
+
baseURL: this.baseURL,
|
|
5263
|
+
modelName: this.modelName,
|
|
5264
|
+
workspaceId: this.workspaceId
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
/** 初始化连接;无效 URL/凭据/超时在请求前失败,错误不回显配置值。 */
|
|
4530
5268
|
constructor(options) {
|
|
5269
|
+
try {
|
|
5270
|
+
const url = new URL(options.baseURL);
|
|
5271
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
5272
|
+
throw new Error();
|
|
5273
|
+
if (!options.modelName?.trim() || !options.apiKey?.trim() || /[\r\n]/.test(options.apiKey) || options.workspaceId !== void 0 && (!options.workspaceId.trim() || /[\r\n]/.test(options.workspaceId)) || options.connectTimeoutMs !== void 0 && (!Number.isSafeInteger(options.connectTimeoutMs) || options.connectTimeoutMs <= 0))
|
|
5274
|
+
throw new Error();
|
|
5275
|
+
} catch {
|
|
5276
|
+
throw new ModelError("Invalid Anthropic connection configuration", {
|
|
5277
|
+
code: "MODEL_INVALID_REQUEST",
|
|
5278
|
+
stage: "request",
|
|
5279
|
+
retryable: false
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
4531
5282
|
this.baseURL = options.baseURL.replace(/\/+$/, "");
|
|
4532
|
-
this.apiKey = options.apiKey;
|
|
4533
5283
|
this.modelName = options.modelName;
|
|
4534
|
-
this.
|
|
4535
|
-
this.
|
|
4536
|
-
this.connectTimeoutMs = options.connectTimeoutMs ??
|
|
5284
|
+
this.apiKey = options.apiKey;
|
|
5285
|
+
this.workspaceId = options.workspaceId;
|
|
5286
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? 6e4;
|
|
5287
|
+
}
|
|
5288
|
+
/** 装配统一 tool 历史;Messages 的 user/tool_result 转换只作用于请求视图。 */
|
|
5289
|
+
assembleToolResults(results) {
|
|
5290
|
+
return assembleCanonicalToolResults(results);
|
|
4537
5291
|
}
|
|
4538
|
-
/**
|
|
5292
|
+
/**
|
|
5293
|
+
* 发送 Messages 流并按索引组装完整内容;增量仅供展示,终态提供有序落盘来源。
|
|
5294
|
+
* maxTokens 包含思考消耗,缺省为 4096;缺终态、坏参数与服务端流错误均抛错。
|
|
5295
|
+
* pause_turn 表示服务端工具暂停;当前不支持其续答,明确失败而非报告完成或调度客户端工具。
|
|
5296
|
+
* 同 run/模型/端点的来源校验允许私有续答;来源不匹配的可见思考降为普通文字。
|
|
5297
|
+
* 取消或消费者提前结束会等待 reader 清理并解除 HTTP 取消监听,不自动重发请求。
|
|
5298
|
+
*/
|
|
4539
5299
|
async *create(req) {
|
|
4540
|
-
|
|
4541
|
-
const
|
|
4542
|
-
const
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
5300
|
+
if (req.signal?.aborted) throw new AbortError();
|
|
5301
|
+
const payload = this.createPayload(req);
|
|
5302
|
+
const origin = createProtocolOrigin(this.binding, req.context);
|
|
5303
|
+
const startedAt = Date.now();
|
|
5304
|
+
const opened = await openModelResponse({
|
|
5305
|
+
protocol: "anthropic",
|
|
5306
|
+
url: `${this.baseURL}/messages`,
|
|
5307
|
+
headers: {
|
|
5308
|
+
// 官方 API 以 x-api-key 为标准鉴权头,部分网关只识别 Bearer;两者同时发送以覆盖两类端点。
|
|
5309
|
+
"x-api-key": this.apiKey,
|
|
5310
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
5311
|
+
"anthropic-version": "2023-06-01",
|
|
5312
|
+
...this.workspaceId ? { "anthropic-workspace-id": this.workspaceId } : {}
|
|
5313
|
+
},
|
|
5314
|
+
payload,
|
|
5315
|
+
signal: req.signal,
|
|
5316
|
+
connectTimeoutMs: this.connectTimeoutMs
|
|
5317
|
+
});
|
|
5318
|
+
const blocks = [];
|
|
5319
|
+
const toolIds = /* @__PURE__ */ new Set();
|
|
5320
|
+
let activeIndex;
|
|
5321
|
+
let messageId = "";
|
|
5322
|
+
let messageStarted = false;
|
|
5323
|
+
let messageDeltaStarted = false;
|
|
5324
|
+
let messageStopped = false;
|
|
5325
|
+
let finishReason;
|
|
5326
|
+
let refusal = false;
|
|
5327
|
+
let ttftMs;
|
|
5328
|
+
const counters = {};
|
|
5329
|
+
const updateUsage = (value) => {
|
|
5330
|
+
if (value === void 0) return;
|
|
5331
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalidResponse2();
|
|
5332
|
+
const usage = value;
|
|
5333
|
+
for (const key of [
|
|
5334
|
+
"input_tokens",
|
|
5335
|
+
"output_tokens",
|
|
5336
|
+
"cache_read_input_tokens",
|
|
5337
|
+
"cache_creation_input_tokens"
|
|
5338
|
+
]) {
|
|
5339
|
+
if (usage[key] === void 0) continue;
|
|
5340
|
+
if (!Number.isSafeInteger(usage[key]) || usage[key] < 0 || counters[key] !== void 0 && usage[key] < counters[key])
|
|
5341
|
+
invalidResponse2();
|
|
5342
|
+
counters[key] = usage[key];
|
|
4548
5343
|
}
|
|
4549
|
-
})) : void 0;
|
|
4550
|
-
const payload = {
|
|
4551
|
-
model: this.modelName,
|
|
4552
|
-
messages: wireMessages,
|
|
4553
|
-
stream: true,
|
|
4554
|
-
stream_options: { include_usage: true },
|
|
4555
|
-
temperature: req.temperature ?? 0.7
|
|
4556
5344
|
};
|
|
4557
|
-
if (wireTools) {
|
|
4558
|
-
payload.tools = wireTools;
|
|
4559
|
-
}
|
|
4560
|
-
if (req.maxTokens) {
|
|
4561
|
-
payload.max_tokens = req.maxTokens;
|
|
4562
|
-
}
|
|
4563
|
-
let response;
|
|
4564
|
-
const timeoutController = new AbortController();
|
|
4565
|
-
const timeoutTimer = setTimeout(() => timeoutController.abort(), this.connectTimeoutMs);
|
|
4566
|
-
const onOuterAbort = () => timeoutController.abort();
|
|
4567
|
-
if (req.signal?.aborted) {
|
|
4568
|
-
timeoutController.abort();
|
|
4569
|
-
} else {
|
|
4570
|
-
req.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
4571
|
-
}
|
|
4572
5345
|
try {
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
5346
|
+
for await (const frame of parseSSEFrames(opened.response.body)) {
|
|
5347
|
+
if (req.signal?.aborted) throw new AbortError();
|
|
5348
|
+
const value = parseWireEvent(frame.data);
|
|
5349
|
+
if (typeof value.type !== "string" || !value.type) invalidResponse2();
|
|
5350
|
+
if (frame.event !== "message" && frame.event !== value.type) invalidResponse2();
|
|
5351
|
+
if (value.type === "ping") continue;
|
|
5352
|
+
if (value.type === "error") {
|
|
5353
|
+
const detail = value.error;
|
|
5354
|
+
const options = {
|
|
5355
|
+
stage: "stream",
|
|
5356
|
+
providerCode: typeof detail?.code === "string" ? detail.code : void 0,
|
|
5357
|
+
providerType: typeof detail?.type === "string" ? detail.type : void 0
|
|
5358
|
+
};
|
|
5359
|
+
if (isContextOverflow(detail, "anthropic"))
|
|
5360
|
+
throw new ContextOverflowError(void 0, options);
|
|
5361
|
+
throw new ModelError("Anthropic stream failed", options);
|
|
4586
5362
|
}
|
|
4587
|
-
if (
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
5363
|
+
if (value.type === "message_start") {
|
|
5364
|
+
if (messageStarted || !value.message || value.message.role !== "assistant" || typeof value.message.id !== "string" || !value.message.id || !Array.isArray(value.message.content) || value.message.content.length)
|
|
5365
|
+
invalidResponse2();
|
|
5366
|
+
messageStarted = true;
|
|
5367
|
+
messageId = value.message.id;
|
|
5368
|
+
updateUsage(value.message.usage);
|
|
5369
|
+
continue;
|
|
4593
5370
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
5371
|
+
if (!messageStarted || messageStopped) invalidResponse2();
|
|
5372
|
+
if (value.type === "content_block_start") {
|
|
5373
|
+
if (messageDeltaStarted || activeIndex !== void 0 || value.index !== blocks.length)
|
|
5374
|
+
invalidResponse2();
|
|
5375
|
+
const block = value.content_block;
|
|
5376
|
+
if (!block || typeof block !== "object") invalidResponse2();
|
|
5377
|
+
let content;
|
|
5378
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
5379
|
+
content = { type: "text", text: block.text };
|
|
5380
|
+
else if (block.type === "thinking" && typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string"))
|
|
5381
|
+
content = {
|
|
5382
|
+
type: "thinking",
|
|
5383
|
+
thinking: block.thinking,
|
|
5384
|
+
...block.signature ? { signature: block.signature } : {},
|
|
5385
|
+
origin
|
|
5386
|
+
};
|
|
5387
|
+
else if (block.type === "redacted_thinking" && typeof block.data === "string" && block.data)
|
|
5388
|
+
content = { type: "redacted_thinking", data: block.data, origin };
|
|
5389
|
+
else if (block.type === "tool_use" && typeof block.id === "string" && block.id && typeof block.name === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(block.name) && block.input && typeof block.input === "object" && !Array.isArray(block.input)) {
|
|
5390
|
+
if (toolIds.has(block.id)) invalidResponse2();
|
|
5391
|
+
toolIds.add(block.id);
|
|
5392
|
+
content = {
|
|
5393
|
+
type: "tool_use",
|
|
5394
|
+
id: block.id,
|
|
5395
|
+
name: block.name,
|
|
5396
|
+
input: structuredClone(block.input)
|
|
5397
|
+
};
|
|
5398
|
+
} else invalidResponse2();
|
|
5399
|
+
const blockId = `${messageId}:${value.index}`;
|
|
5400
|
+
blocks.push({ content, argumentText: "", blockId, stopped: false });
|
|
5401
|
+
activeIndex = value.index;
|
|
5402
|
+
ttftMs ??= Date.now() - startedAt;
|
|
5403
|
+
if (content.type === "thinking") {
|
|
5404
|
+
yield { type: "thinking_block_start", blockId };
|
|
5405
|
+
if (content.thinking)
|
|
5406
|
+
yield { type: "thinking_delta", thinking: content.thinking, blockId };
|
|
5407
|
+
} else if (content.type === "text" && content.text)
|
|
5408
|
+
yield { type: "text_delta", text: content.text };
|
|
5409
|
+
else if (content.type === "tool_use")
|
|
5410
|
+
yield { type: "tool_call_start", id: content.id, name: content.name };
|
|
5411
|
+
continue;
|
|
5412
|
+
}
|
|
5413
|
+
if (value.type === "content_block_delta" || value.type === "content_block_stop") {
|
|
5414
|
+
if (messageDeltaStarted || !Number.isSafeInteger(value.index) || value.index !== activeIndex)
|
|
5415
|
+
invalidResponse2();
|
|
5416
|
+
const entry = blocks[value.index];
|
|
5417
|
+
if (!entry || entry.stopped) invalidResponse2();
|
|
5418
|
+
const content = entry.content;
|
|
5419
|
+
if (value.type === "content_block_stop") {
|
|
5420
|
+
if (content.type === "tool_use") {
|
|
5421
|
+
if (entry.argumentText) content.input = parseToolArguments(entry.argumentText);
|
|
5422
|
+
yield {
|
|
5423
|
+
type: "tool_call_finish",
|
|
5424
|
+
id: content.id,
|
|
5425
|
+
name: content.name,
|
|
5426
|
+
input: structuredClone(content.input)
|
|
5427
|
+
};
|
|
5428
|
+
} else if (content.type === "thinking") {
|
|
5429
|
+
if (!content.signature) invalidResponse2();
|
|
5430
|
+
yield { type: "thinking_block_stop", blockId: entry.blockId };
|
|
5431
|
+
}
|
|
5432
|
+
entry.stopped = true;
|
|
5433
|
+
activeIndex = void 0;
|
|
5434
|
+
continue;
|
|
4610
5435
|
}
|
|
4611
|
-
|
|
4612
|
-
if (
|
|
5436
|
+
const change = value.delta;
|
|
5437
|
+
if (!change || typeof change !== "object") invalidResponse2();
|
|
5438
|
+
if (content.type === "text" && change.type === "text_delta" && typeof change.text === "string") {
|
|
5439
|
+
content.text += change.text;
|
|
5440
|
+
if (change.text) yield { type: "text_delta", text: change.text };
|
|
5441
|
+
} else if (content.type === "thinking" && change.type === "thinking_delta" && typeof change.thinking === "string") {
|
|
5442
|
+
if (content.signature) invalidResponse2();
|
|
5443
|
+
content.thinking += change.thinking;
|
|
5444
|
+
if (change.thinking)
|
|
5445
|
+
yield { type: "thinking_delta", thinking: change.thinking, blockId: entry.blockId };
|
|
5446
|
+
} else if (content.type === "thinking" && change.type === "signature_delta" && typeof change.signature === "string")
|
|
5447
|
+
content.signature = (content.signature ?? "") + change.signature;
|
|
5448
|
+
else if (content.type === "tool_use" && change.type === "input_json_delta" && typeof change.partial_json === "string") {
|
|
5449
|
+
if (Object.keys(content.input).length) invalidResponse2();
|
|
5450
|
+
entry.argumentText += change.partial_json;
|
|
5451
|
+
if (entry.argumentText.length > 16 * 1024 * 1024) invalidResponse2();
|
|
5452
|
+
yield { type: "tool_call_delta", id: content.id, argumentChunk: change.partial_json };
|
|
5453
|
+
} else invalidResponse2();
|
|
5454
|
+
continue;
|
|
4613
5455
|
}
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
5456
|
+
if (value.type === "message_delta") {
|
|
5457
|
+
if (activeIndex !== void 0 || !value.delta || typeof value.delta !== "object")
|
|
5458
|
+
invalidResponse2();
|
|
5459
|
+
messageDeltaStarted = true;
|
|
5460
|
+
updateUsage(value.usage);
|
|
5461
|
+
const reason = value.delta.stop_reason;
|
|
5462
|
+
if (reason === void 0 || reason === null) continue;
|
|
5463
|
+
if (finishReason !== void 0) invalidResponse2();
|
|
5464
|
+
if (reason === "end_turn" || reason === "stop_sequence") finishReason = "stop";
|
|
5465
|
+
else if (reason === "tool_use") finishReason = "tool_calls";
|
|
5466
|
+
else if (reason === "max_tokens" || reason === "model_context_window_exceeded")
|
|
5467
|
+
finishReason = "length";
|
|
5468
|
+
else if (reason === "refusal") {
|
|
5469
|
+
finishReason = "content_filter";
|
|
5470
|
+
refusal = true;
|
|
5471
|
+
} else if (reason === "pause_turn") {
|
|
5472
|
+
throw new ModelError(
|
|
5473
|
+
"Anthropic paused a server-tool turn; continuation is unsupported",
|
|
5474
|
+
{
|
|
5475
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5476
|
+
stage: "stream",
|
|
5477
|
+
retryable: false,
|
|
5478
|
+
providerCode: "pause_turn"
|
|
5479
|
+
}
|
|
5480
|
+
);
|
|
5481
|
+
} else
|
|
5482
|
+
throw new ModelError("Anthropic returned an unsupported stop_reason", {
|
|
5483
|
+
stage: "stream",
|
|
5484
|
+
retryable: false,
|
|
5485
|
+
providerCode: typeof reason === "string" ? reason : void 0
|
|
5486
|
+
});
|
|
5487
|
+
continue;
|
|
5488
|
+
}
|
|
5489
|
+
if (value.type === "message_stop") {
|
|
5490
|
+
if (!messageDeltaStarted || !finishReason || activeIndex !== void 0 || blocks.some((block) => !block.stopped) || finishReason === "tool_calls" && !toolIds.size || finishReason === "stop" && toolIds.size)
|
|
5491
|
+
invalidResponse2();
|
|
5492
|
+
messageStopped = true;
|
|
5493
|
+
break;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
if (req.signal?.aborted) throw new AbortError();
|
|
5497
|
+
if (!messageStopped)
|
|
5498
|
+
throw new ModelError("Anthropic stream ended before message_stop", {
|
|
5499
|
+
code: "MODEL_STREAM_INCOMPLETE",
|
|
5500
|
+
stage: "stream",
|
|
5501
|
+
retryable: false
|
|
5502
|
+
});
|
|
5503
|
+
let usage;
|
|
5504
|
+
if (counters.input_tokens !== void 0 && counters.output_tokens !== void 0) {
|
|
5505
|
+
const promptTokens = counters.input_tokens + (counters.cache_read_input_tokens ?? 0) + (counters.cache_creation_input_tokens ?? 0);
|
|
5506
|
+
usage = {
|
|
5507
|
+
promptTokens,
|
|
5508
|
+
completionTokens: counters.output_tokens,
|
|
5509
|
+
totalTokens: promptTokens + counters.output_tokens,
|
|
5510
|
+
...counters.cache_read_input_tokens !== void 0 ? { cachedPromptTokens: counters.cache_read_input_tokens } : {}
|
|
4620
5511
|
};
|
|
4621
|
-
if ([400, 413, 422].includes(response.status) && isContextOverflow(errorDetails))
|
|
4622
|
-
throw new ContextOverflowError(response.status, { ...options, message: errorMsg });
|
|
4623
|
-
throw new ModelError(errorMsg, options);
|
|
4624
5512
|
}
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
5513
|
+
yield {
|
|
5514
|
+
type: "message_stop",
|
|
5515
|
+
finishReason,
|
|
5516
|
+
finalContent: blocks.map((block) => block.content),
|
|
5517
|
+
...usage ? { usage } : {},
|
|
5518
|
+
...refusal ? { refusal } : {},
|
|
5519
|
+
ttftMs,
|
|
5520
|
+
durationMs: Date.now() - startedAt
|
|
5521
|
+
};
|
|
5522
|
+
} catch (error) {
|
|
5523
|
+
if (req.signal?.aborted) throw new AbortError();
|
|
5524
|
+
if (error instanceof ModelError || error instanceof AbortError) throw error;
|
|
5525
|
+
throw new ModelError("Anthropic response stream interrupted", {
|
|
5526
|
+
code: "MODEL_STREAM_INTERRUPTED",
|
|
5527
|
+
stage: "stream",
|
|
5528
|
+
transportCode: safeTransportCode(error)
|
|
5529
|
+
});
|
|
5530
|
+
} finally {
|
|
5531
|
+
opened.close();
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
/**
|
|
5535
|
+
* 投影 system、普通内容与工具结果;连续结果合并为下一 user 内容并置于文字之前。
|
|
5536
|
+
* 私有块由共享来源规则筛选,原始消息和工具 ID 不被改写;不发送能力标签推测的思考参数。
|
|
5537
|
+
*/
|
|
5538
|
+
createPayload(req) {
|
|
5539
|
+
const maxTokens = req.maxTokens ?? 4096;
|
|
5540
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens <= 0)
|
|
5541
|
+
throw new ModelError("Invalid Anthropic output token budget", {
|
|
5542
|
+
code: "MODEL_INVALID_REQUEST",
|
|
5543
|
+
stage: "request",
|
|
5544
|
+
retryable: false
|
|
5545
|
+
});
|
|
5546
|
+
const messages = [];
|
|
5547
|
+
const system = req.systemPrompt ? [req.systemPrompt] : [];
|
|
5548
|
+
for (const message of projectProtocolHistory(req.messages, this.binding, req.context)) {
|
|
5549
|
+
if (message.role === "system") {
|
|
5550
|
+
for (const block of message.content) if (block.type === "text") system.push(block.text);
|
|
5551
|
+
continue;
|
|
5552
|
+
}
|
|
5553
|
+
const content = [];
|
|
5554
|
+
for (const block of message.content) {
|
|
5555
|
+
if (block.type === "text") content.push({ type: "text", text: block.text });
|
|
5556
|
+
else if (block.type === "tool_use")
|
|
5557
|
+
content.push({
|
|
5558
|
+
type: "tool_use",
|
|
5559
|
+
id: wireToolId(block.id),
|
|
5560
|
+
name: block.name,
|
|
5561
|
+
input: block.input
|
|
5562
|
+
});
|
|
5563
|
+
else if (block.type === "tool_result")
|
|
5564
|
+
content.push({
|
|
5565
|
+
type: "tool_result",
|
|
5566
|
+
tool_use_id: wireToolId(block.toolUseId),
|
|
5567
|
+
content: block.content,
|
|
5568
|
+
...block.isError !== void 0 ? { is_error: block.isError } : {}
|
|
5569
|
+
});
|
|
5570
|
+
else if (block.type === "thinking") {
|
|
5571
|
+
if (!block.signature)
|
|
5572
|
+
throw new ModelError("Invalid Anthropic thinking history", {
|
|
5573
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5574
|
+
stage: "request",
|
|
5575
|
+
retryable: false
|
|
5576
|
+
});
|
|
5577
|
+
content.push({ type: "thinking", thinking: block.thinking, signature: block.signature });
|
|
5578
|
+
} else if (block.type === "redacted_thinking")
|
|
5579
|
+
content.push({ type: "redacted_thinking", data: block.data });
|
|
5580
|
+
}
|
|
5581
|
+
if (!content.length) continue;
|
|
5582
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
5583
|
+
const previous = messages.at(-1);
|
|
5584
|
+
if (role === "user" && previous?.role === "user") {
|
|
5585
|
+
const all = [...previous.content, ...content];
|
|
5586
|
+
previous.content = [
|
|
5587
|
+
...all.filter((block) => block.type === "tool_result"),
|
|
5588
|
+
...all.filter((block) => block.type !== "tool_result")
|
|
5589
|
+
];
|
|
5590
|
+
} else messages.push({ role, content });
|
|
5591
|
+
}
|
|
5592
|
+
return {
|
|
5593
|
+
model: this.modelName,
|
|
5594
|
+
messages,
|
|
5595
|
+
stream: true,
|
|
5596
|
+
max_tokens: maxTokens,
|
|
5597
|
+
...system.length ? { system: system.join("\n\n") } : {},
|
|
5598
|
+
...req.temperature !== void 0 ? { temperature: req.temperature } : {},
|
|
5599
|
+
...req.tools?.length ? {
|
|
5600
|
+
tools: req.tools.map((tool) => ({
|
|
5601
|
+
name: tool.name,
|
|
5602
|
+
description: tool.description,
|
|
5603
|
+
input_schema: tool.parameters
|
|
5604
|
+
}))
|
|
5605
|
+
} : {}
|
|
5606
|
+
};
|
|
5607
|
+
}
|
|
5608
|
+
};
|
|
5609
|
+
|
|
5610
|
+
// src/models/openai-responses/index.ts
|
|
5611
|
+
var import_node_crypto14 = require("crypto");
|
|
5612
|
+
function invalid(message = "Invalid Responses output Item") {
|
|
5613
|
+
throw new ModelError(message, {
|
|
5614
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5615
|
+
stage: "stream",
|
|
5616
|
+
retryable: false
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
function nonempty(value) {
|
|
5620
|
+
return typeof value === "string" && !!value.trim();
|
|
5621
|
+
}
|
|
5622
|
+
function index(value) {
|
|
5623
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
5624
|
+
}
|
|
5625
|
+
function wireCallId(id) {
|
|
5626
|
+
if (typeof id !== "string" || !id)
|
|
5627
|
+
throw new ModelError("Invalid tool history identity", {
|
|
5628
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
5629
|
+
stage: "request",
|
|
5630
|
+
retryable: false
|
|
5631
|
+
});
|
|
5632
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0, import_node_crypto14.createHash)("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
5633
|
+
}
|
|
5634
|
+
function validateItem(item, final) {
|
|
5635
|
+
if (!item || typeof item !== "object" || !nonempty(item.id)) invalid();
|
|
5636
|
+
if (!["message", "function_call", "reasoning"].includes(item.type))
|
|
5637
|
+
invalid("Unsupported Responses output Item");
|
|
5638
|
+
if (final && (item.type !== "reasoning" || item.status !== void 0) && item.status !== "completed")
|
|
5639
|
+
invalid("Responses output Item is not complete");
|
|
5640
|
+
if (item.type === "message") {
|
|
5641
|
+
if (item.role !== "assistant" || !Array.isArray(item.content) || item.phase != null && !["commentary", "final_answer"].includes(item.phase))
|
|
5642
|
+
invalid();
|
|
5643
|
+
for (const part of item.content) {
|
|
5644
|
+
if (!part || !["output_text", "refusal"].includes(part.type) || typeof (part.type === "refusal" ? part.refusal : part.text) !== "string")
|
|
5645
|
+
invalid("Unsupported Responses message content");
|
|
5646
|
+
}
|
|
5647
|
+
} else if (item.type === "function_call") {
|
|
5648
|
+
if (!nonempty(item.call_id) || !nonempty(item.name) || typeof item.arguments !== "string")
|
|
5649
|
+
invalid();
|
|
5650
|
+
if (final) {
|
|
5651
|
+
if (!item.arguments.trim()) invalid("Responses function arguments are missing");
|
|
5652
|
+
parseToolArguments(item.arguments);
|
|
5653
|
+
}
|
|
5654
|
+
} else if (!Array.isArray(item.summary) || item.summary.some(
|
|
5655
|
+
(part) => !part || part.type !== "summary_text" || typeof part.text !== "string"
|
|
5656
|
+
) || item.encrypted_content != null && typeof item.encrypted_content !== "string")
|
|
5657
|
+
invalid();
|
|
5658
|
+
}
|
|
5659
|
+
function itemIdentity(item) {
|
|
5660
|
+
const common = { type: item.type, id: item.id, status: item.status };
|
|
5661
|
+
if (item.type === "message")
|
|
5662
|
+
return JSON.stringify({
|
|
5663
|
+
...common,
|
|
5664
|
+
role: item.role,
|
|
5665
|
+
phase: item.phase ?? void 0,
|
|
5666
|
+
content: item.content.map((part) => ({
|
|
5667
|
+
type: part.type,
|
|
5668
|
+
value: part.type === "refusal" ? part.refusal : part.text
|
|
5669
|
+
}))
|
|
5670
|
+
});
|
|
5671
|
+
if (item.type === "function_call")
|
|
5672
|
+
return JSON.stringify({
|
|
5673
|
+
...common,
|
|
5674
|
+
call_id: item.call_id,
|
|
5675
|
+
name: item.name,
|
|
5676
|
+
arguments: item.arguments
|
|
5677
|
+
});
|
|
5678
|
+
return JSON.stringify({
|
|
5679
|
+
...common,
|
|
5680
|
+
encrypted_content: item.encrypted_content ?? void 0,
|
|
5681
|
+
summary: item.summary.map((part) => ({ type: part.type, text: part.text }))
|
|
5682
|
+
});
|
|
5683
|
+
}
|
|
5684
|
+
function toCanonical(output, origin) {
|
|
5685
|
+
const content = [];
|
|
5686
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
5687
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
5688
|
+
let refusal = false;
|
|
5689
|
+
let finalAnswer = false;
|
|
5690
|
+
for (const [itemIndex, item] of output.entries()) {
|
|
5691
|
+
validateItem(item, true);
|
|
5692
|
+
if (itemIds.has(item.id)) invalid("Duplicate Responses Item identity");
|
|
5693
|
+
itemIds.add(item.id);
|
|
5694
|
+
if (item.type === "function_call") {
|
|
5695
|
+
if (callIds.has(item.call_id)) invalid("Duplicate Responses function call identity");
|
|
5696
|
+
callIds.add(item.call_id);
|
|
5697
|
+
content.push({
|
|
5698
|
+
type: "tool_use",
|
|
5699
|
+
id: item.call_id,
|
|
5700
|
+
name: item.name,
|
|
5701
|
+
input: parseToolArguments(item.arguments),
|
|
5702
|
+
protocolMeta: { origin, itemIndex, itemId: item.id, arguments: item.arguments }
|
|
5703
|
+
});
|
|
5704
|
+
} else if (item.type === "reasoning") {
|
|
5705
|
+
content.push({
|
|
5706
|
+
type: "provider_state",
|
|
5707
|
+
origin,
|
|
5708
|
+
item: {
|
|
5709
|
+
type: "reasoning",
|
|
5710
|
+
id: item.id,
|
|
5711
|
+
summary: item.summary.map((part) => ({
|
|
5712
|
+
type: "summary_text",
|
|
5713
|
+
text: part.text
|
|
5714
|
+
})),
|
|
5715
|
+
...item.encrypted_content != null ? { encrypted_content: item.encrypted_content } : {},
|
|
5716
|
+
...item.status !== void 0 ? { status: item.status } : {}
|
|
5717
|
+
}
|
|
5718
|
+
});
|
|
5719
|
+
} else {
|
|
5720
|
+
if (!item.content.length)
|
|
5721
|
+
content.push({
|
|
5722
|
+
type: "text",
|
|
5723
|
+
text: "",
|
|
5724
|
+
protocolMeta: {
|
|
5725
|
+
origin,
|
|
5726
|
+
itemIndex,
|
|
5727
|
+
itemId: item.id,
|
|
5728
|
+
...item.phase ? { phase: item.phase } : {}
|
|
5729
|
+
}
|
|
5730
|
+
});
|
|
5731
|
+
for (const [contentIndex, part] of item.content.entries()) {
|
|
5732
|
+
const text = part.type === "refusal" ? part.refusal : part.text;
|
|
5733
|
+
refusal ||= part.type === "refusal";
|
|
5734
|
+
finalAnswer ||= !!text.trim() && item.phase !== "commentary";
|
|
5735
|
+
content.push({
|
|
5736
|
+
type: "text",
|
|
5737
|
+
text,
|
|
5738
|
+
protocolMeta: {
|
|
5739
|
+
origin,
|
|
5740
|
+
itemIndex,
|
|
5741
|
+
itemId: item.id,
|
|
5742
|
+
contentIndex,
|
|
5743
|
+
contentType: part.type,
|
|
5744
|
+
...item.phase ? { phase: item.phase } : {}
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
if (refusal && callIds.size) invalid("Responses refusal cannot contain executable functions");
|
|
5751
|
+
if (!callIds.size && !finalAnswer && !(refusal && content.some((block) => block.type === "text" && !!block.text.trim())))
|
|
5752
|
+
invalid("Responses completed without a final answer or function call");
|
|
5753
|
+
validateProtocolContent(content);
|
|
5754
|
+
return { content, refusal, toolCalls: callIds.size > 0 };
|
|
5755
|
+
}
|
|
5756
|
+
function toInput(messages) {
|
|
5757
|
+
const input = [];
|
|
5758
|
+
for (const message of messages) {
|
|
5759
|
+
let group;
|
|
5760
|
+
let groupKey;
|
|
5761
|
+
const canReplayState = message.role === "assistant" && message.content.some((block) => block.type === "tool_use");
|
|
5762
|
+
for (const block of message.content) {
|
|
5763
|
+
if (block.type === "provider_state") {
|
|
5764
|
+
group = void 0;
|
|
5765
|
+
if (canReplayState) input.push(structuredClone(block.item));
|
|
5766
|
+
} else if (block.type === "tool_use") {
|
|
5767
|
+
group = void 0;
|
|
5768
|
+
const argumentsText = block.protocolMeta?.arguments ?? JSON.stringify(block.input);
|
|
5769
|
+
const parsed = parseToolArguments(argumentsText);
|
|
5770
|
+
if (JSON.stringify(parsed) !== JSON.stringify(block.input))
|
|
5771
|
+
invalid("Responses historical function arguments do not match canonical input");
|
|
5772
|
+
input.push({
|
|
5773
|
+
type: "function_call",
|
|
5774
|
+
...block.protocolMeta?.itemId ? { id: block.protocolMeta.itemId } : {},
|
|
5775
|
+
call_id: wireCallId(block.id),
|
|
5776
|
+
name: block.name,
|
|
5777
|
+
arguments: argumentsText,
|
|
5778
|
+
...block.protocolMeta ? { status: "completed" } : {}
|
|
4629
5779
|
});
|
|
5780
|
+
} else if (block.type === "tool_result") {
|
|
5781
|
+
group = void 0;
|
|
5782
|
+
input.push({
|
|
5783
|
+
type: "function_call_output",
|
|
5784
|
+
call_id: wireCallId(block.toolUseId),
|
|
5785
|
+
output: block.content
|
|
5786
|
+
});
|
|
5787
|
+
} else if (block.type === "text" || block.type === "thinking") {
|
|
5788
|
+
const text = block.type === "text" ? block.text : block.thinking;
|
|
5789
|
+
const meta = block.type === "text" ? block.protocolMeta : void 0;
|
|
5790
|
+
if (!text && !meta) continue;
|
|
5791
|
+
const key = meta ? `${meta.origin.runId}:${meta.itemIndex}:${meta.itemId ?? ""}` : "ordinary";
|
|
5792
|
+
if (!group || key !== groupKey) {
|
|
5793
|
+
const sourced = message.role === "assistant" && !!meta;
|
|
5794
|
+
group = {
|
|
5795
|
+
...sourced ? {
|
|
5796
|
+
type: "message",
|
|
5797
|
+
...meta.itemId ? { id: meta.itemId } : {},
|
|
5798
|
+
status: "completed",
|
|
5799
|
+
...meta.phase ? { phase: meta.phase } : {}
|
|
5800
|
+
} : {},
|
|
5801
|
+
role: message.role,
|
|
5802
|
+
content: []
|
|
5803
|
+
};
|
|
5804
|
+
groupKey = key;
|
|
5805
|
+
input.push(group);
|
|
5806
|
+
}
|
|
5807
|
+
if (!text && meta && meta.contentIndex === void 0) continue;
|
|
5808
|
+
const type = message.role === "assistant" ? meta?.contentType ?? "output_text" : "input_text";
|
|
5809
|
+
group.content.push(
|
|
5810
|
+
type === "refusal" ? { type, refusal: text } : { type, text, ...type === "output_text" ? { annotations: [] } : {} }
|
|
5811
|
+
);
|
|
4630
5812
|
}
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
return input;
|
|
5816
|
+
}
|
|
5817
|
+
var OpenAIResponsesProvider = class {
|
|
5818
|
+
name = "openai-responses";
|
|
5819
|
+
binding;
|
|
5820
|
+
options;
|
|
5821
|
+
/** 表明该适配器已装载非空凭据,不暴露凭据内容。 */
|
|
5822
|
+
get credentialsReady() {
|
|
5823
|
+
return Boolean(this.options.apiKey.trim());
|
|
5824
|
+
}
|
|
5825
|
+
/** 绑定明确模型与端点;不从模型名推断协议,也不在失败后自动切换协议。非法配置在请求前失败。 */
|
|
5826
|
+
constructor(options) {
|
|
5827
|
+
try {
|
|
5828
|
+
const url = new URL(options.baseURL);
|
|
5829
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
5830
|
+
throw new Error();
|
|
5831
|
+
if (!options.modelName?.trim() || !options.apiKey?.trim() || /[\r\n]/.test(options.apiKey) || options.connectTimeoutMs !== void 0 && (!Number.isSafeInteger(options.connectTimeoutMs) || options.connectTimeoutMs <= 0))
|
|
5832
|
+
throw new Error();
|
|
5833
|
+
} catch {
|
|
5834
|
+
throw new ModelError("Invalid Responses connection configuration", {
|
|
5835
|
+
code: "MODEL_INVALID_REQUEST",
|
|
5836
|
+
stage: "request",
|
|
5837
|
+
retryable: false
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
this.options = { ...options, baseURL: options.baseURL.replace(/\/+$/, "") };
|
|
5841
|
+
this.binding = Object.freeze({
|
|
5842
|
+
protocol: "openai-responses",
|
|
5843
|
+
modelName: options.modelName,
|
|
5844
|
+
baseURL: this.options.baseURL
|
|
5845
|
+
});
|
|
5846
|
+
}
|
|
5847
|
+
/** 消费完整 Responses 流,增量仅用于展示;终态与全部 Item 合法后才交付工具完成和最终内容。 */
|
|
5848
|
+
async *create(request) {
|
|
5849
|
+
const started = Date.now();
|
|
5850
|
+
let firstToken;
|
|
5851
|
+
const origin = createProtocolOrigin(this.binding, request.context);
|
|
5852
|
+
const projected = projectProtocolHistory(request.messages, this.binding, request.context);
|
|
5853
|
+
const input = toInput(normalizeHistory(projected).messages);
|
|
5854
|
+
const payload = {
|
|
5855
|
+
model: this.options.modelName,
|
|
5856
|
+
stream: true,
|
|
5857
|
+
store: false,
|
|
5858
|
+
input,
|
|
5859
|
+
include: ["reasoning.encrypted_content"]
|
|
5860
|
+
};
|
|
5861
|
+
if (request.systemPrompt) payload.instructions = request.systemPrompt;
|
|
5862
|
+
if (request.maxTokens !== void 0) {
|
|
5863
|
+
if (!Number.isSafeInteger(request.maxTokens) || request.maxTokens <= 0)
|
|
5864
|
+
throw new ModelError("Invalid Responses output budget", {
|
|
5865
|
+
stage: "request",
|
|
5866
|
+
retryable: false
|
|
5867
|
+
});
|
|
5868
|
+
payload.max_output_tokens = request.maxTokens;
|
|
5869
|
+
}
|
|
5870
|
+
if (request.temperature !== void 0 && !this.options.supportsThinking)
|
|
5871
|
+
payload.temperature = request.temperature;
|
|
5872
|
+
if (request.tools?.length)
|
|
5873
|
+
payload.tools = request.tools.map((tool) => ({
|
|
5874
|
+
type: "function",
|
|
5875
|
+
name: tool.name,
|
|
5876
|
+
description: tool.description,
|
|
5877
|
+
parameters: structuredClone(tool.parameters),
|
|
5878
|
+
strict: false
|
|
5879
|
+
}));
|
|
5880
|
+
const { response, close } = await openModelResponse({
|
|
5881
|
+
url: `${this.options.baseURL}/responses`,
|
|
5882
|
+
headers: { Authorization: `Bearer ${this.options.apiKey}` },
|
|
5883
|
+
payload,
|
|
5884
|
+
signal: request.signal,
|
|
5885
|
+
connectTimeoutMs: this.options.connectTimeoutMs
|
|
5886
|
+
});
|
|
5887
|
+
const items = /* @__PURE__ */ new Map();
|
|
5888
|
+
const summariesOpen = /* @__PURE__ */ new Set();
|
|
5889
|
+
let terminal;
|
|
5890
|
+
const target = (event) => {
|
|
5891
|
+
if (!index(event.output_index)) invalid();
|
|
5892
|
+
const item = items.get(event.output_index);
|
|
5893
|
+
if (!item || item.done || event.item_id !== item.item.id)
|
|
5894
|
+
invalid("Responses event Item identity or lifecycle mismatch");
|
|
5895
|
+
return item;
|
|
5896
|
+
};
|
|
5897
|
+
try {
|
|
5898
|
+
for await (const frame of parseSSEFrames(response.body)) {
|
|
5899
|
+
if (frame.event === "ping" && !frame.data.trim()) continue;
|
|
5900
|
+
const event = parseWireEvent(frame.data);
|
|
5901
|
+
if (typeof event.type !== "string" || frame.event !== "message" && frame.event !== event.type)
|
|
5902
|
+
invalid("Responses event type mismatch");
|
|
5903
|
+
if (event.type === "error" || event.error) this.throwWireError(event.error ?? event);
|
|
5904
|
+
if (event.type === "response.failed") this.throwWireError(event.response?.error ?? {});
|
|
5905
|
+
if (event.type === "response.incomplete")
|
|
5906
|
+
throw new ModelError("Responses generation was incomplete", {
|
|
5907
|
+
code: "MODEL_STREAM_INCOMPLETE",
|
|
4648
5908
|
stage: "stream",
|
|
4649
|
-
retryable: false
|
|
5909
|
+
retryable: false,
|
|
5910
|
+
providerCode: event.response?.incomplete_details?.reason
|
|
4650
5911
|
});
|
|
5912
|
+
if (event.type === "response.completed") {
|
|
5913
|
+
if (event.response?.status !== "completed" || !Array.isArray(event.response.output))
|
|
5914
|
+
invalid("Invalid Responses terminal event");
|
|
5915
|
+
terminal = event.response;
|
|
5916
|
+
break;
|
|
4651
5917
|
}
|
|
4652
|
-
if (
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
5918
|
+
if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
|
|
5919
|
+
if (!index(event.output_index)) invalid();
|
|
5920
|
+
validateItem(event.item, event.type.endsWith(".done"));
|
|
5921
|
+
const existing = items.get(event.output_index);
|
|
5922
|
+
if (event.type.endsWith(".added")) {
|
|
5923
|
+
if (existing || [...items.values()].some((item) => item.item.id === event.item.id))
|
|
5924
|
+
invalid("Duplicate Responses streaming Item");
|
|
5925
|
+
items.set(event.output_index, {
|
|
5926
|
+
item: event.item,
|
|
5927
|
+
arguments: "",
|
|
5928
|
+
text: /* @__PURE__ */ new Map(),
|
|
5929
|
+
summaries: /* @__PURE__ */ new Map()
|
|
5930
|
+
});
|
|
5931
|
+
if (event.item.type === "function_call")
|
|
5932
|
+
yield { type: "tool_call_start", id: event.item.call_id, name: event.item.name };
|
|
5933
|
+
} else {
|
|
5934
|
+
if (existing?.done || existing && (existing.item.id !== event.item.id || existing.item.type !== event.item.type || event.item.type === "function_call" && (existing.item.call_id !== event.item.call_id || existing.item.name !== event.item.name)))
|
|
5935
|
+
invalid("Responses completed Item identity mismatch");
|
|
5936
|
+
const item = existing ?? {
|
|
5937
|
+
item: event.item,
|
|
5938
|
+
arguments: "",
|
|
5939
|
+
text: /* @__PURE__ */ new Map(),
|
|
5940
|
+
summaries: /* @__PURE__ */ new Map()
|
|
5941
|
+
};
|
|
5942
|
+
item.done = event.item;
|
|
5943
|
+
items.set(event.output_index, item);
|
|
5944
|
+
}
|
|
5945
|
+
continue;
|
|
4669
5946
|
}
|
|
4670
|
-
if (
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
};
|
|
4676
|
-
|
|
4677
|
-
|
|
5947
|
+
if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
|
|
5948
|
+
const item = target(event);
|
|
5949
|
+
if (item.item.type !== "message" || !index(event.content_index) || typeof event.delta !== "string" || item.textDone?.has(event.content_index))
|
|
5950
|
+
invalid();
|
|
5951
|
+
const type = event.type === "response.refusal.delta" ? "refusal" : "output_text";
|
|
5952
|
+
const part = item.text.get(event.content_index) ?? { type, value: "" };
|
|
5953
|
+
if (part.type !== type) invalid();
|
|
5954
|
+
part.value += event.delta;
|
|
5955
|
+
item.text.set(event.content_index, part);
|
|
5956
|
+
firstToken ??= Date.now();
|
|
5957
|
+
yield { type: "text_delta", text: event.delta };
|
|
5958
|
+
continue;
|
|
4678
5959
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5960
|
+
if (event.type === "response.output_text.done" || event.type === "response.refusal.done") {
|
|
5961
|
+
const item = target(event);
|
|
5962
|
+
const type = event.type === "response.refusal.done" ? "refusal" : "output_text";
|
|
5963
|
+
const value = type === "refusal" ? event.refusal : event.text;
|
|
5964
|
+
if (item.item.type !== "message" || !index(event.content_index) || typeof value !== "string" || item.textDone?.has(event.content_index))
|
|
5965
|
+
invalid();
|
|
5966
|
+
const part = item.text.get(event.content_index);
|
|
5967
|
+
if (part && (part.type !== type || part.value !== value))
|
|
5968
|
+
invalid("Responses completed text does not match its deltas");
|
|
5969
|
+
item.text.set(event.content_index, { type, value });
|
|
5970
|
+
item.textDone ??= /* @__PURE__ */ new Set();
|
|
5971
|
+
item.textDone.add(event.content_index);
|
|
5972
|
+
if (!part && value) yield { type: "text_delta", text: value };
|
|
5973
|
+
continue;
|
|
4686
5974
|
}
|
|
4687
|
-
if (
|
|
4688
|
-
|
|
5975
|
+
if (event.type === "response.content_part.added" || event.type === "response.content_part.done") {
|
|
5976
|
+
const item = target(event);
|
|
5977
|
+
const part = event.part;
|
|
5978
|
+
if (item.item.type !== "message" || !index(event.content_index) || !part || !["output_text", "refusal"].includes(part.type) || typeof (part.type === "refusal" ? part.refusal : part.text) !== "string")
|
|
5979
|
+
invalid("Unsupported Responses message content");
|
|
5980
|
+
if (event.type.endsWith(".done")) {
|
|
5981
|
+
const streamed = item.text.get(event.content_index);
|
|
5982
|
+
if (streamed && (streamed.type !== part.type || streamed.value !== (part.type === "refusal" ? part.refusal : part.text)))
|
|
5983
|
+
invalid("Responses content part does not match streamed text");
|
|
5984
|
+
}
|
|
5985
|
+
continue;
|
|
4689
5986
|
}
|
|
4690
|
-
if (delta.
|
|
4691
|
-
|
|
5987
|
+
if (event.type === "response.function_call_arguments.delta" || event.type === "response.function_call_arguments.done") {
|
|
5988
|
+
const item = target(event);
|
|
5989
|
+
if (item.item.type !== "function_call" || item.argumentDone !== void 0) invalid();
|
|
5990
|
+
if (event.type.endsWith(".delta")) {
|
|
5991
|
+
if (typeof event.delta !== "string") invalid();
|
|
5992
|
+
item.arguments += event.delta;
|
|
5993
|
+
yield { type: "tool_call_delta", id: item.item.call_id, argumentChunk: event.delta };
|
|
5994
|
+
} else {
|
|
5995
|
+
if (typeof event.arguments !== "string" || item.arguments && item.arguments !== event.arguments)
|
|
5996
|
+
invalid("Responses function arguments changed after streaming");
|
|
5997
|
+
item.argumentDone = event.arguments;
|
|
5998
|
+
}
|
|
5999
|
+
continue;
|
|
4692
6000
|
}
|
|
4693
|
-
if (
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
};
|
|
4703
|
-
pendingToolCalls.set(index, entry);
|
|
4704
|
-
yield { type: "tool_call_start", id: entry.id, name: entry.name };
|
|
4705
|
-
}
|
|
4706
|
-
if (tc.function?.name && !entry.name) {
|
|
4707
|
-
entry.name = tc.function.name;
|
|
4708
|
-
}
|
|
4709
|
-
if (tc.function?.arguments) {
|
|
4710
|
-
entry.argumentChunks.push(tc.function.arguments);
|
|
4711
|
-
yield {
|
|
4712
|
-
type: "tool_call_delta",
|
|
4713
|
-
id: entry.id,
|
|
4714
|
-
argumentChunk: tc.function.arguments
|
|
4715
|
-
};
|
|
6001
|
+
if (event.type === "response.reasoning_summary_text.delta" || event.type === "response.reasoning_summary_text.done") {
|
|
6002
|
+
const item = target(event);
|
|
6003
|
+
if (item.item.type !== "reasoning" || !index(event.summary_index)) invalid();
|
|
6004
|
+
const blockId = `${item.item.id}:${event.summary_index}`;
|
|
6005
|
+
const old = item.summaries.get(event.summary_index) ?? "";
|
|
6006
|
+
if (event.type.endsWith(".delta")) {
|
|
6007
|
+
if (typeof event.delta !== "string") invalid();
|
|
6008
|
+
if (!summariesOpen.has(blockId)) {
|
|
6009
|
+
summariesOpen.add(blockId);
|
|
6010
|
+
yield { type: "thinking_block_start", blockId };
|
|
4716
6011
|
}
|
|
6012
|
+
item.summaries.set(event.summary_index, old + event.delta);
|
|
6013
|
+
firstToken ??= Date.now();
|
|
6014
|
+
yield { type: "thinking_delta", blockId, thinking: event.delta };
|
|
6015
|
+
} else {
|
|
6016
|
+
if (typeof event.text !== "string" || old && old !== event.text) invalid();
|
|
6017
|
+
item.summaries.set(event.summary_index, event.text);
|
|
6018
|
+
if (summariesOpen.delete(blockId)) yield { type: "thinking_block_stop", blockId };
|
|
4717
6019
|
}
|
|
6020
|
+
continue;
|
|
4718
6021
|
}
|
|
6022
|
+
if ([
|
|
6023
|
+
"response.created",
|
|
6024
|
+
"response.in_progress",
|
|
6025
|
+
"response.queued",
|
|
6026
|
+
"response.output_text.annotation.added",
|
|
6027
|
+
"response.reasoning_summary_part.added",
|
|
6028
|
+
"response.reasoning_summary_part.done"
|
|
6029
|
+
].includes(event.type))
|
|
6030
|
+
continue;
|
|
6031
|
+
invalid("Unsupported Responses stream event");
|
|
4719
6032
|
}
|
|
4720
|
-
if (!
|
|
4721
|
-
throw new ModelError("
|
|
4722
|
-
retryable: false,
|
|
6033
|
+
if (!terminal)
|
|
6034
|
+
throw new ModelError("Responses stream ended without completion", {
|
|
4723
6035
|
code: "MODEL_STREAM_INCOMPLETE",
|
|
4724
|
-
stage: "stream"
|
|
6036
|
+
stage: "stream",
|
|
6037
|
+
retryable: false
|
|
4725
6038
|
});
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
6039
|
+
const final = toCanonical(terminal.output, origin);
|
|
6040
|
+
const usage = this.toUsage(terminal.usage);
|
|
6041
|
+
for (const [outputIndex, item] of items) {
|
|
6042
|
+
const ended = terminal.output[outputIndex];
|
|
6043
|
+
if (!item.done || !ended || itemIdentity(item.done) !== itemIdentity(ended))
|
|
6044
|
+
invalid("Responses final Items do not match completed stream Items");
|
|
6045
|
+
if (ended.type === "function_call" && (item.arguments && item.arguments !== ended.arguments || item.argumentDone !== void 0 && item.argumentDone !== ended.arguments))
|
|
6046
|
+
invalid("Responses final function arguments do not match stream");
|
|
6047
|
+
for (const [contentIndex, part] of item.text) {
|
|
6048
|
+
const endedPart = ended.content?.[contentIndex];
|
|
6049
|
+
if (endedPart?.type !== part.type || (part.type === "refusal" ? endedPart.refusal : endedPart.text) !== part.value)
|
|
6050
|
+
invalid("Responses final text does not match stream");
|
|
4735
6051
|
}
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
name: tc.name,
|
|
4740
|
-
input: parsedInput,
|
|
4741
|
-
parseError
|
|
4742
|
-
};
|
|
6052
|
+
for (const [summaryIndex, summary] of item.summaries)
|
|
6053
|
+
if (ended.summary?.[summaryIndex]?.text !== summary)
|
|
6054
|
+
invalid("Responses final summary does not match stream");
|
|
4743
6055
|
}
|
|
4744
|
-
const
|
|
4745
|
-
|
|
6056
|
+
for (const blockId of summariesOpen) yield { type: "thinking_block_stop", blockId };
|
|
6057
|
+
for (const [outputIndex, item] of terminal.output.entries()) {
|
|
6058
|
+
if (item.type === "message")
|
|
6059
|
+
for (const [contentIndex, part] of item.content.entries()) {
|
|
6060
|
+
if (!items.get(outputIndex)?.text.has(contentIndex))
|
|
6061
|
+
yield {
|
|
6062
|
+
type: "text_delta",
|
|
6063
|
+
text: part.type === "refusal" ? part.refusal : part.text
|
|
6064
|
+
};
|
|
6065
|
+
}
|
|
6066
|
+
else if (item.type === "reasoning")
|
|
6067
|
+
for (const [summaryIndex, part] of item.summary.entries()) {
|
|
6068
|
+
if (!items.get(outputIndex)?.summaries.has(summaryIndex) && part.text) {
|
|
6069
|
+
const blockId = `${item.id}:${summaryIndex}`;
|
|
6070
|
+
yield { type: "thinking_block_start", blockId };
|
|
6071
|
+
yield { type: "thinking_delta", blockId, thinking: part.text };
|
|
6072
|
+
yield { type: "thinking_block_stop", blockId };
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
else if (item.type === "function_call")
|
|
6076
|
+
yield {
|
|
6077
|
+
type: "tool_call_finish",
|
|
6078
|
+
id: item.call_id,
|
|
6079
|
+
name: item.name,
|
|
6080
|
+
input: parseToolArguments(item.arguments)
|
|
6081
|
+
};
|
|
6082
|
+
}
|
|
6083
|
+
yield {
|
|
6084
|
+
type: "message_stop",
|
|
6085
|
+
finishReason: final.toolCalls ? "tool_calls" : "stop",
|
|
6086
|
+
finalContent: final.content,
|
|
6087
|
+
...final.refusal ? { refusal: true } : {},
|
|
6088
|
+
...usage ? { usage } : {},
|
|
6089
|
+
...firstToken !== void 0 ? { ttftMs: firstToken - started } : {},
|
|
6090
|
+
durationMs: Date.now() - started
|
|
6091
|
+
};
|
|
4746
6092
|
} catch (error) {
|
|
4747
|
-
if (error instanceof ModelError ||
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
transportCode,
|
|
4755
|
-
stage: "stream"
|
|
4756
|
-
}
|
|
4757
|
-
);
|
|
6093
|
+
if (error instanceof ModelError || request.signal?.aborted) throw error;
|
|
6094
|
+
throw new ModelError("Responses stream was interrupted", {
|
|
6095
|
+
code: "MODEL_STREAM_INTERRUPTED",
|
|
6096
|
+
stage: "stream",
|
|
6097
|
+
retryable: false,
|
|
6098
|
+
transportCode: safeTransportCode(error)
|
|
6099
|
+
});
|
|
4758
6100
|
} finally {
|
|
4759
|
-
|
|
6101
|
+
close();
|
|
4760
6102
|
}
|
|
4761
6103
|
}
|
|
6104
|
+
/** 保持消息级 canonical tool 落盘,Responses function_call_output 仅存在于请求投影。 */
|
|
4762
6105
|
assembleToolResults(results) {
|
|
4763
|
-
return results
|
|
4764
|
-
role: "tool",
|
|
4765
|
-
content: [r],
|
|
4766
|
-
timestamp: Date.now()
|
|
4767
|
-
}));
|
|
6106
|
+
return assembleCanonicalToolResults(results);
|
|
4768
6107
|
}
|
|
4769
|
-
/**
|
|
4770
|
-
|
|
4771
|
-
const
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
type: "function",
|
|
4795
|
-
function: {
|
|
4796
|
-
name: block.name,
|
|
4797
|
-
arguments: JSON.stringify(block.input)
|
|
4798
|
-
}
|
|
4799
|
-
});
|
|
4800
|
-
}
|
|
4801
|
-
}
|
|
4802
|
-
if (!text && toolCalls.length === 0) continue;
|
|
4803
|
-
wire.push({
|
|
4804
|
-
role: "assistant",
|
|
4805
|
-
content: text || (this.replayReasoningContent ? "" : null),
|
|
4806
|
-
...this.replayReasoningContent ? {
|
|
4807
|
-
reasoning_content: msg.content.filter((block) => block.type === "thinking").map((block) => block.thinking).join("")
|
|
4808
|
-
} : {},
|
|
4809
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : void 0
|
|
4810
|
-
});
|
|
4811
|
-
} else if (msg.role === "tool") {
|
|
4812
|
-
for (const block of msg.content) {
|
|
4813
|
-
if (block.type === "tool_result") {
|
|
4814
|
-
wire.push({
|
|
4815
|
-
role: "tool",
|
|
4816
|
-
tool_call_id: block.toolUseId,
|
|
4817
|
-
content: block.content
|
|
4818
|
-
});
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
}
|
|
4822
|
-
}
|
|
4823
|
-
return wire;
|
|
6108
|
+
/** 服务端错误仅保留明确标识,不回显请求、输出正文、签名或加密 reasoning。 */
|
|
6109
|
+
throwWireError(error) {
|
|
6110
|
+
const detail = {
|
|
6111
|
+
stage: "stream",
|
|
6112
|
+
retryable: false,
|
|
6113
|
+
providerCode: typeof error.code === "string" ? error.code : void 0,
|
|
6114
|
+
providerType: typeof error.type === "string" && error.type !== "error" ? error.type : void 0
|
|
6115
|
+
};
|
|
6116
|
+
if (isContextOverflow(error)) throw new ContextOverflowError(400, detail);
|
|
6117
|
+
throw new ModelError("Responses API reported an error", detail);
|
|
6118
|
+
}
|
|
6119
|
+
/** 只接受真实且合法的计数;缺失 usage 保持未知,推理 token 已包含在 output_tokens 中。 */
|
|
6120
|
+
toUsage(value) {
|
|
6121
|
+
if (value === void 0 || value === null) return void 0;
|
|
6122
|
+
if (![value.input_tokens, value.output_tokens, value.total_tokens].every(index))
|
|
6123
|
+
invalid("Invalid Responses usage");
|
|
6124
|
+
const cached = value.input_tokens_details?.cached_tokens;
|
|
6125
|
+
if (cached !== void 0 && (!index(cached) || cached > value.input_tokens))
|
|
6126
|
+
invalid("Invalid Responses cached usage");
|
|
6127
|
+
return {
|
|
6128
|
+
promptTokens: value.input_tokens,
|
|
6129
|
+
completionTokens: value.output_tokens,
|
|
6130
|
+
totalTokens: value.total_tokens,
|
|
6131
|
+
...cached !== void 0 ? { cachedPromptTokens: cached } : {}
|
|
6132
|
+
};
|
|
4824
6133
|
}
|
|
4825
6134
|
};
|
|
4826
6135
|
|
|
@@ -4836,7 +6145,9 @@ var JSONLMessageStore = class {
|
|
|
4836
6145
|
import_node_fs11.default.mkdirSync(dir, { recursive: true });
|
|
4837
6146
|
}
|
|
4838
6147
|
}
|
|
6148
|
+
/** 校验协议内容后追加一行 JSON;不补身份、不查重、不 fsync。 */
|
|
4839
6149
|
async append(message) {
|
|
6150
|
+
validateProtocolContent(message.content);
|
|
4840
6151
|
const record = {
|
|
4841
6152
|
ts: message.timestamp ?? Date.now(),
|
|
4842
6153
|
role: message.role,
|
|
@@ -4848,6 +6159,7 @@ var JSONLMessageStore = class {
|
|
|
4848
6159
|
import_node_fs11.default.appendFileSync(this.filePath, `${JSON.stringify(record)}
|
|
4849
6160
|
`, "utf-8");
|
|
4850
6161
|
}
|
|
6162
|
+
/** 读取全部消息;半截损坏行忽略,悬挂 tool_use 就地补 OUTCOME_UNKNOWN 结果。 */
|
|
4851
6163
|
async load() {
|
|
4852
6164
|
if (!import_node_fs11.default.existsSync(this.filePath)) {
|
|
4853
6165
|
return [];
|
|
@@ -4861,6 +6173,7 @@ var JSONLMessageStore = class {
|
|
|
4861
6173
|
try {
|
|
4862
6174
|
const record = JSON.parse(trimmed);
|
|
4863
6175
|
if (record.role && record.content) {
|
|
6176
|
+
validateProtocolContent(record.content);
|
|
4864
6177
|
messages.push({
|
|
4865
6178
|
role: record.role,
|
|
4866
6179
|
content: record.content,
|
|
@@ -4870,12 +6183,14 @@ var JSONLMessageStore = class {
|
|
|
4870
6183
|
state: record.state
|
|
4871
6184
|
});
|
|
4872
6185
|
}
|
|
4873
|
-
} catch {
|
|
6186
|
+
} catch (error) {
|
|
6187
|
+
if (error instanceof ModelError) throw error;
|
|
4874
6188
|
}
|
|
4875
6189
|
}
|
|
4876
6190
|
this.healDanglingToolCalls(messages);
|
|
4877
6191
|
return messages;
|
|
4878
6192
|
}
|
|
6193
|
+
/** 删除整个文件;文件不存在时静默成功。 */
|
|
4879
6194
|
async clear() {
|
|
4880
6195
|
if (import_node_fs11.default.existsSync(this.filePath)) {
|
|
4881
6196
|
import_node_fs11.default.unlinkSync(this.filePath);
|
|
@@ -4956,6 +6271,7 @@ var PromptAssembler = class {
|
|
|
4956
6271
|
this.instructionSources = options.instructionSources ?? [];
|
|
4957
6272
|
this.visibleTools = options.visibleTools;
|
|
4958
6273
|
}
|
|
6274
|
+
/** 按固定层级拼装系统提示词;同一选项集总是产出相同文本,便于审计与缓存。 */
|
|
4959
6275
|
assemble() {
|
|
4960
6276
|
const sections = [];
|
|
4961
6277
|
sections.push(this.getL1Behavior());
|
|
@@ -4969,7 +6285,7 @@ var PromptAssembler = class {
|
|
|
4969
6285
|
if (this.instructionSources.length > 0) {
|
|
4970
6286
|
sections.push(
|
|
4971
6287
|
`# L4 Project Instructions
|
|
4972
|
-
${this.instructionSources.map((source,
|
|
6288
|
+
${this.instructionSources.map((source, index2) => `## ${index2 + 1}. ${source.path}
|
|
4973
6289
|
${source.content}`).join("\n\n")}`
|
|
4974
6290
|
);
|
|
4975
6291
|
}
|
|
@@ -5009,6 +6325,7 @@ Follow these key behavioral rules:
|
|
|
5009
6325
|
var AgentLoop = class {
|
|
5010
6326
|
interactionId;
|
|
5011
6327
|
modelId;
|
|
6328
|
+
modelRole;
|
|
5012
6329
|
projectHistory;
|
|
5013
6330
|
prepareRequest;
|
|
5014
6331
|
onUsage;
|
|
@@ -5028,6 +6345,7 @@ var AgentLoop = class {
|
|
|
5028
6345
|
constructor(options) {
|
|
5029
6346
|
this.interactionId = options.interactionId;
|
|
5030
6347
|
this.modelId = options.modelId;
|
|
6348
|
+
this.modelRole = options.modelRole;
|
|
5031
6349
|
this.projectHistory = options.projectHistory;
|
|
5032
6350
|
this.prepareRequest = options.prepareRequest;
|
|
5033
6351
|
this.onUsage = options.onUsage;
|
|
@@ -5045,6 +6363,14 @@ var AgentLoop = class {
|
|
|
5045
6363
|
this.getMode = options.getMode ?? (() => "Approval");
|
|
5046
6364
|
this.permissionRules = options.permissionRules ?? [];
|
|
5047
6365
|
}
|
|
6366
|
+
/**
|
|
6367
|
+
* 执行一次交互并流式产出事件。
|
|
6368
|
+
*
|
|
6369
|
+
* 语义要点:
|
|
6370
|
+
* - message_stop 携带完整 canonical 消息,是唯一落盘来源;text / thinking 增量仅供展示。
|
|
6371
|
+
* - 模型失败以 error + turn_finish 事件收尾并结束 run(不抛出);用户中止抛 AbortError。
|
|
6372
|
+
* - turn_start / turn_finish 严格配对,错误、熔断与步数耗尽路径均保证收口。
|
|
6373
|
+
*/
|
|
5048
6374
|
async *run(history) {
|
|
5049
6375
|
let step = 0;
|
|
5050
6376
|
let consecutiveErrors = 0;
|
|
@@ -5063,7 +6389,8 @@ var AgentLoop = class {
|
|
|
5063
6389
|
tools: visibleTools(this.tools.list(), this.getMode(), this.permissionRules).map(
|
|
5064
6390
|
toToolDefinition
|
|
5065
6391
|
),
|
|
5066
|
-
signal: this.signal
|
|
6392
|
+
signal: this.signal,
|
|
6393
|
+
context: this.runId ? { runId: this.runId, modelId: this.modelId } : void 0
|
|
5067
6394
|
};
|
|
5068
6395
|
const beforeModelHooks = this.hooks.get("model:before");
|
|
5069
6396
|
const hookCtx = { signal: this.signal };
|
|
@@ -5083,6 +6410,10 @@ var AgentLoop = class {
|
|
|
5083
6410
|
let accumulatedText = "";
|
|
5084
6411
|
let accumulatedThinking = "";
|
|
5085
6412
|
const toolCalls = [];
|
|
6413
|
+
let finalContent;
|
|
6414
|
+
let responseStopped = false;
|
|
6415
|
+
let invalidToolArguments = false;
|
|
6416
|
+
let refused = false;
|
|
5086
6417
|
let turnUsage;
|
|
5087
6418
|
let finishReason;
|
|
5088
6419
|
let turnTtftMs;
|
|
@@ -5121,7 +6452,12 @@ var AgentLoop = class {
|
|
|
5121
6452
|
try {
|
|
5122
6453
|
for await (const event of this.provider.create(request)) {
|
|
5123
6454
|
if (this.signal?.aborted) throw new AbortError();
|
|
5124
|
-
if (
|
|
6455
|
+
if (responseStopped)
|
|
6456
|
+
throw new ModelError("Model emitted output after its stop event", {
|
|
6457
|
+
stage: "stream",
|
|
6458
|
+
retryable: false
|
|
6459
|
+
});
|
|
6460
|
+
if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "thinking_block_start" || event.type === "thinking_block_stop" || event.type === "message_stop" || event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_finish")
|
|
5125
6461
|
outputSeen = true;
|
|
5126
6462
|
if (!firstTokenReceived && (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "tool_call_start")) {
|
|
5127
6463
|
firstTokenReceived = true;
|
|
@@ -5142,8 +6478,11 @@ var AgentLoop = class {
|
|
|
5142
6478
|
yield { type: "text_delta", text: event.text };
|
|
5143
6479
|
} else if (event.type === "thinking_delta") {
|
|
5144
6480
|
accumulatedThinking += event.thinking;
|
|
5145
|
-
yield
|
|
6481
|
+
yield event;
|
|
6482
|
+
} else if (event.type === "thinking_block_start" || event.type === "thinking_block_stop") {
|
|
6483
|
+
yield event;
|
|
5146
6484
|
} else if (event.type === "tool_call_finish") {
|
|
6485
|
+
invalidToolArguments ||= event.parseError === true;
|
|
5147
6486
|
const toolUse = {
|
|
5148
6487
|
type: "tool_use",
|
|
5149
6488
|
id: event.id,
|
|
@@ -5153,6 +6492,10 @@ var AgentLoop = class {
|
|
|
5153
6492
|
};
|
|
5154
6493
|
toolCalls.push(toolUse);
|
|
5155
6494
|
} else if (event.type === "message_stop") {
|
|
6495
|
+
if (responseStopped) throw new ModelError("Duplicate response stop event");
|
|
6496
|
+
responseStopped = true;
|
|
6497
|
+
finalContent = event.finalContent ? structuredClone(event.finalContent) : void 0;
|
|
6498
|
+
refused = event.refusal === true;
|
|
5156
6499
|
finishReason = event.finishReason;
|
|
5157
6500
|
turnUsage = event.usage;
|
|
5158
6501
|
if (event.usage)
|
|
@@ -5167,7 +6510,38 @@ var AgentLoop = class {
|
|
|
5167
6510
|
}
|
|
5168
6511
|
}
|
|
5169
6512
|
if (this.signal?.aborted) throw new AbortError();
|
|
5170
|
-
if (
|
|
6513
|
+
if (!responseStopped)
|
|
6514
|
+
throw new ModelError("Model response ended without a stop event", {
|
|
6515
|
+
code: "MODEL_STREAM_INCOMPLETE",
|
|
6516
|
+
stage: "stream",
|
|
6517
|
+
retryable: false
|
|
6518
|
+
});
|
|
6519
|
+
if (finishReason !== void 0 && !["stop", "tool_calls", "length", "end_turn", "tool_use"].includes(finishReason))
|
|
6520
|
+
throw new ModelError("Model response has an invalid stop reason", {
|
|
6521
|
+
stage: "stream",
|
|
6522
|
+
retryable: false
|
|
6523
|
+
});
|
|
6524
|
+
if (finalContent) {
|
|
6525
|
+
validateProtocolContent(finalContent);
|
|
6526
|
+
if (finalContent.some((block) => block.type === "tool_result"))
|
|
6527
|
+
throw new ModelError("Assistant response cannot supply tool results", {
|
|
6528
|
+
stage: "stream",
|
|
6529
|
+
retryable: false
|
|
6530
|
+
});
|
|
6531
|
+
toolCalls.length = 0;
|
|
6532
|
+
for (const block of finalContent)
|
|
6533
|
+
if (block.type === "tool_use") {
|
|
6534
|
+
block.source = this.tools.get(block.name)?.metadata?.source;
|
|
6535
|
+
toolCalls.push(block);
|
|
6536
|
+
}
|
|
6537
|
+
accumulatedText = finalContent.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
6538
|
+
}
|
|
6539
|
+
if (finishReason !== void 0 && (["tool_calls", "tool_use"].includes(finishReason) && !toolCalls.length || ["stop", "end_turn"].includes(finishReason) && toolCalls.length))
|
|
6540
|
+
throw new ModelError("Model stop reason does not match its tool content", {
|
|
6541
|
+
stage: "stream",
|
|
6542
|
+
retryable: false
|
|
6543
|
+
});
|
|
6544
|
+
if (invalidToolArguments || finishReason === "content_filter" || toolCalls.length && (finishReason === "length" || refused))
|
|
5171
6545
|
throw new ModelError("Model tool call was truncated; no tools executed", {
|
|
5172
6546
|
code: "MODEL_INVALID_RESPONSE",
|
|
5173
6547
|
stage: "stream",
|
|
@@ -5179,7 +6553,13 @@ var AgentLoop = class {
|
|
|
5179
6553
|
)
|
|
5180
6554
|
);
|
|
5181
6555
|
for (const call of toolCalls) {
|
|
5182
|
-
if (!call.
|
|
6556
|
+
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
6557
|
+
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
6558
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
6559
|
+
stage: "stream",
|
|
6560
|
+
retryable: false
|
|
6561
|
+
});
|
|
6562
|
+
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
5183
6563
|
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
5184
6564
|
knownCalls.add(call.id);
|
|
5185
6565
|
}
|
|
@@ -5236,7 +6616,12 @@ var AgentLoop = class {
|
|
|
5236
6616
|
error: modelError,
|
|
5237
6617
|
modelError: describeModelError(
|
|
5238
6618
|
modelError instanceof ModelError ? modelError : new ModelError(modelError.message, { stage: "request" }),
|
|
5239
|
-
{
|
|
6619
|
+
{
|
|
6620
|
+
modelId: this.modelId,
|
|
6621
|
+
modelRole: this.modelRole,
|
|
6622
|
+
provider: this.provider.name,
|
|
6623
|
+
operation: "primary"
|
|
6624
|
+
}
|
|
5240
6625
|
)
|
|
5241
6626
|
};
|
|
5242
6627
|
yield {
|
|
@@ -5273,16 +6658,14 @@ var AgentLoop = class {
|
|
|
5273
6658
|
metadata: { usage: turnUsage, ttftMs: turnTtftMs }
|
|
5274
6659
|
}
|
|
5275
6660
|
};
|
|
5276
|
-
const contentBlocks = [];
|
|
5277
|
-
if (accumulatedThinking) {
|
|
6661
|
+
const contentBlocks = finalContent ?? [];
|
|
6662
|
+
if (!finalContent && accumulatedThinking) {
|
|
5278
6663
|
contentBlocks.push({ type: "thinking", thinking: accumulatedThinking });
|
|
5279
6664
|
}
|
|
5280
|
-
if (accumulatedText) {
|
|
6665
|
+
if (!finalContent && accumulatedText) {
|
|
5281
6666
|
contentBlocks.push({ type: "text", text: accumulatedText });
|
|
5282
6667
|
}
|
|
5283
|
-
|
|
5284
|
-
contentBlocks.push(tc);
|
|
5285
|
-
}
|
|
6668
|
+
if (!finalContent) contentBlocks.push(...toolCalls);
|
|
5286
6669
|
const assistantMessage = {
|
|
5287
6670
|
id: createMessageId(),
|
|
5288
6671
|
interactionId: this.interactionId,
|
|
@@ -5291,7 +6674,49 @@ var AgentLoop = class {
|
|
|
5291
6674
|
content: contentBlocks,
|
|
5292
6675
|
timestamp: Date.now()
|
|
5293
6676
|
};
|
|
6677
|
+
validateProtocolContent(assistantMessage.content);
|
|
6678
|
+
const originalContent = stableSerialize(assistantMessage.content);
|
|
6679
|
+
const identity = stableSerialize({ ...assistantMessage, content: void 0 });
|
|
6680
|
+
const originalTools = stableSerialize(
|
|
6681
|
+
assistantMessage.content.filter(
|
|
6682
|
+
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
6683
|
+
)
|
|
6684
|
+
);
|
|
6685
|
+
const privateOutput = assistantMessage.content.some(
|
|
6686
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
6687
|
+
);
|
|
5294
6688
|
await this.hooks.emit("model:after", hookCtx, { message: assistantMessage });
|
|
6689
|
+
if (identity !== stableSerialize({ ...assistantMessage, content: void 0 }))
|
|
6690
|
+
throw new ModelError("Hook changed assistant message identity", {
|
|
6691
|
+
stage: "request",
|
|
6692
|
+
retryable: false
|
|
6693
|
+
});
|
|
6694
|
+
if (originalTools !== stableSerialize(
|
|
6695
|
+
assistantMessage.content.filter(
|
|
6696
|
+
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
6697
|
+
)
|
|
6698
|
+
))
|
|
6699
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
6700
|
+
stage: "request",
|
|
6701
|
+
retryable: false
|
|
6702
|
+
});
|
|
6703
|
+
if (originalContent !== stableSerialize(assistantMessage.content)) {
|
|
6704
|
+
if (privateOutput || assistantMessage.content.some(
|
|
6705
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
6706
|
+
))
|
|
6707
|
+
throw new ModelError("Hook changed signed or private model output", {
|
|
6708
|
+
stage: "request",
|
|
6709
|
+
retryable: false
|
|
6710
|
+
});
|
|
6711
|
+
if (toolCalls.length)
|
|
6712
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
6713
|
+
stage: "request",
|
|
6714
|
+
retryable: false
|
|
6715
|
+
});
|
|
6716
|
+
for (const block of assistantMessage.content)
|
|
6717
|
+
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
6718
|
+
}
|
|
6719
|
+
validateProtocolContent(assistantMessage.content);
|
|
5295
6720
|
if (toolCalls.length === 0) {
|
|
5296
6721
|
history.push(assistantMessage);
|
|
5297
6722
|
yield {
|
|
@@ -5548,6 +6973,9 @@ var AgentSession = class {
|
|
|
5548
6973
|
cleanupPromise;
|
|
5549
6974
|
sessionEndFinished = false;
|
|
5550
6975
|
activeRun = false;
|
|
6976
|
+
primaryRole = "default";
|
|
6977
|
+
lastPrimaryProfile;
|
|
6978
|
+
lastPrimaryProvider;
|
|
5551
6979
|
plugins = [];
|
|
5552
6980
|
constructor(config) {
|
|
5553
6981
|
this.persistentId = config.conversationId ?? createMessageId();
|
|
@@ -5585,6 +7013,7 @@ var AgentSession = class {
|
|
|
5585
7013
|
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
5586
7014
|
this.context = this.createContextManager();
|
|
5587
7015
|
}
|
|
7016
|
+
/** 幂等初始化:恢复审计残留、指令快照与持久历史,校验会话身份并回放 usage。 */
|
|
5588
7017
|
async init() {
|
|
5589
7018
|
if (this.initialized) return;
|
|
5590
7019
|
if (this.auditRecoveryDirectory) {
|
|
@@ -5643,6 +7072,7 @@ var AgentSession = class {
|
|
|
5643
7072
|
});
|
|
5644
7073
|
this.initialized = true;
|
|
5645
7074
|
}
|
|
7075
|
+
/** 挂载插件并执行 setup;销毁后或运行中拒绝,setup 失败的插件不进入已装列表。 */
|
|
5646
7076
|
async use(plugin) {
|
|
5647
7077
|
if (this.destroyed) {
|
|
5648
7078
|
throw new Error(`Cannot mount plugin '${plugin.name}': session already destroyed`);
|
|
@@ -5656,23 +7086,68 @@ var AgentSession = class {
|
|
|
5656
7086
|
});
|
|
5657
7087
|
this.plugins.push(plugin);
|
|
5658
7088
|
}
|
|
7089
|
+
/**
|
|
7090
|
+
* 空闲时原子替换角色的模型绑定;本方法不改变当前角色或工具权限。
|
|
7091
|
+
* @param profile 模型协议、请求目标和输出预算。
|
|
7092
|
+
* @param role 被配置的主任务或摘要角色。
|
|
7093
|
+
* @param provider 与目标匹配的适配器;省略时只允许复用相同目标。
|
|
7094
|
+
* @throws {ModelError} 协议、模型、端点或 workspace 与适配器错配,旧绑定保留。
|
|
7095
|
+
* @throws {SessionBusyError} 正在执行或清理当前任务。
|
|
7096
|
+
*/
|
|
5659
7097
|
switchModel(profile, role = "default", provider) {
|
|
5660
7098
|
this.assertIdle("switch models");
|
|
5661
7099
|
resolveContextWindow(profile.contextWindow);
|
|
7100
|
+
createContextBudget(profile.contextWindow, profile.maxOutputTokens);
|
|
5662
7101
|
if (!provider) {
|
|
7102
|
+
const current = this.router.getProfile(role);
|
|
7103
|
+
if (current.provider !== profile.provider || current.modelName !== profile.modelName || current.baseURL.replace(/\/+$/, "") !== profile.baseURL.replace(/\/+$/, "") || current.anthropicWorkspaceId !== profile.anthropicWorkspaceId)
|
|
7104
|
+
throw new ModelError("Changing a model binding requires a matching provider", {
|
|
7105
|
+
stage: "request",
|
|
7106
|
+
retryable: false
|
|
7107
|
+
});
|
|
5663
7108
|
provider = this.router.resolve(role);
|
|
5664
7109
|
}
|
|
5665
7110
|
this.router.setRole(role, profile, provider);
|
|
7111
|
+
this.lastPrimaryProfile = void 0;
|
|
7112
|
+
this.lastPrimaryProvider = void 0;
|
|
5666
7113
|
this.context.invalidate();
|
|
5667
7114
|
this.logger?.(`Switched model for role [${role}] to ${profile.name} (${profile.modelName})`);
|
|
5668
7115
|
}
|
|
5669
|
-
|
|
7116
|
+
/** 查询指定绑定或当前明确选择的主任务模型,返回独立配置副本。 */
|
|
7117
|
+
getActiveProfile(role = this.primaryRole) {
|
|
5670
7118
|
return this.router.getProfile(role);
|
|
5671
7119
|
}
|
|
7120
|
+
/** 空闲时显式选择场景;缺少映射或凭据时拒绝,不回退默认模型、不修改权限。 */
|
|
7121
|
+
selectModelRole(role) {
|
|
7122
|
+
this.assertIdle("select model role");
|
|
7123
|
+
this.resolvePrimaryBinding(role);
|
|
7124
|
+
this.primaryRole = role;
|
|
7125
|
+
this.lastPrimaryProfile = void 0;
|
|
7126
|
+
this.lastPrimaryProvider = void 0;
|
|
7127
|
+
this.context.invalidate();
|
|
7128
|
+
}
|
|
7129
|
+
/** 返回本进程明确选择的场景,历史恢复不改变此状态。 */
|
|
7130
|
+
getModelRole() {
|
|
7131
|
+
return this.primaryRole;
|
|
7132
|
+
}
|
|
7133
|
+
/** 严格解析主任务快照,在输入落盘与网络调用之前检查角色、凭据与预算。 */
|
|
7134
|
+
resolvePrimaryBinding(role) {
|
|
7135
|
+
if (!["default", "planning", "execution", "fast"].includes(role))
|
|
7136
|
+
throw new ModelError("Invalid primary model role", { stage: "request", retryable: false });
|
|
7137
|
+
const binding = this.router.getBinding(role, true);
|
|
7138
|
+
createContextBudget(binding.profile.contextWindow, binding.profile.maxOutputTokens);
|
|
7139
|
+
if (binding.provider.credentialsReady === false)
|
|
7140
|
+
throw new ModelError(`Model role '${role}' requires credentials`, {
|
|
7141
|
+
stage: "request",
|
|
7142
|
+
retryable: false
|
|
7143
|
+
});
|
|
7144
|
+
return binding;
|
|
7145
|
+
}
|
|
5672
7146
|
/** 只在空闲时切换本次会话模式;宿主负责 FullAccess 的显式选择交互。 */
|
|
5673
7147
|
switchMode(mode) {
|
|
5674
7148
|
this.assertIdle("switch permission mode");
|
|
5675
7149
|
this.mode = mode;
|
|
7150
|
+
this.context.invalidate();
|
|
5676
7151
|
}
|
|
5677
7152
|
getMode() {
|
|
5678
7153
|
return this.mode;
|
|
@@ -5696,7 +7171,11 @@ var AgentSession = class {
|
|
|
5696
7171
|
/** 查询最近最终请求的预算快照,尚未准备请求时返回 undefined。 */
|
|
5697
7172
|
getContextSnapshot() {
|
|
5698
7173
|
const snapshot = this.context.getSnapshot();
|
|
5699
|
-
|
|
7174
|
+
const profile = this.lastPrimaryProfile ?? this.getActiveProfile();
|
|
7175
|
+
if (snapshot && !snapshot.stale && snapshot.modelId === profile.id || !this.initialized)
|
|
7176
|
+
return snapshot;
|
|
7177
|
+
const provider = this.lastPrimaryProvider ?? this.router.resolve(this.primaryRole);
|
|
7178
|
+
const history = this.context.project(this.history);
|
|
5700
7179
|
const assembler = new PromptAssembler({
|
|
5701
7180
|
rootDir: this.rootDir,
|
|
5702
7181
|
tools: this.tools,
|
|
@@ -5708,14 +7187,14 @@ var AgentSession = class {
|
|
|
5708
7187
|
this.history,
|
|
5709
7188
|
{
|
|
5710
7189
|
systemPrompt: assembler.assemble(),
|
|
5711
|
-
messages:
|
|
7190
|
+
messages: provider.binding ? projectProtocolHistory(history, provider.binding) : history,
|
|
5712
7191
|
tools: visibleTools(this.tools.list(), this.mode, this.permissionRules).map((t) => ({
|
|
5713
7192
|
name: t.name,
|
|
5714
7193
|
description: t.description,
|
|
5715
7194
|
parameters: t.parameters
|
|
5716
7195
|
}))
|
|
5717
7196
|
},
|
|
5718
|
-
|
|
7197
|
+
profile
|
|
5719
7198
|
);
|
|
5720
7199
|
}
|
|
5721
7200
|
/** 执行中止后的工具/摘要与存储收口期间仍返回 true。 */
|
|
@@ -5732,11 +7211,11 @@ var AgentSession = class {
|
|
|
5732
7211
|
contextSnapshot: this.getContextSnapshot(),
|
|
5733
7212
|
totalTurns: this.totalTurns,
|
|
5734
7213
|
totalTokens: { ...this.usageStats },
|
|
5735
|
-
activeModel: this.
|
|
7214
|
+
activeModel: (this.lastPrimaryProfile ?? this.getActiveProfile()).name,
|
|
5736
7215
|
loadedToolsCount: this.tools.list().length,
|
|
5737
7216
|
lastMetrics: this.lastMetrics,
|
|
5738
7217
|
lastRunMetrics: this.lastRunMetrics,
|
|
5739
|
-
contextUsage: this.lastRunMetrics?.contextUsage ?? this.createContextUsage()
|
|
7218
|
+
contextUsage: this.lastPrimaryProfile ? this.lastRunMetrics?.contextUsage ?? this.createContextUsage() : this.createContextUsage()
|
|
5740
7219
|
});
|
|
5741
7220
|
}
|
|
5742
7221
|
/** 返回最近请求指标的独立副本;尚无完成请求时返回 undefined。 */
|
|
@@ -5747,6 +7226,7 @@ var AgentSession = class {
|
|
|
5747
7226
|
getLastRunMetrics() {
|
|
5748
7227
|
return this.lastRunMetrics ? structuredClone(this.lastRunMetrics) : void 0;
|
|
5749
7228
|
}
|
|
7229
|
+
/** 空闲时清空存储与全部内存状态;conversationId 不变,恢复语义等同全新会话。 */
|
|
5750
7230
|
async reset() {
|
|
5751
7231
|
this.assertIdle("reset session");
|
|
5752
7232
|
if (this.store) await this.store.clear();
|
|
@@ -5756,6 +7236,8 @@ var AgentSession = class {
|
|
|
5756
7236
|
this.usageStats = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
5757
7237
|
this.lastMetrics = void 0;
|
|
5758
7238
|
this.lastRunMetrics = void 0;
|
|
7239
|
+
this.lastPrimaryProfile = void 0;
|
|
7240
|
+
this.lastPrimaryProvider = void 0;
|
|
5759
7241
|
this.accountedAttempts.clear();
|
|
5760
7242
|
this.usageKnown = true;
|
|
5761
7243
|
this.summaryAttempts.clear();
|
|
@@ -5771,20 +7253,24 @@ var AgentSession = class {
|
|
|
5771
7253
|
* {@link SessionEvent},分别完成展示、交互和中止控制。
|
|
5772
7254
|
*
|
|
5773
7255
|
* @param userInput 用户本轮输入。
|
|
5774
|
-
* @param options
|
|
7256
|
+
* @param options 可选中止信号与显式主任务角色;角色在整个工具任务内固定。
|
|
5775
7257
|
* @returns 文本、思考、工具、指标和错误等结构化事件的异步流。
|
|
5776
7258
|
* @throws {SessionBusyError} 当前会话已有正在执行的 run 时抛出。
|
|
7259
|
+
* @throws {ModelError} 显式角色缺少绑定或凭据时,在输入落盘和网络调用前抛出。
|
|
5777
7260
|
*/
|
|
5778
7261
|
run(userInput, options) {
|
|
5779
7262
|
return cancellableGenerator(
|
|
5780
|
-
(signal) => this.runOperation(userInput, { signal }),
|
|
7263
|
+
(signal) => this.runOperation(userInput, { signal, role: options?.role }),
|
|
5781
7264
|
options?.signal
|
|
5782
7265
|
);
|
|
5783
7266
|
}
|
|
5784
7267
|
/** 单次任务执行主体;调用方 return 的中止与 finally 清理由外层统一协调。 */
|
|
5785
7268
|
async *runOperation(userInput, options) {
|
|
5786
7269
|
this.assertIdle("start another run");
|
|
5787
|
-
|
|
7270
|
+
const role = options?.role ?? this.primaryRole;
|
|
7271
|
+
const { profile, provider } = this.resolvePrimaryBinding(role);
|
|
7272
|
+
this.lastPrimaryProfile = profile;
|
|
7273
|
+
this.lastPrimaryProvider = provider;
|
|
5788
7274
|
this.activeRun = true;
|
|
5789
7275
|
const runId = createLogId();
|
|
5790
7276
|
const runStartTime = Date.now();
|
|
@@ -5838,7 +7324,8 @@ var AgentSession = class {
|
|
|
5838
7324
|
level: "info",
|
|
5839
7325
|
event: "run.started",
|
|
5840
7326
|
sessionId: this.sessionId,
|
|
5841
|
-
runId
|
|
7327
|
+
runId,
|
|
7328
|
+
fields: { modelId: profile.id, modelRole: role }
|
|
5842
7329
|
});
|
|
5843
7330
|
try {
|
|
5844
7331
|
this.instructionSnapshot = loadInstructions({
|
|
@@ -5864,7 +7351,8 @@ var AgentSession = class {
|
|
|
5864
7351
|
runId,
|
|
5865
7352
|
runtimeSessionId: this.sessionId,
|
|
5866
7353
|
gitBranch: currentProject.gitWorktree ? currentProject.gitBranch : this.gitBranch,
|
|
5867
|
-
modelId:
|
|
7354
|
+
modelId: profile.id,
|
|
7355
|
+
modelRole: role
|
|
5868
7356
|
});
|
|
5869
7357
|
}
|
|
5870
7358
|
runStarted = true;
|
|
@@ -5888,6 +7376,8 @@ var AgentSession = class {
|
|
|
5888
7376
|
visibleTools: visibleTools(this.tools.list(), this.mode, this.permissionRules)
|
|
5889
7377
|
});
|
|
5890
7378
|
const assembledSystemPrompt = promptAssembler.assemble();
|
|
7379
|
+
let previousRequest;
|
|
7380
|
+
let environmentAnchorId = userMessage.id;
|
|
5891
7381
|
const executor = new ToolExecutor({
|
|
5892
7382
|
tools: this.tools,
|
|
5893
7383
|
hooks: this.hooks,
|
|
@@ -5904,12 +7394,20 @@ var AgentSession = class {
|
|
|
5904
7394
|
});
|
|
5905
7395
|
const loop = new AgentLoop({
|
|
5906
7396
|
interactionId: runId,
|
|
5907
|
-
modelId:
|
|
7397
|
+
modelId: profile.id,
|
|
7398
|
+
modelRole: role,
|
|
5908
7399
|
projectHistory: (history) => this.context.project(history),
|
|
5909
7400
|
prepareRequest: (request, history, reason) => {
|
|
5910
7401
|
const dynamicId = `environment-${runId}`;
|
|
5911
7402
|
const messages = request.messages.filter((message) => message.id !== dynamicId);
|
|
5912
|
-
messages.
|
|
7403
|
+
let anchor = messages.findIndex(
|
|
7404
|
+
(message) => message.id === environmentAnchorId || message.interactionId === runId && message.role === "user"
|
|
7405
|
+
);
|
|
7406
|
+
if (anchor < 0 && !previousRequest) {
|
|
7407
|
+
anchor = messages.map((message) => message.role).lastIndexOf("user");
|
|
7408
|
+
if (anchor >= 0) environmentAnchorId = messages[anchor].id;
|
|
7409
|
+
}
|
|
7410
|
+
messages.splice(anchor < 0 ? messages.length : anchor + 1, 0, {
|
|
5913
7411
|
id: dynamicId,
|
|
5914
7412
|
role: "user",
|
|
5915
7413
|
content: [
|
|
@@ -5920,16 +7418,82 @@ var AgentSession = class {
|
|
|
5920
7418
|
],
|
|
5921
7419
|
timestamp: runStartTime
|
|
5922
7420
|
});
|
|
5923
|
-
const
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
{ ...request, messages },
|
|
5927
|
-
this.router.getProfile("default"),
|
|
7421
|
+
const nativeBinding = provider.binding;
|
|
7422
|
+
const projected = nativeBinding ? projectProtocolHistory(messages, nativeBinding, { runId, modelId: profile.id }) : structuredClone(messages);
|
|
7423
|
+
const sourceView = nativeBinding ? projectProtocolHistory(this.context.project(history), nativeBinding, {
|
|
5928
7424
|
runId,
|
|
5929
|
-
|
|
7425
|
+
modelId: profile.id
|
|
7426
|
+
}) : this.context.project(history);
|
|
7427
|
+
for (const message of projected) {
|
|
7428
|
+
const original = sourceView.find((source) => source.id === message.id);
|
|
7429
|
+
const privateBag = /* @__PURE__ */ new Map();
|
|
7430
|
+
const blockBag = /* @__PURE__ */ new Map();
|
|
7431
|
+
for (const block of original?.content ?? []) {
|
|
7432
|
+
const serialized = stableSerialize(block);
|
|
7433
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
7434
|
+
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
7435
|
+
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
7436
|
+
}
|
|
7437
|
+
const take = (bag, serialized) => {
|
|
7438
|
+
const count = bag.get(serialized) ?? 0;
|
|
7439
|
+
if (!count) return false;
|
|
7440
|
+
if (count === 1) bag.delete(serialized);
|
|
7441
|
+
else bag.set(serialized, count - 1);
|
|
7442
|
+
return true;
|
|
7443
|
+
};
|
|
7444
|
+
for (const block of message.content) {
|
|
7445
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
7446
|
+
if (!original || !take(privateBag, stableSerialize(block)))
|
|
7447
|
+
throw new ModelError("Hook changed signed or private request provenance", {
|
|
7448
|
+
stage: "request",
|
|
7449
|
+
retryable: false
|
|
7450
|
+
});
|
|
7451
|
+
}
|
|
7452
|
+
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
7453
|
+
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
7454
|
+
block.protocolMeta = void 0;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
const frozen = Boolean(
|
|
7458
|
+
previousRequest && history.some(
|
|
7459
|
+
(message) => message.interactionId === runId && message.content.some(
|
|
7460
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
7461
|
+
)
|
|
7462
|
+
)
|
|
5930
7463
|
);
|
|
7464
|
+
const candidate = {
|
|
7465
|
+
...request,
|
|
7466
|
+
messages: projected,
|
|
7467
|
+
context: { runId, modelId: profile.id },
|
|
7468
|
+
signal: options?.signal
|
|
7469
|
+
};
|
|
7470
|
+
if (frozen) {
|
|
7471
|
+
const generated = nativeBinding ? projectProtocolHistory(
|
|
7472
|
+
this.context.project(history).filter(
|
|
7473
|
+
(message) => message.interactionId === runId && message.role !== "user"
|
|
7474
|
+
),
|
|
7475
|
+
nativeBinding,
|
|
7476
|
+
candidate.context
|
|
7477
|
+
) : [];
|
|
7478
|
+
for (const original of generated) {
|
|
7479
|
+
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
7480
|
+
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
7481
|
+
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
7482
|
+
stage: "request",
|
|
7483
|
+
retryable: false
|
|
7484
|
+
});
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
7488
|
+
throw new ModelError("Signed continuation prefix changed", {
|
|
7489
|
+
stage: "request",
|
|
7490
|
+
retryable: false
|
|
7491
|
+
});
|
|
7492
|
+
const context = this.context;
|
|
7493
|
+
const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
|
|
5931
7494
|
return (async function* () {
|
|
5932
7495
|
const prepared = yield* preparation;
|
|
7496
|
+
previousRequest = structuredClone({ ...prepared, signal: void 0 });
|
|
5933
7497
|
lastPromptTokens = void 0;
|
|
5934
7498
|
lastEstimatedPromptTokens = context.getSnapshot()?.estimate.total;
|
|
5935
7499
|
return prepared;
|
|
@@ -5942,13 +7506,14 @@ var AgentSession = class {
|
|
|
5942
7506
|
await this.store.appendRecord("usage", {
|
|
5943
7507
|
attemptId,
|
|
5944
7508
|
role: "primary",
|
|
5945
|
-
modelId:
|
|
7509
|
+
modelId: profile.id,
|
|
7510
|
+
modelRole: role,
|
|
5946
7511
|
usage: { ...usage }
|
|
5947
7512
|
});
|
|
5948
7513
|
this.accountUsage(attemptId, usage);
|
|
5949
|
-
this.context.observe(request, usage,
|
|
7514
|
+
this.context.observe(request, usage, profile);
|
|
5950
7515
|
},
|
|
5951
|
-
provider
|
|
7516
|
+
provider,
|
|
5952
7517
|
executor,
|
|
5953
7518
|
tools: this.tools,
|
|
5954
7519
|
hooks: this.hooks,
|
|
@@ -6002,8 +7567,9 @@ var AgentSession = class {
|
|
|
6002
7567
|
runStatus = "failed";
|
|
6003
7568
|
modelFailure = event.modelError ?? (event.error instanceof ModelError ? describeModelError(event.error, {
|
|
6004
7569
|
operation: "primary",
|
|
6005
|
-
modelId:
|
|
6006
|
-
|
|
7570
|
+
modelId: profile.id,
|
|
7571
|
+
modelRole: role,
|
|
7572
|
+
provider: provider.name
|
|
6007
7573
|
}) : void 0);
|
|
6008
7574
|
}
|
|
6009
7575
|
if (event.type === "message_stop" && event.message.content.some((block) => block.type === "tool_use")) {
|
|
@@ -6076,8 +7642,9 @@ var AgentSession = class {
|
|
|
6076
7642
|
if (error instanceof ModelError)
|
|
6077
7643
|
modelFailure = describeModelError(error, {
|
|
6078
7644
|
operation: "primary",
|
|
6079
|
-
modelId:
|
|
6080
|
-
|
|
7645
|
+
modelId: profile.id,
|
|
7646
|
+
modelRole: role,
|
|
7647
|
+
provider: provider.name
|
|
6081
7648
|
});
|
|
6082
7649
|
const status = error instanceof AbortError || options?.signal?.aborted ? "aborted" : "failed";
|
|
6083
7650
|
const metrics = finalizeRun(status);
|
|
@@ -6115,6 +7682,7 @@ var AgentSession = class {
|
|
|
6115
7682
|
}
|
|
6116
7683
|
}
|
|
6117
7684
|
}
|
|
7685
|
+
/** 幂等销毁:卸载插件后发出 session:end;运行中拒绝,teardown 失败聚合抛出且不完成销毁。 */
|
|
6118
7686
|
async destroy() {
|
|
6119
7687
|
if (this.destroyed) return;
|
|
6120
7688
|
if (this.activeRun) throw new SessionBusyError("destroy session");
|
|
@@ -6153,7 +7721,9 @@ var AgentSession = class {
|
|
|
6153
7721
|
* run 累计 promptTokens 会重复计算每一步都重发的历史,不能用于窗口占用率。
|
|
6154
7722
|
*/
|
|
6155
7723
|
createContextUsage(usedTokens, estimatedUsage = false) {
|
|
6156
|
-
const contextWindow = resolveContextWindow(
|
|
7724
|
+
const contextWindow = resolveContextWindow(
|
|
7725
|
+
(this.lastPrimaryProfile ?? this.getActiveProfile()).contextWindow
|
|
7726
|
+
);
|
|
6157
7727
|
return {
|
|
6158
7728
|
usedTokens,
|
|
6159
7729
|
limitTokens: contextWindow.tokens,
|
|
@@ -6202,12 +7772,19 @@ var AgentSession = class {
|
|
|
6202
7772
|
compact(options) {
|
|
6203
7773
|
return cancellableGenerator((signal) => this.compactOperation({ signal }), options?.signal);
|
|
6204
7774
|
}
|
|
6205
|
-
/**
|
|
7775
|
+
/** 手动压缩按主任务协议投影旧历史,不复用旧 run 私有状态;原始历史与落盘内容保持不变。 */
|
|
6206
7776
|
async *compactOperation(options) {
|
|
6207
7777
|
this.assertIdle("compact context");
|
|
6208
7778
|
this.activeRun = true;
|
|
6209
7779
|
try {
|
|
6210
7780
|
if (!this.initialized) await this.init();
|
|
7781
|
+
const profile = this.lastPrimaryProfile ?? this.getActiveProfile();
|
|
7782
|
+
const messages = projectProtocolHistory(this.context.project(this.history), {
|
|
7783
|
+
protocol: profile.provider,
|
|
7784
|
+
modelName: profile.modelName,
|
|
7785
|
+
baseURL: profile.baseURL,
|
|
7786
|
+
workspaceId: profile.anthropicWorkspaceId
|
|
7787
|
+
});
|
|
6211
7788
|
const assembler = new PromptAssembler({
|
|
6212
7789
|
rootDir: this.rootDir,
|
|
6213
7790
|
tools: this.tools,
|
|
@@ -6219,7 +7796,7 @@ var AgentSession = class {
|
|
|
6219
7796
|
this.history,
|
|
6220
7797
|
{
|
|
6221
7798
|
systemPrompt: assembler.assemble(),
|
|
6222
|
-
messages
|
|
7799
|
+
messages,
|
|
6223
7800
|
signal: options?.signal,
|
|
6224
7801
|
tools: visibleTools(this.tools.list(), this.mode, this.permissionRules).map((t) => ({
|
|
6225
7802
|
name: t.name,
|
|
@@ -6227,7 +7804,7 @@ var AgentSession = class {
|
|
|
6227
7804
|
parameters: t.parameters
|
|
6228
7805
|
}))
|
|
6229
7806
|
},
|
|
6230
|
-
|
|
7807
|
+
profile,
|
|
6231
7808
|
void 0,
|
|
6232
7809
|
"manual"
|
|
6233
7810
|
)) {
|
|
@@ -6238,7 +7815,7 @@ var AgentSession = class {
|
|
|
6238
7815
|
this.activeRun = false;
|
|
6239
7816
|
}
|
|
6240
7817
|
}
|
|
6241
|
-
/**
|
|
7818
|
+
/** 压缩与预算超限事件写入操作日志并携带 runId;其他事件不落日志。 */
|
|
6242
7819
|
async logContextEvent(event, runId) {
|
|
6243
7820
|
if (event.type !== "compaction_start" && event.type !== "compaction_finish" && event.type !== "compaction_failed" && event.type !== "context_budget_exceeded")
|
|
6244
7821
|
return;
|
|
@@ -6281,6 +7858,7 @@ var AgentSession = class {
|
|
|
6281
7858
|
if (!this.store) return;
|
|
6282
7859
|
for (const message of messages) await this.store.append(message);
|
|
6283
7860
|
}
|
|
7861
|
+
/** 中止时为缓冲 assistant 的每个 tool_use 生成 OUTCOME_UNKNOWN 结果并入历史,保持事务闭合。 */
|
|
6284
7862
|
closeInterruptedToolCalls(assistantEvent, error) {
|
|
6285
7863
|
const message = {
|
|
6286
7864
|
id: createMessageId(),
|
|
@@ -6302,6 +7880,7 @@ var AgentSession = class {
|
|
|
6302
7880
|
this.history.push(message);
|
|
6303
7881
|
return [message];
|
|
6304
7882
|
}
|
|
7883
|
+
/** 收集历史中紧跟该 assistant 的连续 tool 消息;仅当覆盖全部 tool_use 时视为已闭合。 */
|
|
6305
7884
|
findClosedToolMessages(assistantEvent) {
|
|
6306
7885
|
const assistantIndex = this.history.lastIndexOf(assistantEvent.message);
|
|
6307
7886
|
if (assistantIndex < 0) return [];
|
|
@@ -6309,8 +7888,8 @@ var AgentSession = class {
|
|
|
6309
7888
|
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
6310
7889
|
);
|
|
6311
7890
|
const messages = [];
|
|
6312
|
-
for (let
|
|
6313
|
-
const message = this.history[
|
|
7891
|
+
for (let index2 = assistantIndex + 1; index2 < this.history.length; index2++) {
|
|
7892
|
+
const message = this.history[index2];
|
|
6314
7893
|
if (message.role !== "tool") break;
|
|
6315
7894
|
messages.push(message);
|
|
6316
7895
|
for (const block of message.content) {
|
|
@@ -6325,6 +7904,7 @@ var AgentSession = class {
|
|
|
6325
7904
|
AbortError,
|
|
6326
7905
|
AgentLoop,
|
|
6327
7906
|
AgentSession,
|
|
7907
|
+
AnthropicProvider,
|
|
6328
7908
|
ContextBudgetError,
|
|
6329
7909
|
ContextManager,
|
|
6330
7910
|
ContextOverflowError,
|
|
@@ -6337,6 +7917,7 @@ var AgentSession = class {
|
|
|
6337
7917
|
KapibalaError,
|
|
6338
7918
|
ModelError,
|
|
6339
7919
|
OpenAICompatibleProvider,
|
|
7920
|
+
OpenAIResponsesProvider,
|
|
6340
7921
|
PathSandbox,
|
|
6341
7922
|
PermissionPolicy,
|
|
6342
7923
|
PromptAssembler,
|
|
@@ -6356,6 +7937,7 @@ var AgentSession = class {
|
|
|
6356
7937
|
ToolRegistry,
|
|
6357
7938
|
TransportError,
|
|
6358
7939
|
UnicodeTokenEstimator,
|
|
7940
|
+
assembleCanonicalToolResults,
|
|
6359
7941
|
builtinTools,
|
|
6360
7942
|
classifyBashOutput,
|
|
6361
7943
|
cleanupCommandResults,
|
|
@@ -6395,6 +7977,7 @@ var AgentSession = class {
|
|
|
6395
7977
|
supportsSessionState,
|
|
6396
7978
|
toToolDefinition,
|
|
6397
7979
|
truncateToolOutput,
|
|
7980
|
+
validateModelBinding,
|
|
6398
7981
|
validatePermissionRules,
|
|
6399
7982
|
validateSummary,
|
|
6400
7983
|
visibleTools,
|