@corbat-tech/coco 2.8.0 → 2.8.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
@@ -1012,22 +1012,38 @@ 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
+ for await (const event of stream) {
1035
+ lastActivityTime = Date.now();
1036
+ if (event.type === "content_block_delta") {
1037
+ const delta = event.delta;
1038
+ if (delta.type === "text_delta" && delta.text) {
1039
+ yield { type: "text", text: delta.text };
1040
+ }
1027
1041
  }
1028
1042
  }
1043
+ yield { type: "done" };
1044
+ } finally {
1045
+ clearInterval(timeoutInterval);
1029
1046
  }
1030
- yield { type: "done" };
1031
1047
  } catch (error) {
1032
1048
  throw this.handleError(error);
1033
1049
  }
@@ -1038,90 +1054,106 @@ var init_anthropic = __esm({
1038
1054
  async *streamWithTools(messages, options) {
1039
1055
  this.ensureInitialized();
1040
1056
  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
- });
1057
+ const stream = await this.client.messages.stream(
1058
+ {
1059
+ model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
1060
+ max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1061
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
1062
+ system: this.extractSystem(messages, options?.system),
1063
+ messages: this.convertMessages(messages),
1064
+ tools: this.convertTools(options.tools),
1065
+ tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
1066
+ },
1067
+ { signal: options?.signal }
1068
+ );
1050
1069
  let currentToolCall = null;
1051
1070
  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") {
1071
+ const streamTimeout = this.config.timeout ?? 12e4;
1072
+ let lastActivityTime = Date.now();
1073
+ const checkTimeout = () => {
1074
+ if (Date.now() - lastActivityTime > streamTimeout) {
1075
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
1076
+ }
1077
+ };
1078
+ const timeoutInterval = setInterval(checkTimeout, 5e3);
1079
+ try {
1080
+ for await (const event of stream) {
1081
+ lastActivityTime = Date.now();
1082
+ if (event.type === "content_block_start") {
1083
+ const contentBlock = event.content_block;
1084
+ if (contentBlock.type === "tool_use") {
1085
+ if (currentToolCall) {
1086
+ getLogger().warn(
1087
+ `[Anthropic] content_block_stop missing for tool '${currentToolCall.name}' \u2014 finalizing early to prevent data bleed.`
1088
+ );
1089
+ try {
1090
+ currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1091
+ } catch {
1092
+ currentToolCall.input = {};
1093
+ }
1094
+ yield {
1095
+ type: "tool_use_end",
1096
+ toolCall: { ...currentToolCall }
1097
+ };
1098
+ }
1099
+ currentToolCall = {
1100
+ id: contentBlock.id,
1101
+ name: contentBlock.name
1102
+ };
1103
+ currentToolInputJson = "";
1104
+ yield {
1105
+ type: "tool_use_start",
1106
+ toolCall: { ...currentToolCall }
1107
+ };
1108
+ }
1109
+ } else if (event.type === "content_block_delta") {
1110
+ const delta = event.delta;
1111
+ if (delta.type === "text_delta" && delta.text) {
1112
+ yield { type: "text", text: delta.text };
1113
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
1114
+ currentToolInputJson += delta.partial_json;
1115
+ yield {
1116
+ type: "tool_use_delta",
1117
+ toolCall: {
1118
+ ...currentToolCall
1119
+ },
1120
+ text: delta.partial_json
1121
+ };
1122
+ }
1123
+ } else if (event.type === "content_block_stop") {
1056
1124
  if (currentToolCall) {
1057
- getLogger().warn(
1058
- `[Anthropic] content_block_stop missing for tool '${currentToolCall.name}' \u2014 finalizing early to prevent data bleed.`
1059
- );
1060
1125
  try {
1061
1126
  currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1062
1127
  } catch {
1063
- currentToolCall.input = {};
1128
+ let repaired = false;
1129
+ if (currentToolInputJson) {
1130
+ try {
1131
+ currentToolCall.input = JSON.parse(jsonrepair(currentToolInputJson));
1132
+ repaired = true;
1133
+ getLogger().debug(`Repaired JSON for tool ${currentToolCall.name}`);
1134
+ } catch {
1135
+ }
1136
+ }
1137
+ if (!repaired) {
1138
+ getLogger().warn(
1139
+ `Failed to parse tool call arguments for ${currentToolCall.name}: ${currentToolInputJson?.slice(0, 300)}`
1140
+ );
1141
+ currentToolCall.input = {};
1142
+ }
1064
1143
  }
1065
1144
  yield {
1066
1145
  type: "tool_use_end",
1067
1146
  toolCall: { ...currentToolCall }
1068
1147
  };
1148
+ currentToolCall = null;
1149
+ currentToolInputJson = "";
1069
1150
  }
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
1151
  }
1122
1152
  }
1153
+ yield { type: "done" };
1154
+ } finally {
1155
+ clearInterval(timeoutInterval);
1123
1156
  }
1124
- yield { type: "done" };
1125
1157
  } catch (error) {
1126
1158
  throw this.handleError(error);
1127
1159
  }
@@ -6564,7 +6596,7 @@ CONVERSATION:
6564
6596
  * @param provider - The LLM provider to use for summarization
6565
6597
  * @returns Compacted messages with summary replacing older messages
6566
6598
  */
6567
- async compact(messages, provider) {
6599
+ async compact(messages, provider, signal) {
6568
6600
  const conversationMessages = messages.filter((m) => m.role !== "system");
6569
6601
  if (conversationMessages.length <= this.config.preserveLastN) {
6570
6602
  return {
@@ -6596,7 +6628,7 @@ CONVERSATION:
6596
6628
  }
6597
6629
  const originalTokens = this.estimateTokens(messages, provider);
6598
6630
  const conversationText = this.formatMessagesForSummary(messagesToSummarize);
6599
- const summary = await this.generateSummary(conversationText, provider);
6631
+ const summary = await this.generateSummary(conversationText, provider, signal);
6600
6632
  const systemMessages = messages.filter((m) => m.role === "system");
6601
6633
  const summaryMessage = {
6602
6634
  role: "user",
@@ -6650,16 +6682,30 @@ ${summary}
6650
6682
  /**
6651
6683
  * Generate a summary of the conversation using the LLM
6652
6684
  */
6653
- async generateSummary(conversationText, provider) {
6685
+ async generateSummary(conversationText, provider, signal) {
6686
+ if (signal?.aborted) return "[Compaction cancelled]";
6654
6687
  const prompt = COMPACTION_PROMPT + conversationText;
6655
6688
  try {
6656
- const response = await provider.chat([{ role: "user", content: prompt }], {
6689
+ const chatPromise = provider.chat([{ role: "user", content: prompt }], {
6657
6690
  maxTokens: this.config.summaryMaxTokens,
6658
6691
  temperature: 0.3
6659
6692
  // Lower temperature for more consistent summaries
6660
6693
  });
6694
+ if (signal) {
6695
+ const abortPromise = new Promise((_, reject) => {
6696
+ signal.addEventListener(
6697
+ "abort",
6698
+ () => reject(new DOMException("Aborted", "AbortError")),
6699
+ { once: true }
6700
+ );
6701
+ });
6702
+ const response2 = await Promise.race([chatPromise, abortPromise]);
6703
+ return response2.content;
6704
+ }
6705
+ const response = await chatPromise;
6661
6706
  return response.content;
6662
6707
  } catch (error) {
6708
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
6663
6709
  const errorMessage = error instanceof Error ? error.message : String(error);
6664
6710
  return `[Summary generation failed: ${errorMessage}. Previous conversation had ${conversationText.length} characters.]`;
6665
6711
  }
@@ -6795,7 +6841,14 @@ function addMessage(session, message) {
6795
6841
  session.messages.push(message);
6796
6842
  const maxMessages = session.config.ui.maxHistorySize * 2;
6797
6843
  if (session.messages.length > maxMessages) {
6798
- session.messages = session.messages.slice(-session.config.ui.maxHistorySize);
6844
+ let sliceStart = session.messages.length - session.config.ui.maxHistorySize;
6845
+ while (sliceStart > 0 && sliceStart < session.messages.length) {
6846
+ const msg = session.messages[sliceStart];
6847
+ const isToolResult = Array.isArray(msg?.content) && msg.content.length > 0 && msg.content[0]?.type === "tool_result";
6848
+ if (!isToolResult) break;
6849
+ sliceStart--;
6850
+ }
6851
+ session.messages = session.messages.slice(sliceStart);
6799
6852
  }
6800
6853
  }
6801
6854
  function substituteDynamicContext(body, cwd) {
@@ -7062,7 +7115,7 @@ function updateContextTokens(session, provider) {
7062
7115
  }
7063
7116
  session.contextManager.setUsedTokens(totalTokens);
7064
7117
  }
7065
- async function checkAndCompactContext(session, provider) {
7118
+ async function checkAndCompactContext(session, provider, signal) {
7066
7119
  if (!session.contextManager) {
7067
7120
  initializeContextManager(session, provider);
7068
7121
  }
@@ -7074,7 +7127,7 @@ async function checkAndCompactContext(session, provider) {
7074
7127
  preserveLastN: 4,
7075
7128
  summaryMaxTokens: 1e3
7076
7129
  });
7077
- const result = await compactor.compact(session.messages, provider);
7130
+ const result = await compactor.compact(session.messages, provider, signal);
7078
7131
  if (result.wasCompacted) {
7079
7132
  const compactedNonSystem = result.messages.filter((m) => m.role !== "system");
7080
7133
  session.messages = compactedNonSystem;
@@ -44590,7 +44643,8 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44590
44643
  const toolCallBuilders = /* @__PURE__ */ new Map();
44591
44644
  for await (const chunk of provider.streamWithTools(messages, {
44592
44645
  tools,
44593
- maxTokens: session.config.provider.maxTokens
44646
+ maxTokens: session.config.provider.maxTokens,
44647
+ signal: options.signal
44594
44648
  })) {
44595
44649
  if (options.signal?.aborted) {
44596
44650
  break;
@@ -44864,7 +44918,8 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
44864
44918
  const finalMessages = getConversationContext(session, toolRegistry);
44865
44919
  for await (const chunk of provider.streamWithTools(finalMessages, {
44866
44920
  tools: [],
44867
- maxTokens: session.config.provider.maxTokens
44921
+ maxTokens: session.config.provider.maxTokens,
44922
+ signal: options.signal
44868
44923
  })) {
44869
44924
  if (options.signal?.aborted) break;
44870
44925
  if (chunk.type === "text" && chunk.text) {
@@ -46059,16 +46114,33 @@ async function startRepl(options = {}) {
46059
46114
  const usageBefore = getContextUsagePercent(session);
46060
46115
  let usageForDisplay = usageBefore;
46061
46116
  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
- )
46117
+ const compactAbort = new AbortController();
46118
+ const compactTimeout = setTimeout(() => compactAbort.abort(), 3e4);
46119
+ const compactSigint = () => compactAbort.abort();
46120
+ process.once("SIGINT", compactSigint);
46121
+ const compactSpinner = createSpinner("Compacting context");
46122
+ compactSpinner.start();
46123
+ try {
46124
+ const compactionResult = await checkAndCompactContext(
46125
+ session,
46126
+ provider,
46127
+ compactAbort.signal
46069
46128
  );
46070
- warned75 = false;
46071
- warned90 = false;
46129
+ if (compactionResult?.wasCompacted) {
46130
+ usageForDisplay = getContextUsagePercent(session);
46131
+ compactSpinner.stop(
46132
+ `Context compacted (${usageBefore.toFixed(0)}% \u2192 ${usageForDisplay.toFixed(0)}%)`
46133
+ );
46134
+ warned75 = false;
46135
+ warned90 = false;
46136
+ } else {
46137
+ compactSpinner.clear();
46138
+ }
46139
+ } catch {
46140
+ compactSpinner.clear();
46141
+ } finally {
46142
+ clearTimeout(compactTimeout);
46143
+ process.off("SIGINT", compactSigint);
46072
46144
  }
46073
46145
  } catch {
46074
46146
  }