@corbat-tech/coco 2.22.1 → 2.23.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.
package/dist/cli/index.js CHANGED
@@ -1016,6 +1016,7 @@ var init_anthropic = __esm({
1016
1016
  */
1017
1017
  async *stream(messages, options) {
1018
1018
  this.ensureInitialized();
1019
+ let timeoutTriggered = false;
1019
1020
  try {
1020
1021
  const stream = await this.client.messages.stream(
1021
1022
  {
@@ -1033,6 +1034,7 @@ var init_anthropic = __esm({
1033
1034
  const timeoutInterval = setInterval(() => {
1034
1035
  if (Date.now() - lastActivityTime > streamTimeout) {
1035
1036
  clearInterval(timeoutInterval);
1037
+ timeoutTriggered = true;
1036
1038
  timeoutController.abort();
1037
1039
  }
1038
1040
  }, 5e3);
@@ -1063,6 +1065,11 @@ var init_anthropic = __esm({
1063
1065
  throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1064
1066
  }
1065
1067
  } catch (error) {
1068
+ if (timeoutTriggered) {
1069
+ throw new Error(
1070
+ `Stream timeout: No response from LLM for ${(this.config.timeout ?? 12e4) / 1e3}s`
1071
+ );
1072
+ }
1066
1073
  throw this.handleError(error);
1067
1074
  }
1068
1075
  }
@@ -1071,6 +1078,7 @@ var init_anthropic = __esm({
1071
1078
  */
1072
1079
  async *streamWithTools(messages, options) {
1073
1080
  this.ensureInitialized();
1081
+ let timeoutTriggered = false;
1074
1082
  try {
1075
1083
  const stream = await this.client.messages.stream(
1076
1084
  {
@@ -1092,6 +1100,7 @@ var init_anthropic = __esm({
1092
1100
  const timeoutInterval = setInterval(() => {
1093
1101
  if (Date.now() - lastActivityTime > streamTimeout) {
1094
1102
  clearInterval(timeoutInterval);
1103
+ timeoutTriggered = true;
1095
1104
  timeoutController.abort();
1096
1105
  }
1097
1106
  }, 5e3);
@@ -1186,6 +1195,11 @@ var init_anthropic = __esm({
1186
1195
  throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1187
1196
  }
1188
1197
  } catch (error) {
1198
+ if (timeoutTriggered) {
1199
+ throw new Error(
1200
+ `Stream timeout: No response from LLM for ${(this.config.timeout ?? 12e4) / 1e3}s`
1201
+ );
1202
+ }
1189
1203
  throw this.handleError(error);
1190
1204
  }
1191
1205
  }
@@ -1755,6 +1769,7 @@ var init_openai = __esm({
1755
1769
  yield* this.streamWithToolsViaResponses(messages, options);
1756
1770
  return;
1757
1771
  }
1772
+ let timeoutTriggered = false;
1758
1773
  try {
1759
1774
  const supportsTemp = this.supportsTemperature(model);
1760
1775
  const extraBody = this.getExtraBody(model);
@@ -1783,6 +1798,7 @@ var init_openai = __esm({
1783
1798
  const timeoutInterval = setInterval(() => {
1784
1799
  if (Date.now() - lastActivityTime > streamTimeout) {
1785
1800
  clearInterval(timeoutInterval);
1801
+ timeoutTriggered = true;
1786
1802
  timeoutController.abort();
1787
1803
  }
1788
1804
  }, 5e3);
@@ -1896,6 +1912,11 @@ var init_openai = __esm({
1896
1912
  throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1897
1913
  }
1898
1914
  } catch (error) {
1915
+ if (timeoutTriggered) {
1916
+ throw new Error(
1917
+ `Stream timeout: No response from LLM for ${(this.config.timeout ?? 12e4) / 1e3}s`
1918
+ );
1919
+ }
1899
1920
  throw this.handleError(error);
1900
1921
  }
1901
1922
  }
@@ -2341,6 +2362,7 @@ var init_openai = __esm({
2341
2362
  */
2342
2363
  async *streamViaResponses(messages, options) {
2343
2364
  this.ensureInitialized();
2365
+ let timeoutTriggered = false;
2344
2366
  try {
2345
2367
  const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
2346
2368
  const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
@@ -2359,6 +2381,7 @@ var init_openai = __esm({
2359
2381
  const timeoutInterval = setInterval(() => {
2360
2382
  if (Date.now() - lastActivityTime > streamTimeout) {
2361
2383
  clearInterval(timeoutInterval);
2384
+ timeoutTriggered = true;
2362
2385
  timeoutController.abort();
2363
2386
  }
2364
2387
  }, 5e3);
@@ -2383,6 +2406,11 @@ var init_openai = __esm({
2383
2406
  throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
2384
2407
  }
2385
2408
  } catch (error) {
2409
+ if (timeoutTriggered) {
2410
+ throw new Error(
2411
+ `Stream timeout: No response from LLM for ${(this.config.timeout ?? 12e4) / 1e3}s`
2412
+ );
2413
+ }
2386
2414
  throw this.handleError(error);
2387
2415
  }
2388
2416
  }
@@ -2395,6 +2423,7 @@ var init_openai = __esm({
2395
2423
  */
2396
2424
  async *streamWithToolsViaResponses(messages, options) {
2397
2425
  this.ensureInitialized();
2426
+ let timeoutTriggered = false;
2398
2427
  try {
2399
2428
  const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
2400
2429
  const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
@@ -2421,6 +2450,7 @@ var init_openai = __esm({
2421
2450
  const timeoutInterval = setInterval(() => {
2422
2451
  if (Date.now() - lastActivityTime > streamTimeout) {
2423
2452
  clearInterval(timeoutInterval);
2453
+ timeoutTriggered = true;
2424
2454
  timeoutController.abort();
2425
2455
  }
2426
2456
  }, 5e3);
@@ -2506,6 +2536,11 @@ var init_openai = __esm({
2506
2536
  throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
2507
2537
  }
2508
2538
  } catch (error) {
2539
+ if (timeoutTriggered) {
2540
+ throw new Error(
2541
+ `Stream timeout: No response from LLM for ${(this.config.timeout ?? 12e4) / 1e3}s`
2542
+ );
2543
+ }
2509
2544
  throw this.handleError(error);
2510
2545
  }
2511
2546
  }
@@ -9582,7 +9617,15 @@ Responses are short and direct by default. Lead with the answer or action, not r
9582
9617
 
9583
9618
  ## File Changes
9584
9619
 
9585
- **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.`;
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.
9621
+
9622
+ ## Output Discipline
9623
+
9624
+ **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.
9625
+
9626
+ **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.
9627
+
9628
+ **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.`;
9586
9629
  SHELL_METACHARACTERS = /[;|&`$(){}<>!\n\\'"]/;
9587
9630
  SAFE_COMMAND_VALIDATORS = {
9588
9631
  git: (args) => {
@@ -47017,6 +47060,8 @@ var streamingIndicatorInterval = null;
47017
47060
  var streamingIndicatorFrame = 0;
47018
47061
  var STREAMING_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
47019
47062
  var getTerminalWidth2 = () => process.stdout.columns || 80;
47063
+ var MAX_STREAMING_CODE_LINES = 50;
47064
+ var STREAMING_CODE_HEAD_LINES = 40;
47020
47065
  function startStreamingIndicator() {
47021
47066
  if (streamingIndicatorActive) return;
47022
47067
  streamingIndicatorActive = true;
@@ -47340,11 +47385,17 @@ function renderSimpleCodeBlock(lang, lines, blockId) {
47340
47385
  const isDiff = lang === "diff" || !lang && looksLikeDiff(lines);
47341
47386
  const title = lang || "Code";
47342
47387
  console.log(chalk2.dim(`${title} \xB7 #${blockId}`));
47388
+ let displayLines = lines;
47389
+ let truncatedCount = 0;
47390
+ if (!isDiff && lines.length > MAX_STREAMING_CODE_LINES) {
47391
+ displayLines = lines.slice(0, STREAMING_CODE_HEAD_LINES);
47392
+ truncatedCount = lines.length - STREAMING_CODE_HEAD_LINES;
47393
+ }
47343
47394
  const bgDel = chalk2.bgRgb(80, 20, 20);
47344
47395
  const bgAdd = chalk2.bgRgb(20, 60, 20);
47345
47396
  let oldLineNo = 0;
47346
47397
  let newLineNo = 0;
47347
- for (const line of lines) {
47398
+ for (const line of displayLines) {
47348
47399
  if (isDiff) {
47349
47400
  const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
47350
47401
  if (hunkMatch) {
@@ -47372,6 +47423,9 @@ function renderSimpleCodeBlock(lang, lines, blockId) {
47372
47423
  }
47373
47424
  }
47374
47425
  }
47426
+ if (truncatedCount > 0) {
47427
+ console.log(chalk2.dim(` \u2026 ${truncatedCount} more lines \xB7 /copy ${blockId} for full content`));
47428
+ }
47375
47429
  console.log(chalk2.dim(` #${blockId} \xB7 /copy ${blockId}`));
47376
47430
  }
47377
47431
  function formatDiffLineNo(line, oldLineNo, newLineNo) {
@@ -50023,6 +50077,8 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
50023
50077
  const maxIterations = session.config.agent.maxToolIterations;
50024
50078
  const toolErrorCounts = /* @__PURE__ */ new Map();
50025
50079
  const MAX_CONSECUTIVE_TOOL_ERRORS = 3;
50080
+ const ITERATION_LIMIT_WARNING_RATIO = 0.75;
50081
+ 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.]`;
50026
50082
  const INLINE_RESULT_MAX_CHARS = 8e3;
50027
50083
  const INLINE_RESULT_HEAD_CHARS = 6500;
50028
50084
  const INLINE_RESULT_TAIL_CHARS = 1e3;
@@ -50037,6 +50093,8 @@ ${tail}`;
50037
50093
  }
50038
50094
  while (iteration < maxIterations) {
50039
50095
  iteration++;
50096
+ const isLastIteration = iteration === maxIterations;
50097
+ const iterationTextChunks = [];
50040
50098
  if (options.signal?.aborted) {
50041
50099
  return abortReturn();
50042
50100
  }
@@ -50064,7 +50122,7 @@ ${tail}`;
50064
50122
  }
50065
50123
  responseContent += chunk.text;
50066
50124
  finalContent += chunk.text;
50067
- options.onStream?.(chunk);
50125
+ iterationTextChunks.push(chunk);
50068
50126
  }
50069
50127
  if (chunk.type === "tool_use_start" && chunk.toolCall) {
50070
50128
  flushLineBuffer();
@@ -50141,6 +50199,11 @@ ${tail}`;
50141
50199
  error: errorMsg
50142
50200
  };
50143
50201
  }
50202
+ if (collectedToolCalls.length === 0) {
50203
+ for (const bufferedChunk of iterationTextChunks) {
50204
+ options.onStream?.(bufferedChunk);
50205
+ }
50206
+ }
50144
50207
  const inputText = messages.map((m) => {
50145
50208
  if (typeof m.content === "string") return m.content;
50146
50209
  try {
@@ -50316,10 +50379,20 @@ ${tail}`;
50316
50379
  });
50317
50380
  const declineReason = declinedTools.get(toolCall.id);
50318
50381
  if (declineReason) {
50382
+ let skipContent;
50383
+ if (declineReason === "User declined") {
50384
+ skipContent = "The user explicitly declined this tool call. You MUST find a different approach \u2014 do not retry the same action or parameters.";
50385
+ } else if (declineReason.toLowerCase().includes("timeout") || declineReason.toLowerCase().includes("timed out")) {
50386
+ skipContent = `Tool execution timed out: ${declineReason}. Try a simpler or faster alternative, or break the task into smaller steps.`;
50387
+ } else if (declineReason.toLowerCase().includes("abort")) {
50388
+ skipContent = "Tool execution was cancelled.";
50389
+ } else {
50390
+ skipContent = `Tool execution was skipped: ${declineReason}`;
50391
+ }
50319
50392
  toolResults.push({
50320
50393
  type: "tool_result",
50321
50394
  tool_use_id: toolCall.id,
50322
- content: `Tool execution was declined: ${declineReason}`,
50395
+ content: skipContent,
50323
50396
  is_error: true
50324
50397
  });
50325
50398
  continue;
@@ -50373,6 +50446,28 @@ ${tail}`;
50373
50446
  }
50374
50447
  }
50375
50448
  }
50449
+ if (toolResults.length > 0) {
50450
+ const warningThreshold = Math.ceil(maxIterations * ITERATION_LIMIT_WARNING_RATIO);
50451
+ const lastIdx = toolResults.length - 1;
50452
+ const last = toolResults[lastIdx];
50453
+ if (isLastIteration && !stuckInErrorLoop) {
50454
+ toolResults[lastIdx] = {
50455
+ ...last,
50456
+ content: typeof last.content === "string" ? last.content + `
50457
+
50458
+ ${ITERATION_LIMIT_SUMMARY_PROMPT}` : ITERATION_LIMIT_SUMMARY_PROMPT
50459
+ };
50460
+ } else if (iteration === warningThreshold) {
50461
+ const remaining = maxIterations - iteration;
50462
+ const warning = `
50463
+
50464
+ [System: Iteration budget warning \u2014 ${iteration} of ${maxIterations} iterations used, ${remaining} remaining. Begin wrapping up your task. Prioritize the most critical remaining steps.]`;
50465
+ toolResults[lastIdx] = {
50466
+ ...last,
50467
+ content: typeof last.content === "string" ? last.content + warning : warning
50468
+ };
50469
+ }
50470
+ }
50376
50471
  const assistantContent = response.content ? [{ type: "text", text: response.content }, ...toolUses] : toolUses;
50377
50472
  addMessage(session, {
50378
50473
  role: "assistant",
@@ -50417,14 +50512,38 @@ ${tail}`;
50417
50512
  }
50418
50513
  break;
50419
50514
  }
50420
- }
50421
- if (iteration >= maxIterations) {
50422
- const notice = `
50515
+ if (isLastIteration && toolResults.length > 0) {
50516
+ let summaryThinkingEnded = false;
50517
+ options.onThinkingStart?.();
50518
+ try {
50519
+ const finalMessages = getConversationContext(session, toolRegistry);
50520
+ for await (const chunk of provider.streamWithTools(finalMessages, {
50521
+ tools: [],
50522
+ maxTokens: session.config.provider.maxTokens,
50523
+ signal: options.signal
50524
+ })) {
50525
+ if (options.signal?.aborted) break;
50526
+ if (chunk.type === "text" && chunk.text) {
50527
+ if (!summaryThinkingEnded) {
50528
+ options.onThinkingEnd?.();
50529
+ summaryThinkingEnded = true;
50530
+ }
50531
+ finalContent += chunk.text;
50532
+ options.onStream?.(chunk);
50533
+ }
50534
+ if (chunk.type === "done") break;
50535
+ }
50536
+ } catch {
50537
+ const notice = `
50423
50538
 
50424
- ---
50425
- _Reached the iteration limit (${maxIterations}). The task may be incomplete. You can say "continue" to resume._`;
50426
- finalContent += notice;
50427
- options.onStream?.({ type: "text", text: notice });
50539
+ 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.`;
50540
+ finalContent += notice;
50541
+ options.onStream?.({ type: "text", text: notice });
50542
+ } finally {
50543
+ if (!summaryThinkingEnded) options.onThinkingEnd?.();
50544
+ }
50545
+ break;
50546
+ }
50428
50547
  }
50429
50548
  options.onStream?.({ type: "done" });
50430
50549
  return {
@@ -51623,6 +51742,9 @@ ${imagePrompts}`.trim() : imagePrompts;
51623
51742
  result.toolCalls.length
51624
51743
  );
51625
51744
  }
51745
+ if (result.toolCalls.length > 0) {
51746
+ console.log(chalk2.dim(" Type your request again to resume where you left off."));
51747
+ }
51626
51748
  console.log();
51627
51749
  continue;
51628
51750
  }
@@ -51646,6 +51768,9 @@ ${imagePrompts}`.trim() : imagePrompts;
51646
51768
  console.log(
51647
51769
  chalk2.dim(" Recovery failed or error is non-retryable. Returning to prompt.")
51648
51770
  );
51771
+ console.log(
51772
+ chalk2.dim(" Tip: Try /provider or /model to switch, then rephrase and retry.")
51773
+ );
51649
51774
  consecutiveErrors = 0;
51650
51775
  }
51651
51776
  console.log();
@@ -51713,12 +51838,20 @@ ${imagePrompts}`.trim() : imagePrompts;
51713
51838
  compactSpinner.clear();
51714
51839
  }
51715
51840
  } catch {
51716
- compactSpinner.clear();
51841
+ compactSpinner.stop("\u26A0 Context compaction failed");
51842
+ console.log(
51843
+ chalk2.yellow(
51844
+ " \u26A0 Context compaction failed \u2014 context unchanged. Use /clear if needed."
51845
+ )
51846
+ );
51717
51847
  } finally {
51718
51848
  clearTimeout(compactTimeout);
51719
51849
  process.off("SIGINT", compactSigint);
51720
51850
  }
51721
51851
  } catch {
51852
+ console.log(
51853
+ chalk2.yellow(" \u26A0 Context compaction failed \u2014 context unchanged. Use /clear if needed.")
51854
+ );
51722
51855
  }
51723
51856
  renderStatusBar(session.projectPath, session.config, gitContext, usageForDisplay);
51724
51857
  if (usageForDisplay >= 90 && !warned90) {
@@ -51825,11 +51958,15 @@ ${imagePrompts}`.trim() : imagePrompts;
51825
51958
  session.messages.length = preCallMessageLength;
51826
51959
  renderError(errorMsg);
51827
51960
  console.log(chalk2.dim(" Recovery failed after multiple attempts. Returning to prompt."));
51961
+ console.log(
51962
+ chalk2.dim(" Tip: Try /provider or /model to switch, then rephrase and retry.")
51963
+ );
51828
51964
  continue;
51829
51965
  }
51830
51966
  session.messages.length = preCallMessageLength;
51831
51967
  consecutiveErrors = 0;
51832
51968
  renderError(errorMsg);
51969
+ console.log(chalk2.dim(" Tip: Try /provider or /model to switch, then rephrase and retry."));
51833
51970
  } finally {
51834
51971
  clearSpinner();
51835
51972
  if (originalSystemPrompt !== void 0) {