@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.
@@ -2,6 +2,9 @@
2
2
 
3
3
  var vertexSdk = require('@anthropic-ai/vertex-sdk');
4
4
  var crypto = require('crypto');
5
+ var ai = require('ai');
6
+ var googleVertex = require('@ai-sdk/google-vertex');
7
+ var anthropic = require('@ai-sdk/google-vertex/anthropic');
5
8
  var googleAuthLibrary = require('google-auth-library');
6
9
 
7
10
  // src/server/tools/types.ts
@@ -72,8 +75,8 @@ async function runAgent(input) {
72
75
  const toolResults = [];
73
76
  for (const tc of response.toolCalls) {
74
77
  transcript.push({ kind: "tool_use", name: tc.name, input: tc.input });
75
- const tool = input.tools[tc.name];
76
- if (!tool) {
78
+ const tool2 = input.tools[tc.name];
79
+ if (!tool2) {
77
80
  const errResult = {
78
81
  ok: false,
79
82
  error: { code: "UNKNOWN_TOOL", message: `Unknown tool: ${tc.name}` }
@@ -87,7 +90,7 @@ async function runAgent(input) {
87
90
  });
88
91
  continue;
89
92
  }
90
- const result = await tool.execute(tc.input, {
93
+ const result = await tool2.execute(tc.input, {
91
94
  ...input.ctx,
92
95
  toolCallCount
93
96
  });
@@ -649,7 +652,7 @@ async function* streamClaudeNarration(opts) {
649
652
  });
650
653
  const stream = await client.messages.stream({
651
654
  model: opts.modelId,
652
- max_tokens: 400,
655
+ max_tokens: opts.maxTokens,
653
656
  system: NARRATIVE_SYSTEM,
654
657
  messages: [{ role: "user", content: buildNarrativeUserMessage(opts.input) }]
655
658
  });
@@ -709,7 +712,7 @@ async function* streamGeminiNarration(opts) {
709
712
  parts: [{ text: buildNarrativeUserMessage(opts.input) }]
710
713
  }
711
714
  ],
712
- generationConfig: { maxOutputTokens: 400, temperature: 0 }
715
+ generationConfig: { maxOutputTokens: opts.maxTokens, temperature: 0 }
713
716
  })
714
717
  });
715
718
  if (!res.ok || !res.body) {
@@ -785,7 +788,7 @@ async function* streamGrokNarration(opts) {
785
788
  },
786
789
  body: JSON.stringify({
787
790
  model: opts.modelId,
788
- max_tokens: 400,
791
+ max_tokens: opts.maxTokens,
789
792
  stream: true,
790
793
  messages: [
791
794
  { role: "system", content: NARRATIVE_SYSTEM3 },
@@ -1008,7 +1011,8 @@ data: ${JSON.stringify(data)}
1008
1011
  ctx: toolContext,
1009
1012
  tools: tools.tools,
1010
1013
  systemBlocks,
1011
- provider
1014
+ provider,
1015
+ maxOutputTokens: aiSettings.maxOutputTokens
1012
1016
  });
1013
1017
  if (!agentResult.ok) {
1014
1018
  persistedError = agentResult.error;
@@ -1034,6 +1038,7 @@ data: ${JSON.stringify(data)}
1034
1038
  projectId: vertex.projectId,
1035
1039
  location: aiSettings.gcpLocation,
1036
1040
  modelId: narratorModelId,
1041
+ maxTokens: aiSettings.maxOutputTokens,
1037
1042
  input: {
1038
1043
  question,
1039
1044
  structured,
@@ -1137,6 +1142,297 @@ data: {}
1137
1142
  }
1138
1143
  };
1139
1144
  }
1145
+ function buildVercelTools(tools, ctx, data, onPresent) {
1146
+ const result = {};
1147
+ let toolCallCount = 0;
1148
+ for (const [name, def] of Object.entries(tools)) {
1149
+ if (!def.zodSchema) {
1150
+ throw new Error(
1151
+ `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).`
1152
+ );
1153
+ }
1154
+ if (name === TERMINAL_TOOL_NAME) {
1155
+ result[name] = ai.tool({
1156
+ description: def.schema.description,
1157
+ // The Zod schema doubles as the runtime parameter validator the SDK
1158
+ // hands the model. We accept whatever Zod shape the host registered;
1159
+ // the SDK uses it to validate the tool-call arguments before dispatch.
1160
+ parameters: def.zodSchema,
1161
+ execute: async (input) => {
1162
+ if (toolCallCount < 2) {
1163
+ return {
1164
+ error: {
1165
+ code: "SELF_VERIFY_REQUIRED",
1166
+ 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."
1167
+ }
1168
+ };
1169
+ }
1170
+ const payload = input;
1171
+ for (let i = 0; i < payload.blocks.length; i++) {
1172
+ data.append({
1173
+ type: "block",
1174
+ value: { index: i, ...payload.blocks[i] }
1175
+ });
1176
+ }
1177
+ onPresent(payload);
1178
+ return { ok: true };
1179
+ }
1180
+ });
1181
+ continue;
1182
+ }
1183
+ result[name] = ai.tool({
1184
+ description: def.schema.description,
1185
+ parameters: def.zodSchema,
1186
+ execute: async (input) => {
1187
+ const res = await def.execute(input, { ...ctx, toolCallCount });
1188
+ toolCallCount += 1;
1189
+ if (res.ok) return res.data;
1190
+ return { error: res.error };
1191
+ }
1192
+ });
1193
+ }
1194
+ return result;
1195
+ }
1196
+
1197
+ // src/server/routes/agent-vercel.ts
1198
+ var VALID_MODELS = /* @__PURE__ */ new Set(["claude", "gemini"]);
1199
+ function jsonError2(status, code, message) {
1200
+ return new Response(JSON.stringify({ error: { code, message } }), {
1201
+ status,
1202
+ headers: { "Content-Type": "application/json" }
1203
+ });
1204
+ }
1205
+ function defaultGenerateSessionId2() {
1206
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
1207
+ }
1208
+ function createAgentVercelRoutes(ctx) {
1209
+ const { persistence, auth, scope, tools, vertex, logger, hooks } = ctx;
1210
+ return {
1211
+ /** Next.js-compatible POST handler. */
1212
+ POST: async (req) => {
1213
+ if (hooks?.onRequest) {
1214
+ const short = await hooks.onRequest(req);
1215
+ if (short) return short;
1216
+ }
1217
+ const authResult = await auth.requireAuth(req);
1218
+ if (!authResult.ok) return authResult.response;
1219
+ const { scope: callerScope, userId } = authResult;
1220
+ if (hooks?.onAuthenticated) {
1221
+ const short = await hooks.onAuthenticated({
1222
+ req,
1223
+ scope: callerScope,
1224
+ userId
1225
+ });
1226
+ if (short) return short;
1227
+ }
1228
+ const body = await req.json().catch(() => null);
1229
+ const question = typeof body?.question === "string" ? body.question.trim() : "";
1230
+ if (!question) {
1231
+ return jsonError2(
1232
+ 400,
1233
+ "VALIDATION_FAILED",
1234
+ "question must be a non-empty string."
1235
+ );
1236
+ }
1237
+ const rawChatSessionId = body?.chatSessionId;
1238
+ const incomingChatSessionId = typeof rawChatSessionId === "number" && Number.isInteger(rawChatSessionId) ? rawChatSessionId : null;
1239
+ const rawModel = body?.model;
1240
+ const requestedModel = typeof rawModel === "string" && VALID_MODELS.has(rawModel) ? rawModel : null;
1241
+ const aiSettings = await persistence.getAiSettings();
1242
+ let chatSessionId;
1243
+ if (incomingChatSessionId !== null) {
1244
+ const owned = await persistence.getSession(
1245
+ incomingChatSessionId,
1246
+ userId
1247
+ );
1248
+ if (!owned) {
1249
+ return jsonError2(404, "NOT_FOUND", "Chat session not found.");
1250
+ }
1251
+ chatSessionId = owned.id;
1252
+ } else {
1253
+ const created = await persistence.createSession({
1254
+ userId,
1255
+ title: question.slice(0, 200)
1256
+ });
1257
+ chatSessionId = created.id;
1258
+ }
1259
+ await persistence.appendMessage({
1260
+ sessionId: chatSessionId,
1261
+ role: "user",
1262
+ question
1263
+ });
1264
+ const sessionId = hooks?.generateSessionId ? await hooks.generateSessionId({
1265
+ scope: callerScope,
1266
+ userId,
1267
+ chatSessionId: incomingChatSessionId
1268
+ }) : defaultGenerateSessionId2();
1269
+ const scopeSummary = await scope.buildScopeSummary(callerScope);
1270
+ const scopeLabel = await scope.resolveScopeLabel(callerScope);
1271
+ const toolContext = {
1272
+ scope: callerScope,
1273
+ sessionId,
1274
+ scopeSummary,
1275
+ toolCallCount: 0
1276
+ };
1277
+ const systemBlocks = await tools.buildSystemBlocks(toolContext);
1278
+ const provider = requestedModel ?? aiSettings.toolProvider;
1279
+ if (!VALID_MODELS.has(provider)) {
1280
+ return jsonError2(
1281
+ 400,
1282
+ "INVALID_PROVIDER",
1283
+ `Vercel chat only supports 'claude' or 'gemini'; got '${provider}'.`
1284
+ );
1285
+ }
1286
+ const data = new ai.StreamData();
1287
+ let presentPayload = null;
1288
+ let persistedError = null;
1289
+ let sessionStarted = false;
1290
+ try {
1291
+ if (hooks?.onSessionStart) {
1292
+ await hooks.onSessionStart({
1293
+ scope: callerScope,
1294
+ sessionId,
1295
+ userId
1296
+ });
1297
+ }
1298
+ sessionStarted = true;
1299
+ const vercelTools = buildVercelTools(
1300
+ tools.tools,
1301
+ toolContext,
1302
+ data,
1303
+ (p) => {
1304
+ presentPayload = p;
1305
+ }
1306
+ );
1307
+ data.append({
1308
+ type: "meta",
1309
+ value: { chatSessionId, scopeLabel }
1310
+ });
1311
+ const system = systemBlocks.map((b) => b.text).join("\n\n");
1312
+ const model = provider === "claude" ? anthropic.createVertexAnthropic({
1313
+ project: vertex.projectId,
1314
+ location: vertex.defaultLocation,
1315
+ googleAuthOptions: {}
1316
+ })(vertex.modelIds.claude) : googleVertex.createVertex({
1317
+ project: vertex.projectId,
1318
+ location: aiSettings.gcpLocation,
1319
+ googleAuthOptions: {}
1320
+ })(vertex.modelIds.gemini);
1321
+ const result = ai.streamText({
1322
+ model,
1323
+ system,
1324
+ messages: [{ role: "user", content: question }],
1325
+ tools: vercelTools,
1326
+ maxSteps: 12,
1327
+ maxTokens: aiSettings.maxOutputTokens,
1328
+ onFinish: async ({ text }) => {
1329
+ try {
1330
+ let blocks = presentPayload?.blocks ?? [];
1331
+ const prose = {};
1332
+ const trimmed = (text ?? "").trim();
1333
+ if (presentPayload === null && trimmed) {
1334
+ const topic = question.length > 80 ? question.slice(0, 77) + "..." : question;
1335
+ const synthetic = {
1336
+ kind: "paragraph_brief",
1337
+ topic,
1338
+ key_facts: [trimmed]
1339
+ };
1340
+ blocks = [synthetic];
1341
+ prose[0] = trimmed;
1342
+ data.append({
1343
+ type: "block",
1344
+ value: { index: 0, ...synthetic }
1345
+ });
1346
+ } else if (text) {
1347
+ const firstPbIdx = blocks.findIndex(
1348
+ (b) => b.kind === "paragraph_brief"
1349
+ );
1350
+ if (firstPbIdx >= 0) prose[firstPbIdx] = text;
1351
+ }
1352
+ await persistence.appendMessage({
1353
+ sessionId: chatSessionId,
1354
+ role: "assistant",
1355
+ blocks: blocks.length ? blocks : null,
1356
+ prose: Object.keys(prose).length ? prose : null,
1357
+ errorJson: persistedError
1358
+ });
1359
+ } catch (err2) {
1360
+ logger?.warn?.(
1361
+ {
1362
+ chatSessionId,
1363
+ sessionId,
1364
+ err: err2.message
1365
+ },
1366
+ "[agent-vercel] failed to persist assistant turn"
1367
+ );
1368
+ } finally {
1369
+ try {
1370
+ await data.close();
1371
+ } catch {
1372
+ }
1373
+ }
1374
+ }
1375
+ });
1376
+ return result.toDataStreamResponse({ data });
1377
+ } catch (e) {
1378
+ const message = e.message ?? "Internal error";
1379
+ persistedError = { code: "INTERNAL", message };
1380
+ logger?.error?.(
1381
+ { chatSessionId, sessionId, err: message },
1382
+ "[agent-vercel] route errored"
1383
+ );
1384
+ try {
1385
+ data.append({
1386
+ type: "error",
1387
+ value: { code: "INTERNAL", message }
1388
+ });
1389
+ } catch {
1390
+ }
1391
+ try {
1392
+ await data.close();
1393
+ } catch {
1394
+ }
1395
+ try {
1396
+ await persistence.appendMessage({
1397
+ sessionId: chatSessionId,
1398
+ role: "assistant",
1399
+ blocks: null,
1400
+ prose: null,
1401
+ errorJson: persistedError
1402
+ });
1403
+ } catch (err2) {
1404
+ logger?.warn?.(
1405
+ { chatSessionId, sessionId, err: err2.message },
1406
+ "[agent-vercel] failed to persist error turn"
1407
+ );
1408
+ }
1409
+ return jsonError2(500, "INTERNAL", message);
1410
+ } finally {
1411
+ if (hooks?.onSessionEnd) {
1412
+ const cause = req.signal.aborted ? "abort" : persistedError ? "error" : "complete";
1413
+ try {
1414
+ await hooks.onSessionEnd({
1415
+ scope: callerScope,
1416
+ sessionId,
1417
+ userId,
1418
+ cause
1419
+ });
1420
+ } catch (err2) {
1421
+ logger?.warn?.(
1422
+ {
1423
+ chatSessionId,
1424
+ sessionId,
1425
+ sessionStarted,
1426
+ err: err2.message
1427
+ },
1428
+ "[agent-vercel] onSessionEnd hook failed"
1429
+ );
1430
+ }
1431
+ }
1432
+ }
1433
+ }
1434
+ };
1435
+ }
1140
1436
 
1141
1437
  // src/server/routes/chat-sessions.ts
1142
1438
  var DEFAULT_TITLE = "New chat";
@@ -1317,6 +1613,8 @@ function createChatSessionsRoutes(ctx) {
1317
1613
 
1318
1614
  // src/server/routes/admin-settings.ts
1319
1615
  var VALID_LOCATIONS = ["us-east5", "global"];
1616
+ var MIN_MAX_OUTPUT_TOKENS = 256;
1617
+ var MAX_MAX_OUTPUT_TOKENS = 64e3;
1320
1618
  function isStringRecord(v) {
1321
1619
  return typeof v === "object" && v !== null && !Array.isArray(v);
1322
1620
  }
@@ -1331,6 +1629,8 @@ function toWire(settings) {
1331
1629
  tool_provider: settings.toolProvider,
1332
1630
  gcp_location: settings.gcpLocation,
1333
1631
  chat_interface: settings.chatInterface,
1632
+ max_output_tokens: settings.maxOutputTokens,
1633
+ role_prompt: settings.rolePrompt,
1334
1634
  updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
1335
1635
  updated_by_user_id: settings.updatedByUserId
1336
1636
  };
@@ -1410,11 +1710,29 @@ function createAdminSettingsRoutes(ctx) {
1410
1710
  }
1411
1711
  patch.chatInterface = v;
1412
1712
  }
1413
- if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0) {
1713
+ if ("max_output_tokens" in body) {
1714
+ const v = body.max_output_tokens;
1715
+ if (typeof v !== "number" || !Number.isInteger(v) || v < MIN_MAX_OUTPUT_TOKENS || v > MAX_MAX_OUTPUT_TOKENS) {
1716
+ return jsonResponse({ error: "invalid_max_output_tokens" }, 400);
1717
+ }
1718
+ patch.maxOutputTokens = v;
1719
+ }
1720
+ if ("role_prompt" in body) {
1721
+ const v = body.role_prompt;
1722
+ if (v === null) {
1723
+ patch.rolePrompt = null;
1724
+ } else if (typeof v === "string") {
1725
+ const trimmed = v.trim();
1726
+ patch.rolePrompt = trimmed === "" ? null : trimmed;
1727
+ } else {
1728
+ return jsonResponse({ error: "invalid_role_prompt" }, 400);
1729
+ }
1730
+ }
1731
+ if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
1414
1732
  return jsonResponse(
1415
1733
  {
1416
1734
  error: "empty_patch",
1417
- message: "Body must set at least one of tool_provider, gcp_location, chat_interface."
1735
+ message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
1418
1736
  },
1419
1737
  400
1420
1738
  );
@@ -1439,16 +1757,27 @@ function configureAiChat(opts) {
1439
1757
  ];
1440
1758
  const getProvider = (id) => toolProviders2.find((p) => p.id === id) ?? getToolProvider(id);
1441
1759
  const chatInterfaces = opts.chatInterfaces ?? BUILTIN_CHAT_INTERFACE_IDS.map((id) => ({ id }));
1442
- const tools = opts.rolePrompt ? {
1760
+ const staticRolePrompt = opts.rolePrompt;
1761
+ const tools = {
1443
1762
  tools: opts.tools.tools,
1444
1763
  async buildSystemBlocks(ctx) {
1445
1764
  const inner = await opts.tools.buildSystemBlocks(ctx);
1446
- const rolePrompt = opts.rolePrompt;
1447
- const role = typeof rolePrompt === "function" ? await rolePrompt(ctx) : rolePrompt;
1448
- if (!role || !role.trim()) return inner;
1765
+ let role = null;
1766
+ try {
1767
+ const settings = await opts.persistence.getAiSettings();
1768
+ if (settings.rolePrompt && settings.rolePrompt.trim()) {
1769
+ role = settings.rolePrompt;
1770
+ }
1771
+ } catch {
1772
+ }
1773
+ if (!role && staticRolePrompt) {
1774
+ const resolved = typeof staticRolePrompt === "function" ? await staticRolePrompt(ctx) : staticRolePrompt;
1775
+ if (resolved && resolved.trim()) role = resolved;
1776
+ }
1777
+ if (!role) return inner;
1449
1778
  return [{ text: role, cached: true }, ...inner];
1450
1779
  }
1451
- } : opts.tools;
1780
+ };
1452
1781
  const runAgentBound = async ({
1453
1782
  question,
1454
1783
  ctx,
@@ -1497,6 +1826,15 @@ function configureAiChat(opts) {
1497
1826
  resolveNarratorId: opts.resolveNarratorId,
1498
1827
  hooks: opts.hooks
1499
1828
  });
1829
+ const agentVercel = createAgentVercelRoutes({
1830
+ persistence: opts.persistence,
1831
+ auth: opts.auth,
1832
+ scope: opts.scope,
1833
+ tools,
1834
+ vertex: opts.vertex,
1835
+ logger: opts.logger,
1836
+ hooks: opts.hooks
1837
+ });
1500
1838
  const chatSessions = createChatSessionsRoutes({
1501
1839
  persistence: opts.persistence,
1502
1840
  auth: opts.auth,
@@ -1513,7 +1851,7 @@ function configureAiChat(opts) {
1513
1851
  });
1514
1852
  return {
1515
1853
  runAgent: runAgentBound,
1516
- routes: { agentCustom, chatSessions, adminSettings },
1854
+ routes: { agentCustom, agentVercel, chatSessions, adminSettings },
1517
1855
  registries: { toolProviders: toolProviders2, chatInterfaces }
1518
1856
  };
1519
1857
  }