@firstlovecenter/ai-chat 0.1.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.
@@ -0,0 +1,487 @@
1
+ import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-DNwFvL-C.cjs';
2
+ export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-DNwFvL-C.cjs';
3
+ import { GoogleAuth } from 'google-auth-library';
4
+ export { GoogleAuth } from 'google-auth-library';
5
+
6
+ /**
7
+ * Provider-agnostic tool-calling abstraction.
8
+ *
9
+ * The agent loop is written against `ToolProvider`, not against any one
10
+ * vendor SDK. Two adapters live alongside this file — `claude.ts`
11
+ * (Anthropic Messages on Vertex) and `gemini.ts` (Google Gemini on
12
+ * Vertex) — and they translate between this normalized shape and each
13
+ * vendor's wire format.
14
+ *
15
+ * Design notes:
16
+ *
17
+ * - `NormalizedMessage` carries the full conversation turn-by-turn:
18
+ * user prompts, assistant replies (text + tool calls), and the
19
+ * matching tool results. Storing both `id` and `name` on every tool
20
+ * call lets us survive Gemini's name-keyed function responses (no
21
+ * IDs) and Anthropic's id-keyed `tool_result` blocks with one shape.
22
+ *
23
+ * - `SystemBlock.cached` is advisory: Anthropic uses it to mark
24
+ * `cache_control: ephemeral`, Gemini ignores it (Vertex Gemini
25
+ * auto-caches stable prefixes). Both still see the same text, in
26
+ * the same order — semantics are preserved across the swap.
27
+ *
28
+ * - The schema we accept here is the JSON Schema declared on each
29
+ * tool. The Gemini-side adapter calls `toGeminiSchema()` to
30
+ * translate `oneOf` → `anyOf`, `const` → enum, etc.
31
+ *
32
+ * - `SystemBlock` and `ToolSchema` are owned by `../tools/types` so the
33
+ * package has a single source of truth. We re-export them from this
34
+ * module so callers can keep `import { SystemBlock } from './types'`.
35
+ */
36
+
37
+ type ToolProviderId = 'claude' | 'gemini';
38
+ /** A single tool invocation requested by the model. */
39
+ type NormalizedToolCall = {
40
+ /** Stable per-turn id we generate or echo from the provider. */
41
+ id: string;
42
+ name: string;
43
+ input: Record<string, unknown>;
44
+ };
45
+ /** A single tool result fed back to the model on the next turn. */
46
+ type NormalizedToolResult = {
47
+ /** Matches the corresponding NormalizedToolCall.id from the prior turn. */
48
+ toolCallId: string;
49
+ toolName: string;
50
+ isError: boolean;
51
+ /** JSON-stringified result body. */
52
+ content: string;
53
+ };
54
+ type NormalizedMessage = {
55
+ role: 'user';
56
+ text: string;
57
+ } | {
58
+ role: 'assistant';
59
+ /** Free text the model emitted (zero-or-more text blocks joined as-is). */
60
+ text: string;
61
+ toolCalls: NormalizedToolCall[];
62
+ /**
63
+ * Vendor-opaque blob the producing adapter wants echoed back on
64
+ * the next turn. Used by the Gemini adapter to preserve the raw
65
+ * response parts (incl. `thoughtSignature` + `thought` summaries)
66
+ * which Gemini 2.5+/3.x thinking mode REQUIRES round-tripped, or
67
+ * the next request fails with `INVALID_ARGUMENT: missing
68
+ * thought_signature`. Other adapters can ignore.
69
+ */
70
+ providerData?: unknown;
71
+ } | {
72
+ role: 'tool';
73
+ results: NormalizedToolResult[];
74
+ };
75
+ type AgentTurnInput = {
76
+ system: SystemBlock[];
77
+ tools: ToolSchema[];
78
+ messages: NormalizedMessage[];
79
+ maxOutputTokens: number;
80
+ };
81
+ type AgentTurn = {
82
+ /** Concatenated text the model emitted in this turn (may be empty). */
83
+ text: string;
84
+ /** Tool calls the model wants run. Empty means the model ended its turn. */
85
+ toolCalls: NormalizedToolCall[];
86
+ /** Vendor stop reason, normalized loosely. */
87
+ stopReason: 'tool_use' | 'end_turn' | 'max_tokens' | 'other';
88
+ /** Opaque vendor data the agent loop should attach to the assistant message and replay on the next turn. See `NormalizedMessage.providerData`. */
89
+ providerData?: unknown;
90
+ };
91
+ interface ToolProvider {
92
+ readonly id: ToolProviderId;
93
+ /** Run one model turn. Adapters MUST NOT throw on tool errors — those are passed back as `tool` results on the next call. */
94
+ runTurn(input: AgentTurnInput): Promise<AgentTurn>;
95
+ }
96
+
97
+ /**
98
+ * Agent tool loop.
99
+ *
100
+ * Drives an injected `ToolProvider` (Claude on Vertex, Gemini on Vertex,
101
+ * or any future adapter that satisfies the contract) with a host-supplied
102
+ * tool catalogue until the model invokes the terminal `present` tool (or
103
+ * hits a hard stop without calling it). Returns the structured
104
+ * `PresentPayload`; the prose narrator pass happens in the route handler.
105
+ *
106
+ * Caching: the system prompt blocks are passed through as the host built
107
+ * them. Blocks marked `cached: true` become Anthropic ephemeral cache
108
+ * markers; Gemini ignores the flag and relies on Vertex's automatic prefix
109
+ * caching. Either way a follow-up question within the cache TTL only pays
110
+ * for the question + tool turns.
111
+ *
112
+ * This package's loop is project-agnostic: tools, prompts, and the
113
+ * provider instance are all injected via `AgentInput`. The host owns the
114
+ * registry (`ToolsPort.tools`), the system blocks (`ToolsPort.buildSystemBlocks`),
115
+ * and the constructed provider (resolved through `toolProviders[id].createProvider`).
116
+ */
117
+
118
+ declare const DEFAULT_MAX_TOOL_TURNS = 12;
119
+ declare const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
120
+ type AgentResult = {
121
+ ok: true;
122
+ structured: PresentPayload;
123
+ toolCallCount: number;
124
+ transcript: TranscriptEntry[];
125
+ } | {
126
+ ok: false;
127
+ error: {
128
+ code: string;
129
+ message: string;
130
+ };
131
+ transcript: TranscriptEntry[];
132
+ };
133
+ type TranscriptEntry = {
134
+ kind: 'user';
135
+ text: string;
136
+ } | {
137
+ kind: 'assistant_text';
138
+ text: string;
139
+ } | {
140
+ kind: 'tool_use';
141
+ name: string;
142
+ input: unknown;
143
+ } | {
144
+ kind: 'tool_result';
145
+ name: string;
146
+ result: unknown;
147
+ };
148
+ type AgentInput<S = unknown> = {
149
+ question: string;
150
+ ctx: ToolContext<S>;
151
+ /** Injected tool registry (host-supplied via ToolsPort). MUST include the terminal `present` tool. */
152
+ tools: Record<string, ToolDefinition<unknown, S>>;
153
+ /** Pre-built system blocks (host-supplied via ToolsPort.buildSystemBlocks). */
154
+ systemBlocks: SystemBlock[];
155
+ /** Constructed ToolProvider — caller resolves the right one via toolProviders[id].createProvider({...}). */
156
+ provider: ToolProvider;
157
+ /** Optional caps. Default both. */
158
+ maxToolTurns?: number;
159
+ maxOutputTokens?: number;
160
+ };
161
+ declare function runAgent<S = unknown>(input: AgentInput<S>): Promise<AgentResult>;
162
+
163
+ /**
164
+ * Tool-calling provider registry.
165
+ *
166
+ * The agent loop and the routes layer drive providers through this
167
+ * factory; vendor specifics stay inside `claude.ts` / `gemini.ts`.
168
+ * Adding a third provider would slot in alongside without further
169
+ * changes to the agent loop.
170
+ *
171
+ * The host injects credentials (a `GoogleAuth` instance), the GCP
172
+ * project id, the default region, and the pinned model ids via
173
+ * `VertexPort`. A per-request region override flows in as
174
+ * `ProviderInitOpts.location` so admin-managed `aiSettings.gcpLocation`
175
+ * can flip Vertex regions without rebuilding the registry.
176
+ */
177
+
178
+ type ProviderInitOpts = {
179
+ /** Pre-built GoogleAuth instance (host-supplied). */
180
+ auth: GoogleAuth;
181
+ /** GCP project id. */
182
+ projectId: string;
183
+ /** Default Vertex region used when `location` is omitted. */
184
+ defaultLocation: string;
185
+ /** Vertex model ids pinned by the host. */
186
+ modelIds: {
187
+ claude: string;
188
+ gemini: string;
189
+ };
190
+ /** Per-request override for `aiSettings.gcpLocation`. Falls back to defaultLocation. */
191
+ location?: string;
192
+ };
193
+ type ToolProviderDef = {
194
+ id: ToolProviderId;
195
+ label: string;
196
+ description: string;
197
+ createProvider(opts: ProviderInitOpts): ToolProvider;
198
+ };
199
+ declare const toolProviders: ToolProviderDef[];
200
+ declare function getToolProvider(id: string): ToolProviderDef | undefined;
201
+
202
+ /**
203
+ * Narrators — the prose pass that turns a structured answer into the
204
+ * paragraph_brief block's flowing text. The agent's tool loop decides
205
+ * WHAT to say (key_facts); the narrator decides HOW to say it.
206
+ *
207
+ * Three implementations:
208
+ * - streamClaudeNarration — Anthropic Messages on GCP Vertex
209
+ * - streamGrokNarration — xAI Grok via Vertex's OpenAI-compatible endpoint
210
+ * - streamGeminiNarration — Google Gemini on GCP Vertex
211
+ *
212
+ * All three expose the same shape: an async generator yielding raw text
213
+ * deltas. The route layer is responsible for any SSE framing on top — the
214
+ * package keeps the streaming primitive provider-agnostic.
215
+ *
216
+ * Lifted from the host's `src/ai/narrators/`. The host's class-based
217
+ * `NarrativeProvider` interface and `createNarrativeProvider` factory are
218
+ * replaced with plain streaming functions plus a `getNarrator(id)` lookup.
219
+ * Every credential field comes in as an explicit argument; no `@/...`
220
+ * imports remain.
221
+ */
222
+
223
+ type NarratorId = 'claude' | 'gemini' | 'grok';
224
+
225
+ /**
226
+ * Common lifecycle hooks shared across every route factory. Hooks return
227
+ * `Response | null`: a non-null Response short-circuits the request (the
228
+ * factory returns it untouched), `null` continues the normal flow.
229
+ */
230
+ type RouteHooks$1<S> = {
231
+ /** Runs before auth. Return a Response to short-circuit (e.g. 503 during shutdown). */
232
+ onRequest?(req: Request): Promise<Response | null>;
233
+ /** Runs after successful auth. Return a Response to short-circuit (e.g. 429 rate-limited). */
234
+ onAuthenticated?(args: {
235
+ req: Request;
236
+ scope: S;
237
+ userId: number;
238
+ }): Promise<Response | null>;
239
+ };
240
+ /**
241
+ * Streaming-route hooks. Adds three lifecycle points specific to the SSE
242
+ * `agent-custom` path so consumers can plumb in per-request resources
243
+ * (e.g. SQL view creation/cleanup keyed off a fresh sessionId).
244
+ */
245
+ type AgentCustomHooks$1<S> = RouteHooks$1<S> & {
246
+ /**
247
+ * Generate the per-request session id used in ToolContext (and any
248
+ * project-specific resources keyed off it, e.g. SQL view names).
249
+ * Defaults to a random URL-safe id (16 hex chars from a UUID).
250
+ */
251
+ generateSessionId?(args: {
252
+ scope: S;
253
+ userId: number;
254
+ chatSessionId: number | null;
255
+ }): string | Promise<string>;
256
+ /**
257
+ * Runs once after the session id is resolved, before the agent loop.
258
+ * Throw to abort the request (the route catches and surfaces the error
259
+ * via the SSE error frame + persistence).
260
+ */
261
+ onSessionStart?(args: {
262
+ scope: S;
263
+ sessionId: string;
264
+ userId: number;
265
+ }): Promise<void>;
266
+ /**
267
+ * Always runs in `finally`, regardless of how the stream ended.
268
+ * The route never throws out of this hook — its errors are logged via
269
+ * ctx.logger but don't surface to the client.
270
+ */
271
+ onSessionEnd?(args: {
272
+ scope: S;
273
+ sessionId: string;
274
+ userId: number;
275
+ cause: 'complete' | 'error' | 'abort';
276
+ }): Promise<void>;
277
+ };
278
+ type AgentCustomRouteCtx<S> = {
279
+ persistence: PersistencePort;
280
+ auth: AuthPort<S>;
281
+ scope: ScopePort<S>;
282
+ tools: ToolsPort;
283
+ vertex: VertexPort;
284
+ logger?: LoggerPort;
285
+ /**
286
+ * Resolve which narrator to use for prose generation. Default:
287
+ * `() => aiSettings.toolProvider` (which is only ever `claude` or
288
+ * `gemini` from the registry). Hosts that surface a per-user
289
+ * `narrative_provider` (e.g. allowing `grok`) wire their existing
290
+ * lookup here.
291
+ */
292
+ resolveNarratorId?: (scope: S) => Promise<NarratorId>;
293
+ /**
294
+ * Optional lifecycle hooks. See `AgentCustomHooks` for the available
295
+ * extension points (shutdown gating, rate limiting, per-request
296
+ * resource setup/teardown).
297
+ */
298
+ hooks?: AgentCustomHooks$1<S>;
299
+ };
300
+ declare function createAgentCustomRoutes<S>(ctx: AgentCustomRouteCtx<S>): {
301
+ /** Next.js-compatible POST handler. */
302
+ POST: (req: Request) => Promise<Response>;
303
+ };
304
+
305
+ /**
306
+ * `chat-sessions` route factory — host-agnostic CRUD for chat sessions.
307
+ *
308
+ * Mounts at `/api/chat/sessions` (list+create) and `/api/chat/sessions/[id]`
309
+ * (get one with messages, rename, delete). Auth and persistence cross the
310
+ * boundary as ports; the package never touches a DB or session adapter.
311
+ *
312
+ * Wire format mirrors the host's pre-extraction Next route handlers so the
313
+ * existing UI keeps working unchanged when the host swaps to these factories.
314
+ */
315
+
316
+ type ChatSessionsRouteCtx<S> = {
317
+ persistence: PersistencePort;
318
+ auth: AuthPort<S>;
319
+ logger?: LoggerPort;
320
+ /**
321
+ * Optional pre-auth / post-auth hooks (e.g. shutdown gate, rate
322
+ * limiting). The streaming-specific hooks on `AgentCustomHooks` are
323
+ * ignored here — only `onRequest` and `onAuthenticated` apply.
324
+ */
325
+ hooks?: RouteHooks$1<S>;
326
+ };
327
+ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
328
+ list: {
329
+ /**
330
+ * `GET /api/chat/sessions` — caller's recent sessions, newest first,
331
+ * capped at 100. Response: `{ sessions: [{ id, title, createdAt, updatedAt }] }`.
332
+ */
333
+ GET: (req: Request) => Promise<Response>;
334
+ /**
335
+ * `POST /api/chat/sessions` — body `{ title?: string }`. Trims and caps
336
+ * title at 200 chars; defaults to "New chat" when blank.
337
+ * Response: `{ session: { id, title, createdAt, updatedAt } }`.
338
+ */
339
+ POST: (req: Request) => Promise<Response>;
340
+ };
341
+ detail: {
342
+ /**
343
+ * `GET /api/chat/sessions/[id]` — session metadata + ordered messages.
344
+ * 404 when the id doesn't exist or doesn't belong to the caller (we never
345
+ * differentiate the two, to avoid leaking the id space).
346
+ * Response: `{ session: { id, title, createdAt, updatedAt },
347
+ * messages: [{ id, role, question, blocks, prose, errorJson, createdAt }] }`.
348
+ */
349
+ GET: (req: Request, params: {
350
+ id: string;
351
+ }) => Promise<Response>;
352
+ /**
353
+ * `PATCH /api/chat/sessions/[id]` — rename. Body `{ title: string }`,
354
+ * trimmed and capped at 200 chars. Response: `{ ok: true }`.
355
+ */
356
+ PATCH: (req: Request, params: {
357
+ id: string;
358
+ }) => Promise<Response>;
359
+ /**
360
+ * `DELETE /api/chat/sessions/[id]` — drop session and its messages.
361
+ * Response: `{ ok: true }`.
362
+ */
363
+ DELETE: (req: Request, params: {
364
+ id: string;
365
+ }) => Promise<Response>;
366
+ };
367
+ };
368
+
369
+ /**
370
+ * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
371
+ *
372
+ * Three patchable fields on the singleton settings row:
373
+ * - `tool_provider` — vendor that drives the agent tool loop. Validated
374
+ * against the registered `toolProviders` registry passed in via ctx.
375
+ * - `gcp_location` — the Vertex region every provider call hits. Stays
376
+ * a fixed list ('us-east5', 'global') because those are the only
377
+ * regions Claude/Gemini are published in on Vertex.
378
+ * - `chat_interface` — which chat UI module renders globally. Validated
379
+ * against the `chatInterfaces` registry passed in via ctx (the actual
380
+ * registry lives in `@firstlovecenter/ai-chat/ui` so the host wires it through;
381
+ * the route stays free of UI imports).
382
+ *
383
+ * Wire format is snake_case to preserve byte-for-byte parity with the
384
+ * host route the package replaces — existing host UIs keep working
385
+ * unmodified.
386
+ */
387
+
388
+ type AdminSettingsRouteCtx<S> = {
389
+ persistence: PersistencePort;
390
+ auth: AuthPort<S>;
391
+ /** Registered tool providers (default: built-in toolProviders array). */
392
+ toolProviders: {
393
+ id: string;
394
+ label?: string;
395
+ description?: string;
396
+ }[];
397
+ /** Registered chat interface ids (host or UI module supplies the list). */
398
+ chatInterfaces: {
399
+ id: string;
400
+ }[];
401
+ logger?: LoggerPort;
402
+ /**
403
+ * Optional pre-auth / post-auth hooks (e.g. shutdown gate, rate
404
+ * limiting). Streaming-specific hooks are not applicable here.
405
+ */
406
+ hooks?: RouteHooks$1<S>;
407
+ };
408
+ declare function createAdminSettingsRoutes<S>(ctx: AdminSettingsRouteCtx<S>): {
409
+ GET: (req: Request) => Promise<Response>;
410
+ PATCH: (req: Request) => Promise<Response>;
411
+ };
412
+
413
+ /** Default ids that match the components shipped in `@firstlovecenter/ai-chat/ui`. */
414
+ declare const BUILTIN_CHAT_INTERFACE_IDS: readonly ["custom", "vercel"];
415
+ type ChatInterfaceRegistryEntry = {
416
+ id: string;
417
+ };
418
+ type ConfigureAiChatOpts<S = unknown> = {
419
+ persistence: PersistencePort;
420
+ auth: AuthPort<S>;
421
+ scope: ScopePort<S>;
422
+ tools: ToolsPort;
423
+ vertex: VertexPort;
424
+ logger?: LoggerPort;
425
+ /**
426
+ * Resolve which narrator drives the prose pass. Defaults to the
427
+ * current `aiSettings.toolProvider` (claude → claude narrator, gemini →
428
+ * gemini narrator). Override when the host stores a per-user choice.
429
+ */
430
+ resolveNarratorId?: (scope: S) => Promise<'claude' | 'gemini' | 'grok'>;
431
+ /**
432
+ * Chat-interface ids the admin route accepts. Defaults to
433
+ * BUILTIN_CHAT_INTERFACE_IDS (custom, vercel). Hosts that ship only one
434
+ * UI can subset; hosts that register a new one extend.
435
+ */
436
+ chatInterfaces?: ChatInterfaceRegistryEntry[];
437
+ /**
438
+ * Additional tool-calling providers beyond the built-in claude/gemini.
439
+ * Merged into the registry the admin route validates against.
440
+ */
441
+ extraToolProviders?: ToolProviderDef[];
442
+ /**
443
+ * Optional lifecycle hooks shared across all three route factories.
444
+ * The full superset (`AgentCustomHooks`) covers the SSE chat route;
445
+ * `chatSessions` and `adminSettings` only consume the
446
+ * `onRequest` / `onAuthenticated` subset.
447
+ *
448
+ * Common host plumbing this enables (without forking the package):
449
+ * - `onRequest` — shutdown gating (503 + Retry-After).
450
+ * - `onAuthenticated` — per-user rate limiting (429 + Retry-After).
451
+ * - `generateSessionId`— project-specific session ids (e.g. SQL view names).
452
+ * - `onSessionStart` — per-request resource setup (e.g. CREATE VIEW).
453
+ * - `onSessionEnd` — cleanup (always-runs, never throws out).
454
+ */
455
+ hooks?: AgentCustomHooks$1<S>;
456
+ };
457
+ type AiChatRuntime<S = unknown> = {
458
+ /**
459
+ * Pre-bound agent loop. Most hosts call routes instead — but the bound
460
+ * runner is exposed for non-HTTP entry points (jobs, eval harnesses).
461
+ */
462
+ runAgent: (input: {
463
+ question: string;
464
+ ctx: ToolContext<S>;
465
+ /** Override the provider id picked from `aiSettings.toolProvider`. */
466
+ providerId?: string;
467
+ /** Override the location picked from `aiSettings.gcpLocation`. */
468
+ location?: string;
469
+ maxToolTurns?: number;
470
+ maxOutputTokens?: number;
471
+ }) => Promise<AgentResult>;
472
+ routes: {
473
+ agentCustom: ReturnType<typeof createAgentCustomRoutes<S>>;
474
+ chatSessions: ReturnType<typeof createChatSessionsRoutes<S>>;
475
+ adminSettings: ReturnType<typeof createAdminSettingsRoutes<S>>;
476
+ };
477
+ registries: {
478
+ toolProviders: ToolProviderDef[];
479
+ chatInterfaces: ChatInterfaceRegistryEntry[];
480
+ };
481
+ };
482
+ declare function configureAiChat<S = unknown>(opts: ConfigureAiChatOpts<S>): AiChatRuntime<S>;
483
+
484
+ type RouteHooks<S> = RouteHooks$1<S>;
485
+ type AgentCustomHooks<S> = AgentCustomHooks$1<S>;
486
+
487
+ export { type AgentCustomHooks, type AgentInput, type AgentResult, type AiChatRuntime, AuthPort, BUILTIN_CHAT_INTERFACE_IDS, type ChatInterfaceRegistryEntry, type ConfigureAiChatOpts, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MAX_TOOL_TURNS, LoggerPort, PersistencePort, PresentPayload, type ProviderInitOpts, type RouteHooks, ScopePort, SystemBlock, ToolContext, ToolDefinition, type ToolProviderDef, ToolSchema, ToolsPort, type TranscriptEntry, VertexPort, configureAiChat, getToolProvider, runAgent, toolProviders };