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