@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 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 import_node_crypto2 = require("crypto");
132
+ var import_node_crypto3 = require("crypto");
129
133
 
130
- // src/types/identity.ts
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, import_node_crypto.randomBytes)(10).toString("hex")}`) & sequenceMask;
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, import_node_crypto2.createHash)("sha256").update(`${source}\0${position}\0${JSON.stringify(message)}`).digest("hex")}`
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((b) => b.type === "text" && b.text.trim()) && !normalized.repairs.length)
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 import_node_crypto3 = require("crypto");
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, import_node_crypto3.randomUUID)(), pid: process.pid, createdAt: Date.now() };
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, import_node_crypto3.randomUUID)()}`;
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 import_node_crypto4 = require("crypto");
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, import_node_crypto4.createHash)("sha256").update(normalized).digest("hex"),
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, import_node_crypto4.createHash)("sha256").update(import_node_fs4.default.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
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/manager.ts
1472
+ // src/context/budget.ts
1269
1473
  var import_node_crypto6 = require("crypto");
1270
1474
 
1271
- // src/errors/index.ts
1272
- function sanitizeModelErrorMessage(value) {
1273
- return value.replace(
1274
- /\b(?:Authorization|Proxy-Authorization)\s*:\s*(?:Bearer|Basic)\s+[^\s,;"']+/gi,
1275
- "Authorization: [redacted]"
1276
- ).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(
1277
- /((?:api[_-]?key|access[_-]?token|token|password|secret|authorization|cookie)["']?\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;&]+)/gi,
1278
- "$1[redacted]"
1279
- ).replace(/https?:\/\/[^\s<>"']+/gi, (raw) => {
1280
- try {
1281
- const url = new URL(raw);
1282
- if (url.username || url.password) {
1283
- url.username = "redacted";
1284
- url.password = "redacted";
1285
- }
1286
- if (url.search) url.search = "?redacted";
1287
- if (url.hash) url.hash = "#redacted";
1288
- return url.toString();
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
- }).slice(0, 600);
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
- var ModelError = class extends KapibalaError {
1380
- status;
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
- var ContextOverflowError = class extends ModelError {
1411
- /** 保留明确上下文错误的脱敏服务端原因;只允许尚无输出的请求进入一次预算处理。 */
1412
- constructor(status, details = {}) {
1413
- super(details.message ?? "Model context window exceeded", {
1414
- ...details,
1415
- status,
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 TransportError = class extends KapibalaError {
1423
- retryable;
1424
- constructor(message, retryable = true) {
1425
- super(message);
1426
- this.name = "TransportError";
1427
- this.retryable = retryable;
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
- var ToolError = class extends KapibalaError {
1431
- constructor(message) {
1432
- super(message, {
1433
- code: "TOOL_ERROR",
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
- var ToolNotFound = class extends KapibalaError {
1441
- toolName;
1442
- availableTools;
1443
- constructor(toolName, availableTools = []) {
1444
- super(`Tool '${toolName}' not found. Available tools: ${availableTools.join(", ") || "none"}`, {
1445
- code: "TOOL_NOT_FOUND",
1446
- retryPolicy: "never",
1447
- safeMessage: "Requested tool is unavailable"
1448
- });
1449
- this.name = "ToolNotFound";
1450
- this.toolName = toolName;
1451
- this.availableTools = availableTools;
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
- var AbortError = class extends KapibalaError {
1455
- constructor(message = "Execution aborted by user") {
1456
- super(message, {
1457
- code: "ABORTED",
1458
- retryPolicy: "after_user_action",
1459
- safeMessage: "Operation cancelled"
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
- this.name = "AbortError";
1462
- }
1463
- };
1464
- var FatalError = class extends KapibalaError {
1465
- constructor(message) {
1466
- super(message);
1467
- this.name = "FatalError";
1468
- }
1469
- };
1470
- var SessionBusyError = class extends KapibalaError {
1471
- constructor(operation) {
1472
- super(`Session is already running; cannot ${operation}`);
1473
- this.name = "SessionBusyError";
1474
- }
1475
- };
1476
- var ContextBudgetError = class extends KapibalaError {
1477
- constructor(usedTokens, inputBudget) {
1478
- super(
1479
- `Context input requires approximately ${usedTokens} tokens; budget is ${inputBudget}. Choose a larger context window or start a smaller task.`,
1480
- {
1481
- code: "CONTEXT_BUDGET_EXCEEDED",
1482
- retryPolicy: "after_user_action",
1483
- safeMessage: "\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u8DB3\uFF0C\u8BF7\u9009\u62E9\u66F4\u5927\u7A97\u53E3\u6216\u62C6\u5206\u4EFB\u52A1\u3002"
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
- this.usedTokens = usedTokens;
1487
- this.inputBudget = inputBudget;
1488
- this.name = "ContextBudgetError";
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
- usedTokens;
1491
- inputBudget;
1492
- };
1493
- var ContextStorageError = class extends KapibalaError {
1494
- constructor(message) {
1495
- super(message, {
1496
- code: "CONTEXT_STORAGE_ERROR",
1497
- retryPolicy: "after_user_action",
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 budget = createContextBudget(profile.contextWindow);
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(structuredClone(message));
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
- temperature: 0
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, import_node_crypto6.createHash)("sha256").update(stableSerialize(value)).digest("hex");
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 之后统一预算;输出 reserve 同时约束实际 maxTokens,超限零 Provider 调用。
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 = createContextBudget(profile.contextWindow);
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: Math.min(request.maxTokens ?? budget.outputReserve, budget.outputReserve)
2023
+ maxTokens: budget.outputReserve
1866
2024
  };
1867
2025
  const estimate = (req) => this.estimator.estimate(req, `${profile.id}:${profile.modelName}`);
1868
- const base = this.project(history);
1869
- const index = indexInteractions(history, this.terminals);
1870
- const protectedIds = new Set(index.protectedMessageIds);
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 baseSources = normalizeHistory(history.slice(coveredPosition + 1)).sources;
1874
- const baseRaw = normalizeHistory(history.slice(coveredPosition + 1)).messages;
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 index.interactions) {
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, index.protectedMessageIds);
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, index.protectedMessageIds);
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, index.protectedMessageIds);
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, index.protectedMessageIds);
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, index.protectedMessageIds);
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
- /** 从当前静态投影估算,不执行 Hook/Provider 或更新状态;恢复后即可查看检查点。 */
2277
+ /**
2278
+ * 从当前静态投影估算,不执行 Hook/Provider 或更新状态;恢复后即可查看检查点。
2279
+ * 输出预留与准备请求使用同一 Profile/请求覆盖规则;非法覆盖也在只读检查时失败。
2280
+ */
2109
2281
  inspect(history, request, profile) {
2110
- const budget = createContextBudget(profile.contextWindow);
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 index = indexInteractions(history, this.terminals);
2196
- if (history.slice(0, end + 1).some((m) => index.protectedMessageIds.includes(m.id)))
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 (!index.interactions.some(
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 (index.interactions.filter(
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 index = indexInteractions(history, this.terminals);
2278
- const protectedIds = new Set(index.protectedMessageIds);
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) && index.interactions.some(
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 import_node_crypto7 = require("crypto");
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, import_node_crypto7.randomUUID)();
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, import_node_crypto7.createHash)("sha256").update(command, "utf8").digest("hex"),
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, import_node_crypto7.createHash)("sha256").update(
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 import_node_crypto8 = require("crypto");
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, import_node_crypto8.createHash)("sha256").update(JSON.stringify([executable, stat.size, stat.mtimeMs])).digest("hex")
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 import_node_crypto9 = require("crypto");
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, import_node_crypto9.createHash)("sha256").update(script).update("\0").update(import_node_fs8.default.readFileSync(script)).digest("hex");
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, import_node_crypto9.randomUUID)(),
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, import_node_crypto9.randomUUID)()}.tmp`);
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, import_node_crypto9.createHash)("sha256").update(JSON.stringify([environment.executable, stat.size, stat.mtimeMs])).digest("hex");
3107
- const environmentDigest = (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify(limitedEnvironment(environment.kind))).digest("hex");
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, import_node_crypto9.randomUUID)()}.txt`);
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 [index, rule] of rules.entries()) {
3443
- const ruleNumber = index + 1;
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 import_node_crypto10 = require("crypto");
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, import_node_crypto10.createHash)("sha256").update(
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 import_node_crypto11 = require("crypto");
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
- "Earlier tool call requires user action",
4116
- "SKIPPED_AFTER_USER_ACTION",
4521
+ "Audit unavailable; tool was not started",
4522
+ "AUDIT_UNAVAILABLE",
4117
4523
  "after_user_action"
4118
4524
  );
4119
4525
  }
4120
- const hookCtx = { signal: this.signal, logger: this.logger };
4121
- let currentInput = call.input;
4526
+ let outputStr;
4527
+ let isError = false;
4528
+ let errorCode;
4529
+ let retryPolicy;
4122
4530
  try {
4123
- for (const hook of this.hooks.get("tool:before")) {
4124
- const decision = await hook(hookCtx, { id: call.id, name: call.name, input: currentInput });
4125
- if (decision.action === "modify") currentInput = decision.input;
4126
- if (decision.action === "skip") {
4127
- await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
4128
- await audit("tool.finished", { status: "skipped" });
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
- } catch {
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
- await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
4141
- await audit("tool.finished", { status: "failed" });
4142
- } catch {
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
- return failure("Tool preparation failed", "HOOK_ERROR");
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
- tool = this.tools.resolve(call.name);
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
- const known = error instanceof KapibalaError ? error : void 0;
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 audit("tool.decided", { decision: "deny", decisionSource: "unavailable" });
4153
- await audit("tool.finished", { status: "failed" });
4724
+ detail = JSON.parse(await response.text())?.error;
4154
4725
  } catch {
4155
4726
  }
4156
- return failure(
4157
- known?.safeMessage ?? "Requested tool is unavailable",
4158
- known?.code ?? "TOOL_NOT_FOUND"
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
- return await this.executeResolved(
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
- async executeResolved(call, currentInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
4185
- let shell;
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 audit("tool.decided", { decision: "deny", decisionSource: "invalid_target" });
4191
- await audit("tool.finished", { status: "failed", errorCode: "INVALID_TARGET" });
4192
- } catch {
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
- return failure("Invalid tool execution target", "INVALID_TARGET");
4195
- }
4196
- let auditFields = {};
4197
- for (let attempt = 0; attempt < 2; attempt++) {
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
- await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
4211
- await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
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
- return failure(
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
- return failure(
4220
- "Identical failed tool call cannot be repeated in this run",
4221
- "REPEAT_BLOCKED"
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
- const cached = this.approvalCache.get(request);
4225
- const decision = this.policy.decide({
4226
- mode: typeof this.mode === "function" ? this.mode() : this.mode,
4227
- toolName: call.name,
4228
- capabilities: request.capabilities,
4229
- dangerous: tool.metadata?.dangerous,
4230
- cachedApproval: cached === "allow",
4231
- cachedDenial: cached === "deny",
4232
- rules: this.permissionRules,
4233
- ...shell ? { shell } : {}
4234
- });
4235
- let permitted = decision.decision === "allow";
4236
- let source = decision.source;
4237
- let approvalChoice;
4238
- if (decision.decision === "ask") {
4239
- let choice = "deny_once";
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
- choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
4999
+ json = JSON.parse(chunk);
4242
5000
  } catch {
4243
- choice = "cancel";
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
- approvalChoice = choice;
4246
- if (choice === "allow_session" && request.sessionAllowed)
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
- return failure(
4281
- this.signal?.aborted ? "Operation cancelled" : "Tool permission denied",
4282
- code,
4283
- "after_user_action"
4284
- );
4285
- }
4286
- if (shell) {
4287
- try {
4288
- const next = tool.approvalScope?.(currentInput, this.rootDir);
4289
- if (next?.executionFingerprint !== shell.executionFingerprint) {
4290
- if (attempt === 0 && next) {
4291
- try {
4292
- await audit("tool.target_changed", { status: "redeciding" });
4293
- } catch {
4294
- return failure(
4295
- "Audit unavailable; tool was not started",
4296
- "AUDIT_UNAVAILABLE",
4297
- "after_user_action"
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
- shell = next;
4301
- continue;
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
- } catch {
4395
- resultBlock.content = JSON.stringify({
4396
- code: "HOOK_ERROR",
4397
- retryPolicy: "after_user_action",
4398
- message: "Tool finished but result processing failed"
4399
- });
4400
- resultBlock.isError = true;
4401
- resultBlock.errorCode = "HOOK_ERROR";
4402
- resultBlock.retryPolicy = "after_user_action";
4403
- }
4404
- resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
4405
- if (resultBlock.isError && resultBlock.retryPolicy === "never") {
4406
- this.neverFailures.add(
4407
- (0, import_node_crypto11.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex")
4408
- );
4409
- }
4410
- try {
4411
- await audit("tool.finished", {
4412
- status: resultBlock.isError ? "failed" : "completed",
4413
- ...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
4414
- durationMs: resultBlock.durationMs
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
- } catch {
4417
- return failure(
4418
- "Tool outcome is unknown because the audit result could not be written",
4419
- "OUTCOME_UNKNOWN",
4420
- "after_user_action"
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
- * 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
4427
- * 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
4428
- *
4429
- * 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
4430
- * 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
5158
+ * 将工具结果装配为 canonical tool 消息,wire 角色仅在下一请求投影时转换。
5159
+ * @param results 已完成的工具结果,保持调用 ID 与内容,不重放工具。
5160
+ * @returns 逐条克隆的 canonical tool 消息,允许消息级落盘。
4431
5161
  */
4432
- withTimeout(promise, toolName, executionController) {
4433
- let timer;
4434
- const timeout = new Promise((_, reject) => {
4435
- timer = setTimeout(() => {
4436
- const error = new KapibalaError(
4437
- `Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
4438
- {
4439
- code: "OUTCOME_UNKNOWN",
4440
- retryPolicy: "after_user_action",
4441
- safeMessage: "Tool timed out; completion of side effects is unknown"
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 (trimmed.startsWith("data:")) {
4470
- const dataContent = trimmed.slice(5).trim();
4471
- if (dataContent === "[DONE]") {
4472
- onDone?.();
4473
- return;
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
- if (buffer.trim().startsWith("data:")) {
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/openai-compatible/index.ts
4495
- var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
4496
- function safeTransportCode(error) {
4497
- const allowed = /* @__PURE__ */ new Set([
4498
- "UND_ERR_SOCKET",
4499
- "UND_ERR_BODY_TIMEOUT",
4500
- "UND_ERR_HEADERS_TIMEOUT",
4501
- "UND_ERR_CONNECT_TIMEOUT",
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 isContextOverflow(error) {
4517
- if (!error || typeof error !== "object") return false;
4518
- const code = error.code;
4519
- return code === "context_length_exceeded" || code === "context_window_exceeded" || code === "max_context_length_exceeded";
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 OpenAICompatibleProvider = class {
4522
- name = "openai-compatible";
5247
+ var AnthropicProvider = class {
5248
+ name = "anthropic";
4523
5249
  baseURL;
4524
- apiKey;
4525
5250
  modelName;
4526
- supportsThinking;
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.supportsThinking = options.supportsThinking ?? false;
4535
- this.replayReasoningContent = options.replayReasoningContent ?? new URL(this.baseURL).hostname === "api.deepseek.com";
4536
- this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
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
- const url = `${this.baseURL}/chat/completions`;
4541
- const wireMessages = this.translateMessagesToWire(req.messages, req.systemPrompt);
4542
- const wireTools = req.tools && req.tools.length > 0 ? req.tools.map((t) => ({
4543
- type: "function",
4544
- function: {
4545
- name: t.name,
4546
- description: t.description,
4547
- parameters: t.parameters
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
- try {
4574
- response = await fetch(url, {
4575
- method: "POST",
4576
- headers: {
4577
- "Content-Type": "application/json",
4578
- Authorization: `Bearer ${this.apiKey}`
4579
- },
4580
- body: JSON.stringify(payload),
4581
- signal: timeoutController.signal
4582
- });
4583
- } catch (err) {
4584
- if (req.signal?.aborted) {
4585
- throw err;
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 (timeoutController.signal.aborted) {
4588
- throw new ModelError(`Connection to ${url} timed out after ${this.connectTimeoutMs}ms`, {
4589
- status: 408,
4590
- retryable: true,
4591
- stage: "connect"
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
- throw new ModelError(`Failed to connect to ${url}: ${err.message}`, {
4595
- transportCode: safeTransportCode(err),
4596
- stage: "connect"
4597
- });
4598
- } finally {
4599
- clearTimeout(timeoutTimer);
4600
- }
4601
- if (!response.ok) {
4602
- const errorText = await response.text();
4603
- let errorDetails;
4604
- let errorMsg = `API request failed with status ${response.status}: ${response.statusText}`;
4605
- try {
4606
- const errorJson = JSON.parse(errorText);
4607
- errorDetails = errorJson.error;
4608
- if (typeof errorJson.error?.message === "string") {
4609
- errorMsg = errorJson.error.message;
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
- } catch {
4612
- if (errorText) errorMsg += ` - ${errorText.slice(0, 300)}`;
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
- const details = errorDetails;
4615
- const options = {
4616
- status: response.status,
4617
- providerCode: typeof details?.code === "string" ? details.code : void 0,
4618
- providerType: typeof details?.type === "string" ? details.type : void 0,
4619
- stage: "response"
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
- if (!response.body) {
4626
- throw new ModelError("Response body is null", {
4627
- code: "MODEL_INVALID_RESPONSE",
4628
- stage: "response"
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
- const pendingToolCalls = /* @__PURE__ */ new Map();
4632
- let usage;
4633
- let finishReason;
4634
- let streamDone = false;
4635
- const requestStartTime = Date.now();
4636
- let ttftMs;
4637
- for await (const chunk of parseSSEStream(response.body, () => {
4638
- streamDone = true;
4639
- })) {
4640
- if (req.signal?.aborted) return;
4641
- if (!chunk) continue;
4642
- let json;
4643
- try {
4644
- json = JSON.parse(chunk);
4645
- } catch {
4646
- throw new ModelError("Model response contained invalid JSON", {
4647
- code: "MODEL_INVALID_RESPONSE",
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 (!json || typeof json !== "object" || Array.isArray(json))
4653
- throw new ModelError("Model response contained an invalid event", {
4654
- code: "MODEL_INVALID_RESPONSE",
4655
- stage: "stream",
4656
- retryable: false
4657
- });
4658
- if (json.error) {
4659
- const message = typeof json.error.message === "string" ? json.error.message : "Model API stream error";
4660
- const details = [json.error.type, json.error.code].filter((value) => typeof value === "string" && value.length > 0).join(", ");
4661
- const options = {
4662
- providerCode: typeof json.error.code === "string" ? json.error.code : void 0,
4663
- providerType: typeof json.error.type === "string" ? json.error.type : void 0,
4664
- stage: "stream"
4665
- };
4666
- if (isContextOverflow(json.error))
4667
- throw new ContextOverflowError(void 0, { ...options, message });
4668
- throw new ModelError(details ? `${message} (${details})` : message, options);
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 (json.usage) {
4671
- usage = {
4672
- promptTokens: json.usage.prompt_tokens ?? 0,
4673
- completionTokens: json.usage.completion_tokens ?? 0,
4674
- totalTokens: json.usage.total_tokens ?? 0
4675
- };
4676
- const cached = json.usage.prompt_tokens_details?.cached_tokens ?? json.usage.prompt_cache_hit_tokens;
4677
- if (Number.isSafeInteger(cached) && cached >= 0) usage.cachedPromptTokens = cached;
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
- const choice = json.choices?.[0];
4680
- if (!choice) continue;
4681
- if (typeof choice.finish_reason === "string") finishReason = choice.finish_reason;
4682
- const delta = choice.delta;
4683
- if (!delta) continue;
4684
- if ((delta.content || delta.reasoning_content || delta.tool_calls && delta.tool_calls.length > 0) && ttftMs === void 0) {
4685
- ttftMs = Date.now() - requestStartTime;
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 (delta.content) {
4688
- yield { type: "text_delta", text: delta.content };
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.reasoning_content) {
4691
- yield { type: "thinking_delta", thinking: delta.reasoning_content };
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 (delta.tool_calls && Array.isArray(delta.tool_calls)) {
4694
- for (const tc of delta.tool_calls) {
4695
- const index = tc.index ?? 0;
4696
- let entry = pendingToolCalls.get(index);
4697
- if (!entry) {
4698
- entry = {
4699
- id: tc.id ?? `call_${Date.now()}_${index}`,
4700
- name: tc.function?.name ?? "",
4701
- argumentChunks: []
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 (!streamDone && finishReason === void 0)
4721
- throw new ModelError("Model response stream ended without a completion marker", {
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
- for (const [, tc] of pendingToolCalls.entries()) {
4727
- const fullArgs = tc.argumentChunks.join("");
4728
- let parsedInput = {};
4729
- let parseError;
4730
- try {
4731
- parsedInput = fullArgs.trim() ? JSON.parse(fullArgs) : {};
4732
- } catch {
4733
- parsedInput = { _raw: fullArgs };
4734
- parseError = true;
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
- yield {
4737
- type: "tool_call_finish",
4738
- id: tc.id,
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 durationMs = Date.now() - requestStartTime;
4745
- yield { type: "message_stop", usage, ttftMs, durationMs, finishReason };
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 || req.signal?.aborted) throw error;
4748
- const transportCode = safeTransportCode(error);
4749
- throw new ModelError(
4750
- `Model response stream failed${transportCode ? ` (${transportCode})` : ""}`,
4751
- {
4752
- retryable: false,
4753
- code: "MODEL_STREAM_INTERRUPTED",
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
- req.signal?.removeEventListener("abort", onOuterAbort);
6101
+ close();
4760
6102
  }
4761
6103
  }
6104
+ /** 保持消息级 canonical tool 落盘,Responses function_call_output 仅存在于请求投影。 */
4762
6105
  assembleToolResults(results) {
4763
- return results.map((r) => ({
4764
- role: "tool",
4765
- content: [r],
4766
- timestamp: Date.now()
4767
- }));
6106
+ return assembleCanonicalToolResults(results);
4768
6107
  }
4769
- /** 将历史投影为协议消息;兼容端点完整回传思考片段,普通端点不发送该扩展字段。 */
4770
- translateMessagesToWire(messages, systemPrompt) {
4771
- const wire = [];
4772
- if (systemPrompt?.trim()) {
4773
- wire.push({ role: "system", content: systemPrompt.trim() });
4774
- }
4775
- for (const msg of messages) {
4776
- if (msg.role === "system") {
4777
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
4778
- if (text) wire.push({ role: "system", content: text });
4779
- } else if (msg.role === "user") {
4780
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
4781
- const previous = wire[wire.length - 1];
4782
- if (previous?.role === "user" && typeof previous.content === "string") {
4783
- previous.content = [previous.content, text].filter(Boolean).join("\n\n");
4784
- } else {
4785
- wire.push({ role: "user", content: text });
4786
- }
4787
- } else if (msg.role === "assistant") {
4788
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
4789
- const toolCalls = [];
4790
- for (const block of msg.content) {
4791
- if (block.type === "tool_use") {
4792
- toolCalls.push({
4793
- id: block.id,
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, index) => `## ${index + 1}. ${source.path}
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 (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_finish")
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 { type: "thinking_delta", thinking: event.thinking };
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 (toolCalls.length && finishReason === "length")
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.id || knownCalls.has(call.id))
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
- { modelId: this.modelId, provider: this.provider.name, operation: "primary" }
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
- for (const tc of toolCalls) {
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
- getActiveProfile(role = "default") {
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
- if (snapshot || !this.initialized) return snapshot;
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: this.context.project(this.history),
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
- this.router.getProfile("default")
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.router.getProfile("default").name,
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
- resolveContextWindow(this.router.getProfile("default").contextWindow);
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: this.router.getProfile("default").id
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: this.router.getProfile("default").id,
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.push({
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 context = this.context;
5924
- const preparation = context.prepare(
5925
- history,
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
- reason
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: this.router.getProfile("default").id,
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, this.router.getProfile("default"));
7514
+ this.context.observe(request, usage, profile);
5950
7515
  },
5951
- provider: this.router.resolve("default"),
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: this.router.getProfile("default").id,
6006
- provider: this.router.resolve("default").name
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: this.router.getProfile("default").id,
6080
- provider: this.router.resolve("default").name
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(this.router.getProfile("default").contextWindow);
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: this.context.project(this.history),
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
- this.router.getProfile("default"),
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
- /** 以模型 attempt 身份幂等累加真实消耗;非法或缺失 usage 保持未知。 */
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 index = assistantIndex + 1; index < this.history.length; index++) {
6313
- const message = this.history[index];
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,