@firstlovecenter/ai-chat 0.2.3 → 0.6.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.
@@ -1,7 +1,8 @@
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';
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-CQntnyDJ.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-CQntnyDJ.cjs';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
+ import 'zod';
5
6
 
6
7
  /**
7
8
  * Provider-agnostic tool-calling abstraction.
@@ -302,6 +303,34 @@ declare function createAgentCustomRoutes<S>(ctx: AgentCustomRouteCtx<S>): {
302
303
  POST: (req: Request) => Promise<Response>;
303
304
  };
304
305
 
306
+ /**
307
+ * Streaming-route hooks for the Vercel AI SDK chat.
308
+ *
309
+ * Identical to `AgentCustomHooks` — the lifecycle (pre-auth → auth →
310
+ * post-auth → resolve session → onSessionStart → stream → onSessionEnd)
311
+ * is the same. We re-alias rather than introducing a new type so consumers
312
+ * can plug a single hook bag into both routes.
313
+ */
314
+ type AgentVercelHooks$1<S> = AgentCustomHooks$1<S>;
315
+ type AgentVercelRouteCtx<S> = {
316
+ persistence: PersistencePort;
317
+ auth: AuthPort<S>;
318
+ scope: ScopePort<S>;
319
+ tools: ToolsPort;
320
+ vertex: VertexPort;
321
+ logger?: LoggerPort;
322
+ /**
323
+ * Optional lifecycle hooks. See `AgentCustomHooks` for the available
324
+ * extension points (shutdown gating, rate limiting, per-request
325
+ * resource setup/teardown).
326
+ */
327
+ hooks?: AgentVercelHooks$1<S>;
328
+ };
329
+ declare function createAgentVercelRoutes<S>(ctx: AgentVercelRouteCtx<S>): {
330
+ /** Next.js-compatible POST handler. */
331
+ POST: (req: Request) => Promise<Response>;
332
+ };
333
+
305
334
  /**
306
335
  * `chat-sessions` route factory — host-agnostic CRUD for chat sessions.
307
336
  *
@@ -369,7 +398,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
369
398
  /**
370
399
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
371
400
  *
372
- * Three patchable fields on the singleton settings row:
401
+ * Five patchable fields on the singleton settings row:
373
402
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
374
403
  * against the registered `toolProviders` registry passed in via ctx.
375
404
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -379,6 +408,15 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
379
408
  * against the `chatInterfaces` registry passed in via ctx (the actual
380
409
  * registry lives in `@firstlovecenter/ai-chat/ui` so the host wires it through;
381
410
  * the route stays free of UI imports).
411
+ * - `max_output_tokens` — caps the agent loop's per-turn output AND each
412
+ * narrator's prose pass. Bounded `[256, 64000]` — anything below 256 can't
413
+ * fit a useful response, anything above 64000 exceeds the headroom of any
414
+ * model currently routed through Vertex.
415
+ * - `role_prompt` — admin-editable persona string. Empty string and the
416
+ * explicit JSON `null` both clear the override back to the host's static
417
+ * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
+ * whitespace-only string to `null` on write so the "no override" state has
419
+ * a single representation in storage.
382
420
  *
383
421
  * Wire format is snake_case to preserve byte-for-byte parity with the
384
422
  * host route the package replaces — existing host UIs keep working
@@ -501,6 +539,7 @@ type AiChatRuntime<S = unknown> = {
501
539
  }) => Promise<AgentResult>;
502
540
  routes: {
503
541
  agentCustom: ReturnType<typeof createAgentCustomRoutes<S>>;
542
+ agentVercel: ReturnType<typeof createAgentVercelRoutes<S>>;
504
543
  chatSessions: ReturnType<typeof createChatSessionsRoutes<S>>;
505
544
  adminSettings: ReturnType<typeof createAdminSettingsRoutes<S>>;
506
545
  };
@@ -513,5 +552,12 @@ declare function configureAiChat<S = unknown>(opts: ConfigureAiChatOpts<S>): AiC
513
552
 
514
553
  type RouteHooks<S> = RouteHooks$1<S>;
515
554
  type AgentCustomHooks<S> = AgentCustomHooks$1<S>;
555
+ /**
556
+ * Alias of `AgentCustomHooks` — the Vercel AI SDK chat reuses the same
557
+ * lifecycle (pre-auth → auth → post-auth → resolve session → onSessionStart
558
+ * → stream → onSessionEnd). Re-exported for callers that prefer the more
559
+ * specific name.
560
+ */
561
+ type AgentVercelHooks<S> = AgentVercelHooks$1<S>;
516
562
 
517
- 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 };
563
+ export { type AgentCustomHooks, type AgentInput, type AgentResult, type AgentVercelHooks, 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 };
@@ -1,7 +1,8 @@
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.js';
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.js';
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-CQntnyDJ.js';
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-CQntnyDJ.js';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
+ import 'zod';
5
6
 
6
7
  /**
7
8
  * Provider-agnostic tool-calling abstraction.
@@ -302,6 +303,34 @@ declare function createAgentCustomRoutes<S>(ctx: AgentCustomRouteCtx<S>): {
302
303
  POST: (req: Request) => Promise<Response>;
303
304
  };
304
305
 
306
+ /**
307
+ * Streaming-route hooks for the Vercel AI SDK chat.
308
+ *
309
+ * Identical to `AgentCustomHooks` — the lifecycle (pre-auth → auth →
310
+ * post-auth → resolve session → onSessionStart → stream → onSessionEnd)
311
+ * is the same. We re-alias rather than introducing a new type so consumers
312
+ * can plug a single hook bag into both routes.
313
+ */
314
+ type AgentVercelHooks$1<S> = AgentCustomHooks$1<S>;
315
+ type AgentVercelRouteCtx<S> = {
316
+ persistence: PersistencePort;
317
+ auth: AuthPort<S>;
318
+ scope: ScopePort<S>;
319
+ tools: ToolsPort;
320
+ vertex: VertexPort;
321
+ logger?: LoggerPort;
322
+ /**
323
+ * Optional lifecycle hooks. See `AgentCustomHooks` for the available
324
+ * extension points (shutdown gating, rate limiting, per-request
325
+ * resource setup/teardown).
326
+ */
327
+ hooks?: AgentVercelHooks$1<S>;
328
+ };
329
+ declare function createAgentVercelRoutes<S>(ctx: AgentVercelRouteCtx<S>): {
330
+ /** Next.js-compatible POST handler. */
331
+ POST: (req: Request) => Promise<Response>;
332
+ };
333
+
305
334
  /**
306
335
  * `chat-sessions` route factory — host-agnostic CRUD for chat sessions.
307
336
  *
@@ -369,7 +398,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
369
398
  /**
370
399
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
371
400
  *
372
- * Three patchable fields on the singleton settings row:
401
+ * Five patchable fields on the singleton settings row:
373
402
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
374
403
  * against the registered `toolProviders` registry passed in via ctx.
375
404
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -379,6 +408,15 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
379
408
  * against the `chatInterfaces` registry passed in via ctx (the actual
380
409
  * registry lives in `@firstlovecenter/ai-chat/ui` so the host wires it through;
381
410
  * the route stays free of UI imports).
411
+ * - `max_output_tokens` — caps the agent loop's per-turn output AND each
412
+ * narrator's prose pass. Bounded `[256, 64000]` — anything below 256 can't
413
+ * fit a useful response, anything above 64000 exceeds the headroom of any
414
+ * model currently routed through Vertex.
415
+ * - `role_prompt` — admin-editable persona string. Empty string and the
416
+ * explicit JSON `null` both clear the override back to the host's static
417
+ * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
+ * whitespace-only string to `null` on write so the "no override" state has
419
+ * a single representation in storage.
382
420
  *
383
421
  * Wire format is snake_case to preserve byte-for-byte parity with the
384
422
  * host route the package replaces — existing host UIs keep working
@@ -501,6 +539,7 @@ type AiChatRuntime<S = unknown> = {
501
539
  }) => Promise<AgentResult>;
502
540
  routes: {
503
541
  agentCustom: ReturnType<typeof createAgentCustomRoutes<S>>;
542
+ agentVercel: ReturnType<typeof createAgentVercelRoutes<S>>;
504
543
  chatSessions: ReturnType<typeof createChatSessionsRoutes<S>>;
505
544
  adminSettings: ReturnType<typeof createAdminSettingsRoutes<S>>;
506
545
  };
@@ -513,5 +552,12 @@ declare function configureAiChat<S = unknown>(opts: ConfigureAiChatOpts<S>): AiC
513
552
 
514
553
  type RouteHooks<S> = RouteHooks$1<S>;
515
554
  type AgentCustomHooks<S> = AgentCustomHooks$1<S>;
555
+ /**
556
+ * Alias of `AgentCustomHooks` — the Vercel AI SDK chat reuses the same
557
+ * lifecycle (pre-auth → auth → post-auth → resolve session → onSessionStart
558
+ * → stream → onSessionEnd). Re-exported for callers that prefer the more
559
+ * specific name.
560
+ */
561
+ type AgentVercelHooks<S> = AgentVercelHooks$1<S>;
516
562
 
517
- 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 };
563
+ export { type AgentCustomHooks, type AgentInput, type AgentResult, type AgentVercelHooks, 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 };
@@ -1,5 +1,8 @@
1
1
  import { AnthropicVertex } from '@anthropic-ai/vertex-sdk';
2
2
  import { randomUUID } from 'crypto';
3
+ import { StreamData, streamText, tool } from 'ai';
4
+ import { createVertex } from '@ai-sdk/google-vertex';
5
+ import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
3
6
  export { GoogleAuth } from 'google-auth-library';
4
7
 
5
8
  // src/server/tools/types.ts
@@ -70,8 +73,8 @@ async function runAgent(input) {
70
73
  const toolResults = [];
71
74
  for (const tc of response.toolCalls) {
72
75
  transcript.push({ kind: "tool_use", name: tc.name, input: tc.input });
73
- const tool = input.tools[tc.name];
74
- if (!tool) {
76
+ const tool2 = input.tools[tc.name];
77
+ if (!tool2) {
75
78
  const errResult = {
76
79
  ok: false,
77
80
  error: { code: "UNKNOWN_TOOL", message: `Unknown tool: ${tc.name}` }
@@ -85,7 +88,7 @@ async function runAgent(input) {
85
88
  });
86
89
  continue;
87
90
  }
88
- const result = await tool.execute(tc.input, {
91
+ const result = await tool2.execute(tc.input, {
89
92
  ...input.ctx,
90
93
  toolCallCount
91
94
  });
@@ -647,7 +650,7 @@ async function* streamClaudeNarration(opts) {
647
650
  });
648
651
  const stream = await client.messages.stream({
649
652
  model: opts.modelId,
650
- max_tokens: 400,
653
+ max_tokens: opts.maxTokens,
651
654
  system: NARRATIVE_SYSTEM,
652
655
  messages: [{ role: "user", content: buildNarrativeUserMessage(opts.input) }]
653
656
  });
@@ -707,7 +710,7 @@ async function* streamGeminiNarration(opts) {
707
710
  parts: [{ text: buildNarrativeUserMessage(opts.input) }]
708
711
  }
709
712
  ],
710
- generationConfig: { maxOutputTokens: 400, temperature: 0 }
713
+ generationConfig: { maxOutputTokens: opts.maxTokens, temperature: 0 }
711
714
  })
712
715
  });
713
716
  if (!res.ok || !res.body) {
@@ -783,7 +786,7 @@ async function* streamGrokNarration(opts) {
783
786
  },
784
787
  body: JSON.stringify({
785
788
  model: opts.modelId,
786
- max_tokens: 400,
789
+ max_tokens: opts.maxTokens,
787
790
  stream: true,
788
791
  messages: [
789
792
  { role: "system", content: NARRATIVE_SYSTEM3 },
@@ -1006,7 +1009,8 @@ data: ${JSON.stringify(data)}
1006
1009
  ctx: toolContext,
1007
1010
  tools: tools.tools,
1008
1011
  systemBlocks,
1009
- provider
1012
+ provider,
1013
+ maxOutputTokens: aiSettings.maxOutputTokens
1010
1014
  });
1011
1015
  if (!agentResult.ok) {
1012
1016
  persistedError = agentResult.error;
@@ -1032,6 +1036,7 @@ data: ${JSON.stringify(data)}
1032
1036
  projectId: vertex.projectId,
1033
1037
  location: aiSettings.gcpLocation,
1034
1038
  modelId: narratorModelId,
1039
+ maxTokens: aiSettings.maxOutputTokens,
1035
1040
  input: {
1036
1041
  question,
1037
1042
  structured,
@@ -1135,6 +1140,297 @@ data: {}
1135
1140
  }
1136
1141
  };
1137
1142
  }
1143
+ function buildVercelTools(tools, ctx, data, onPresent) {
1144
+ const result = {};
1145
+ let toolCallCount = 0;
1146
+ for (const [name, def] of Object.entries(tools)) {
1147
+ if (!def.zodSchema) {
1148
+ throw new Error(
1149
+ `Tool '${name}' has no zodSchema; required for the Vercel AI SDK chat. Add a Zod schema to the tool definition (or remove it from the registry if the host only uses the custom SSE chat).`
1150
+ );
1151
+ }
1152
+ if (name === TERMINAL_TOOL_NAME) {
1153
+ result[name] = tool({
1154
+ description: def.schema.description,
1155
+ // The Zod schema doubles as the runtime parameter validator the SDK
1156
+ // hands the model. We accept whatever Zod shape the host registered;
1157
+ // the SDK uses it to validate the tool-call arguments before dispatch.
1158
+ parameters: def.zodSchema,
1159
+ execute: async (input) => {
1160
+ if (toolCallCount < 2) {
1161
+ return {
1162
+ error: {
1163
+ code: "SELF_VERIFY_REQUIRED",
1164
+ message: "Per FR-8.3 you must run at least one CROSS-CHECK tool call (a different metric, a different period, or a run_sql sanity-check) before present. Make that extra call now, then call present again."
1165
+ }
1166
+ };
1167
+ }
1168
+ const payload = input;
1169
+ for (let i = 0; i < payload.blocks.length; i++) {
1170
+ data.append({
1171
+ type: "block",
1172
+ value: { index: i, ...payload.blocks[i] }
1173
+ });
1174
+ }
1175
+ onPresent(payload);
1176
+ return { ok: true };
1177
+ }
1178
+ });
1179
+ continue;
1180
+ }
1181
+ result[name] = tool({
1182
+ description: def.schema.description,
1183
+ parameters: def.zodSchema,
1184
+ execute: async (input) => {
1185
+ const res = await def.execute(input, { ...ctx, toolCallCount });
1186
+ toolCallCount += 1;
1187
+ if (res.ok) return res.data;
1188
+ return { error: res.error };
1189
+ }
1190
+ });
1191
+ }
1192
+ return result;
1193
+ }
1194
+
1195
+ // src/server/routes/agent-vercel.ts
1196
+ var VALID_MODELS = /* @__PURE__ */ new Set(["claude", "gemini"]);
1197
+ function jsonError2(status, code, message) {
1198
+ return new Response(JSON.stringify({ error: { code, message } }), {
1199
+ status,
1200
+ headers: { "Content-Type": "application/json" }
1201
+ });
1202
+ }
1203
+ function defaultGenerateSessionId2() {
1204
+ return randomUUID().replace(/-/g, "").slice(0, 16);
1205
+ }
1206
+ function createAgentVercelRoutes(ctx) {
1207
+ const { persistence, auth, scope, tools, vertex, logger, hooks } = ctx;
1208
+ return {
1209
+ /** Next.js-compatible POST handler. */
1210
+ POST: async (req) => {
1211
+ if (hooks?.onRequest) {
1212
+ const short = await hooks.onRequest(req);
1213
+ if (short) return short;
1214
+ }
1215
+ const authResult = await auth.requireAuth(req);
1216
+ if (!authResult.ok) return authResult.response;
1217
+ const { scope: callerScope, userId } = authResult;
1218
+ if (hooks?.onAuthenticated) {
1219
+ const short = await hooks.onAuthenticated({
1220
+ req,
1221
+ scope: callerScope,
1222
+ userId
1223
+ });
1224
+ if (short) return short;
1225
+ }
1226
+ const body = await req.json().catch(() => null);
1227
+ const question = typeof body?.question === "string" ? body.question.trim() : "";
1228
+ if (!question) {
1229
+ return jsonError2(
1230
+ 400,
1231
+ "VALIDATION_FAILED",
1232
+ "question must be a non-empty string."
1233
+ );
1234
+ }
1235
+ const rawChatSessionId = body?.chatSessionId;
1236
+ const incomingChatSessionId = typeof rawChatSessionId === "number" && Number.isInteger(rawChatSessionId) ? rawChatSessionId : null;
1237
+ const rawModel = body?.model;
1238
+ const requestedModel = typeof rawModel === "string" && VALID_MODELS.has(rawModel) ? rawModel : null;
1239
+ const aiSettings = await persistence.getAiSettings();
1240
+ let chatSessionId;
1241
+ if (incomingChatSessionId !== null) {
1242
+ const owned = await persistence.getSession(
1243
+ incomingChatSessionId,
1244
+ userId
1245
+ );
1246
+ if (!owned) {
1247
+ return jsonError2(404, "NOT_FOUND", "Chat session not found.");
1248
+ }
1249
+ chatSessionId = owned.id;
1250
+ } else {
1251
+ const created = await persistence.createSession({
1252
+ userId,
1253
+ title: question.slice(0, 200)
1254
+ });
1255
+ chatSessionId = created.id;
1256
+ }
1257
+ await persistence.appendMessage({
1258
+ sessionId: chatSessionId,
1259
+ role: "user",
1260
+ question
1261
+ });
1262
+ const sessionId = hooks?.generateSessionId ? await hooks.generateSessionId({
1263
+ scope: callerScope,
1264
+ userId,
1265
+ chatSessionId: incomingChatSessionId
1266
+ }) : defaultGenerateSessionId2();
1267
+ const scopeSummary = await scope.buildScopeSummary(callerScope);
1268
+ const scopeLabel = await scope.resolveScopeLabel(callerScope);
1269
+ const toolContext = {
1270
+ scope: callerScope,
1271
+ sessionId,
1272
+ scopeSummary,
1273
+ toolCallCount: 0
1274
+ };
1275
+ const systemBlocks = await tools.buildSystemBlocks(toolContext);
1276
+ const provider = requestedModel ?? aiSettings.toolProvider;
1277
+ if (!VALID_MODELS.has(provider)) {
1278
+ return jsonError2(
1279
+ 400,
1280
+ "INVALID_PROVIDER",
1281
+ `Vercel chat only supports 'claude' or 'gemini'; got '${provider}'.`
1282
+ );
1283
+ }
1284
+ const data = new StreamData();
1285
+ let presentPayload = null;
1286
+ let persistedError = null;
1287
+ let sessionStarted = false;
1288
+ try {
1289
+ if (hooks?.onSessionStart) {
1290
+ await hooks.onSessionStart({
1291
+ scope: callerScope,
1292
+ sessionId,
1293
+ userId
1294
+ });
1295
+ }
1296
+ sessionStarted = true;
1297
+ const vercelTools = buildVercelTools(
1298
+ tools.tools,
1299
+ toolContext,
1300
+ data,
1301
+ (p) => {
1302
+ presentPayload = p;
1303
+ }
1304
+ );
1305
+ data.append({
1306
+ type: "meta",
1307
+ value: { chatSessionId, scopeLabel }
1308
+ });
1309
+ const system = systemBlocks.map((b) => b.text).join("\n\n");
1310
+ const model = provider === "claude" ? createVertexAnthropic({
1311
+ project: vertex.projectId,
1312
+ location: vertex.defaultLocation,
1313
+ googleAuthOptions: {}
1314
+ })(vertex.modelIds.claude) : createVertex({
1315
+ project: vertex.projectId,
1316
+ location: aiSettings.gcpLocation,
1317
+ googleAuthOptions: {}
1318
+ })(vertex.modelIds.gemini);
1319
+ const result = streamText({
1320
+ model,
1321
+ system,
1322
+ messages: [{ role: "user", content: question }],
1323
+ tools: vercelTools,
1324
+ maxSteps: 12,
1325
+ maxTokens: aiSettings.maxOutputTokens,
1326
+ onFinish: async ({ text }) => {
1327
+ try {
1328
+ let blocks = presentPayload?.blocks ?? [];
1329
+ const prose = {};
1330
+ const trimmed = (text ?? "").trim();
1331
+ if (presentPayload === null && trimmed) {
1332
+ const topic = question.length > 80 ? question.slice(0, 77) + "..." : question;
1333
+ const synthetic = {
1334
+ kind: "paragraph_brief",
1335
+ topic,
1336
+ key_facts: [trimmed]
1337
+ };
1338
+ blocks = [synthetic];
1339
+ prose[0] = trimmed;
1340
+ data.append({
1341
+ type: "block",
1342
+ value: { index: 0, ...synthetic }
1343
+ });
1344
+ } else if (text) {
1345
+ const firstPbIdx = blocks.findIndex(
1346
+ (b) => b.kind === "paragraph_brief"
1347
+ );
1348
+ if (firstPbIdx >= 0) prose[firstPbIdx] = text;
1349
+ }
1350
+ await persistence.appendMessage({
1351
+ sessionId: chatSessionId,
1352
+ role: "assistant",
1353
+ blocks: blocks.length ? blocks : null,
1354
+ prose: Object.keys(prose).length ? prose : null,
1355
+ errorJson: persistedError
1356
+ });
1357
+ } catch (err2) {
1358
+ logger?.warn?.(
1359
+ {
1360
+ chatSessionId,
1361
+ sessionId,
1362
+ err: err2.message
1363
+ },
1364
+ "[agent-vercel] failed to persist assistant turn"
1365
+ );
1366
+ } finally {
1367
+ try {
1368
+ await data.close();
1369
+ } catch {
1370
+ }
1371
+ }
1372
+ }
1373
+ });
1374
+ return result.toDataStreamResponse({ data });
1375
+ } catch (e) {
1376
+ const message = e.message ?? "Internal error";
1377
+ persistedError = { code: "INTERNAL", message };
1378
+ logger?.error?.(
1379
+ { chatSessionId, sessionId, err: message },
1380
+ "[agent-vercel] route errored"
1381
+ );
1382
+ try {
1383
+ data.append({
1384
+ type: "error",
1385
+ value: { code: "INTERNAL", message }
1386
+ });
1387
+ } catch {
1388
+ }
1389
+ try {
1390
+ await data.close();
1391
+ } catch {
1392
+ }
1393
+ try {
1394
+ await persistence.appendMessage({
1395
+ sessionId: chatSessionId,
1396
+ role: "assistant",
1397
+ blocks: null,
1398
+ prose: null,
1399
+ errorJson: persistedError
1400
+ });
1401
+ } catch (err2) {
1402
+ logger?.warn?.(
1403
+ { chatSessionId, sessionId, err: err2.message },
1404
+ "[agent-vercel] failed to persist error turn"
1405
+ );
1406
+ }
1407
+ return jsonError2(500, "INTERNAL", message);
1408
+ } finally {
1409
+ if (hooks?.onSessionEnd) {
1410
+ const cause = req.signal.aborted ? "abort" : persistedError ? "error" : "complete";
1411
+ try {
1412
+ await hooks.onSessionEnd({
1413
+ scope: callerScope,
1414
+ sessionId,
1415
+ userId,
1416
+ cause
1417
+ });
1418
+ } catch (err2) {
1419
+ logger?.warn?.(
1420
+ {
1421
+ chatSessionId,
1422
+ sessionId,
1423
+ sessionStarted,
1424
+ err: err2.message
1425
+ },
1426
+ "[agent-vercel] onSessionEnd hook failed"
1427
+ );
1428
+ }
1429
+ }
1430
+ }
1431
+ }
1432
+ };
1433
+ }
1138
1434
 
1139
1435
  // src/server/routes/chat-sessions.ts
1140
1436
  var DEFAULT_TITLE = "New chat";
@@ -1315,6 +1611,8 @@ function createChatSessionsRoutes(ctx) {
1315
1611
 
1316
1612
  // src/server/routes/admin-settings.ts
1317
1613
  var VALID_LOCATIONS = ["us-east5", "global"];
1614
+ var MIN_MAX_OUTPUT_TOKENS = 256;
1615
+ var MAX_MAX_OUTPUT_TOKENS = 64e3;
1318
1616
  function isStringRecord(v) {
1319
1617
  return typeof v === "object" && v !== null && !Array.isArray(v);
1320
1618
  }
@@ -1329,6 +1627,8 @@ function toWire(settings) {
1329
1627
  tool_provider: settings.toolProvider,
1330
1628
  gcp_location: settings.gcpLocation,
1331
1629
  chat_interface: settings.chatInterface,
1630
+ max_output_tokens: settings.maxOutputTokens,
1631
+ role_prompt: settings.rolePrompt,
1332
1632
  updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
1333
1633
  updated_by_user_id: settings.updatedByUserId
1334
1634
  };
@@ -1408,11 +1708,29 @@ function createAdminSettingsRoutes(ctx) {
1408
1708
  }
1409
1709
  patch.chatInterface = v;
1410
1710
  }
1411
- if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0) {
1711
+ if ("max_output_tokens" in body) {
1712
+ const v = body.max_output_tokens;
1713
+ if (typeof v !== "number" || !Number.isInteger(v) || v < MIN_MAX_OUTPUT_TOKENS || v > MAX_MAX_OUTPUT_TOKENS) {
1714
+ return jsonResponse({ error: "invalid_max_output_tokens" }, 400);
1715
+ }
1716
+ patch.maxOutputTokens = v;
1717
+ }
1718
+ if ("role_prompt" in body) {
1719
+ const v = body.role_prompt;
1720
+ if (v === null) {
1721
+ patch.rolePrompt = null;
1722
+ } else if (typeof v === "string") {
1723
+ const trimmed = v.trim();
1724
+ patch.rolePrompt = trimmed === "" ? null : trimmed;
1725
+ } else {
1726
+ return jsonResponse({ error: "invalid_role_prompt" }, 400);
1727
+ }
1728
+ }
1729
+ if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
1412
1730
  return jsonResponse(
1413
1731
  {
1414
1732
  error: "empty_patch",
1415
- message: "Body must set at least one of tool_provider, gcp_location, chat_interface."
1733
+ message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
1416
1734
  },
1417
1735
  400
1418
1736
  );
@@ -1437,16 +1755,27 @@ function configureAiChat(opts) {
1437
1755
  ];
1438
1756
  const getProvider = (id) => toolProviders2.find((p) => p.id === id) ?? getToolProvider(id);
1439
1757
  const chatInterfaces = opts.chatInterfaces ?? BUILTIN_CHAT_INTERFACE_IDS.map((id) => ({ id }));
1440
- const tools = opts.rolePrompt ? {
1758
+ const staticRolePrompt = opts.rolePrompt;
1759
+ const tools = {
1441
1760
  tools: opts.tools.tools,
1442
1761
  async buildSystemBlocks(ctx) {
1443
1762
  const inner = await opts.tools.buildSystemBlocks(ctx);
1444
- const rolePrompt = opts.rolePrompt;
1445
- const role = typeof rolePrompt === "function" ? await rolePrompt(ctx) : rolePrompt;
1446
- if (!role || !role.trim()) return inner;
1763
+ let role = null;
1764
+ try {
1765
+ const settings = await opts.persistence.getAiSettings();
1766
+ if (settings.rolePrompt && settings.rolePrompt.trim()) {
1767
+ role = settings.rolePrompt;
1768
+ }
1769
+ } catch {
1770
+ }
1771
+ if (!role && staticRolePrompt) {
1772
+ const resolved = typeof staticRolePrompt === "function" ? await staticRolePrompt(ctx) : staticRolePrompt;
1773
+ if (resolved && resolved.trim()) role = resolved;
1774
+ }
1775
+ if (!role) return inner;
1447
1776
  return [{ text: role, cached: true }, ...inner];
1448
1777
  }
1449
- } : opts.tools;
1778
+ };
1450
1779
  const runAgentBound = async ({
1451
1780
  question,
1452
1781
  ctx,
@@ -1495,6 +1824,15 @@ function configureAiChat(opts) {
1495
1824
  resolveNarratorId: opts.resolveNarratorId,
1496
1825
  hooks: opts.hooks
1497
1826
  });
1827
+ const agentVercel = createAgentVercelRoutes({
1828
+ persistence: opts.persistence,
1829
+ auth: opts.auth,
1830
+ scope: opts.scope,
1831
+ tools,
1832
+ vertex: opts.vertex,
1833
+ logger: opts.logger,
1834
+ hooks: opts.hooks
1835
+ });
1498
1836
  const chatSessions = createChatSessionsRoutes({
1499
1837
  persistence: opts.persistence,
1500
1838
  auth: opts.auth,
@@ -1511,7 +1849,7 @@ function configureAiChat(opts) {
1511
1849
  });
1512
1850
  return {
1513
1851
  runAgent: runAgentBound,
1514
- routes: { agentCustom, chatSessions, adminSettings },
1852
+ routes: { agentCustom, agentVercel, chatSessions, adminSettings },
1515
1853
  registries: { toolProviders: toolProviders2, chatInterfaces }
1516
1854
  };
1517
1855
  }