@sema-agent/core 1.294.0 → 1.295.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/agents/subagent.d.ts.map +1 -1
  2. package/dist/agents/subagent.js +24 -3
  3. package/dist/agents/subagent.js.map +1 -1
  4. package/dist/bin/sema-tb.d.ts.map +1 -1
  5. package/dist/bin/sema-tb.js +7 -2
  6. package/dist/bin/sema-tb.js.map +1 -1
  7. package/dist/brain/anthropic.d.ts.map +1 -1
  8. package/dist/brain/anthropic.js +9 -3
  9. package/dist/brain/anthropic.js.map +1 -1
  10. package/dist/brain/openai.d.ts +1 -0
  11. package/dist/brain/openai.d.ts.map +1 -1
  12. package/dist/brain/openai.js +320 -298
  13. package/dist/brain/openai.js.map +1 -1
  14. package/dist/core/hooks.d.ts +3 -3
  15. package/dist/core/hooks.d.ts.map +1 -1
  16. package/dist/core/hooks.js +11 -4
  17. package/dist/core/hooks.js.map +1 -1
  18. package/dist/core/present-plan-tool.d.ts.map +1 -1
  19. package/dist/core/present-plan-tool.js +12 -13
  20. package/dist/core/present-plan-tool.js.map +1 -1
  21. package/dist/core/runner/prepare-task.d.ts.map +1 -1
  22. package/dist/core/runner/prepare-task.js +42 -15
  23. package/dist/core/runner/prepare-task.js.map +1 -1
  24. package/dist/core/runner/runtask.d.ts.map +1 -1
  25. package/dist/core/runner/runtask.js +9 -5
  26. package/dist/core/runner/runtask.js.map +1 -1
  27. package/dist/core/sensitive-path-policy.d.ts +2 -1
  28. package/dist/core/sensitive-path-policy.d.ts.map +1 -1
  29. package/dist/core/sensitive-path-policy.js +22 -0
  30. package/dist/core/sensitive-path-policy.js.map +1 -1
  31. package/dist/core/task-registry.d.ts +4 -0
  32. package/dist/core/task-registry.d.ts.map +1 -1
  33. package/dist/core/task-registry.js +83 -15
  34. package/dist/core/task-registry.js.map +1 -1
  35. package/dist/core/tool-policy.d.ts +5 -2
  36. package/dist/core/tool-policy.d.ts.map +1 -1
  37. package/dist/core/tool-policy.js +9 -0
  38. package/dist/core/tool-policy.js.map +1 -1
  39. package/dist/core/types.d.ts +4 -0
  40. package/dist/core/types.d.ts.map +1 -1
  41. package/dist/engine/compaction/utils.d.ts.map +1 -1
  42. package/dist/engine/compaction/utils.js +3 -1
  43. package/dist/engine/compaction/utils.js.map +1 -1
  44. package/dist/engine/harness/messages.d.ts +2 -0
  45. package/dist/engine/harness/messages.d.ts.map +1 -1
  46. package/dist/engine/harness/messages.js +52 -3
  47. package/dist/engine/harness/messages.js.map +1 -1
  48. package/dist/engine/llm/types.d.ts.map +1 -1
  49. package/dist/engine/session/import-validate.d.ts.map +1 -1
  50. package/dist/engine/session/import-validate.js +64 -0
  51. package/dist/engine/session/import-validate.js.map +1 -1
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/scenarios/full-body.d.ts.map +1 -1
  57. package/dist/scenarios/full-body.js +3 -2
  58. package/dist/scenarios/full-body.js.map +1 -1
  59. package/dist/tools/fs/index.d.ts +4 -1
  60. package/dist/tools/fs/index.d.ts.map +1 -1
  61. package/dist/tools/fs/index.js +59 -20
  62. package/dist/tools/fs/index.js.map +1 -1
  63. package/dist/tools/fs/safety.d.ts +1 -1
  64. package/dist/tools/fs/safety.d.ts.map +1 -1
  65. package/dist/tools/fs/safety.js +3 -1
  66. package/dist/tools/fs/safety.js.map +1 -1
  67. package/dist/tools/fs/search.d.ts +7 -2
  68. package/dist/tools/fs/search.d.ts.map +1 -1
  69. package/dist/tools/fs/search.js +465 -57
  70. package/dist/tools/fs/search.js.map +1 -1
  71. package/dist/tools/monitor.d.ts.map +1 -1
  72. package/dist/tools/monitor.js +12 -1
  73. package/dist/tools/monitor.js.map +1 -1
  74. package/dist/tools/task-list.d.ts.map +1 -1
  75. package/dist/tools/task-list.js.map +1 -1
  76. package/dist/tools/worktree.d.ts.map +1 -1
  77. package/dist/tools/worktree.js +15 -2
  78. package/dist/tools/worktree.js.map +1 -1
  79. package/package.json +1 -1
@@ -258,324 +258,346 @@ function computeUsage(model, raw) {
258
258
  cost: { input: costInput, output: costOutput, cacheRead: 0, cacheWrite: 0, total: costInput + costOutput },
259
259
  };
260
260
  }
261
+ export function inferMaxTokensField(modelId) {
262
+ const id = modelId.toLowerCase();
263
+ if (/^gpt-5/.test(id) || /^chatgpt-4o/.test(id) || /^o[134](?:$|[-.])/.test(id))
264
+ return "max_completion_tokens";
265
+ return "max_tokens";
266
+ }
261
267
  export function createOpenAIBrain(config = {}) {
262
268
  const doFetch = config.fetchImpl ?? fetch;
263
- const stream = (model, context, options) => runStreamingBrain({
264
- model,
265
- doFetch,
266
- signal: options?.signal,
267
- config,
268
- httpLabel: "gateway",
269
- buildRequest: () => {
270
- const apiKey = options?.apiKey ?? config.apiKey;
271
- const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
272
- if (!root) {
273
- throw new Error("createOpenAIBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
274
- }
275
- const body = {
276
- model: model.id,
277
- messages: toOpenAIMessages(context, config.replayThinking, model),
278
- stream: true,
279
- stream_options: { include_usage: true },
280
- };
281
- const tools = toOpenAITools(context);
282
- if (tools)
283
- body.tools = tools;
284
- if (options?.temperature !== undefined)
285
- body.temperature = options.temperature;
286
- if (options?.maxTokens !== undefined) {
287
- const mtField = (model.compat ?? {}).maxTokensField;
288
- body[mtField ?? "max_tokens"] = options.maxTokens;
289
- }
290
- if (options?.stop)
291
- body.stop = options.stop;
292
- applyThinking(body, model, options?.reasoning);
293
- const headers = {
294
- ...model.headers,
295
- ...config.headers,
296
- ...options?.headers,
297
- };
298
- headers["content-type"] = "application/json";
299
- if (apiKey)
300
- headers["authorization"] = `Bearer ${apiKey}`;
301
- return { url: `${root}/chat/completions`, headers, body: JSON.stringify(applyExtraBody(body, model.extraBody, OPENAI_RESERVED)) };
302
- },
303
- makeParser: (ctrl) => {
304
- const { out, partial } = ctrl;
305
- let accumText = "";
306
- let accumReasoning = "";
307
- let accumRefusal = "";
308
- let degenerate = false;
309
- let degenFace;
310
- let repCut;
311
- const repSpared = new Map();
312
- const pollRepetition = (text) => {
313
- const insp = inspectDegenerate(text);
314
- for (const s of insp.spared) {
315
- const k = `${s.rule} ${s.segment}`;
316
- const prev = repSpared.get(k);
317
- if (prev === undefined)
318
- repSpared.set(k, s);
319
- else if (s.reps > prev.reps)
320
- prev.reps = s.reps;
321
- }
322
- if (insp.degenerate)
323
- repCut = insp.cut;
324
- return insp.degenerate;
325
- };
326
- let walltimeCut = false;
327
- const callDeadlineMs = options?.callDeadlineMs;
328
- let lastDegenCheck = 0;
329
- let lastDegenCheckReasoning = 0;
330
- const detectRep = config.detectRepetition !== false;
331
- let emittedTextStart = false;
332
- let emittedThinkingStart = false;
333
- let thinkingClosed = false;
334
- let textIndex = 0;
335
- const toolAccum = new Map();
336
- let finishReason = null;
337
- let usageRaw;
338
- const thinkingBlock = { type: "thinking", thinking: "" };
339
- const textBlock = { type: "text", text: "" };
340
- const pushReasoningDelta = (delta) => {
341
- if (!delta)
342
- return;
343
- ctrl.sawContentToken();
344
- if (thinkingClosed)
345
- return;
346
- if (!emittedThinkingStart) {
347
- emittedThinkingStart = true;
348
- partial.content = [thinkingBlock];
349
- out.push({ type: "thinking_start", contentIndex: 0, partial: { ...partial } });
350
- }
351
- accumReasoning += delta;
352
- thinkingBlock.thinking = accumReasoning;
353
- out.push({ type: "thinking_delta", contentIndex: 0, delta, partial: { ...partial } });
354
- if (detectRep && !degenerate && accumReasoning.length - lastDegenCheckReasoning >= 64) {
355
- lastDegenCheckReasoning = accumReasoning.length;
356
- if (pollRepetition(accumReasoning)) {
357
- degenerate = true;
358
- degenFace = "reasoning";
359
- ctrl.cancel();
360
- }
269
+ const stream = (model, context, options) => {
270
+ let sentMaxTokens;
271
+ let sentMaxTokensLane = "unset";
272
+ return runStreamingBrain({
273
+ model,
274
+ doFetch,
275
+ signal: options?.signal,
276
+ config,
277
+ httpLabel: "gateway",
278
+ buildRequest: () => {
279
+ const apiKey = options?.apiKey ?? config.apiKey;
280
+ const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
281
+ if (!root) {
282
+ throw new Error("createOpenAIBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
361
283
  }
362
- };
363
- const closeThinking = () => {
364
- if (emittedThinkingStart && !thinkingClosed) {
365
- thinkingClosed = true;
366
- out.push({ type: "thinking_end", contentIndex: 0, content: accumReasoning, partial: { ...partial } });
284
+ const body = {
285
+ model: model.id,
286
+ messages: toOpenAIMessages(context, config.replayThinking, model),
287
+ stream: true,
288
+ stream_options: { include_usage: true },
289
+ };
290
+ const tools = toOpenAITools(context);
291
+ if (tools)
292
+ body.tools = tools;
293
+ if (options?.temperature !== undefined)
294
+ body.temperature = options.temperature;
295
+ const requestedMaxTokens = options?.maxTokens ?? model.maxTokens;
296
+ if (requestedMaxTokens !== undefined) {
297
+ const declared = (model.compat ?? {}).maxTokensField;
298
+ const mtField = declared ?? inferMaxTokensField(model.id);
299
+ const capped = Math.max(1, requestedMaxTokens);
300
+ body[mtField] = capped;
301
+ sentMaxTokens = capped;
302
+ sentMaxTokensLane = options?.maxTokens !== undefined ? "options" : "model";
367
303
  }
368
- };
369
- const pushTextDelta = (delta) => {
370
- if (!delta)
371
- return;
372
- ctrl.sawContentToken();
373
- if (!emittedTextStart) {
374
- closeThinking();
375
- emittedTextStart = true;
376
- textIndex = emittedThinkingStart ? 1 : 0;
377
- partial.content = [textBlock];
378
- out.push({ type: "text_start", contentIndex: textIndex, partial: { ...partial } });
304
+ else {
305
+ sentMaxTokens = undefined;
306
+ sentMaxTokensLane = "unset";
379
307
  }
380
- accumText += delta;
381
- textBlock.text = accumText;
382
- out.push({ type: "text_delta", contentIndex: textIndex, delta, partial: { ...partial } });
383
- if (detectRep && !degenerate && accumText.length - lastDegenCheck >= 64) {
384
- lastDegenCheck = accumText.length;
385
- if (pollRepetition(accumText)) {
386
- degenerate = true;
387
- degenFace = "text";
388
- ctrl.cancel();
308
+ if (options?.stop)
309
+ body.stop = options.stop;
310
+ applyThinking(body, model, options?.reasoning);
311
+ const headers = {
312
+ ...model.headers,
313
+ ...config.headers,
314
+ ...options?.headers,
315
+ };
316
+ headers["content-type"] = "application/json";
317
+ if (apiKey)
318
+ headers["authorization"] = `Bearer ${apiKey}`;
319
+ return { url: `${root}/chat/completions`, headers, body: JSON.stringify(applyExtraBody(body, model.extraBody, OPENAI_RESERVED)) };
320
+ },
321
+ makeParser: (ctrl) => {
322
+ const { out, partial } = ctrl;
323
+ let accumText = "";
324
+ let accumReasoning = "";
325
+ let accumRefusal = "";
326
+ let degenerate = false;
327
+ let degenFace;
328
+ let repCut;
329
+ const repSpared = new Map();
330
+ const pollRepetition = (text) => {
331
+ const insp = inspectDegenerate(text);
332
+ for (const s of insp.spared) {
333
+ const k = `${s.rule} ${s.segment}`;
334
+ const prev = repSpared.get(k);
335
+ if (prev === undefined)
336
+ repSpared.set(k, s);
337
+ else if (s.reps > prev.reps)
338
+ prev.reps = s.reps;
389
339
  }
390
- }
391
- };
392
- return {
393
- snapshot() {
394
- const hasCompletedToolCall = [...toolAccum.values()].some((a) => a.closedTc !== undefined);
395
- const hasSubstantiveText = accumText.trim() !== "" || accumRefusal.trim() !== "";
396
- return {
397
- hasSubstantiveText,
398
- hasCompletedToolCall,
399
- hasOnlyThinking: accumReasoning.length > 0 && !hasSubstantiveText && !hasCompletedToolCall,
400
- };
401
- },
402
- sealForRetry() {
403
- closeThinking();
404
- },
405
- onLine(rawLine) {
406
- if (callDeadlineMs !== undefined && !walltimeCut && !degenerate && Date.now() >= callDeadlineMs) {
407
- walltimeCut = true;
408
- ctrl.cancel();
409
- return;
410
- }
411
- if (walltimeCut)
412
- return;
413
- const line = rawLine.trim();
414
- if (!line.startsWith("data:"))
340
+ if (insp.degenerate)
341
+ repCut = insp.cut;
342
+ return insp.degenerate;
343
+ };
344
+ let walltimeCut = false;
345
+ const callDeadlineMs = options?.callDeadlineMs;
346
+ let lastDegenCheck = 0;
347
+ let lastDegenCheckReasoning = 0;
348
+ const detectRep = config.detectRepetition !== false;
349
+ let emittedTextStart = false;
350
+ let emittedThinkingStart = false;
351
+ let thinkingClosed = false;
352
+ let textIndex = 0;
353
+ const toolAccum = new Map();
354
+ let finishReason = null;
355
+ let usageRaw;
356
+ const thinkingBlock = { type: "thinking", thinking: "" };
357
+ const textBlock = { type: "text", text: "" };
358
+ const pushReasoningDelta = (delta) => {
359
+ if (!delta)
415
360
  return;
416
- const data = line.slice(5).trim();
417
- if (!data || data === "[DONE]")
361
+ ctrl.sawContentToken();
362
+ if (thinkingClosed)
418
363
  return;
419
- let chunk;
420
- try {
421
- chunk = JSON.parse(data);
364
+ if (!emittedThinkingStart) {
365
+ emittedThinkingStart = true;
366
+ partial.content = [thinkingBlock];
367
+ out.push({ type: "thinking_start", contentIndex: 0, partial: { ...partial } });
422
368
  }
423
- catch {
424
- return;
369
+ accumReasoning += delta;
370
+ thinkingBlock.thinking = accumReasoning;
371
+ out.push({ type: "thinking_delta", contentIndex: 0, delta, partial: { ...partial } });
372
+ if (detectRep && !degenerate && accumReasoning.length - lastDegenCheckReasoning >= 64) {
373
+ lastDegenCheckReasoning = accumReasoning.length;
374
+ if (pollRepetition(accumReasoning)) {
375
+ degenerate = true;
376
+ degenFace = "reasoning";
377
+ ctrl.cancel();
378
+ }
379
+ }
380
+ };
381
+ const closeThinking = () => {
382
+ if (emittedThinkingStart && !thinkingClosed) {
383
+ thinkingClosed = true;
384
+ out.push({ type: "thinking_end", contentIndex: 0, content: accumReasoning, partial: { ...partial } });
425
385
  }
426
- if (chunk.usage)
427
- usageRaw = chunk.usage;
428
- const choice = chunk.choices?.[0];
429
- if (!choice)
386
+ };
387
+ const pushTextDelta = (delta) => {
388
+ if (!delta)
430
389
  return;
431
- if (choice.finish_reason)
432
- finishReason = choice.finish_reason;
433
- const delta = choice.delta;
434
- if (delta?.reasoning)
435
- pushReasoningDelta(delta.reasoning);
436
- else if (delta?.reasoning_content)
437
- pushReasoningDelta(delta.reasoning_content);
438
- if (delta?.refusal) {
439
- ctrl.sawContentToken();
440
- accumRefusal += delta.refusal;
390
+ ctrl.sawContentToken();
391
+ if (!emittedTextStart) {
392
+ closeThinking();
393
+ emittedTextStart = true;
394
+ textIndex = emittedThinkingStart ? 1 : 0;
395
+ partial.content = [textBlock];
396
+ out.push({ type: "text_start", contentIndex: textIndex, partial: { ...partial } });
397
+ }
398
+ accumText += delta;
399
+ textBlock.text = accumText;
400
+ out.push({ type: "text_delta", contentIndex: textIndex, delta, partial: { ...partial } });
401
+ if (detectRep && !degenerate && accumText.length - lastDegenCheck >= 64) {
402
+ lastDegenCheck = accumText.length;
403
+ if (pollRepetition(accumText)) {
404
+ degenerate = true;
405
+ degenFace = "text";
406
+ ctrl.cancel();
407
+ }
441
408
  }
442
- if (delta?.content)
443
- pushTextDelta(delta.content);
444
- if (delta?.tool_calls) {
445
- ctrl.sawContentToken();
446
- for (const tc of delta.tool_calls) {
447
- const idx = tc.index ?? (tc.id ? toolAccum.size : Math.max(0, toolAccum.size - 1));
448
- if (!toolAccum.has(idx)) {
449
- for (const [prevIdx, prevAcc] of [...toolAccum.entries()].sort((a, b) => a[0] - b[0])) {
450
- if (prevIdx >= idx || prevAcc.closedTc)
451
- continue;
452
- if (prevAcc.args === "")
453
- continue;
454
- const closed = closeToolCallAccum(prevAcc);
455
- if (closed) {
456
- out.push({ type: "toolcall_end", contentIndex: prevIdx, toolCall: closed, partial: { ...partial } });
409
+ };
410
+ return {
411
+ snapshot() {
412
+ const hasCompletedToolCall = [...toolAccum.values()].some((a) => a.closedTc !== undefined);
413
+ const hasSubstantiveText = accumText.trim() !== "" || accumRefusal.trim() !== "";
414
+ return {
415
+ hasSubstantiveText,
416
+ hasCompletedToolCall,
417
+ hasOnlyThinking: accumReasoning.length > 0 && !hasSubstantiveText && !hasCompletedToolCall,
418
+ };
419
+ },
420
+ sealForRetry() {
421
+ closeThinking();
422
+ },
423
+ onLine(rawLine) {
424
+ if (callDeadlineMs !== undefined && !walltimeCut && !degenerate && Date.now() >= callDeadlineMs) {
425
+ walltimeCut = true;
426
+ ctrl.cancel();
427
+ return;
428
+ }
429
+ if (walltimeCut)
430
+ return;
431
+ const line = rawLine.trim();
432
+ if (!line.startsWith("data:"))
433
+ return;
434
+ const data = line.slice(5).trim();
435
+ if (!data || data === "[DONE]")
436
+ return;
437
+ let chunk;
438
+ try {
439
+ chunk = JSON.parse(data);
440
+ }
441
+ catch {
442
+ return;
443
+ }
444
+ if (chunk.usage)
445
+ usageRaw = chunk.usage;
446
+ const choice = chunk.choices?.[0];
447
+ if (!choice)
448
+ return;
449
+ if (choice.finish_reason)
450
+ finishReason = choice.finish_reason;
451
+ const delta = choice.delta;
452
+ if (delta?.reasoning)
453
+ pushReasoningDelta(delta.reasoning);
454
+ else if (delta?.reasoning_content)
455
+ pushReasoningDelta(delta.reasoning_content);
456
+ if (delta?.refusal) {
457
+ ctrl.sawContentToken();
458
+ accumRefusal += delta.refusal;
459
+ }
460
+ if (delta?.content)
461
+ pushTextDelta(delta.content);
462
+ if (delta?.tool_calls) {
463
+ ctrl.sawContentToken();
464
+ for (const tc of delta.tool_calls) {
465
+ const idx = tc.index ?? (tc.id ? toolAccum.size : Math.max(0, toolAccum.size - 1));
466
+ if (!toolAccum.has(idx)) {
467
+ for (const [prevIdx, prevAcc] of [...toolAccum.entries()].sort((a, b) => a[0] - b[0])) {
468
+ if (prevIdx >= idx || prevAcc.closedTc)
469
+ continue;
470
+ if (prevAcc.args === "")
471
+ continue;
472
+ const closed = closeToolCallAccum(prevAcc);
473
+ if (closed) {
474
+ out.push({ type: "toolcall_end", contentIndex: prevIdx, toolCall: closed, partial: { ...partial } });
475
+ }
457
476
  }
458
477
  }
478
+ const acc = toolAccum.get(idx) ?? { id: "", name: "", args: "" };
479
+ if (tc.id)
480
+ acc.id = tc.id;
481
+ if (tc.function?.name)
482
+ acc.name = tc.function.name;
483
+ if (tc.function?.arguments && !acc.closedTc)
484
+ acc.args += tc.function.arguments;
485
+ toolAccum.set(idx, acc);
459
486
  }
460
- const acc = toolAccum.get(idx) ?? { id: "", name: "", args: "" };
461
- if (tc.id)
462
- acc.id = tc.id;
463
- if (tc.function?.name)
464
- acc.name = tc.function.name;
465
- if (tc.function?.arguments && !acc.closedTc)
466
- acc.args += tc.function.arguments;
467
- toolAccum.set(idx, acc);
468
- }
469
- }
470
- },
471
- finalize() {
472
- closeThinking();
473
- if (emittedTextStart) {
474
- out.push({ type: "text_end", contentIndex: textIndex, content: accumText, partial: { ...partial } });
475
- }
476
- const finalContent = [];
477
- if (accumReasoning)
478
- finalContent.push({ type: "thinking", thinking: accumReasoning });
479
- const textFace = degenerate && degenFace === "text" && repCut !== undefined ? trimDegenerateTail(accumText, repCut) : accumText;
480
- if (textFace)
481
- finalContent.push({ type: "text", text: textFace });
482
- const toolCalls = [];
483
- const malformed = [];
484
- for (const acc of [...toolAccum.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
485
- if (!acc.name)
486
- continue;
487
- const tc = closeToolCallAccum(acc);
488
- if (tc) {
489
- toolCalls.push(tc);
490
- finalContent.push(tc);
491
487
  }
492
- else {
493
- malformed.push(`${acc.name}(${acc.args.slice(0, 200)})`);
488
+ },
489
+ finalize() {
490
+ closeThinking();
491
+ if (emittedTextStart) {
492
+ out.push({ type: "text_end", contentIndex: textIndex, content: accumText, partial: { ...partial } });
494
493
  }
495
- }
496
- if (config.repairTextToolCalls && toolCalls.length === 0 && textFace && context.tools && context.tools.length > 0) {
497
- const names = new Set(context.tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
498
- const repaired = repairTextToolCalls(textFace, names);
499
- if (repaired.toolCalls.length > 0) {
500
- finalContent.length = 0;
501
- if (accumReasoning)
502
- finalContent.push({ type: "thinking", thinking: accumReasoning });
503
- if (repaired.cleanedText)
504
- finalContent.push({ type: "text", text: repaired.cleanedText });
505
- for (const tc of repaired.toolCalls) {
506
- finalContent.push(tc);
494
+ const finalContent = [];
495
+ if (accumReasoning)
496
+ finalContent.push({ type: "thinking", thinking: accumReasoning });
497
+ const textFace = degenerate && degenFace === "text" && repCut !== undefined ? trimDegenerateTail(accumText, repCut) : accumText;
498
+ if (textFace)
499
+ finalContent.push({ type: "text", text: textFace });
500
+ const toolCalls = [];
501
+ const malformed = [];
502
+ for (const acc of [...toolAccum.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
503
+ if (!acc.name)
504
+ continue;
505
+ const tc = closeToolCallAccum(acc);
506
+ if (tc) {
507
507
  toolCalls.push(tc);
508
+ finalContent.push(tc);
509
+ }
510
+ else {
511
+ malformed.push(`${acc.name}(${acc.args.slice(0, 200)})`);
508
512
  }
509
513
  }
510
- }
511
- const toolError = malformed.length > 0
512
- ? `tool call argument(s) not valid JSON (likely truncated, finish_reason="${finishReason ?? "?"}"): ${malformed.join("; ")}`
513
- : undefined;
514
- const noUsableContent = toolCalls.length === 0 && !accumText.trim();
515
- if (toolError !== undefined) {
516
- finalContent.push({
517
- type: "text",
518
- text: `\n[note: ${malformed.length} tool call(s) were truncated (finish_reason="${finishReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
519
- });
520
- }
521
- const truncatedEmpty = finishReason === "length" && noUsableContent && !toolError;
522
- const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
523
- const safetyCut = finishReason === "content_filter";
524
- const refused = accumRefusal.trim().length > 0;
525
- const emptyNoFinish = noUsableContent && finishReason == null && !degenerate && !walltimeCut;
526
- const errorMessage = walltimeCut
527
- ? WALLTIME_CUTOFF_MESSAGE
528
- : degenerate
529
- ? DEGENERATE_MESSAGE
530
- : safetyCut
531
- ? `response cut by the provider content filter (finish_reason="content_filter") — the output was censored/truncated for policy and is unreliable`
532
- : refused
533
- ? `response refused by the model for policy reasons: ${accumRefusal.trim()}`
534
- : (toolError ??
535
- (truncatedEmpty && !dynamicCut
536
- ? accumReasoning.trim()
537
- ? `response truncated at max_tokens (finish_reason="length") with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
538
- : `response truncated at max_tokens (finish_reason="length") with no answer text — raise max_tokens`
539
- : emptyNoFinish
540
- ? `model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)`
541
- : undefined));
542
- const errored = walltimeCut ||
543
- degenerate ||
544
- safetyCut ||
545
- refused ||
546
- emptyNoFinish ||
547
- (toolError !== undefined && noUsableContent) ||
548
- (truncatedEmpty && !dynamicCut);
549
- const errorKind = !walltimeCut && !degenerate && !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
550
- ? "length_empty"
551
- : undefined;
552
- const finalMessage = {
553
- ...partial,
554
- content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
555
- stopReason: errored ? "error" : mapFinishReason(finishReason, toolCalls.length > 0),
556
- usage: computeUsage(model, usageRaw),
557
- ...(usageRaw === undefined ? { usageMissing: true } : {}),
558
- timestamp: Date.now(),
559
- ...(errorMessage ? { errorMessage } : {}),
560
- ...(errorKind ? { errorKind } : {}),
561
- ...(repCut !== undefined || repSpared.size > 0
562
- ? { repetition: { ...(repCut !== undefined ? { cut: repCut } : {}), ...(repSpared.size > 0 ? { spared: [...repSpared.values()] } : {}) } }
563
- : {}),
564
- };
565
- if (errored) {
566
- out.push({ type: "error", reason: "error", error: finalMessage });
567
- }
568
- else {
569
- out.push({
570
- type: "done",
571
- reason: finalMessage.stopReason,
572
- message: finalMessage,
573
- });
574
- }
575
- },
576
- };
577
- },
578
- });
514
+ if (config.repairTextToolCalls && toolCalls.length === 0 && textFace && context.tools && context.tools.length > 0) {
515
+ const names = new Set(context.tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
516
+ const repaired = repairTextToolCalls(textFace, names);
517
+ if (repaired.toolCalls.length > 0) {
518
+ finalContent.length = 0;
519
+ if (accumReasoning)
520
+ finalContent.push({ type: "thinking", thinking: accumReasoning });
521
+ if (repaired.cleanedText)
522
+ finalContent.push({ type: "text", text: repaired.cleanedText });
523
+ for (const tc of repaired.toolCalls) {
524
+ finalContent.push(tc);
525
+ toolCalls.push(tc);
526
+ }
527
+ }
528
+ }
529
+ const toolError = malformed.length > 0
530
+ ? `tool call argument(s) not valid JSON (likely truncated, finish_reason="${finishReason ?? "?"}"): ${malformed.join("; ")}`
531
+ : undefined;
532
+ const noUsableContent = toolCalls.length === 0 && !accumText.trim();
533
+ if (toolError !== undefined) {
534
+ finalContent.push({
535
+ type: "text",
536
+ text: `\n[note: ${malformed.length} tool call(s) were truncated (finish_reason="${finishReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
537
+ });
538
+ }
539
+ const truncatedEmpty = finishReason === "length" && noUsableContent && !toolError;
540
+ const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
541
+ const safetyCut = finishReason === "content_filter";
542
+ const refused = accumRefusal.trim().length > 0;
543
+ const emptyNoFinish = noUsableContent && finishReason == null && !degenerate && !walltimeCut;
544
+ const maxTokensNote = sentMaxTokens !== undefined
545
+ ? `effective max_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : "model.maxTokens"})`
546
+ : "max_tokens not set (neither options.maxTokens nor model.maxTokens — the provider's default cap applied)";
547
+ const errorMessage = walltimeCut
548
+ ? WALLTIME_CUTOFF_MESSAGE
549
+ : degenerate
550
+ ? DEGENERATE_MESSAGE
551
+ : safetyCut
552
+ ? `response cut by the provider content filter (finish_reason="content_filter") the output was censored/truncated for policy and is unreliable`
553
+ : refused
554
+ ? `response refused by the model for policy reasons: ${accumRefusal.trim()}`
555
+ : (toolError ??
556
+ (truncatedEmpty && !dynamicCut
557
+ ? accumReasoning.trim()
558
+ ? `response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
559
+ : `response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — raise max_tokens`
560
+ : emptyNoFinish
561
+ ? `model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)`
562
+ : undefined));
563
+ const errored = walltimeCut ||
564
+ degenerate ||
565
+ safetyCut ||
566
+ refused ||
567
+ emptyNoFinish ||
568
+ (toolError !== undefined && noUsableContent) ||
569
+ (truncatedEmpty && !dynamicCut);
570
+ const errorKind = !walltimeCut && !degenerate && !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
571
+ ? "length_empty"
572
+ : undefined;
573
+ const finalMessage = {
574
+ ...partial,
575
+ content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
576
+ stopReason: errored ? "error" : mapFinishReason(finishReason, toolCalls.length > 0),
577
+ usage: computeUsage(model, usageRaw),
578
+ ...(usageRaw === undefined ? { usageMissing: true } : {}),
579
+ timestamp: Date.now(),
580
+ ...(errorMessage ? { errorMessage } : {}),
581
+ ...(errorKind ? { errorKind } : {}),
582
+ ...(repCut !== undefined || repSpared.size > 0
583
+ ? { repetition: { ...(repCut !== undefined ? { cut: repCut } : {}), ...(repSpared.size > 0 ? { spared: [...repSpared.values()] } : {}) } }
584
+ : {}),
585
+ };
586
+ if (errored) {
587
+ out.push({ type: "error", reason: "error", error: finalMessage });
588
+ }
589
+ else {
590
+ out.push({
591
+ type: "done",
592
+ reason: finalMessage.stopReason,
593
+ message: finalMessage,
594
+ });
595
+ }
596
+ },
597
+ };
598
+ },
599
+ });
600
+ };
579
601
  return { stream };
580
602
  }
581
603
  //# sourceMappingURL=openai.js.map