@cairnvibe/sdk 0.3.0 → 0.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/dist/cairn-widget.js +14 -9
- package/dist/cursor-overlay.d.ts +19 -0
- package/dist/cursor-overlay.js +126 -0
- package/dist/index.js +22 -7
- package/dist/realtime-server.js +14 -3
- package/dist/server.d.ts +17 -0
- package/dist/server.js +32 -9
- package/dist/verb-executor.js +128 -60
- package/package.json +1 -1
- package/src/cursor-overlay.ts +130 -0
- package/src/index.tsx +21 -7
- package/src/realtime-server.ts +16 -3
- package/src/server.ts +43 -7
- package/src/verb-executor.ts +123 -57
- package/src/web-component.ts +15 -7
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animates the synthetic cursor to `el`'s center and resolves once it has
|
|
3
|
+
* genuinely arrived (plus a brief real hover pause) — callers await this
|
|
4
|
+
* BEFORE performing the real action, so the cursor is seen gliding there
|
|
5
|
+
* first. Deliberately timer-driven (`window.setTimeout`), not
|
|
6
|
+
* `transitionend`/`Element.animate().finished`-driven — this repo's test
|
|
7
|
+
* environment is plain Node, not a real browser (see waitForDomSettle's own
|
|
8
|
+
* doc comment for the same discipline), and a fixed, known duration is what
|
|
9
|
+
* makes this testable with fake timers instead of needing real animation-
|
|
10
|
+
* completion events that a headless/no-DOM environment may never fire.
|
|
11
|
+
*
|
|
12
|
+
* SSR/no-DOM safe — same defensive guard `waitForDomSettle` already uses —
|
|
13
|
+
* so a caller never needs its own environment check before calling this.
|
|
14
|
+
*/
|
|
15
|
+
export declare function moveCursorTo(el: HTMLElement): Promise<void>;
|
|
16
|
+
/** Fades the synthetic cursor out — called once the widget itself closes or
|
|
17
|
+
* unmounts, so it doesn't sit visible on screen after the conversation
|
|
18
|
+
* ends. Safe to call even if the cursor was never created. */
|
|
19
|
+
export declare function hideCursor(): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A visible, animated synthetic cursor that glides to whatever element the
|
|
3
|
+
// agent is about to act on, and genuinely arrives — before anything actually
|
|
4
|
+
// happens on screen — rather than a click just occurring with no visible
|
|
5
|
+
// lead-up. Real, watchable proof of what the agent resolved, the same way
|
|
6
|
+
// watching a person's own mouse move tells you where they're about to click
|
|
7
|
+
// before it happens; matches this SDK's own "verified, not trusted"
|
|
8
|
+
// discipline in a form a user can literally see, not just read.
|
|
9
|
+
//
|
|
10
|
+
// Purely additive and deliberately decoupled from highlightElement
|
|
11
|
+
// (element-ladder.ts) — that function's own scroll+glow behavior is
|
|
12
|
+
// unchanged and still called by every site that used it before. This module
|
|
13
|
+
// only adds the moving cursor itself; a caller awaits moveCursorTo(el)
|
|
14
|
+
// before firing the real action so the cursor is seen arriving first, never
|
|
15
|
+
// after the fact — see verb-executor.ts's own call sites for the exact
|
|
16
|
+
// sequencing.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.moveCursorTo = moveCursorTo;
|
|
19
|
+
exports.hideCursor = hideCursor;
|
|
20
|
+
const CURSOR_ID = "cairn-cursor";
|
|
21
|
+
const MOVE_MS = 550;
|
|
22
|
+
const ARRIVE_PAUSE_MS = 160;
|
|
23
|
+
// The CSS side already disables the cursor's transition/animation under
|
|
24
|
+
// prefers-reduced-motion (see #cairn-cursor in the injected <style> block),
|
|
25
|
+
// which makes it jump instead of glide — but without this, the real delay
|
|
26
|
+
// before the action fires would stay the full ~710ms even though there's
|
|
27
|
+
// nothing left to watch. Mirrors the visual change with a real timing one.
|
|
28
|
+
const REDUCED_MOVE_MS = 60;
|
|
29
|
+
const REDUCED_ARRIVE_PAUSE_MS = 40;
|
|
30
|
+
function prefersReducedMotion() {
|
|
31
|
+
return (typeof window !== "undefined" &&
|
|
32
|
+
typeof window.matchMedia === "function" &&
|
|
33
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
34
|
+
}
|
|
35
|
+
// Module-scope, not per-call — the whole point is a SINGLE cursor that
|
|
36
|
+
// glides from wherever it last was, the way a real mouse never teleports
|
|
37
|
+
// between two unrelated screen positions.
|
|
38
|
+
let lastX = null;
|
|
39
|
+
let lastY = null;
|
|
40
|
+
function ensureCursorEl() {
|
|
41
|
+
if (typeof document === "undefined" || !document.body)
|
|
42
|
+
return null;
|
|
43
|
+
let el = document.getElementById(CURSOR_ID);
|
|
44
|
+
if (el)
|
|
45
|
+
return el;
|
|
46
|
+
el = document.createElement("div");
|
|
47
|
+
el.id = CURSOR_ID;
|
|
48
|
+
el.setAttribute("aria-hidden", "true");
|
|
49
|
+
// A simple filled pointer shape — matches the widget's own ember accent,
|
|
50
|
+
// with a thin dark stroke so it reads clearly on light AND dark pages
|
|
51
|
+
// (the host app's own background is never something this SDK controls).
|
|
52
|
+
el.innerHTML =
|
|
53
|
+
'<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
|
54
|
+
'<path d="M2 1.5 L2 18.2 L6.3 14.4 L9.1 20.6 L11.7 19.4 L8.9 13.3 L14.6 13.1 Z" fill="#E07A3F" stroke="#1B1815" stroke-width="1.1" stroke-linejoin="round"/>' +
|
|
55
|
+
"</svg>";
|
|
56
|
+
el.style.cssText =
|
|
57
|
+
"position:fixed;left:0;top:0;z-index:2147483001;pointer-events:none;opacity:0;transition:opacity 180ms ease;will-change:transform;filter:drop-shadow(0 3px 6px rgba(0,0,0,0.35));";
|
|
58
|
+
document.body.appendChild(el);
|
|
59
|
+
return el;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Animates the synthetic cursor to `el`'s center and resolves once it has
|
|
63
|
+
* genuinely arrived (plus a brief real hover pause) — callers await this
|
|
64
|
+
* BEFORE performing the real action, so the cursor is seen gliding there
|
|
65
|
+
* first. Deliberately timer-driven (`window.setTimeout`), not
|
|
66
|
+
* `transitionend`/`Element.animate().finished`-driven — this repo's test
|
|
67
|
+
* environment is plain Node, not a real browser (see waitForDomSettle's own
|
|
68
|
+
* doc comment for the same discipline), and a fixed, known duration is what
|
|
69
|
+
* makes this testable with fake timers instead of needing real animation-
|
|
70
|
+
* completion events that a headless/no-DOM environment may never fire.
|
|
71
|
+
*
|
|
72
|
+
* SSR/no-DOM safe — same defensive guard `waitForDomSettle` already uses —
|
|
73
|
+
* so a caller never needs its own environment check before calling this.
|
|
74
|
+
*/
|
|
75
|
+
function moveCursorTo(el) {
|
|
76
|
+
if (typeof document === "undefined" || typeof window === "undefined" || typeof el.getBoundingClientRect !== "function") {
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
}
|
|
79
|
+
const cursor = ensureCursorEl();
|
|
80
|
+
if (!cursor)
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
const rect = el.getBoundingClientRect();
|
|
83
|
+
const x = rect.left + rect.width / 2;
|
|
84
|
+
const y = rect.top + rect.height / 2;
|
|
85
|
+
if (lastX === null || lastY === null) {
|
|
86
|
+
// The very first move of the session starts from the widget's own
|
|
87
|
+
// corner (bottom-right, where the FAB lives) instead of materializing
|
|
88
|
+
// at (0,0) — reads as "coming from Cairn," not appearing from nowhere.
|
|
89
|
+
lastX = window.innerWidth - 40;
|
|
90
|
+
lastY = window.innerHeight - 40;
|
|
91
|
+
cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
|
|
92
|
+
}
|
|
93
|
+
const reduced = prefersReducedMotion();
|
|
94
|
+
const moveMs = reduced ? REDUCED_MOVE_MS : MOVE_MS;
|
|
95
|
+
const arrivePauseMs = reduced ? REDUCED_ARRIVE_PAUSE_MS : ARRIVE_PAUSE_MS;
|
|
96
|
+
cursor.style.transition = `transform ${moveMs}ms cubic-bezier(.4,0,.2,1), opacity 180ms ease`;
|
|
97
|
+
cursor.style.opacity = "1";
|
|
98
|
+
// Forces a style flush so the browser animates FROM the current position
|
|
99
|
+
// TO the new one instead of jumping straight there — reading a layout
|
|
100
|
+
// property is the standard, harmless way to force this without a real
|
|
101
|
+
// animation API (which, per this function's own doc comment, this
|
|
102
|
+
// deliberately avoids depending on for its completion signal anyway).
|
|
103
|
+
void cursor.offsetHeight;
|
|
104
|
+
cursor.style.transform = `translate(${x}px, ${y}px)`;
|
|
105
|
+
lastX = x;
|
|
106
|
+
lastY = y;
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
window.setTimeout(() => {
|
|
109
|
+
cursor.classList.add("cairn-cursor-hover");
|
|
110
|
+
window.setTimeout(() => {
|
|
111
|
+
cursor.classList.remove("cairn-cursor-hover");
|
|
112
|
+
resolve();
|
|
113
|
+
}, arrivePauseMs);
|
|
114
|
+
}, moveMs);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Fades the synthetic cursor out — called once the widget itself closes or
|
|
118
|
+
* unmounts, so it doesn't sit visible on screen after the conversation
|
|
119
|
+
* ends. Safe to call even if the cursor was never created. */
|
|
120
|
+
function hideCursor() {
|
|
121
|
+
if (typeof document === "undefined")
|
|
122
|
+
return;
|
|
123
|
+
const el = document.getElementById(CURSOR_ID);
|
|
124
|
+
if (el)
|
|
125
|
+
el.style.opacity = "0";
|
|
126
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const lucide_react_1 = require("lucide-react");
|
|
|
13
13
|
const core_1 = require("@cairnvibe/core");
|
|
14
14
|
const agent_loop_1 = require("./agent-loop");
|
|
15
15
|
const context_collector_1 = require("./context-collector");
|
|
16
|
+
const cursor_overlay_1 = require("./cursor-overlay");
|
|
16
17
|
const element_ladder_1 = require("./element-ladder");
|
|
17
18
|
const runtime_scan_1 = require("./runtime-scan");
|
|
18
19
|
const webmcp_client_1 = require("./webmcp-client");
|
|
@@ -40,6 +41,13 @@ function Copilot({ endpoint = "/api/copilot", registeredActions = [], onDo, repo
|
|
|
40
41
|
// loadPersistedConversation's own doc comment) had showing a moment ago,
|
|
41
42
|
// so a real reload never again looks like the conversation simply ended.
|
|
42
43
|
const [open, setOpen] = (0, react_1.useState)(false);
|
|
44
|
+
// Fades the synthetic cursor out once the panel closes (or the widget
|
|
45
|
+
// itself unmounts) instead of leaving it sitting visible on the page.
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
if (!open)
|
|
48
|
+
(0, cursor_overlay_1.hideCursor)();
|
|
49
|
+
return () => (0, cursor_overlay_1.hideCursor)();
|
|
50
|
+
}, [open]);
|
|
43
51
|
// Collapsed by default so the panel only ever shows the current exchange
|
|
44
52
|
// — the full archived transcript (built up over a long conversation)
|
|
45
53
|
// stays out of the way behind an explicit toggle instead of always being
|
|
@@ -1901,9 +1909,13 @@ const COPILOT_STYLES = `
|
|
|
1901
1909
|
0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
|
|
1902
1910
|
70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
|
1903
1911
|
}
|
|
1904
|
-
@keyframes cairn-pulse-
|
|
1905
|
-
0%, 100% { box-shadow: 0 0 0 0 rgba(
|
|
1906
|
-
70% { box-shadow: 0 0 0 10px rgba(
|
|
1912
|
+
@keyframes cairn-pulse-ember {
|
|
1913
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(224, 122, 63, 0.4); }
|
|
1914
|
+
70% { box-shadow: 0 0 0 10px rgba(224, 122, 63, 0); }
|
|
1915
|
+
}
|
|
1916
|
+
@keyframes cairn-cursor-arrive {
|
|
1917
|
+
0% { box-shadow: 0 0 0 0 rgba(224, 122, 63, 0.55); }
|
|
1918
|
+
100% { box-shadow: 0 0 0 9px rgba(224, 122, 63, 0); }
|
|
1907
1919
|
}
|
|
1908
1920
|
@keyframes cairn-spin {
|
|
1909
1921
|
from { transform: rotate(0deg); }
|
|
@@ -1919,7 +1931,7 @@ const COPILOT_STYLES = `
|
|
|
1919
1931
|
}
|
|
1920
1932
|
@keyframes cairn-word-sweep {
|
|
1921
1933
|
0% { opacity: 0.35; text-shadow: none; }
|
|
1922
|
-
35% { opacity: 1; color: #
|
|
1934
|
+
35% { opacity: 1; color: #E07A3F; text-shadow: 0 0 10px rgba(224, 122, 63, 0.45); }
|
|
1923
1935
|
100% { opacity: 1; color: inherit; text-shadow: none; }
|
|
1924
1936
|
}
|
|
1925
1937
|
@keyframes cairn-thinking-bounce {
|
|
@@ -1927,16 +1939,19 @@ const COPILOT_STYLES = `
|
|
|
1927
1939
|
40% { opacity: 0.9; transform: translateY(-3px); }
|
|
1928
1940
|
}
|
|
1929
1941
|
.cairn-glow {
|
|
1930
|
-
animation: cairn-pulse-
|
|
1931
|
-
outline: 2px solid #
|
|
1942
|
+
animation: cairn-pulse-ember 1.1s ease-out 2;
|
|
1943
|
+
outline: 2px solid #E07A3F;
|
|
1932
1944
|
outline-offset: 3px;
|
|
1933
1945
|
border-radius: 8px;
|
|
1934
1946
|
}
|
|
1947
|
+
.cairn-cursor-hover {
|
|
1948
|
+
animation: cairn-cursor-arrive 0.3s ease-out;
|
|
1949
|
+
}
|
|
1935
1950
|
.cairn-spin {
|
|
1936
1951
|
animation: cairn-spin 0.8s linear infinite;
|
|
1937
1952
|
}
|
|
1938
1953
|
@media (prefers-reduced-motion: reduce) {
|
|
1939
|
-
.cairn-fab, .cairn-panel, .cairn-bubble, .cairn-word, .cairn-thinking-dot {
|
|
1954
|
+
.cairn-fab, .cairn-panel, .cairn-bubble, .cairn-word, .cairn-thinking-dot, #cairn-cursor {
|
|
1940
1955
|
animation: none !important;
|
|
1941
1956
|
transition: none !important;
|
|
1942
1957
|
}
|
package/dist/realtime-server.js
CHANGED
|
@@ -105,11 +105,22 @@ async function handleRememberFactTool(memory, scopeId, args) {
|
|
|
105
105
|
function createRealtimeServer(options) {
|
|
106
106
|
const registeredActions = options.registeredActions ?? [];
|
|
107
107
|
const capability = options.capability ?? "act";
|
|
108
|
-
|
|
108
|
+
// One shared rotator across all three LLM roles — see
|
|
109
|
+
// CreateCopilotHandlerOptions.keyRotator's own doc comment for the real
|
|
110
|
+
// gap this closes (a key one role confirmed dead used to stay invisible
|
|
111
|
+
// to the other two, which kept rediscovering it fresh on every call).
|
|
112
|
+
// Only built for groq — anthropic's createXLLM calls ignore keyRotator
|
|
113
|
+
// entirely, so building one for it would be dead work. Respects a
|
|
114
|
+
// caller-supplied options.keyRotator (e.g. shared with the typed/HTTP
|
|
115
|
+
// transport in the same process) instead of always building a fresh one.
|
|
116
|
+
const sharedOptions = options.provider === "groq" && !options.keyRotator
|
|
117
|
+
? { ...options, keyRotator: options.apiKeys ? new server_1.KeyRotator(options.apiKeys) : options.apiKey ? new server_1.KeyRotator([options.apiKey]) : server_1.KeyRotator.fromEnvList(process.env.GROQ_API_KEYS) ?? undefined }
|
|
118
|
+
: options;
|
|
119
|
+
const llm = (0, server_1.createVerbLLM)(sharedOptions);
|
|
109
120
|
// Phase 3 steps 2-3 — real, separately-configured Planner/Critic LLMs.
|
|
110
121
|
// See finalizeTurn's own doc comment for how they're actually used.
|
|
111
|
-
const planLLM = (0, server_1.createPlanLLM)(
|
|
112
|
-
const criticLLM = (0, server_1.createCriticLLM)(
|
|
122
|
+
const planLLM = (0, server_1.createPlanLLM)(sharedOptions);
|
|
123
|
+
const criticLLM = (0, server_1.createCriticLLM)(sharedOptions);
|
|
113
124
|
// "text" is optional on highlight/open/navigate/do in the base prompt —
|
|
114
125
|
// fine for the typed/HTTP path, which always has a visible answer area,
|
|
115
126
|
// but silence reads as broken in a live voice conversation (the client
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CriticVerdict, type HistoryTurn, type LiveElement, type Manifest, type Plan, type Skill, type SkillSummary, type Task, type UiPatternId, type VerbResponse, type WebMcpTool } from "@cairnvibe/core";
|
|
2
2
|
import { type MemoryStore } from "./memory-sqlite";
|
|
3
|
+
export { KeyRotator } from "./key-rotator";
|
|
3
4
|
import { KeyRotator } from "./key-rotator";
|
|
4
5
|
import type { SkillStore } from "./skill-store";
|
|
5
6
|
/**
|
|
@@ -16,6 +17,22 @@ export interface CreateCopilotHandlerOptions {
|
|
|
16
17
|
/** Single API key. For groq, prefer `apiKeys` to round-robin; falls back to GROQ_API_KEYS env. */
|
|
17
18
|
apiKey?: string;
|
|
18
19
|
apiKeys?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* A pre-built rotator to share across multiple LLM roles (verb, plan,
|
|
22
|
+
* critic) instead of each one building its own from `apiKeys`/`apiKey`/
|
|
23
|
+
* env. Real, live-found gap this closes: createVerbLLM/createPlanLLM/
|
|
24
|
+
* createCriticLLM each called createToolLLM independently, and each one
|
|
25
|
+
* built a BRAND NEW KeyRotator from the same GROQ_API_KEYS list — so a
|
|
26
|
+
* key one of them confirmed dead via a real 401 (KeyRotator.markDead)
|
|
27
|
+
* stayed invisible to the other two, which went on rediscovering the
|
|
28
|
+
* exact same dead key from scratch on every one of their own calls,
|
|
29
|
+
* wasting real round trips and, worse, stacking up wasted attempts
|
|
30
|
+
* against the SAME small number of retries each call is bounded to.
|
|
31
|
+
* Takes precedence over `apiKeys`/`apiKey`/env when provided. See
|
|
32
|
+
* groq-llm.ts in examples/demo-app for the intended usage: build one
|
|
33
|
+
* KeyRotator at module scope, pass it to all three createXLLM calls.
|
|
34
|
+
*/
|
|
35
|
+
keyRotator?: KeyRotator;
|
|
19
36
|
model?: string;
|
|
20
37
|
/** Action ids this deployment actually supports. "do" is refused for anything else. */
|
|
21
38
|
registeredActions?: string[];
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.GroqStreamingTextLLM = exports.GroqVerbLLM = exports.AnthropicStreamingTextLLM = exports.AnthropicVerbLLM = void 0;
|
|
10
|
+
exports.GroqStreamingTextLLM = exports.GroqVerbLLM = exports.AnthropicStreamingTextLLM = exports.AnthropicVerbLLM = exports.KeyRotator = void 0;
|
|
11
11
|
exports.createCopilotHandler = createCopilotHandler;
|
|
12
12
|
exports.createCopilotHandlerWithLLM = createCopilotHandlerWithLLM;
|
|
13
13
|
exports.resolveVerb = resolveVerb;
|
|
@@ -34,7 +34,9 @@ const zod_1 = require("zod");
|
|
|
34
34
|
const core_1 = require("@cairnvibe/core");
|
|
35
35
|
const agent_loop_1 = require("./agent-loop");
|
|
36
36
|
const memory_sqlite_1 = require("./memory-sqlite");
|
|
37
|
-
|
|
37
|
+
var key_rotator_1 = require("./key-rotator");
|
|
38
|
+
Object.defineProperty(exports, "KeyRotator", { enumerable: true, get: function () { return key_rotator_1.KeyRotator; } });
|
|
39
|
+
const key_rotator_2 = require("./key-rotator");
|
|
38
40
|
const VERB_TOOL_NAME = "respond_with_verb";
|
|
39
41
|
const PLAN_TOOL_NAME = "create_plan";
|
|
40
42
|
const PLAN_TOOL_DESCRIPTION = "Submit an ordered task plan for achieving the user's real end goal.";
|
|
@@ -372,11 +374,12 @@ function createCriticLLM(options = {}) {
|
|
|
372
374
|
function createToolLLM(options, toolSchema, toolName, toolDescription) {
|
|
373
375
|
const provider = options.provider ?? "anthropic";
|
|
374
376
|
if (provider === "groq") {
|
|
375
|
-
const rotator = options.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
377
|
+
const rotator = options.keyRotator
|
|
378
|
+
?? (options.apiKeys
|
|
379
|
+
? new key_rotator_2.KeyRotator(options.apiKeys)
|
|
380
|
+
: options.apiKey
|
|
381
|
+
? new key_rotator_2.KeyRotator([options.apiKey])
|
|
382
|
+
: key_rotator_2.KeyRotator.fromEnvList(process.env.GROQ_API_KEYS));
|
|
380
383
|
if (!rotator) {
|
|
381
384
|
throw new Error("createToolLLM: provider 'groq' needs apiKey(s), or GROQ_API_KEYS in env");
|
|
382
385
|
}
|
|
@@ -650,7 +653,20 @@ class GroqVerbLLM {
|
|
|
650
653
|
clientFactory;
|
|
651
654
|
toolName;
|
|
652
655
|
toolDescription;
|
|
653
|
-
constructor(keys, model, toolSchema,
|
|
656
|
+
constructor(keys, model, toolSchema,
|
|
657
|
+
// maxRetries: 0 — real, live-found latency bug this closes: the Groq
|
|
658
|
+
// SDK's own default (2 automatic retries with exponential backoff) ran
|
|
659
|
+
// UNDERNEATH respond()'s own key-rotation retry loop, so a single 429
|
|
660
|
+
// key attempt could silently eat several real seconds of SDK-internal
|
|
661
|
+
// backoff before respond() ever saw the rejection and moved on to a
|
|
662
|
+
// DIFFERENT key. With several keys in rotation genuinely rate-limited
|
|
663
|
+
// at once (the common case this closes for), that compounded into a
|
|
664
|
+
// real, live-reported multi-second-to-a-minute hang with no visible
|
|
665
|
+
// progress — worse than useless, since respond()'s own retry already
|
|
666
|
+
// tries a different key/quota entirely, which the SDK's blind same-key
|
|
667
|
+
// backoff can never fix. respond() is the sole source of retry policy
|
|
668
|
+
// here now.
|
|
669
|
+
clientFactory = (apiKey) => new groq_sdk_1.default({ apiKey, maxRetries: 0 }), toolName = VERB_TOOL_NAME, toolDescription = VERB_TOOL_DESCRIPTION) {
|
|
654
670
|
this.keys = keys;
|
|
655
671
|
this.model = model;
|
|
656
672
|
this.toolSchema = toolSchema;
|
|
@@ -784,7 +800,14 @@ class GroqStreamingTextLLM {
|
|
|
784
800
|
keys;
|
|
785
801
|
model;
|
|
786
802
|
clientFactory;
|
|
787
|
-
constructor(keys, model,
|
|
803
|
+
constructor(keys, model,
|
|
804
|
+
// maxRetries: 0 — same real latency bug as GroqVerbLLM's own
|
|
805
|
+
// clientFactory default; see its doc comment for the full reasoning.
|
|
806
|
+
// respondStreamed below already has its own key-rotation retry loop
|
|
807
|
+
// (maxAttempts bounded by keys.size), which makes the SDK's blind
|
|
808
|
+
// same-key backoff redundant AND a source of silent multi-second
|
|
809
|
+
// delay stacked underneath it.
|
|
810
|
+
clientFactory = (apiKey) => new groq_sdk_1.default({ apiKey, maxRetries: 0 })) {
|
|
788
811
|
this.keys = keys;
|
|
789
812
|
this.model = model;
|
|
790
813
|
this.clientFactory = clientFactory;
|