@cairnvibe/sdk 0.2.12 → 0.2.13

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/server.js CHANGED
@@ -223,18 +223,26 @@ class GroqVerbLLM {
223
223
  return await this.attemptRespond(systemPrompt, userMessage);
224
224
  }
225
225
  catch (err) {
226
- // Real, live bug, not theoretical: openai/gpt-oss-120b (a reasoning-
227
- // capable open model) occasionally "thinks out loud" in plain prose
228
- // instead of emitting the forced tool call — Groq's own server-side
229
- // validation rejects that outright, a 400 with code
230
- // "output_parse_failed", before this code ever sees a real response
231
- // to work with. Non-deterministic (found live re-asking the exact
232
- // same question a moment later succeeded cleanly), so one retry —
233
- // not exponential backoff, this is a latency-sensitive voice/chat
234
- // pathgenuinely helps rather than just delaying the same
235
- // failure. Anything else still propagates to resolveVerb's own
226
+ // Real, live bugs, not theoretical two distinct non-deterministic
227
+ // failure modes from openai/gpt-oss-120b (a reasoning-capable open
228
+ // model), both rejected by Groq's own server-side validation before
229
+ // this code ever sees a real response to work with, and both found
230
+ // to recover cleanly on an identical retry a moment later:
231
+ // - "output_parse_failed": the model "thinks out loud" in plain
232
+ // prose instead of emitting the forced tool call.
233
+ // - "tool_use_failed": the model hallucinates a slightly-wrong tool
234
+ // name ("json", "response_with_verb" seen live, both against
235
+ // the real, correctly-configured VERB_TOOL_NAME) instead of the
236
+ // one forced tool it was actually given. This one was the actual
237
+ // cause behind a real "voice keeps breaking" report — found live
238
+ // running the new eval harness's synthetic-voice scenario, where
239
+ // it surfaced as "Something went wrong on my end" with no other
240
+ // symptom, exactly matching what got reported.
241
+ // One retry — not exponential backoff, this is a latency-sensitive
242
+ // voice/chat path — genuinely helps rather than just delaying the
243
+ // same failure. Anything else still propagates to resolveVerb's own
236
244
  // catch, unchanged.
237
- if (isOutputParseFailure(err)) {
245
+ if (isRetryableToolCallFailure(err)) {
238
246
  return await this.attemptRespond(systemPrompt, userMessage);
239
247
  }
240
248
  throw err;
@@ -277,13 +285,21 @@ exports.GroqVerbLLM = GroqVerbLLM;
277
285
  * actually been observed to surface — a thrown APIError with a nested
278
286
  * `.error.code`, a plain `.code`, or just the code string showing up
279
287
  * somewhere in the message — rather than relying on exactly one of them. */
280
- function isOutputParseFailure(err) {
288
+ function isRetryableToolCallFailure(err) {
281
289
  if (!err || typeof err !== "object")
282
290
  return false;
283
291
  const e = err;
284
- if (e.code === "output_parse_failed" || e.error?.code === "output_parse_failed")
292
+ const code = e.code ?? e.error?.code;
293
+ const message = typeof e.message === "string" ? e.message : "";
294
+ if (code === "output_parse_failed" || message.includes("output_parse_failed"))
285
295
  return true;
286
- return typeof e.message === "string" && e.message.includes("output_parse_failed");
296
+ // "attempted to call tool 'json' which was not in request.tools" — the
297
+ // model calling a tool name it invented instead of VERB_TOOL_NAME, the
298
+ // only one actually offered. See respond()'s doc comment for how this
299
+ // was found.
300
+ if (code === "tool_use_failed" && message.includes("attempted to call tool"))
301
+ return true;
302
+ return false;
287
303
  }
288
304
  // ---------------------------------------------------------------------------
289
305
  // Shared tool schema / system prompt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnvibe/sdk",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "In-app AI copilot — <Copilot/> for React/Next.js, <cairn-widget> for any framework — plus the server handlers and realtime voice relay behind them.",
5
5
  "license": "MIT",
6
6
  "publishConfig": { "access": "public" },
package/src/server.ts CHANGED
@@ -317,18 +317,26 @@ export class GroqVerbLLM implements VerbLLM {
317
317
  try {
318
318
  return await this.attemptRespond(systemPrompt, userMessage);
319
319
  } catch (err) {
320
- // Real, live bug, not theoretical: openai/gpt-oss-120b (a reasoning-
321
- // capable open model) occasionally "thinks out loud" in plain prose
322
- // instead of emitting the forced tool call — Groq's own server-side
323
- // validation rejects that outright, a 400 with code
324
- // "output_parse_failed", before this code ever sees a real response
325
- // to work with. Non-deterministic (found live re-asking the exact
326
- // same question a moment later succeeded cleanly), so one retry —
327
- // not exponential backoff, this is a latency-sensitive voice/chat
328
- // pathgenuinely helps rather than just delaying the same
329
- // failure. Anything else still propagates to resolveVerb's own
320
+ // Real, live bugs, not theoretical two distinct non-deterministic
321
+ // failure modes from openai/gpt-oss-120b (a reasoning-capable open
322
+ // model), both rejected by Groq's own server-side validation before
323
+ // this code ever sees a real response to work with, and both found
324
+ // to recover cleanly on an identical retry a moment later:
325
+ // - "output_parse_failed": the model "thinks out loud" in plain
326
+ // prose instead of emitting the forced tool call.
327
+ // - "tool_use_failed": the model hallucinates a slightly-wrong tool
328
+ // name ("json", "response_with_verb" seen live, both against
329
+ // the real, correctly-configured VERB_TOOL_NAME) instead of the
330
+ // one forced tool it was actually given. This one was the actual
331
+ // cause behind a real "voice keeps breaking" report — found live
332
+ // running the new eval harness's synthetic-voice scenario, where
333
+ // it surfaced as "Something went wrong on my end" with no other
334
+ // symptom, exactly matching what got reported.
335
+ // One retry — not exponential backoff, this is a latency-sensitive
336
+ // voice/chat path — genuinely helps rather than just delaying the
337
+ // same failure. Anything else still propagates to resolveVerb's own
330
338
  // catch, unchanged.
331
- if (isOutputParseFailure(err)) {
339
+ if (isRetryableToolCallFailure(err)) {
332
340
  return await this.attemptRespond(systemPrompt, userMessage);
333
341
  }
334
342
  throw err;
@@ -371,11 +379,18 @@ export class GroqVerbLLM implements VerbLLM {
371
379
  * actually been observed to surface — a thrown APIError with a nested
372
380
  * `.error.code`, a plain `.code`, or just the code string showing up
373
381
  * somewhere in the message — rather than relying on exactly one of them. */
374
- function isOutputParseFailure(err: unknown): boolean {
382
+ function isRetryableToolCallFailure(err: unknown): boolean {
375
383
  if (!err || typeof err !== "object") return false;
376
384
  const e = err as { code?: unknown; error?: { code?: unknown }; message?: unknown };
377
- if (e.code === "output_parse_failed" || e.error?.code === "output_parse_failed") return true;
378
- return typeof e.message === "string" && e.message.includes("output_parse_failed");
385
+ const code = e.code ?? e.error?.code;
386
+ const message = typeof e.message === "string" ? e.message : "";
387
+ if (code === "output_parse_failed" || message.includes("output_parse_failed")) return true;
388
+ // "attempted to call tool 'json' which was not in request.tools" — the
389
+ // model calling a tool name it invented instead of VERB_TOOL_NAME, the
390
+ // only one actually offered. See respond()'s doc comment for how this
391
+ // was found.
392
+ if (code === "tool_use_failed" && message.includes("attempted to call tool")) return true;
393
+ return false;
379
394
  }
380
395
 
381
396
  // ---------------------------------------------------------------------------