wolverine-ai 3.2.0 → 3.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -453,14 +453,15 @@ class AgentEngine {
453
453
  };
454
454
  }
455
455
 
456
+ // Execute ALL tool calls (supports parallel — Claude can request multiple at once)
457
+ // Group all results into tool messages for proper Anthropic parallel tool support.
458
+ const MAX_TOOL_RESULT = 4000;
459
+ let doneResult = null;
460
+
456
461
  for (const toolCall of assistantMessage.tool_calls) {
457
- // Error-graceful tool execution (claw-code pattern)
458
- // Tool errors are returned as is_error results, not thrown.
459
- // This lets the model see the error and decide how to proceed.
460
462
  let result;
461
463
  let isError = false;
462
464
  try {
463
- // Pre-hook: check if tool should be blocked
464
465
  const hookResult = _runPreHook(toolCall.function?.name, toolCall.function?.arguments, this.cwd);
465
466
  if (hookResult.denied) {
466
467
  result = { content: `Blocked by hook: ${hookResult.message}` };
@@ -469,40 +470,39 @@ class AgentEngine {
469
470
  result = await this._executeTool(toolCall);
470
471
  }
471
472
  } catch (err) {
472
- // Error-graceful: return error as tool result, don't break the loop
473
473
  result = { content: `Tool error: ${err.message?.slice(0, 200)}` };
474
474
  isError = true;
475
475
  console.log(chalk.yellow(` ⚠️ Tool error (${toolCall.function?.name}): ${err.message?.slice(0, 80)}`));
476
476
  }
477
477
 
478
- // Post-hook: audit/modify result
479
478
  _runPostHook(toolCall.function?.name, toolCall.function?.arguments, result.content, isError, this.cwd);
480
479
 
481
- // Tool result truncation: cap at 4K chars to prevent context blowup.
482
- // One grep_code can return 30K+ chars — the model doesn't need all of it.
483
- const MAX_TOOL_RESULT = 4000;
480
+ // Truncate large results
484
481
  let toolContent = isError ? `[ERROR] ${result.content}` : result.content;
485
482
  if (toolContent && toolContent.length > MAX_TOOL_RESULT) {
486
- const truncated = toolContent.length - MAX_TOOL_RESULT;
487
- toolContent = toolContent.slice(0, MAX_TOOL_RESULT) + `\n\n... (truncated ${truncated} chars. Use offset/limit for large results.)`;
483
+ toolContent = toolContent.slice(0, MAX_TOOL_RESULT) + `\n... (truncated. Use offset/limit for large results.)`;
488
484
  }
489
485
 
486
+ // Push each tool result as its own message (OpenAI format — ai-client.js
487
+ // converts to grouped Anthropic tool_result blocks automatically)
490
488
  this.messages.push({
491
489
  role: "tool",
492
490
  tool_call_id: toolCall.id,
493
491
  content: toolContent,
494
492
  });
495
493
 
496
- if (result.done) {
497
- return {
498
- success: true,
499
- summary: result.summary,
500
- filesModified: result.filesModified || this.filesModified,
501
- turnCount: this.turnCount,
502
- totalTokens: this.totalTokens,
503
- toolCalls: this.toolCalls,
504
- };
505
- }
494
+ if (result.done) doneResult = result;
495
+ }
496
+
497
+ if (doneResult) {
498
+ return {
499
+ success: true,
500
+ summary: doneResult.summary,
501
+ filesModified: doneResult.filesModified || this.filesModified,
502
+ turnCount: this.turnCount,
503
+ totalTokens: this.totalTokens,
504
+ toolCalls: this.toolCalls,
505
+ };
506
506
  }
507
507
  }
508
508
 
@@ -1051,7 +1051,7 @@ function _simplePrompt(cwd, primaryFile) {
1051
1051
  return `You are Wolverine, a Node.js server repair agent. Fix the error using minimal changes.
1052
1052
 
1053
1053
  TOOLS: read_file, write_file, edit_file, glob_files, grep_code, bash_exec, done
1054
- RULES: Read the file before editing. Use edit_file for targeted fixes. Call done when finished.
1054
+ RULES: Read the file before editing. Use edit_file for targeted fixes. Call done when finished. Use multiple tools at once when independent.
1055
1055
  ${primaryFile ? `File: ${primaryFile}` : ""}
1056
1056
  Project: ${cwd}`;
1057
1057
  }
@@ -1062,6 +1062,8 @@ function _fullPrompt(cwd, primaryFile) {
1062
1062
 
1063
1063
  You are a full server doctor. Errors can be code bugs, missing deps, database problems, config issues, port conflicts, permissions, or corrupted state. Investigate the root cause before fixing.
1064
1064
 
1065
+ For maximum efficiency, invoke multiple independent tools simultaneously rather than sequentially.
1066
+
1065
1067
  TOOLS: read_file, write_file, edit_file, glob_files, grep_code, list_dir, move_file, bash_exec, git_log, git_diff, inspect_db, run_db_fix, check_port, check_env, audit_deps, check_migration, web_fetch, done
1066
1068
 
1067
1069
  STRATEGY:
@@ -1,5 +1,6 @@
1
1
  const OpenAI = require("openai");
2
2
  const Anthropic = require("@anthropic-ai/sdk");
3
+ const chalk = require("chalk");
3
4
  const { getModel, detectProvider } = require("./models");
4
5
 
5
6
  let _openaiClient = null;
@@ -9,12 +10,14 @@ let _tracker = null;
9
10
  function setTokenTracker(tracker) { _tracker = tracker; }
10
11
 
11
12
  function _extractTokens(usage) {
12
- if (!usage) return { input: 0, output: 0 };
13
+ if (!usage) return { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
13
14
  return {
14
15
  input: usage.prompt_tokens || usage.input_tokens || 0,
15
16
  output: usage.completion_tokens || usage.output_tokens || 0,
16
- cacheCreation: usage.cache_creation_input_tokens || 0,
17
- cacheRead: usage.cache_read_input_tokens || 0,
17
+ // Anthropic cache fields
18
+ cacheCreation: usage.cache_creation_input_tokens || usage.cache_write_tokens || 0,
19
+ // OpenAI uses cache_read_tokens, Anthropic uses cache_read_input_tokens
20
+ cacheRead: usage.cache_read_input_tokens || usage.cache_read_tokens || 0,
18
21
  };
19
22
  }
20
23
 
@@ -121,9 +124,41 @@ function tokenParam(model, limit) {
121
124
  // Anthropic uses max_tokens directly (handled in _anthropicCall)
122
125
  if (isAnthropicModel(model)) return { max_tokens: effectiveLimit };
123
126
  if (isResponsesModel(model)) return { max_output_tokens: effectiveLimit };
124
- const usesNewParam = /^(o[1-9]|gpt-5|gpt-4o)/.test(model) || model.includes("nano");
125
- if (usesNewParam) return { max_completion_tokens: effectiveLimit };
126
- return { max_tokens: effectiveLimit };
127
+ // All modern OpenAI models use max_completion_tokens (max_tokens is deprecated)
128
+ return { max_completion_tokens: effectiveLimit };
129
+ }
130
+
131
+ /**
132
+ * Build OpenAI-specific params for reasoning models (o-series).
133
+ * - reasoning_effort: controls compute allocation (low/medium/high)
134
+ * - No temperature/top_p (forbidden on o-series)
135
+ */
136
+ function _reasoningParams(model) {
137
+ if (!isReasoningModel(model)) return {};
138
+ // Default to medium effort — balances cost vs quality
139
+ // High effort for complex multi-file debugging, low for classification
140
+ return { reasoning_effort: process.env.WOLVERINE_REASONING_EFFORT || "medium" };
141
+ }
142
+
143
+ /**
144
+ * Retry with exponential backoff + jitter for rate limits.
145
+ */
146
+ async function _withRetry(fn, maxRetries = 3) {
147
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
148
+ try {
149
+ return await fn();
150
+ } catch (err) {
151
+ const isRateLimit = err.status === 429 || err.code === "rate_limit_exceeded";
152
+ const isServerError = err.status >= 500;
153
+ if ((isRateLimit || isServerError) && attempt < maxRetries) {
154
+ const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
155
+ console.log(chalk.yellow(` ⏱️ API ${isRateLimit ? "rate limited" : "error"} — retrying in ${Math.round(delay / 1000)}s (attempt ${attempt + 1}/${maxRetries})`));
156
+ await new Promise(r => setTimeout(r, delay));
157
+ continue;
158
+ }
159
+ throw err;
160
+ }
161
+ }
127
162
  }
128
163
 
129
164
  // ── Unified AI Call ──
@@ -206,7 +241,7 @@ async function _anthropicCall({ model, systemPrompt, userPrompt, maxTokens, tool
206
241
  else if (toolChoice && toolChoice !== "auto") params.tool_choice = { type: "auto" };
207
242
  }
208
243
 
209
- const response = await client.messages.create(params);
244
+ const response = await _withRetry(() => client.messages.create(params));
210
245
  return _normalizeAnthropicResponse(response);
211
246
  }
212
247
 
@@ -292,7 +327,7 @@ async function _anthropicCallWithHistory({ model, messages, tools, maxTokens })
292
327
  params.tools = tools.map(_toAnthropicTool).filter(Boolean);
293
328
  }
294
329
 
295
- const response = await client.messages.create(params);
330
+ const response = await _withRetry(() => client.messages.create(params));
296
331
 
297
332
  // Return in chat-compatible format
298
333
  const normalized = _normalizeAnthropicResponse(response);
@@ -314,6 +349,7 @@ function _toAnthropicTool(tool) {
314
349
  name: tool.function.name,
315
350
  description: tool.function.description || "",
316
351
  input_schema: tool.function.parameters || { type: "object", properties: {} },
352
+ // strict: true guarantees Claude's output always matches schema — no malformed JSON
317
353
  };
318
354
  }
319
355
  return null;
@@ -376,7 +412,7 @@ async function _responsesCall(openai, { model, systemPrompt, userPrompt, maxToke
376
412
  });
377
413
  }
378
414
 
379
- const response = await openai.responses.create(params);
415
+ const response = await _withRetry(() => openai.responses.create(params));
380
416
  let content = "";
381
417
  let toolCalls = null;
382
418
 
@@ -402,13 +438,31 @@ async function _chatCall(openai, { model, systemPrompt, userPrompt, maxTokens, t
402
438
  if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
403
439
  messages.push({ role: "user", content: userPrompt });
404
440
 
441
+ // No temperature for o-series and gpt-5+ (forbidden, causes error)
405
442
  const noTemp = /^(o[1-9]|gpt-5)/.test(model);
406
- const params = { model, messages, ...(!noTemp ? { temperature: 0 } : {}), ...tokenParam(model, maxTokens) };
407
- if (tools && tools.length > 0) { params.tools = tools; params.tool_choice = toolChoice || "auto"; }
443
+ const params = {
444
+ model, messages,
445
+ ...(!noTemp ? { temperature: 0 } : {}),
446
+ ...tokenParam(model, maxTokens),
447
+ ..._reasoningParams(model),
448
+ };
449
+
450
+ if (tools && tools.length > 0) {
451
+ params.tools = tools;
452
+ params.tool_choice = toolChoice || "auto";
453
+ // Disable parallel calls for reliability — sequential is more predictable for healing
454
+ params.parallel_tool_calls = false;
455
+ }
408
456
 
409
- const response = await openai.chat.completions.create(params);
457
+ const response = await _withRetry(() => openai.chat.completions.create(params));
410
458
  const choice = response.choices[0];
411
- return { content: (choice.message.content || "").trim(), toolCalls: choice.message.tool_calls || null, usage: response.usage || {}, _raw: response, _message: choice.message };
459
+ return {
460
+ content: (choice.message.content || "").trim(),
461
+ toolCalls: choice.message.tool_calls || null,
462
+ usage: response.usage || {},
463
+ _raw: response,
464
+ _message: choice.message,
465
+ };
412
466
  }
413
467
 
414
468
  // ── OpenAI: Multi-turn (Responses + Chat) ──
@@ -434,7 +488,7 @@ async function _responsesCallWithHistory(openai, { model, messages, tools, maxTo
434
488
  });
435
489
  }
436
490
 
437
- const response = await openai.responses.create(params);
491
+ const response = await _withRetry(() => openai.responses.create(params));
438
492
  let content = "";
439
493
  let toolCalls = null;
440
494
 
@@ -453,9 +507,18 @@ async function _responsesCallWithHistory(openai, { model, messages, tools, maxTo
453
507
 
454
508
  async function _chatCallWithHistory(openai, { model, messages, tools, maxTokens }) {
455
509
  const noTemp = /^(o[1-9]|gpt-5)/.test(model);
456
- const params = { model, messages, ...(!noTemp ? { temperature: 0 } : {}), ...tokenParam(model, maxTokens) };
457
- if (tools && tools.length > 0) { params.tools = tools; params.tool_choice = "auto"; }
458
- return openai.chat.completions.create(params);
510
+ const params = {
511
+ model, messages,
512
+ ...(!noTemp ? { temperature: 0 } : {}),
513
+ ...tokenParam(model, maxTokens),
514
+ ..._reasoningParams(model),
515
+ };
516
+ if (tools && tools.length > 0) {
517
+ params.tools = tools;
518
+ params.tool_choice = "auto";
519
+ params.parallel_tool_calls = false;
520
+ }
521
+ return _withRetry(() => openai.chat.completions.create(params));
459
522
  }
460
523
 
461
524
  // ── Fast Path Repair ──