@cairnvibe/sdk 0.2.13 → 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.
Files changed (44) hide show
  1. package/dist/agent-loop.d.ts +113 -0
  2. package/dist/agent-loop.js +128 -0
  3. package/dist/cairn-widget.js +14 -9
  4. package/dist/cursor-overlay.d.ts +19 -0
  5. package/dist/cursor-overlay.js +126 -0
  6. package/dist/element-ladder.d.ts +71 -0
  7. package/dist/element-ladder.js +168 -0
  8. package/dist/index.d.ts +79 -1
  9. package/dist/index.js +886 -96
  10. package/dist/key-rotator.d.ts +28 -0
  11. package/dist/key-rotator.js +57 -3
  12. package/dist/memory-sqlite.d.ts +86 -0
  13. package/dist/memory-sqlite.js +230 -0
  14. package/dist/realtime-cli.js +22 -1
  15. package/dist/realtime-server.d.ts +83 -2
  16. package/dist/realtime-server.js +561 -121
  17. package/dist/server.d.ts +266 -5
  18. package/dist/server.js +1013 -83
  19. package/dist/skill-store.d.ts +17 -0
  20. package/dist/skill-store.js +78 -0
  21. package/dist/tts-stream.d.ts +25 -0
  22. package/dist/tts-stream.js +32 -0
  23. package/dist/vad.d.ts +27 -0
  24. package/dist/vad.js +128 -0
  25. package/dist/verb-executor.d.ts +32 -11
  26. package/dist/verb-executor.js +315 -39
  27. package/dist/webmcp-client.d.ts +14 -1
  28. package/dist/webmcp-client.js +22 -1
  29. package/package.json +3 -1
  30. package/src/agent-loop.ts +222 -0
  31. package/src/cursor-overlay.ts +130 -0
  32. package/src/element-ladder.ts +170 -0
  33. package/src/index.tsx +935 -100
  34. package/src/key-rotator.ts +57 -2
  35. package/src/memory-sqlite.ts +283 -0
  36. package/src/realtime-cli.ts +24 -1
  37. package/src/realtime-server.ts +669 -123
  38. package/src/server.ts +1119 -83
  39. package/src/skill-store.ts +88 -0
  40. package/src/tts-stream.ts +30 -0
  41. package/src/vad.ts +153 -0
  42. package/src/verb-executor.ts +329 -42
  43. package/src/web-component.ts +97 -24
  44. package/src/webmcp-client.ts +30 -2
package/dist/server.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { type HistoryTurn, type LiveElement, type Manifest, type VerbResponse, type WebMcpTool } from "@cairnvibe/core";
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
+ import { type MemoryStore } from "./memory-sqlite";
3
+ export { KeyRotator } from "./key-rotator";
2
4
  import { KeyRotator } from "./key-rotator";
5
+ import type { SkillStore } from "./skill-store";
3
6
  /**
4
7
  * What the agent is allowed to do, independent of which specific "do"
5
8
  * actions are registered:
@@ -14,13 +17,69 @@ export interface CreateCopilotHandlerOptions {
14
17
  /** Single API key. For groq, prefer `apiKeys` to round-robin; falls back to GROQ_API_KEYS env. */
15
18
  apiKey?: string;
16
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;
17
36
  model?: string;
18
37
  /** Action ids this deployment actually supports. "do" is refused for anything else. */
19
38
  registeredActions?: string[];
39
+ /**
40
+ * Phase 4, layer 5 — real, human-written descriptions for `registeredActions`
41
+ * ids, e.g. `{ archiveInvoice: "Archives the invoice; cannot be undone." }`.
42
+ * Optional and purely additive: an id with no entry here still works
43
+ * exactly as before (rendered bare, no description) — this was the
44
+ * weakest-typed of Cairn's three action-invocation mechanisms (a
45
+ * registered action id carried literally zero server-visible metadata,
46
+ * unlike a WebMCP tool's own description or an element's `does` text);
47
+ * this closes that gap without changing what the model must echo back
48
+ * in "action" (still the bare id — see renderRegisteredActions).
49
+ */
50
+ actionDescriptions?: Record<string, string>;
20
51
  /** What the agent is allowed to do at all. Defaults to "act". See `CapabilityTier`. */
21
52
  capability?: CapabilityTier;
22
53
  /** Display name / identity for the agent, woven into its system prompt and shown in the widget. Defaults to "Cairn". */
23
54
  persona?: string;
55
+ /**
56
+ * Phase 5 step 4 — real cross-session memory for the typed/HTTP
57
+ * transport (packages/sdk/src/memory-sqlite.ts, or any store
58
+ * implementing the same interface). Optional — omitting it keeps
59
+ * every request exactly as memory-less as before this existed.
60
+ * Scoped by whatever `scopeId` string the request itself carries
61
+ * (`CopilotRequestSchema.scopeId`) — this SDK invents no identity of
62
+ * its own. Unlike the realtime relay (one persistent connection
63
+ * remembers a scopeId once), this transport is stateless per
64
+ * request: `resolveVerb`'s own callers seed from memory only when the
65
+ * REQUEST's own `history` arrives empty (a genuinely fresh session —
66
+ * see `createCopilotHandlerWithLLM`), never on every request, so a
67
+ * session already accumulating its own history client-side isn't
68
+ * re-seeded on top of itself.
69
+ */
70
+ memory?: MemoryStore;
71
+ /**
72
+ * Architecture Pillar 3 (Skill half) — real, per-deployment Skill
73
+ * storage (packages/sdk/src/skill-store.ts). A DIFFERENT scope axis
74
+ * than `memory` above — see skill-store.ts's own doc comment. Optional;
75
+ * omitting it keeps every request exactly as it was before this
76
+ * existed. Consumed by `createPlanHandler` (retrieval — a matching
77
+ * Skill's full instructions get surfaced to the Planner) and
78
+ * `createSkillSaveHandler` (the Formulator's own save side).
79
+ */
80
+ skills?: SkillStore;
81
+ /** The deployment-wide scope Skills are stored/looked up under when `skills` is configured. Defaults to "default" when omitted. */
82
+ skillsScopeId?: string;
24
83
  }
25
84
  export interface CopilotHandlerResult {
26
85
  status: number;
@@ -52,6 +111,8 @@ export declare function createCopilotHandlerWithLLM(manifest: Manifest, llm: Ver
52
111
  registeredActions?: string[];
53
112
  capability?: CapabilityTier;
54
113
  persona?: string;
114
+ actionDescriptions?: Record<string, string>;
115
+ memory?: MemoryStore;
55
116
  }): CopilotHandler;
56
117
  /**
57
118
  * The safety-critical core, shared by the HTTP handler above and the
@@ -67,15 +128,184 @@ export declare function resolveVerb(llm: VerbLLM, systemPrompt: string, manifest
67
128
  liveElements?: LiveElement[];
68
129
  webMcpTools?: WebMcpTool[];
69
130
  }): Promise<VerbResponse>;
131
+ /**
132
+ * Phase 3, step 2 (see DEVELOPMENT.md/the plan file) — the Planner half
133
+ * of the Planner/Executor/Critic/Talker redesign. Decomposes a real end
134
+ * goal into an ordered task list BEFORE any execution happens, mirroring
135
+ * resolveVerb's own resilience discipline: never throws to the caller,
136
+ * degrades to a real, usable single-task fallback plan on any failure
137
+ * (a bad LLM response, a schema mismatch, a network error) rather than
138
+ * blocking the turn on a Planner hiccup. `version`/each task's `status`
139
+ * are harness-owned, not asked of the model (PlannerOutputSchema's own
140
+ * doc comment) — assembled here around the model's raw output.
141
+ *
142
+ * Deliberately does NOT yet change what the loop actually does with the
143
+ * result — step 2's own scope is observability only (see the doc comment
144
+ * on this function's call site in realtime-server.ts). The Critic (step
145
+ * 3) is what makes a Plan's tasks/doneContracts actually drive behavior.
146
+ */
147
+ export declare function resolvePlan(llm: VerbLLM, goal: string, version?: number, manifest?: Manifest, actionsText?: string, skills?: {
148
+ summariesText?: string;
149
+ suggestedInstructions?: string;
150
+ }): Promise<Plan>;
151
+ /** The real, single-task plan used when the Planner call itself fails —
152
+ * "do the whole goal as one task" is always a valid (if unstructured)
153
+ * plan, so a Planner hiccup degrades the redesign back to today's
154
+ * behavior instead of blocking the turn. Exported so every caller that
155
+ * needs "a plan, even a trivial one, right now" (e.g. a Critic call that
156
+ * fires before a real Planner result has come back) builds the exact
157
+ * same shape instead of hand-rolling a duplicate literal — realtime-
158
+ * server.ts's own finalizeTurn and index.tsx's runTypedAgentLoop both do
159
+ * this, for the same reason. */
160
+ export declare function fallbackPlan(goal: string, version: number): Plan;
161
+ /**
162
+ * Phase 3, step 3 — the Critic. A genuinely SEPARATE pass over the
163
+ * step's real observation, decoupled from the Executor/model's own
164
+ * self-report — this is the direct fix for the diagnosed bug (a batch
165
+ * of 2 clicks succeeded, and the model kept looping 4 more iterations
166
+ * before giving up, never recognizing its own success). Mirrors
167
+ * packages/evals/src/judge.ts's own judgeScenario shape on purpose (a
168
+ * separate model looking at real state, forced tool call, structured
169
+ * verdict) — same real precedent already proven and tested in this repo,
170
+ * not a new pattern invented for this. Same resilience discipline as
171
+ * resolveVerb/resolvePlan: never throws, degrades to a real "continue"
172
+ * verdict (harmless — the loop just behaves as if the Critic weren't
173
+ * there for this one step) on any failure.
174
+ */
175
+ export declare function resolveCritic(llm: VerbLLM, task: Task, goal: string, verb: VerbResponse, observation: string | null | undefined): Promise<CriticVerdict>;
176
+ /** Same real rotation/model-selection logic as createVerbLLM/createPlanLLM,
177
+ * configured for the Critic's own tool instead — see resolveCritic. */
178
+ export declare function createCriticLLM(options?: CreateCopilotHandlerOptions): VerbLLM;
70
179
  /** Builds the provider-appropriate VerbLLM from the same options createCopilotHandler accepts — reused by the realtime relay. */
71
180
  export declare function createVerbLLM(options?: CreateCopilotHandlerOptions): VerbLLM;
181
+ /** Same real rotation/model-selection logic as createVerbLLM, configured
182
+ * for the Planner's own tool instead — see resolvePlan. */
183
+ export declare function createPlanLLM(options?: CreateCopilotHandlerOptions): VerbLLM;
184
+ /**
185
+ * Architecture Pillar 3 (Skill half) — the Formulator. Runs once a task
186
+ * genuinely completes (not per-step — cheap on purpose, matching the plan
187
+ * file's own framing), compiling whatever real, Critic-verified
188
+ * `learnedFact`s were collected along the way (CriticVerdictSchema's own
189
+ * doc comment is the enforcement point for "never user data") into one
190
+ * Skill. Deliberately DETERMINISTIC, not a fourth kind of real LLM call —
191
+ * every fact it compiles already passed through the Critic's own
192
+ * verification, so there's nothing left to "figure out" that would
193
+ * justify the added cost/latency/failure surface of another model round
194
+ * trip; see DEVELOPMENT.md's own entry for the real cost reasoning
195
+ * (this session already hit genuine Groq quota exhaustion more than once
196
+ * from cumulative call volume). Returns null when nothing was learned —
197
+ * the common case, not an error; a caller should simply not save anything.
198
+ */
199
+ export declare function compileSkill(goal: string, learnedFacts: string[], pattern?: UiPatternId): Skill | null;
200
+ /**
201
+ * Architecture Pillar 3 (Skill half) — the retrieval side. A cheap,
202
+ * deterministic keyword-overlap match against a NEW goal (never another
203
+ * real LLM call, same reasoning as compileSkill above) — real progressive
204
+ * disclosure: every Skill's summary is cheap enough to always list (see
205
+ * SkillStore's own doc comment), but only the ONE Skill whose own name
206
+ * shares real, significant words with the current goal gets its full
207
+ * instructions loaded. A caller still needs its own SkillStore.getSkill
208
+ * call to fetch those full instructions for whatever this returns — this
209
+ * function only ever sees cheap summaries, never a full Skill.
210
+ */
211
+ export declare function matchSkillByGoal(summaries: SkillSummary[], goal: string): SkillSummary | null;
212
+ /** Same rendering discipline as renderRegisteredActions — "id (description)" per Skill, for the Planner's own userMessage. */
213
+ export declare function renderSkillSummaries(summaries: SkillSummary[]): string;
214
+ export type PlanHandler = (body: unknown) => Promise<{
215
+ status: number;
216
+ body: Plan | {
217
+ error: string;
218
+ };
219
+ }>;
220
+ export type CriticHandler = (body: unknown) => Promise<{
221
+ status: number;
222
+ body: CriticVerdict | {
223
+ error: string;
224
+ };
225
+ }>;
226
+ /**
227
+ * Architecture Pillar 4 — the typed/HTTP transport's own real Planner
228
+ * endpoint, closing the gap the plan file names directly: "the typed/
229
+ * HTTP path (index.tsx's runTypedAgentLoop) has zero Planner/Critic
230
+ * wiring at all... today explicitly realtime-only by deferral, not by
231
+ * decision." A thin HTTP wrapper around the exact same resolvePlan the
232
+ * realtime relay already calls in-process — the LLM call itself only
233
+ * ever needs to happen server-side (it holds the real API key), so a
234
+ * client-side caller (index.tsx) reaches it over a real request instead
235
+ * of importing resolvePlan directly, same reasoning as createCopilotHandler
236
+ * itself.
237
+ */
238
+ export declare function createPlanHandler(manifest: Manifest, options?: CreateCopilotHandlerOptions): PlanHandler;
239
+ /** Same as createPlanHandler, but with the LLM injected — used by tests to fake it, same pattern as createCopilotHandlerWithLLM. */
240
+ export declare function createPlanHandlerWithLLM(manifest: Manifest, planLLM: VerbLLM, options?: {
241
+ registeredActions?: string[];
242
+ actionDescriptions?: Record<string, string>;
243
+ skills?: SkillStore;
244
+ skillsScopeId?: string;
245
+ }): PlanHandler;
246
+ /** Architecture Pillar 4's Critic counterpart to createPlanHandler — see
247
+ * its own doc comment. A thin HTTP wrapper around the same resolveCritic
248
+ * the realtime relay already calls in-process. */
249
+ export declare function createCriticHandler(options?: CreateCopilotHandlerOptions): CriticHandler;
250
+ /** Same as createCriticHandler, but with the LLM injected — used by tests to fake it. */
251
+ export declare function createCriticHandlerWithLLM(criticLLM: VerbLLM): CriticHandler;
252
+ export type SkillSaveHandler = (body: unknown) => Promise<{
253
+ status: number;
254
+ body: {
255
+ saved: boolean;
256
+ } | {
257
+ error: string;
258
+ };
259
+ }>;
260
+ /**
261
+ * Architecture Pillar 3 (Skill half) — the typed transport's own save
262
+ * side (the Formulator's HTTP counterpart to realtime-server.ts's own
263
+ * in-process `compileSkill`+`saveSkill` call at the end of `finalizeTurn`).
264
+ * No LLM involved — `compileSkill` is deterministic (see its own doc
265
+ * comment for why) — so this needs no `-WithLLM` variant; it's real
266
+ * client-callable storage access, nothing more. The caller (index.tsx's
267
+ * runTypedAgentLoop) accumulates `learnedFacts` from its own Critic calls
268
+ * across one whole turn and posts here exactly once, after the turn
269
+ * concludes — never per-step, matching the Formulator's own "cheap on
270
+ * purpose" framing.
271
+ */
272
+ export declare function createSkillSaveHandler(skills: SkillStore, skillsScopeId?: string): SkillSaveHandler;
273
+ /**
274
+ * Phase 2 step 1 — a genuinely UNSTRUCTURED, streamed call: no tools, no
275
+ * forced choice, just the model's plain spoken answer to the user's
276
+ * question, delivered incrementally. Exists because a real, live spike
277
+ * against Groq's actual API (see DEVELOPMENT.md/the plan file's Phase 2
278
+ * entry) found that a FORCED tool call never streams at the field level
279
+ * even with stream:true — the whole structured object arrives in one
280
+ * chunk. Plain, unforced generation genuinely streams token-by-token on
281
+ * both providers, and finishes faster besides — this is what makes "LLM
282
+ * tokens streamed straight into TTS" possible at all.
283
+ */
284
+ export interface StreamingTextLLM {
285
+ respondStreamed(systemPrompt: string, userMessage: string, onChunk: (delta: string) => void): Promise<string>;
286
+ }
72
287
  export declare class AnthropicVerbLLM implements VerbLLM {
73
288
  private client;
74
289
  private model;
75
290
  private toolSchema;
76
- constructor(client: MessagesClient, model: string, toolSchema: Record<string, unknown>);
291
+ private toolName;
292
+ private toolDescription;
293
+ constructor(client: MessagesClient, model: string, toolSchema: Record<string, unknown>, toolName?: string, toolDescription?: string);
77
294
  respond(systemPrompt: string, userMessage: string): Promise<unknown>;
78
295
  }
296
+ /** Minimal shape AnthropicStreamingTextLLM needs — narrow enough to fake in tests (a plain async generator, no real SDK stream class). */
297
+ export interface StreamingMessagesClient {
298
+ messages: {
299
+ create: (params: any) => Promise<AsyncIterable<any>>;
300
+ };
301
+ }
302
+ /** No tools, no tool_choice — see StreamingTextLLM's own doc comment for why plain, unforced generation is what streams. */
303
+ export declare class AnthropicStreamingTextLLM implements StreamingTextLLM {
304
+ private client;
305
+ private model;
306
+ constructor(client: StreamingMessagesClient, model: string);
307
+ respondStreamed(systemPrompt: string, userMessage: string, onChunk: (delta: string) => void): Promise<string>;
308
+ }
79
309
  /** Minimal shape GroqVerbLLM needs — narrow enough to fake in tests. */
80
310
  export interface GroqLikeClient {
81
311
  chat: {
@@ -91,11 +321,42 @@ export declare class GroqVerbLLM implements VerbLLM {
91
321
  private model;
92
322
  private toolSchema;
93
323
  private clientFactory;
94
- constructor(keys: KeyRotator, model: string, toolSchema: Record<string, unknown>, clientFactory?: (apiKey: string) => GroqLikeClient);
324
+ private toolName;
325
+ private toolDescription;
326
+ constructor(keys: KeyRotator, model: string, toolSchema: Record<string, unknown>, clientFactory?: (apiKey: string) => GroqLikeClient, toolName?: string, toolDescription?: string);
95
327
  respond(systemPrompt: string, userMessage: string): Promise<unknown>;
96
328
  private attemptRespond;
97
329
  }
98
- export declare function buildVerbToolSchema(registeredActions: string[]): Record<string, unknown>;
330
+ /** Minimal shape GroqStreamingTextLLM needs — narrow enough to fake in tests (a plain async generator, no real SDK stream class). */
331
+ export interface GroqLikeStreamingClient {
332
+ chat: {
333
+ completions: {
334
+ create: (params: any) => Promise<AsyncIterable<any>>;
335
+ };
336
+ };
337
+ }
338
+ /** No tools, no tool_choice — see StreamingTextLLM's own doc comment for why plain, unforced generation is what streams. No retry-on-hallucinated-tool-name logic here (GroqVerbLLM's own real, live-found bug) — there's no tool to hallucinate the name of. */
339
+ export declare class GroqStreamingTextLLM implements StreamingTextLLM {
340
+ private keys;
341
+ private model;
342
+ private clientFactory;
343
+ constructor(keys: KeyRotator, model: string, clientFactory?: (apiKey: string) => GroqLikeStreamingClient);
344
+ respondStreamed(systemPrompt: string, userMessage: string, onChunk: (delta: string) => void): Promise<string>;
345
+ }
346
+ /**
347
+ * Phase 4, layer 5 — the ONE place a registered action id is rendered
348
+ * with its (optional) real description, shared by buildVerbToolSchema,
349
+ * buildSystemPrompt's own do-verb text, and resolvePlan's userMessage —
350
+ * so the Executor and the Planner describe the exact same capability the
351
+ * exact same way, and there's no risk of the three drifting out of sync.
352
+ * Deliberately renders "id (description)" rather than baking the
353
+ * description into what the model must echo back — resolveVerb's own
354
+ * `registeredActions.includes(parsedVerb.data.action)` check (server.ts)
355
+ * needs the RAW id back, verbatim, or a real registered action would
356
+ * silently stop being recognized.
357
+ */
358
+ export declare function renderRegisteredActions(registeredActions: string[], actionDescriptions?: Record<string, string>): string;
359
+ export declare function buildVerbToolSchema(registeredActions: string[], actionDescriptions?: Record<string, string>): Record<string, unknown>;
99
360
  /**
100
361
  * A compact route directory — NOT every element on every page. Found live
101
362
  * and necessary, not theoretical: a real 17-page production app's full
@@ -111,4 +372,4 @@ export declare function buildVerbToolSchema(registeredActions: string[]): Record
111
372
  * element detail is attached separately, per request, in resolveVerb —
112
373
  * see buildPageElements.
113
374
  */
114
- export declare function buildSystemPrompt(manifest: Manifest, registeredActions: string[], persona?: string): string;
375
+ export declare function buildSystemPrompt(manifest: Manifest, registeredActions: string[], persona?: string, actionDescriptions?: Record<string, string>): string;