@corbat-tech/coco 2.22.2 → 2.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1792,6 +1792,7 @@ var init_openai = __esm({
1792
1792
  requestParams
1793
1793
  );
1794
1794
  const toolCallBuilders = /* @__PURE__ */ new Map();
1795
+ let lastToolCallKey = null;
1795
1796
  const streamTimeout = this.config.timeout ?? 12e4;
1796
1797
  let lastActivityTime = Date.now();
1797
1798
  const timeoutController = new AbortController();
@@ -1841,9 +1842,9 @@ var init_openai = __esm({
1841
1842
  }
1842
1843
  if (delta?.tool_calls) {
1843
1844
  for (const toolCallDelta of delta.tool_calls) {
1844
- const index = toolCallDelta.index ?? toolCallBuilders.size;
1845
- if (!toolCallBuilders.has(index)) {
1846
- toolCallBuilders.set(index, {
1845
+ const key = typeof toolCallDelta.index === "number" ? `index:${toolCallDelta.index}` : typeof toolCallDelta.id === "string" && toolCallDelta.id.length > 0 ? `id:${toolCallDelta.id}` : toolCallBuilders.size === 1 ? Array.from(toolCallBuilders.keys())[0] ?? `fallback:${toolCallBuilders.size}` : lastToolCallKey ?? `fallback:${toolCallBuilders.size}`;
1846
+ if (!toolCallBuilders.has(key)) {
1847
+ toolCallBuilders.set(key, {
1847
1848
  id: toolCallDelta.id ?? "",
1848
1849
  name: toolCallDelta.function?.name ?? "",
1849
1850
  arguments: ""
@@ -1856,7 +1857,8 @@ var init_openai = __esm({
1856
1857
  }
1857
1858
  };
1858
1859
  }
1859
- const builder = toolCallBuilders.get(index);
1860
+ const builder = toolCallBuilders.get(key);
1861
+ lastToolCallKey = key;
1860
1862
  if (toolCallDelta.id) {
1861
1863
  builder.id = toolCallDelta.id;
1862
1864
  }
@@ -2444,6 +2446,7 @@ var init_openai = __esm({
2444
2446
  requestParams
2445
2447
  );
2446
2448
  const fnCallBuilders = /* @__PURE__ */ new Map();
2449
+ const outputIndexToBuilderKey = /* @__PURE__ */ new Map();
2447
2450
  const streamTimeout = this.config.timeout ?? 12e4;
2448
2451
  let lastActivityTime = Date.now();
2449
2452
  const timeoutController = new AbortController();
@@ -2473,8 +2476,11 @@ var init_openai = __esm({
2473
2476
  fnCallBuilders.set(itemKey, {
2474
2477
  callId: fc.call_id,
2475
2478
  name: fc.name,
2476
- arguments: ""
2479
+ arguments: fc.arguments ?? ""
2477
2480
  });
2481
+ if (typeof event.output_index === "number") {
2482
+ outputIndexToBuilderKey.set(event.output_index, itemKey);
2483
+ }
2478
2484
  yield {
2479
2485
  type: "tool_use_start",
2480
2486
  toolCall: { id: fc.call_id, name: fc.name }
@@ -2483,7 +2489,9 @@ var init_openai = __esm({
2483
2489
  break;
2484
2490
  case "response.function_call_arguments.delta":
2485
2491
  {
2486
- const builder = fnCallBuilders.get(event.item_id);
2492
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
2493
+ if (!builderKey) break;
2494
+ const builder = fnCallBuilders.get(builderKey);
2487
2495
  if (builder) {
2488
2496
  builder.arguments += event.delta;
2489
2497
  }
@@ -2491,17 +2499,22 @@ var init_openai = __esm({
2491
2499
  break;
2492
2500
  case "response.function_call_arguments.done":
2493
2501
  {
2494
- const builder = fnCallBuilders.get(event.item_id);
2502
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
2503
+ if (!builderKey) break;
2504
+ const builder = fnCallBuilders.get(builderKey);
2495
2505
  if (builder) {
2496
2506
  yield {
2497
2507
  type: "tool_use_end",
2498
2508
  toolCall: {
2499
2509
  id: builder.callId,
2500
2510
  name: builder.name,
2501
- input: this.parseResponsesArguments(event.arguments)
2511
+ input: this.parseResponsesArguments(event.arguments ?? builder.arguments)
2502
2512
  }
2503
2513
  };
2504
- fnCallBuilders.delete(event.item_id);
2514
+ fnCallBuilders.delete(builderKey);
2515
+ for (const [idx, key] of outputIndexToBuilderKey.entries()) {
2516
+ if (key === builderKey) outputIndexToBuilderKey.delete(idx);
2517
+ }
2505
2518
  }
2506
2519
  }
2507
2520
  break;
@@ -4640,6 +4653,7 @@ var init_codex = __esm({
4640
4653
  let outputTokens = 0;
4641
4654
  const toolCalls = [];
4642
4655
  const fnCallBuilders = /* @__PURE__ */ new Map();
4656
+ const outputIndexToBuilderKey = /* @__PURE__ */ new Map();
4643
4657
  await this.readSSEStream(response, (event) => {
4644
4658
  if (event.id) responseId = event.id;
4645
4659
  switch (event.type) {
@@ -4656,25 +4670,35 @@ var init_codex = __esm({
4656
4670
  fnCallBuilders.set(itemKey, {
4657
4671
  callId: item.call_id,
4658
4672
  name: item.name,
4659
- arguments: ""
4673
+ arguments: item.arguments ?? ""
4660
4674
  });
4675
+ if (typeof event.output_index === "number") {
4676
+ outputIndexToBuilderKey.set(event.output_index, itemKey);
4677
+ }
4661
4678
  }
4662
4679
  break;
4663
4680
  }
4664
4681
  case "response.function_call_arguments.delta": {
4665
- const builder = fnCallBuilders.get(event.item_id);
4682
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
4683
+ if (!builderKey) break;
4684
+ const builder = fnCallBuilders.get(builderKey);
4666
4685
  if (builder) builder.arguments += event.delta ?? "";
4667
4686
  break;
4668
4687
  }
4669
4688
  case "response.function_call_arguments.done": {
4670
- const builder = fnCallBuilders.get(event.item_id);
4689
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
4690
+ if (!builderKey) break;
4691
+ const builder = fnCallBuilders.get(builderKey);
4671
4692
  if (builder) {
4672
4693
  toolCalls.push({
4673
4694
  id: builder.callId,
4674
4695
  name: builder.name,
4675
- input: parseArguments(event.arguments)
4696
+ input: parseArguments(event.arguments ?? builder.arguments)
4676
4697
  });
4677
- fnCallBuilders.delete(event.item_id);
4698
+ fnCallBuilders.delete(builderKey);
4699
+ for (const [idx, key] of outputIndexToBuilderKey.entries()) {
4700
+ if (key === builderKey) outputIndexToBuilderKey.delete(idx);
4701
+ }
4678
4702
  }
4679
4703
  break;
4680
4704
  }
@@ -4787,6 +4811,7 @@ var init_codex = __esm({
4787
4811
  const decoder = new TextDecoder();
4788
4812
  let buffer = "";
4789
4813
  const fnCallBuilders = /* @__PURE__ */ new Map();
4814
+ const outputIndexToBuilderKey = /* @__PURE__ */ new Map();
4790
4815
  let lastActivityTime = Date.now();
4791
4816
  const timeoutController = new AbortController();
4792
4817
  const timeoutInterval = setInterval(() => {
@@ -4825,8 +4850,11 @@ var init_codex = __esm({
4825
4850
  fnCallBuilders.set(itemKey, {
4826
4851
  callId: item.call_id,
4827
4852
  name: item.name,
4828
- arguments: ""
4853
+ arguments: item.arguments ?? ""
4829
4854
  });
4855
+ if (typeof event.output_index === "number") {
4856
+ outputIndexToBuilderKey.set(event.output_index, itemKey);
4857
+ }
4830
4858
  yield {
4831
4859
  type: "tool_use_start",
4832
4860
  toolCall: { id: item.call_id, name: item.name }
@@ -4835,14 +4863,18 @@ var init_codex = __esm({
4835
4863
  break;
4836
4864
  }
4837
4865
  case "response.function_call_arguments.delta": {
4838
- const builder = fnCallBuilders.get(event.item_id);
4866
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
4867
+ if (!builderKey) break;
4868
+ const builder = fnCallBuilders.get(builderKey);
4839
4869
  if (builder) {
4840
4870
  builder.arguments += event.delta ?? "";
4841
4871
  }
4842
4872
  break;
4843
4873
  }
4844
4874
  case "response.function_call_arguments.done": {
4845
- const builder = fnCallBuilders.get(event.item_id);
4875
+ const builderKey = (event.item_id && fnCallBuilders.has(event.item_id) ? event.item_id : null) ?? (typeof event.output_index === "number" ? outputIndexToBuilderKey.get(event.output_index) ?? null : null) ?? (fnCallBuilders.size === 1 ? Array.from(fnCallBuilders.keys())[0] : null);
4876
+ if (!builderKey) break;
4877
+ const builder = fnCallBuilders.get(builderKey);
4846
4878
  if (builder) {
4847
4879
  yield {
4848
4880
  type: "tool_use_end",
@@ -4852,7 +4884,10 @@ var init_codex = __esm({
4852
4884
  input: parseArguments(event.arguments ?? builder.arguments)
4853
4885
  }
4854
4886
  };
4855
- fnCallBuilders.delete(event.item_id);
4887
+ fnCallBuilders.delete(builderKey);
4888
+ for (const [idx, key] of outputIndexToBuilderKey.entries()) {
4889
+ if (key === builderKey) outputIndexToBuilderKey.delete(idx);
4890
+ }
4856
4891
  }
4857
4892
  break;
4858
4893
  }
@@ -5243,8 +5278,8 @@ var init_gemini = __esm({
5243
5278
  const { history, lastMessage } = this.convertMessages(messages);
5244
5279
  const chat = model.startChat({ history });
5245
5280
  const result = await chat.sendMessageStream(lastMessage);
5246
- const emittedToolCalls = /* @__PURE__ */ new Set();
5247
5281
  let streamStopReason;
5282
+ let streamToolCallCounter = 0;
5248
5283
  for await (const chunk of result.stream) {
5249
5284
  const text13 = chunk.text();
5250
5285
  if (text13) {
@@ -5259,30 +5294,23 @@ var init_gemini = __esm({
5259
5294
  for (const part of candidate.content.parts) {
5260
5295
  if ("functionCall" in part && part.functionCall) {
5261
5296
  const funcCall = part.functionCall;
5262
- const sortedArgs = funcCall.args ? Object.keys(funcCall.args).sort().map(
5263
- (k) => `${k}:${JSON.stringify(funcCall.args[k])}`
5264
- ).join(",") : "";
5265
- const callKey = `${funcCall.name}-${sortedArgs}`;
5266
- if (!emittedToolCalls.has(callKey)) {
5267
- emittedToolCalls.add(callKey);
5268
- const toolCall = {
5269
- id: funcCall.name,
5270
- // Gemini uses name as ID
5271
- name: funcCall.name,
5272
- input: funcCall.args ?? {}
5273
- };
5274
- yield {
5275
- type: "tool_use_start",
5276
- toolCall: {
5277
- id: toolCall.id,
5278
- name: toolCall.name
5279
- }
5280
- };
5281
- yield {
5282
- type: "tool_use_end",
5283
- toolCall
5284
- };
5285
- }
5297
+ streamToolCallCounter++;
5298
+ const toolCall = {
5299
+ id: `gemini_call_${streamToolCallCounter}`,
5300
+ name: funcCall.name,
5301
+ input: funcCall.args ?? {}
5302
+ };
5303
+ yield {
5304
+ type: "tool_use_start",
5305
+ toolCall: {
5306
+ id: toolCall.id,
5307
+ name: toolCall.name
5308
+ }
5309
+ };
5310
+ yield {
5311
+ type: "tool_use_end",
5312
+ toolCall
5313
+ };
5286
5314
  }
5287
5315
  }
5288
5316
  }
@@ -5357,13 +5385,13 @@ var init_gemini = __esm({
5357
5385
  * Convert messages to Gemini format
5358
5386
  */
5359
5387
  convertMessages(messages) {
5388
+ const toolNameByUseId = this.buildToolUseNameMap(messages);
5389
+ const conversation = messages.filter((m) => m.role !== "system");
5360
5390
  const history = [];
5361
5391
  let lastUserMessage = "";
5362
- for (const msg of messages) {
5363
- if (msg.role === "system") {
5364
- continue;
5365
- }
5366
- const parts = this.convertContent(msg.content);
5392
+ for (let i = 0; i < conversation.length; i++) {
5393
+ const msg = conversation[i];
5394
+ const isLastMessage = i === conversation.length - 1;
5367
5395
  if (msg.role === "user") {
5368
5396
  if (Array.isArray(msg.content) && msg.content[0]?.type === "tool_result") {
5369
5397
  const functionResponses = [];
@@ -5372,23 +5400,49 @@ var init_gemini = __esm({
5372
5400
  const toolResult = block;
5373
5401
  functionResponses.push({
5374
5402
  functionResponse: {
5375
- name: toolResult.tool_use_id,
5376
- // Gemini uses name, we store it in tool_use_id
5403
+ // Gemini expects the function name in functionResponse.name.
5404
+ // Recover it from prior assistant tool_use blocks when possible.
5405
+ name: toolNameByUseId.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
5377
5406
  response: { result: toolResult.content }
5378
5407
  }
5379
5408
  });
5380
5409
  }
5381
5410
  }
5382
- history.push({ role: "user", parts: functionResponses });
5411
+ if (isLastMessage) {
5412
+ lastUserMessage = functionResponses;
5413
+ } else {
5414
+ history.push({ role: "user", parts: functionResponses });
5415
+ }
5383
5416
  } else {
5384
- lastUserMessage = parts;
5417
+ const parts = this.convertContent(msg.content);
5418
+ if (isLastMessage) {
5419
+ lastUserMessage = parts;
5420
+ } else {
5421
+ history.push({ role: "user", parts });
5422
+ }
5385
5423
  }
5386
5424
  } else if (msg.role === "assistant") {
5425
+ const parts = this.convertContent(msg.content);
5387
5426
  history.push({ role: "model", parts });
5388
5427
  }
5389
5428
  }
5390
5429
  return { history, lastMessage: lastUserMessage };
5391
5430
  }
5431
+ /**
5432
+ * Build a map from tool_use IDs to function names from assistant history.
5433
+ */
5434
+ buildToolUseNameMap(messages) {
5435
+ const map = /* @__PURE__ */ new Map();
5436
+ for (const msg of messages) {
5437
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
5438
+ for (const block of msg.content) {
5439
+ if (block.type === "tool_use") {
5440
+ map.set(block.id, block.name);
5441
+ }
5442
+ }
5443
+ }
5444
+ return map;
5445
+ }
5392
5446
  /**
5393
5447
  * Convert content to Gemini parts
5394
5448
  */
@@ -5465,14 +5519,15 @@ var init_gemini = __esm({
5465
5519
  let textContent = "";
5466
5520
  const toolCalls = [];
5467
5521
  if (candidate?.content?.parts) {
5522
+ let toolIndex = 0;
5468
5523
  for (const part of candidate.content.parts) {
5469
5524
  if ("text" in part && part.text) {
5470
5525
  textContent += part.text;
5471
5526
  }
5472
5527
  if ("functionCall" in part && part.functionCall) {
5528
+ toolIndex++;
5473
5529
  toolCalls.push({
5474
- id: part.functionCall.name,
5475
- // Use name as ID for Gemini
5530
+ id: `gemini_call_${toolIndex}`,
5476
5531
  name: part.functionCall.name,
5477
5532
  input: part.functionCall.args ?? {}
5478
5533
  });
@@ -9617,7 +9672,15 @@ Responses are short and direct by default. Lead with the answer or action, not r
9617
9672
 
9618
9673
  ## File Changes
9619
9674
 
9620
- **Never output raw diff or unified diff format in your responses.** Use edit_file and write_file to make changes \u2014 Coco renders a visual diff automatically. Do not quote file contents back to the user after reading them unless explicitly asked.`;
9675
+ **Never output raw diff or unified diff format in your responses.** Use edit_file and write_file to make changes \u2014 Coco renders a visual diff automatically.
9676
+
9677
+ ## Output Discipline
9678
+
9679
+ **NEVER echo file contents in your responses.** When you read a file, extract only the specific information needed \u2014 do NOT reprint whole files, functions, or large excerpts. The terminal shows which files you read and their line counts \u2014 the user does not need to see the content again.
9680
+
9681
+ **During tool-calling iterations, keep text minimal.** A single short orienting line before tool calls is acceptable. Do NOT explain every step, narrate what you are about to do, or produce paragraphs between tool calls. Reserve explanatory text for your final response after all tools have completed.
9682
+
9683
+ **Code blocks in responses are expensive.** Only include a code block when the user explicitly asks to see code, or when the code IS the deliverable (e.g., a script to paste in a terminal). Never include a code block to "show your work" when you can write the file directly instead.`;
9621
9684
  SHELL_METACHARACTERS = /[;|&`$(){}<>!\n\\'"]/;
9622
9685
  SAFE_COMMAND_VALIDATORS = {
9623
9686
  git: (args) => {
@@ -31538,6 +31601,15 @@ async function selectModelInteractively(models, currentModelId) {
31538
31601
  renderMenu();
31539
31602
  });
31540
31603
  }
31604
+ async function persistModelPreference(provider, model) {
31605
+ try {
31606
+ await saveProviderPreference(provider, model);
31607
+ } catch (error) {
31608
+ const reason = error instanceof Error ? error.message : String(error);
31609
+ console.log(chalk2.yellow(`\u26A0 Could not persist model preference: ${reason}`));
31610
+ console.log(chalk2.dim(" Model changed for this session only.\n"));
31611
+ }
31612
+ }
31541
31613
  var modelCommand = {
31542
31614
  name: "model",
31543
31615
  aliases: ["m"],
@@ -31586,7 +31658,7 @@ var modelCommand = {
31586
31658
  return false;
31587
31659
  }
31588
31660
  session.config.provider.model = selectedModel;
31589
- await saveProviderPreference(currentProvider, selectedModel);
31661
+ await persistModelPreference(currentProvider, selectedModel);
31590
31662
  const modelInfo2 = providerDef.models.find((m) => m.id === selectedModel);
31591
31663
  console.log(chalk2.green(`\u2713 Switched to ${modelInfo2?.name ?? selectedModel}
31592
31664
  `));
@@ -31608,7 +31680,7 @@ var modelCommand = {
31608
31680
  if (!foundInProvider) {
31609
31681
  console.log(chalk2.yellow(`Model "${newModel}" not in known list, setting anyway...`));
31610
31682
  session.config.provider.model = newModel;
31611
- await saveProviderPreference(currentProvider, newModel);
31683
+ await persistModelPreference(currentProvider, newModel);
31612
31684
  console.log(chalk2.green(`\u2713 Model set to: ${newModel}
31613
31685
  `));
31614
31686
  return false;
@@ -31623,7 +31695,7 @@ var modelCommand = {
31623
31695
  return false;
31624
31696
  }
31625
31697
  session.config.provider.model = newModel;
31626
- await saveProviderPreference(currentProvider, newModel);
31698
+ await persistModelPreference(currentProvider, newModel);
31627
31699
  const modelInfo = providerDef.models.find((m) => m.id === newModel);
31628
31700
  console.log(chalk2.green(`\u2713 Switched to ${modelInfo?.name ?? newModel}
31629
31701
  `));
@@ -47052,6 +47124,8 @@ var streamingIndicatorInterval = null;
47052
47124
  var streamingIndicatorFrame = 0;
47053
47125
  var STREAMING_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
47054
47126
  var getTerminalWidth2 = () => process.stdout.columns || 80;
47127
+ var MAX_STREAMING_CODE_LINES = 50;
47128
+ var STREAMING_CODE_HEAD_LINES = 40;
47055
47129
  function startStreamingIndicator() {
47056
47130
  if (streamingIndicatorActive) return;
47057
47131
  streamingIndicatorActive = true;
@@ -47375,11 +47449,17 @@ function renderSimpleCodeBlock(lang, lines, blockId) {
47375
47449
  const isDiff = lang === "diff" || !lang && looksLikeDiff(lines);
47376
47450
  const title = lang || "Code";
47377
47451
  console.log(chalk2.dim(`${title} \xB7 #${blockId}`));
47452
+ let displayLines = lines;
47453
+ let truncatedCount = 0;
47454
+ if (!isDiff && lines.length > MAX_STREAMING_CODE_LINES) {
47455
+ displayLines = lines.slice(0, STREAMING_CODE_HEAD_LINES);
47456
+ truncatedCount = lines.length - STREAMING_CODE_HEAD_LINES;
47457
+ }
47378
47458
  const bgDel = chalk2.bgRgb(80, 20, 20);
47379
47459
  const bgAdd = chalk2.bgRgb(20, 60, 20);
47380
47460
  let oldLineNo = 0;
47381
47461
  let newLineNo = 0;
47382
- for (const line of lines) {
47462
+ for (const line of displayLines) {
47383
47463
  if (isDiff) {
47384
47464
  const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
47385
47465
  if (hunkMatch) {
@@ -47407,6 +47487,9 @@ function renderSimpleCodeBlock(lang, lines, blockId) {
47407
47487
  }
47408
47488
  }
47409
47489
  }
47490
+ if (truncatedCount > 0) {
47491
+ console.log(chalk2.dim(` \u2026 ${truncatedCount} more lines \xB7 /copy ${blockId} for full content`));
47492
+ }
47410
47493
  console.log(chalk2.dim(` #${blockId} \xB7 /copy ${blockId}`));
47411
47494
  }
47412
47495
  function formatDiffLineNo(line, oldLineNo, newLineNo) {
@@ -50058,6 +50141,8 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
50058
50141
  const maxIterations = session.config.agent.maxToolIterations;
50059
50142
  const toolErrorCounts = /* @__PURE__ */ new Map();
50060
50143
  const MAX_CONSECUTIVE_TOOL_ERRORS = 3;
50144
+ const ITERATION_LIMIT_WARNING_RATIO = 0.75;
50145
+ const ITERATION_LIMIT_SUMMARY_PROMPT = `[System: You have now used all allowed iterations and tool calls are no longer available. Write your final response immediately: (1) briefly state what was completed, (2) describe what still needs to be done, (3) give specific next steps so the user can continue. Be concise and direct.]`;
50061
50146
  const INLINE_RESULT_MAX_CHARS = 8e3;
50062
50147
  const INLINE_RESULT_HEAD_CHARS = 6500;
50063
50148
  const INLINE_RESULT_TAIL_CHARS = 1e3;
@@ -50072,6 +50157,8 @@ ${tail}`;
50072
50157
  }
50073
50158
  while (iteration < maxIterations) {
50074
50159
  iteration++;
50160
+ const isLastIteration = iteration === maxIterations;
50161
+ const iterationTextChunks = [];
50075
50162
  if (options.signal?.aborted) {
50076
50163
  return abortReturn();
50077
50164
  }
@@ -50099,7 +50186,7 @@ ${tail}`;
50099
50186
  }
50100
50187
  responseContent += chunk.text;
50101
50188
  finalContent += chunk.text;
50102
- options.onStream?.(chunk);
50189
+ iterationTextChunks.push(chunk);
50103
50190
  }
50104
50191
  if (chunk.type === "tool_use_start" && chunk.toolCall) {
50105
50192
  flushLineBuffer();
@@ -50176,6 +50263,11 @@ ${tail}`;
50176
50263
  error: errorMsg
50177
50264
  };
50178
50265
  }
50266
+ if (collectedToolCalls.length === 0) {
50267
+ for (const bufferedChunk of iterationTextChunks) {
50268
+ options.onStream?.(bufferedChunk);
50269
+ }
50270
+ }
50179
50271
  const inputText = messages.map((m) => {
50180
50272
  if (typeof m.content === "string") return m.content;
50181
50273
  try {
@@ -50351,10 +50443,20 @@ ${tail}`;
50351
50443
  });
50352
50444
  const declineReason = declinedTools.get(toolCall.id);
50353
50445
  if (declineReason) {
50446
+ let skipContent;
50447
+ if (declineReason === "User declined") {
50448
+ skipContent = "The user explicitly declined this tool call. You MUST find a different approach \u2014 do not retry the same action or parameters.";
50449
+ } else if (declineReason.toLowerCase().includes("timeout") || declineReason.toLowerCase().includes("timed out")) {
50450
+ skipContent = `Tool execution timed out: ${declineReason}. Try a simpler or faster alternative, or break the task into smaller steps.`;
50451
+ } else if (declineReason.toLowerCase().includes("abort")) {
50452
+ skipContent = "Tool execution was cancelled.";
50453
+ } else {
50454
+ skipContent = `Tool execution was skipped: ${declineReason}`;
50455
+ }
50354
50456
  toolResults.push({
50355
50457
  type: "tool_result",
50356
50458
  tool_use_id: toolCall.id,
50357
- content: `Tool execution was declined: ${declineReason}`,
50459
+ content: skipContent,
50358
50460
  is_error: true
50359
50461
  });
50360
50462
  continue;
@@ -50408,6 +50510,28 @@ ${tail}`;
50408
50510
  }
50409
50511
  }
50410
50512
  }
50513
+ if (toolResults.length > 0) {
50514
+ const warningThreshold = Math.ceil(maxIterations * ITERATION_LIMIT_WARNING_RATIO);
50515
+ const lastIdx = toolResults.length - 1;
50516
+ const last = toolResults[lastIdx];
50517
+ if (isLastIteration && !stuckInErrorLoop) {
50518
+ toolResults[lastIdx] = {
50519
+ ...last,
50520
+ content: typeof last.content === "string" ? last.content + `
50521
+
50522
+ ${ITERATION_LIMIT_SUMMARY_PROMPT}` : ITERATION_LIMIT_SUMMARY_PROMPT
50523
+ };
50524
+ } else if (iteration === warningThreshold) {
50525
+ const remaining = maxIterations - iteration;
50526
+ const warning = `
50527
+
50528
+ [System: Iteration budget warning \u2014 ${iteration} of ${maxIterations} iterations used, ${remaining} remaining. Begin wrapping up your task. Prioritize the most critical remaining steps.]`;
50529
+ toolResults[lastIdx] = {
50530
+ ...last,
50531
+ content: typeof last.content === "string" ? last.content + warning : warning
50532
+ };
50533
+ }
50534
+ }
50411
50535
  const assistantContent = response.content ? [{ type: "text", text: response.content }, ...toolUses] : toolUses;
50412
50536
  addMessage(session, {
50413
50537
  role: "assistant",
@@ -50452,14 +50576,38 @@ ${tail}`;
50452
50576
  }
50453
50577
  break;
50454
50578
  }
50455
- }
50456
- if (iteration >= maxIterations) {
50457
- const notice = `
50579
+ if (isLastIteration && toolResults.length > 0) {
50580
+ let summaryThinkingEnded = false;
50581
+ options.onThinkingStart?.();
50582
+ try {
50583
+ const finalMessages = getConversationContext(session, toolRegistry);
50584
+ for await (const chunk of provider.streamWithTools(finalMessages, {
50585
+ tools: [],
50586
+ maxTokens: session.config.provider.maxTokens,
50587
+ signal: options.signal
50588
+ })) {
50589
+ if (options.signal?.aborted) break;
50590
+ if (chunk.type === "text" && chunk.text) {
50591
+ if (!summaryThinkingEnded) {
50592
+ options.onThinkingEnd?.();
50593
+ summaryThinkingEnded = true;
50594
+ }
50595
+ finalContent += chunk.text;
50596
+ options.onStream?.(chunk);
50597
+ }
50598
+ if (chunk.type === "done") break;
50599
+ }
50600
+ } catch {
50601
+ const notice = `
50458
50602
 
50459
- ---
50460
- _Reached the iteration limit (${maxIterations}). The task may be incomplete. You can say "continue" to resume._`;
50461
- finalContent += notice;
50462
- options.onStream?.({ type: "text", text: notice });
50603
+ I have reached the maximum iteration limit (${maxIterations}). The task may be incomplete. Type "continue" to give me more iterations, or describe what you'd like me to focus on next.`;
50604
+ finalContent += notice;
50605
+ options.onStream?.({ type: "text", text: notice });
50606
+ } finally {
50607
+ if (!summaryThinkingEnded) options.onThinkingEnd?.();
50608
+ }
50609
+ break;
50610
+ }
50463
50611
  }
50464
50612
  options.onStream?.({ type: "done" });
50465
50613
  return {