@corbat-tech/coco 2.8.0 → 2.8.2

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
@@ -1012,22 +1012,44 @@ var init_anthropic = __esm({
1012
1012
  async *stream(messages, options) {
1013
1013
  this.ensureInitialized();
1014
1014
  try {
1015
- const stream = await this.client.messages.stream({
1016
- model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
1017
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1018
- temperature: options?.temperature ?? this.config.temperature ?? 0,
1019
- system: this.extractSystem(messages, options?.system),
1020
- messages: this.convertMessages(messages)
1021
- });
1022
- for await (const event of stream) {
1023
- if (event.type === "content_block_delta") {
1024
- const delta = event.delta;
1025
- if (delta.type === "text_delta" && delta.text) {
1026
- yield { type: "text", text: delta.text };
1015
+ const stream = await this.client.messages.stream(
1016
+ {
1017
+ model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
1018
+ max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1019
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
1020
+ system: this.extractSystem(messages, options?.system),
1021
+ messages: this.convertMessages(messages)
1022
+ },
1023
+ { signal: options?.signal }
1024
+ );
1025
+ const streamTimeout = this.config.timeout ?? 12e4;
1026
+ let lastActivityTime = Date.now();
1027
+ const checkTimeout = () => {
1028
+ if (Date.now() - lastActivityTime > streamTimeout) {
1029
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1030
+ }
1031
+ };
1032
+ const timeoutInterval = setInterval(checkTimeout, 5e3);
1033
+ try {
1034
+ let streamStopReason;
1035
+ for await (const event of stream) {
1036
+ lastActivityTime = Date.now();
1037
+ if (event.type === "content_block_delta") {
1038
+ const delta = event.delta;
1039
+ if (delta.type === "text_delta" && delta.text) {
1040
+ yield { type: "text", text: delta.text };
1041
+ }
1042
+ } else if (event.type === "message_delta") {
1043
+ const delta = event.delta;
1044
+ if (delta.stop_reason) {
1045
+ streamStopReason = this.mapStopReason(delta.stop_reason);
1046
+ }
1027
1047
  }
1028
1048
  }
1049
+ yield { type: "done", stopReason: streamStopReason };
1050
+ } finally {
1051
+ clearInterval(timeoutInterval);
1029
1052
  }
1030
- yield { type: "done" };
1031
1053
  } catch (error) {
1032
1054
  throw this.handleError(error);
1033
1055
  }
@@ -1038,90 +1060,112 @@ var init_anthropic = __esm({
1038
1060
  async *streamWithTools(messages, options) {
1039
1061
  this.ensureInitialized();
1040
1062
  try {
1041
- const stream = await this.client.messages.stream({
1042
- model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
1043
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1044
- temperature: options?.temperature ?? this.config.temperature ?? 0,
1045
- system: this.extractSystem(messages, options?.system),
1046
- messages: this.convertMessages(messages),
1047
- tools: this.convertTools(options.tools),
1048
- tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
1049
- });
1063
+ const stream = await this.client.messages.stream(
1064
+ {
1065
+ model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
1066
+ max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1067
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
1068
+ system: this.extractSystem(messages, options?.system),
1069
+ messages: this.convertMessages(messages),
1070
+ tools: this.convertTools(options.tools),
1071
+ tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
1072
+ },
1073
+ { signal: options?.signal }
1074
+ );
1050
1075
  let currentToolCall = null;
1051
1076
  let currentToolInputJson = "";
1052
- for await (const event of stream) {
1053
- if (event.type === "content_block_start") {
1054
- const contentBlock = event.content_block;
1055
- if (contentBlock.type === "tool_use") {
1077
+ const streamTimeout = this.config.timeout ?? 12e4;
1078
+ let lastActivityTime = Date.now();
1079
+ const checkTimeout = () => {
1080
+ if (Date.now() - lastActivityTime > streamTimeout) {
1081
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1082
+ }
1083
+ };
1084
+ const timeoutInterval = setInterval(checkTimeout, 5e3);
1085
+ try {
1086
+ let streamStopReason;
1087
+ for await (const event of stream) {
1088
+ lastActivityTime = Date.now();
1089
+ if (event.type === "message_delta") {
1090
+ const delta = event.delta;
1091
+ if (delta.stop_reason) {
1092
+ streamStopReason = this.mapStopReason(delta.stop_reason);
1093
+ }
1094
+ } else if (event.type === "content_block_start") {
1095
+ const contentBlock = event.content_block;
1096
+ if (contentBlock.type === "tool_use") {
1097
+ if (currentToolCall) {
1098
+ getLogger().warn(
1099
+ `[Anthropic] content_block_stop missing for tool '${currentToolCall.name}' \u2014 finalizing early to prevent data bleed.`
1100
+ );
1101
+ try {
1102
+ currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1103
+ } catch {
1104
+ currentToolCall.input = {};
1105
+ }
1106
+ yield {
1107
+ type: "tool_use_end",
1108
+ toolCall: { ...currentToolCall }
1109
+ };
1110
+ }
1111
+ currentToolCall = {
1112
+ id: contentBlock.id,
1113
+ name: contentBlock.name
1114
+ };
1115
+ currentToolInputJson = "";
1116
+ yield {
1117
+ type: "tool_use_start",
1118
+ toolCall: { ...currentToolCall }
1119
+ };
1120
+ }
1121
+ } else if (event.type === "content_block_delta") {
1122
+ const delta = event.delta;
1123
+ if (delta.type === "text_delta" && delta.text) {
1124
+ yield { type: "text", text: delta.text };
1125
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
1126
+ currentToolInputJson += delta.partial_json;
1127
+ yield {
1128
+ type: "tool_use_delta",
1129
+ toolCall: {
1130
+ ...currentToolCall
1131
+ },
1132
+ text: delta.partial_json
1133
+ };
1134
+ }
1135
+ } else if (event.type === "content_block_stop") {
1056
1136
  if (currentToolCall) {
1057
- getLogger().warn(
1058
- `[Anthropic] content_block_stop missing for tool '${currentToolCall.name}' \u2014 finalizing early to prevent data bleed.`
1059
- );
1060
1137
  try {
1061
1138
  currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1062
1139
  } catch {
1063
- currentToolCall.input = {};
1140
+ let repaired = false;
1141
+ if (currentToolInputJson) {
1142
+ try {
1143
+ currentToolCall.input = JSON.parse(jsonrepair(currentToolInputJson));
1144
+ repaired = true;
1145
+ getLogger().debug(`Repaired JSON for tool ${currentToolCall.name}`);
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ if (!repaired) {
1150
+ getLogger().warn(
1151
+ `Failed to parse tool call arguments for ${currentToolCall.name}: ${currentToolInputJson?.slice(0, 300)}`
1152
+ );
1153
+ currentToolCall.input = {};
1154
+ }
1064
1155
  }
1065
1156
  yield {
1066
1157
  type: "tool_use_end",
1067
1158
  toolCall: { ...currentToolCall }
1068
1159
  };
1160
+ currentToolCall = null;
1161
+ currentToolInputJson = "";
1069
1162
  }
1070
- currentToolCall = {
1071
- id: contentBlock.id,
1072
- name: contentBlock.name
1073
- };
1074
- currentToolInputJson = "";
1075
- yield {
1076
- type: "tool_use_start",
1077
- toolCall: { ...currentToolCall }
1078
- };
1079
- }
1080
- } else if (event.type === "content_block_delta") {
1081
- const delta = event.delta;
1082
- if (delta.type === "text_delta" && delta.text) {
1083
- yield { type: "text", text: delta.text };
1084
- } else if (delta.type === "input_json_delta" && delta.partial_json) {
1085
- currentToolInputJson += delta.partial_json;
1086
- yield {
1087
- type: "tool_use_delta",
1088
- toolCall: {
1089
- ...currentToolCall
1090
- },
1091
- text: delta.partial_json
1092
- };
1093
- }
1094
- } else if (event.type === "content_block_stop") {
1095
- if (currentToolCall) {
1096
- try {
1097
- currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1098
- } catch {
1099
- let repaired = false;
1100
- if (currentToolInputJson) {
1101
- try {
1102
- currentToolCall.input = JSON.parse(jsonrepair(currentToolInputJson));
1103
- repaired = true;
1104
- getLogger().debug(`Repaired JSON for tool ${currentToolCall.name}`);
1105
- } catch {
1106
- }
1107
- }
1108
- if (!repaired) {
1109
- getLogger().warn(
1110
- `Failed to parse tool call arguments for ${currentToolCall.name}: ${currentToolInputJson?.slice(0, 300)}`
1111
- );
1112
- currentToolCall.input = {};
1113
- }
1114
- }
1115
- yield {
1116
- type: "tool_use_end",
1117
- toolCall: { ...currentToolCall }
1118
- };
1119
- currentToolCall = null;
1120
- currentToolInputJson = "";
1121
1163
  }
1122
1164
  }
1165
+ yield { type: "done", stopReason: streamStopReason };
1166
+ } finally {
1167
+ clearInterval(timeoutInterval);
1123
1168
  }
1124
- yield { type: "done" };
1125
1169
  } catch (error) {
1126
1170
  throw this.handleError(error);
1127
1171
  }
@@ -1621,13 +1665,18 @@ var init_openai = __esm({
1621
1665
  stream: true,
1622
1666
  ...supportsTemp && { temperature: options?.temperature ?? this.config.temperature ?? 0 }
1623
1667
  });
1668
+ let streamStopReason;
1624
1669
  for await (const chunk of stream) {
1625
1670
  const delta = chunk.choices[0]?.delta;
1626
1671
  if (delta?.content) {
1627
1672
  yield { type: "text", text: delta.content };
1628
1673
  }
1674
+ const finishReason = chunk.choices[0]?.finish_reason;
1675
+ if (finishReason) {
1676
+ streamStopReason = this.mapFinishReason(finishReason);
1677
+ }
1629
1678
  }
1630
- yield { type: "done" };
1679
+ yield { type: "done", stopReason: streamStopReason };
1631
1680
  } catch (error) {
1632
1681
  throw this.handleError(error);
1633
1682
  }
@@ -1692,6 +1741,7 @@ var init_openai = __esm({
1692
1741
  return input;
1693
1742
  };
1694
1743
  try {
1744
+ let streamStopReason;
1695
1745
  for await (const chunk of stream) {
1696
1746
  const delta = chunk.choices[0]?.delta;
1697
1747
  if (delta?.content || delta?.tool_calls) {
@@ -1738,6 +1788,9 @@ var init_openai = __esm({
1738
1788
  }
1739
1789
  }
1740
1790
  const finishReason = chunk.choices[0]?.finish_reason;
1791
+ if (finishReason) {
1792
+ streamStopReason = this.mapFinishReason(finishReason);
1793
+ }
1741
1794
  if (finishReason && toolCallBuilders.size > 0) {
1742
1795
  for (const [, builder] of toolCallBuilders) {
1743
1796
  yield {
@@ -1762,7 +1815,7 @@ var init_openai = __esm({
1762
1815
  }
1763
1816
  };
1764
1817
  }
1765
- yield { type: "done" };
1818
+ yield { type: "done", stopReason: streamStopReason };
1766
1819
  } finally {
1767
1820
  clearInterval(timeoutInterval);
1768
1821
  }
@@ -3569,7 +3622,7 @@ var init_codex = __esm({
3569
3622
  }
3570
3623
  }
3571
3624
  }
3572
- yield { type: "done" };
3625
+ yield { type: "done", stopReason: response.stopReason };
3573
3626
  }
3574
3627
  /**
3575
3628
  * Stream a chat response with tool use
@@ -3737,13 +3790,18 @@ var init_gemini = __esm({
3737
3790
  const { history, lastMessage } = this.convertMessages(messages);
3738
3791
  const chat = model.startChat({ history });
3739
3792
  const result = await chat.sendMessageStream(lastMessage);
3793
+ let streamStopReason;
3740
3794
  for await (const chunk of result.stream) {
3741
3795
  const text13 = chunk.text();
3742
3796
  if (text13) {
3743
3797
  yield { type: "text", text: text13 };
3744
3798
  }
3799
+ const finishReason = chunk.candidates?.[0]?.finishReason;
3800
+ if (finishReason) {
3801
+ streamStopReason = this.mapFinishReason(finishReason);
3802
+ }
3745
3803
  }
3746
- yield { type: "done" };
3804
+ yield { type: "done", stopReason: streamStopReason };
3747
3805
  } catch (error) {
3748
3806
  throw this.handleError(error);
3749
3807
  }
@@ -3778,11 +3836,16 @@ var init_gemini = __esm({
3778
3836
  const chat = model.startChat({ history });
3779
3837
  const result = await chat.sendMessageStream(lastMessage);
3780
3838
  const emittedToolCalls = /* @__PURE__ */ new Set();
3839
+ let streamStopReason;
3781
3840
  for await (const chunk of result.stream) {
3782
3841
  const text13 = chunk.text();
3783
3842
  if (text13) {
3784
3843
  yield { type: "text", text: text13 };
3785
3844
  }
3845
+ const finishReason = chunk.candidates?.[0]?.finishReason;
3846
+ if (finishReason) {
3847
+ streamStopReason = this.mapFinishReason(finishReason);
3848
+ }
3786
3849
  const candidate = chunk.candidates?.[0];
3787
3850
  if (candidate?.content?.parts) {
3788
3851
  for (const part of candidate.content.parts) {
@@ -3816,7 +3879,7 @@ var init_gemini = __esm({
3816
3879
  }
3817
3880
  }
3818
3881
  }
3819
- yield { type: "done" };
3882
+ yield { type: "done", stopReason: streamStopReason };
3820
3883
  } catch (error) {
3821
3884
  throw this.handleError(error);
3822
3885
  }
@@ -6564,7 +6627,7 @@ CONVERSATION:
6564
6627
  * @param provider - The LLM provider to use for summarization
6565
6628
  * @returns Compacted messages with summary replacing older messages
6566
6629
  */
6567
- async compact(messages, provider) {
6630
+ async compact(messages, provider, signal) {
6568
6631
  const conversationMessages = messages.filter((m) => m.role !== "system");
6569
6632
  if (conversationMessages.length <= this.config.preserveLastN) {
6570
6633
  return {
@@ -6596,7 +6659,7 @@ CONVERSATION:
6596
6659
  }
6597
6660
  const originalTokens = this.estimateTokens(messages, provider);
6598
6661
  const conversationText = this.formatMessagesForSummary(messagesToSummarize);
6599
- const summary = await this.generateSummary(conversationText, provider);
6662
+ const summary = await this.generateSummary(conversationText, provider, signal);
6600
6663
  const systemMessages = messages.filter((m) => m.role === "system");
6601
6664
  const summaryMessage = {
6602
6665
  role: "user",
@@ -6650,16 +6713,30 @@ ${summary}
6650
6713
  /**
6651
6714
  * Generate a summary of the conversation using the LLM
6652
6715
  */
6653
- async generateSummary(conversationText, provider) {
6716
+ async generateSummary(conversationText, provider, signal) {
6717
+ if (signal?.aborted) return "[Compaction cancelled]";
6654
6718
  const prompt = COMPACTION_PROMPT + conversationText;
6655
6719
  try {
6656
- const response = await provider.chat([{ role: "user", content: prompt }], {
6720
+ const chatPromise = provider.chat([{ role: "user", content: prompt }], {
6657
6721
  maxTokens: this.config.summaryMaxTokens,
6658
6722
  temperature: 0.3
6659
6723
  // Lower temperature for more consistent summaries
6660
6724
  });
6725
+ if (signal) {
6726
+ const abortPromise = new Promise((_, reject) => {
6727
+ signal.addEventListener(
6728
+ "abort",
6729
+ () => reject(new DOMException("Aborted", "AbortError")),
6730
+ { once: true }
6731
+ );
6732
+ });
6733
+ const response2 = await Promise.race([chatPromise, abortPromise]);
6734
+ return response2.content;
6735
+ }
6736
+ const response = await chatPromise;
6661
6737
  return response.content;
6662
6738
  } catch (error) {
6739
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
6663
6740
  const errorMessage = error instanceof Error ? error.message : String(error);
6664
6741
  return `[Summary generation failed: ${errorMessage}. Previous conversation had ${conversationText.length} characters.]`;
6665
6742
  }
@@ -6795,7 +6872,14 @@ function addMessage(session, message) {
6795
6872
  session.messages.push(message);
6796
6873
  const maxMessages = session.config.ui.maxHistorySize * 2;
6797
6874
  if (session.messages.length > maxMessages) {
6798
- session.messages = session.messages.slice(-session.config.ui.maxHistorySize);
6875
+ let sliceStart = session.messages.length - session.config.ui.maxHistorySize;
6876
+ while (sliceStart > 0 && sliceStart < session.messages.length) {
6877
+ const msg = session.messages[sliceStart];
6878
+ const isToolResult = Array.isArray(msg?.content) && msg.content.length > 0 && msg.content[0]?.type === "tool_result";
6879
+ if (!isToolResult) break;
6880
+ sliceStart--;
6881
+ }
6882
+ session.messages = session.messages.slice(sliceStart);
6799
6883
  }
6800
6884
  }
6801
6885
  function substituteDynamicContext(body, cwd) {
@@ -7062,7 +7146,7 @@ function updateContextTokens(session, provider) {
7062
7146
  }
7063
7147
  session.contextManager.setUsedTokens(totalTokens);
7064
7148
  }
7065
- async function checkAndCompactContext(session, provider) {
7149
+ async function checkAndCompactContext(session, provider, signal) {
7066
7150
  if (!session.contextManager) {
7067
7151
  initializeContextManager(session, provider);
7068
7152
  }
@@ -7074,7 +7158,7 @@ async function checkAndCompactContext(session, provider) {
7074
7158
  preserveLastN: 4,
7075
7159
  summaryMaxTokens: 1e3
7076
7160
  });
7077
- const result = await compactor.compact(session.messages, provider);
7161
+ const result = await compactor.compact(session.messages, provider, signal);
7078
7162
  if (result.wasCompacted) {
7079
7163
  const compactedNonSystem = result.messages.filter((m) => m.role !== "system");
7080
7164
  session.messages = compactedNonSystem;
@@ -44587,10 +44671,12 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44587
44671
  let responseContent = "";
44588
44672
  const collectedToolCalls = [];
44589
44673
  let thinkingEnded = false;
44674
+ let lastStopReason;
44590
44675
  const toolCallBuilders = /* @__PURE__ */ new Map();
44591
44676
  for await (const chunk of provider.streamWithTools(messages, {
44592
44677
  tools,
44593
- maxTokens: session.config.provider.maxTokens
44678
+ maxTokens: session.config.provider.maxTokens,
44679
+ signal: options.signal
44594
44680
  })) {
44595
44681
  if (options.signal?.aborted) {
44596
44682
  break;
@@ -44640,6 +44726,9 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44640
44726
  }
44641
44727
  }
44642
44728
  if (chunk.type === "done") {
44729
+ if (chunk.stopReason) {
44730
+ lastStopReason = chunk.stopReason;
44731
+ }
44643
44732
  if (!thinkingEnded) {
44644
44733
  options.onThinkingEnd?.();
44645
44734
  thinkingEnded = true;
@@ -44658,6 +44747,14 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44658
44747
  if (options.signal?.aborted) {
44659
44748
  return abortReturn();
44660
44749
  }
44750
+ if (lastStopReason === "max_tokens" && responseContent) {
44751
+ addMessage(session, { role: "assistant", content: responseContent });
44752
+ addMessage(session, {
44753
+ role: "user",
44754
+ content: "[System: Your previous response was cut off due to the output token limit. Continue exactly where you left off.]"
44755
+ });
44756
+ continue;
44757
+ }
44661
44758
  addMessage(session, { role: "assistant", content: responseContent });
44662
44759
  break;
44663
44760
  }
@@ -44864,7 +44961,8 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44864
44961
  const finalMessages = getConversationContext(session, toolRegistry);
44865
44962
  for await (const chunk of provider.streamWithTools(finalMessages, {
44866
44963
  tools: [],
44867
- maxTokens: session.config.provider.maxTokens
44964
+ maxTokens: session.config.provider.maxTokens,
44965
+ signal: options.signal
44868
44966
  })) {
44869
44967
  if (options.signal?.aborted) break;
44870
44968
  if (chunk.type === "text" && chunk.text) {
@@ -44878,6 +44976,14 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44878
44976
  break;
44879
44977
  }
44880
44978
  }
44979
+ if (iteration >= maxIterations) {
44980
+ const notice = `
44981
+
44982
+ ---
44983
+ _Reached the iteration limit (${maxIterations}). The task may be incomplete. You can say "continue" to resume._`;
44984
+ finalContent += notice;
44985
+ options.onStream?.({ type: "text", text: notice });
44986
+ }
44881
44987
  options.onStream?.({ type: "done" });
44882
44988
  return {
44883
44989
  content: finalContent,
@@ -46059,16 +46165,33 @@ async function startRepl(options = {}) {
46059
46165
  const usageBefore = getContextUsagePercent(session);
46060
46166
  let usageForDisplay = usageBefore;
46061
46167
  try {
46062
- const compactionResult = await checkAndCompactContext(session, provider);
46063
- if (compactionResult?.wasCompacted) {
46064
- usageForDisplay = getContextUsagePercent(session);
46065
- console.log(
46066
- chalk25.dim(
46067
- `Context compacted (${usageBefore.toFixed(0)}% -> ${usageForDisplay.toFixed(0)}%)`
46068
- )
46168
+ const compactAbort = new AbortController();
46169
+ const compactTimeout = setTimeout(() => compactAbort.abort(), 3e4);
46170
+ const compactSigint = () => compactAbort.abort();
46171
+ process.once("SIGINT", compactSigint);
46172
+ const compactSpinner = createSpinner("Compacting context");
46173
+ compactSpinner.start();
46174
+ try {
46175
+ const compactionResult = await checkAndCompactContext(
46176
+ session,
46177
+ provider,
46178
+ compactAbort.signal
46069
46179
  );
46070
- warned75 = false;
46071
- warned90 = false;
46180
+ if (compactionResult?.wasCompacted) {
46181
+ usageForDisplay = getContextUsagePercent(session);
46182
+ compactSpinner.stop(
46183
+ `Context compacted (${usageBefore.toFixed(0)}% \u2192 ${usageForDisplay.toFixed(0)}%)`
46184
+ );
46185
+ warned75 = false;
46186
+ warned90 = false;
46187
+ } else {
46188
+ compactSpinner.clear();
46189
+ }
46190
+ } catch {
46191
+ compactSpinner.clear();
46192
+ } finally {
46193
+ clearTimeout(compactTimeout);
46194
+ process.off("SIGINT", compactSigint);
46072
46195
  }
46073
46196
  } catch {
46074
46197
  }