codebot-ai 1.4.0 → 1.4.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/agent.d.ts CHANGED
@@ -28,9 +28,16 @@ export declare class Agent {
28
28
  after: number;
29
29
  };
30
30
  getMessages(): Message[];
31
- /** Ensure every assistant message with tool_calls has matching tool response messages.
32
- * OpenAI returns 400 if any tool_call_id lacks a response. This can happen if
33
- * a previous LLM call errored out mid-flow. */
31
+ /**
32
+ * Validate and repair message history to prevent OpenAI 400 errors.
33
+ * Handles three types of corruption:
34
+ * 1. Orphaned tool messages — tool_call_id doesn't match any preceding assistant's tool_calls
35
+ * 2. Duplicate tool responses — multiple tool messages for the same tool_call_id
36
+ * 3. Missing tool responses — assistant has tool_calls but no matching tool response
37
+ *
38
+ * This runs before every LLM call to self-heal from stream errors, compaction artifacts,
39
+ * or session resume corruption.
40
+ */
34
41
  private repairToolCallMessages;
35
42
  private buildSystemPrompt;
36
43
  }
package/dist/agent.js CHANGED
@@ -38,6 +38,7 @@ const readline = __importStar(require("readline"));
38
38
  const tools_1 = require("./tools");
39
39
  const parser_1 = require("./parser");
40
40
  const manager_1 = require("./context/manager");
41
+ const retry_1 = require("./retry");
41
42
  const repo_map_1 = require("./context/repo-map");
42
43
  const memory_1 = require("./memory");
43
44
  const registry_1 = require("./providers/registry");
@@ -98,6 +99,9 @@ class Agent {
98
99
  yield { type: 'compaction', text: 'Context compacted (summary unavailable).' };
99
100
  }
100
101
  }
102
+ // Circuit breaker: track consecutive identical errors
103
+ let consecutiveErrors = 0;
104
+ let lastErrorMsg = '';
101
105
  for (let i = 0; i < this.maxIterations; i++) {
102
106
  // Validate message integrity: ensure every tool_call has a matching tool response
103
107
  // This prevents cascading 400 errors from OpenAI when a previous call failed
@@ -136,11 +140,29 @@ class Agent {
136
140
  const msg = err instanceof Error ? err.message : String(err);
137
141
  streamError = `Stream error: ${msg}`;
138
142
  }
139
- // On error: yield it to the UI but DON'T return — continue to next iteration
140
143
  if (streamError) {
141
144
  yield { type: 'error', error: streamError };
145
+ // Fatal errors (missing API key, auth failure, billing, etc.) — stop immediately
146
+ if ((0, retry_1.isFatalError)(streamError)) {
147
+ return;
148
+ }
149
+ // Circuit breaker: stop after 3 consecutive identical errors
150
+ if (streamError === lastErrorMsg) {
151
+ consecutiveErrors++;
152
+ if (consecutiveErrors >= 3) {
153
+ yield { type: 'error', error: `Same error repeated ${consecutiveErrors} times — stopping. Fix the issue and try again.` };
154
+ return;
155
+ }
156
+ }
157
+ else {
158
+ consecutiveErrors = 1;
159
+ lastErrorMsg = streamError;
160
+ }
142
161
  continue;
143
162
  }
163
+ // Reset error tracking on success
164
+ consecutiveErrors = 0;
165
+ lastErrorMsg = '';
144
166
  // If no native tool calls, try parsing from text
145
167
  if (toolCalls.length === 0 && fullText) {
146
168
  toolCalls = (0, parser_1.parseToolCalls)(fullText);
@@ -238,10 +260,45 @@ class Agent {
238
260
  getMessages() {
239
261
  return [...this.messages];
240
262
  }
241
- /** Ensure every assistant message with tool_calls has matching tool response messages.
242
- * OpenAI returns 400 if any tool_call_id lacks a response. This can happen if
243
- * a previous LLM call errored out mid-flow. */
263
+ /**
264
+ * Validate and repair message history to prevent OpenAI 400 errors.
265
+ * Handles three types of corruption:
266
+ * 1. Orphaned tool messages — tool_call_id doesn't match any preceding assistant's tool_calls
267
+ * 2. Duplicate tool responses — multiple tool messages for the same tool_call_id
268
+ * 3. Missing tool responses — assistant has tool_calls but no matching tool response
269
+ *
270
+ * This runs before every LLM call to self-heal from stream errors, compaction artifacts,
271
+ * or session resume corruption.
272
+ */
244
273
  repairToolCallMessages() {
274
+ // Phase 1: Collect all valid tool_call_ids from assistant messages (in order)
275
+ const validToolCallIds = new Set();
276
+ for (const msg of this.messages) {
277
+ if (msg.role === 'assistant' && msg.tool_calls?.length) {
278
+ for (const tc of msg.tool_calls) {
279
+ validToolCallIds.add(tc.id);
280
+ }
281
+ }
282
+ }
283
+ // Phase 2: Remove orphaned tool messages and duplicates
284
+ const seenToolResponseIds = new Set();
285
+ this.messages = this.messages.filter(msg => {
286
+ if (msg.role !== 'tool')
287
+ return true;
288
+ const tcId = msg.tool_call_id;
289
+ // No tool_call_id at all — malformed, remove
290
+ if (!tcId)
291
+ return false;
292
+ // Orphaned: tool_call_id doesn't match any assistant's tool_calls
293
+ if (!validToolCallIds.has(tcId))
294
+ return false;
295
+ // Duplicate: already have a response for this tool_call_id
296
+ if (seenToolResponseIds.has(tcId))
297
+ return false;
298
+ seenToolResponseIds.add(tcId);
299
+ return true;
300
+ });
301
+ // Phase 3: Add missing tool responses (assistant has tool_calls but no tool response)
245
302
  const toolResponseIds = new Set();
246
303
  for (const msg of this.messages) {
247
304
  if (msg.role === 'tool' && msg.tool_call_id) {
@@ -253,13 +310,12 @@ class Agent {
253
310
  if (msg.role === 'assistant' && msg.tool_calls?.length) {
254
311
  for (const tc of msg.tool_calls) {
255
312
  if (!toolResponseIds.has(tc.id)) {
256
- // Missing tool response — inject one right after the assistant message
257
313
  const repairMsg = {
258
314
  role: 'tool',
259
315
  content: 'Error: tool call was not executed (interrupted).',
260
316
  tool_call_id: tc.id,
261
317
  };
262
- // Find the right position: after the assistant message and any existing tool responses
318
+ // Insert after the assistant message and any existing tool responses
263
319
  let insertAt = i + 1;
264
320
  while (insertAt < this.messages.length && this.messages[insertAt].role === 'tool') {
265
321
  insertAt++;
package/dist/cli.js CHANGED
@@ -44,7 +44,7 @@ const setup_1 = require("./setup");
44
44
  const banner_1 = require("./banner");
45
45
  const tools_1 = require("./tools");
46
46
  const scheduler_1 = require("./scheduler");
47
- const VERSION = '1.4.0';
47
+ const VERSION = '1.4.2';
48
48
  // Session-wide token tracking
49
49
  let sessionTokens = { input: 0, output: 0, total: 0 };
50
50
  const C = {
@@ -10,6 +10,11 @@ class AnthropicProvider {
10
10
  this.name = config.model;
11
11
  }
12
12
  async *chat(messages, tools) {
13
+ // Early check: Anthropic always requires an API key
14
+ if (!this.config.apiKey) {
15
+ yield { type: 'error', error: `No API key configured for ${this.config.model}. Set ANTHROPIC_API_KEY or run: codebot --setup` };
16
+ return;
17
+ }
13
18
  const { systemPrompt, apiMessages } = this.convertMessages(messages);
14
19
  const body = {
15
20
  model: this.config.model,
@@ -66,7 +71,28 @@ class AnthropicProvider {
66
71
  }
67
72
  if (!response || !response.ok) {
68
73
  const text = response ? await response.text().catch(() => '') : '';
69
- yield { type: 'error', error: `Anthropic error after retries: ${lastError}${text ? ` — ${text}` : ''}` };
74
+ // Extract readable error message from JSON response
75
+ let errorMessage = '';
76
+ try {
77
+ const json = JSON.parse(text);
78
+ errorMessage = json?.error?.message || json?.message || '';
79
+ }
80
+ catch {
81
+ errorMessage = text.substring(0, 200);
82
+ }
83
+ const status = response?.status;
84
+ if (status === 401 || (errorMessage && errorMessage.toLowerCase().includes('api key'))) {
85
+ yield { type: 'error', error: `Authentication failed (${status}): ${errorMessage || 'Invalid API key'}. Set ANTHROPIC_API_KEY or run: codebot --setup` };
86
+ }
87
+ else if (status === 403) {
88
+ yield { type: 'error', error: `Access denied (403): ${errorMessage || 'Permission denied'}. Check your API key permissions.` };
89
+ }
90
+ else if (status === 404) {
91
+ yield { type: 'error', error: `Model not found (404): ${errorMessage || `"${this.config.model}" may not be available`}.` };
92
+ }
93
+ else {
94
+ yield { type: 'error', error: `Anthropic error (${status || 'unknown'}): ${errorMessage || lastError || 'Unknown error'}` };
95
+ }
70
96
  return;
71
97
  }
72
98
  if (!response.body) {
@@ -6,6 +6,10 @@ export declare class OpenAIProvider implements LLMProvider {
6
6
  constructor(config: ProviderConfig);
7
7
  chat(messages: Message[], tools?: ToolSchema[]): AsyncGenerator<StreamEvent>;
8
8
  listModels(): Promise<string[]>;
9
+ /** Get a helpful hint about which env var to set for the current provider */
10
+ private getApiKeyHint;
11
+ /** Format API error responses into readable messages (not raw JSON) */
12
+ private formatApiError;
9
13
  private formatMessage;
10
14
  }
11
15
  //# sourceMappingURL=openai.d.ts.map
@@ -13,6 +13,13 @@ class OpenAIProvider {
13
13
  this.supportsTools = (0, registry_1.getModelInfo)(config.model).supportsToolCalling;
14
14
  }
15
15
  async *chat(messages, tools) {
16
+ const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
17
+ // Early check: cloud providers require an API key
18
+ if (!isLocal && !this.config.apiKey) {
19
+ const hint = this.getApiKeyHint();
20
+ yield { type: 'error', error: `No API key configured for ${this.config.model}. ${hint}` };
21
+ return;
22
+ }
16
23
  const body = {
17
24
  model: this.config.model,
18
25
  messages: messages.map(m => this.formatMessage(m)),
@@ -22,7 +29,6 @@ class OpenAIProvider {
22
29
  body.tools = tools;
23
30
  }
24
31
  // Ollama/local provider optimizations: set context window and keep model loaded
25
- const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
26
32
  if (isLocal) {
27
33
  const modelInfo = (0, registry_1.getModelInfo)(this.config.model);
28
34
  body.options = { num_ctx: modelInfo.contextWindow };
@@ -69,7 +75,8 @@ class OpenAIProvider {
69
75
  }
70
76
  if (!response || !response.ok) {
71
77
  const text = response ? await response.text().catch(() => '') : '';
72
- yield { type: 'error', error: `LLM error after retries: ${lastError}${text ? ` — ${text}` : ''}` };
78
+ const friendlyError = this.formatApiError(response?.status, text, lastError);
79
+ yield { type: 'error', error: friendlyError };
73
80
  return;
74
81
  }
75
82
  if (!response.body) {
@@ -240,6 +247,51 @@ class OpenAIProvider {
240
247
  return [];
241
248
  }
242
249
  }
250
+ /** Get a helpful hint about which env var to set for the current provider */
251
+ getApiKeyHint() {
252
+ const url = this.config.baseUrl.toLowerCase();
253
+ if (url.includes('openai.com'))
254
+ return 'Set OPENAI_API_KEY or run: codebot --setup';
255
+ if (url.includes('deepseek'))
256
+ return 'Set DEEPSEEK_API_KEY or run: codebot --setup';
257
+ if (url.includes('groq'))
258
+ return 'Set GROQ_API_KEY or run: codebot --setup';
259
+ if (url.includes('mistral'))
260
+ return 'Set MISTRAL_API_KEY or run: codebot --setup';
261
+ if (url.includes('generativelanguage.googleapis') || url.includes('gemini'))
262
+ return 'Set GEMINI_API_KEY or run: codebot --setup';
263
+ if (url.includes('x.ai') || url.includes('grok'))
264
+ return 'Set XAI_API_KEY or run: codebot --setup';
265
+ return 'Set your API key or run: codebot --setup';
266
+ }
267
+ /** Format API error responses into readable messages (not raw JSON) */
268
+ formatApiError(status, responseText, lastError) {
269
+ // Try to extract a useful message from JSON error response
270
+ let errorMessage = '';
271
+ try {
272
+ const json = JSON.parse(responseText);
273
+ errorMessage = json?.error?.message || json?.message || json?.error || '';
274
+ }
275
+ catch {
276
+ errorMessage = responseText.substring(0, 200);
277
+ }
278
+ const hint = this.getApiKeyHint();
279
+ if (status === 401 || (errorMessage && errorMessage.toLowerCase().includes('api key'))) {
280
+ return `Authentication failed (${status || 'no status'}): ${errorMessage || 'Invalid or missing API key'}. ${hint}`;
281
+ }
282
+ if (status === 403) {
283
+ return `Access denied (403): ${errorMessage || 'Permission denied'}. Check your API key permissions.`;
284
+ }
285
+ if (status === 404) {
286
+ return `Model not found (404): ${errorMessage || `"${this.config.model}" may not be available`}. Check the model name.`;
287
+ }
288
+ if (status === 429) {
289
+ return `Rate limited (429): ${errorMessage || 'Too many requests'}. Wait a moment and try again.`;
290
+ }
291
+ // Generic fallback — still clean, not raw JSON
292
+ const statusStr = status ? `(${status})` : '';
293
+ return `LLM error ${statusStr}: ${errorMessage || lastError || 'Unknown error'}`;
294
+ }
243
295
  formatMessage(msg) {
244
296
  const formatted = { role: msg.role, content: msg.content };
245
297
  if (msg.tool_calls) {
package/dist/retry.d.ts CHANGED
@@ -18,5 +18,10 @@ export declare function isRetryable(error: unknown, status?: number, opts?: Retr
18
18
  */
19
19
  export declare function getRetryDelay(attempt: number, retryAfterHeader?: string | null, opts?: RetryOptions): number;
20
20
  export declare function sleep(ms: number): Promise<void>;
21
+ /**
22
+ * Returns true if the error message indicates a fatal/permanent failure
23
+ * that will never succeed on retry (missing API key, auth failure, billing, etc.).
24
+ */
25
+ export declare function isFatalError(errorMsg: string): boolean;
21
26
  export { DEFAULTS as RETRY_DEFAULTS };
22
27
  //# sourceMappingURL=retry.d.ts.map
package/dist/retry.js CHANGED
@@ -9,6 +9,7 @@ exports.RETRY_DEFAULTS = void 0;
9
9
  exports.isRetryable = isRetryable;
10
10
  exports.getRetryDelay = getRetryDelay;
11
11
  exports.sleep = sleep;
12
+ exports.isFatalError = isFatalError;
12
13
  const DEFAULTS = {
13
14
  maxRetries: 3,
14
15
  baseDelayMs: 1000,
@@ -56,4 +57,27 @@ function getRetryDelay(attempt, retryAfterHeader, opts) {
56
57
  function sleep(ms) {
57
58
  return new Promise(resolve => setTimeout(resolve, ms));
58
59
  }
60
+ /**
61
+ * Returns true if the error message indicates a fatal/permanent failure
62
+ * that will never succeed on retry (missing API key, auth failure, billing, etc.).
63
+ */
64
+ function isFatalError(errorMsg) {
65
+ const lower = errorMsg.toLowerCase();
66
+ return (lower.includes('api key') ||
67
+ lower.includes('api_key') ||
68
+ lower.includes('apikey') ||
69
+ lower.includes('authentication') ||
70
+ lower.includes('unauthorized') ||
71
+ lower.includes('invalid_request_error') ||
72
+ lower.includes('invalid request') ||
73
+ lower.includes('permission denied') ||
74
+ lower.includes('account deactivated') ||
75
+ lower.includes('account suspended') ||
76
+ lower.includes('billing') ||
77
+ (lower.includes('quota') && lower.includes('exceeded')) ||
78
+ lower.includes('insufficient_quota') ||
79
+ lower.includes('model not found') ||
80
+ lower.includes('does not exist') ||
81
+ lower.includes('access denied'));
82
+ }
59
83
  //# sourceMappingURL=retry.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebot-ai",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Zero-dependency autonomous AI agent. Code, browse, search, automate. Works with any LLM — Ollama, Claude, GPT, Gemini, DeepSeek, Groq, Mistral, Grok.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",