@firstlovecenter/ai-chat 0.6.1 → 0.8.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.
@@ -25,7 +25,10 @@ async function runAgent(input) {
25
25
  const maxOutputTokens = input.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
26
26
  const transcript = [];
27
27
  transcript.push({ kind: "user", text: input.question });
28
- const messages = [{ role: "user", text: input.question }];
28
+ const messages = [
29
+ ...input.priorMessages ?? [],
30
+ { role: "user", text: input.question }
31
+ ];
29
32
  const system = input.systemBlocks;
30
33
  const toolSchemas = Object.values(input.tools).map((t) => t.schema);
31
34
  let toolCallCount = 0;
@@ -218,11 +221,28 @@ function toAnthropicMessages(messages) {
218
221
  const out = [];
219
222
  for (const msg of messages) {
220
223
  if (msg.role === "user") {
221
- out.push({ role: "user", content: msg.text });
224
+ if (msg.cached) {
225
+ out.push({
226
+ role: "user",
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: msg.text,
231
+ cache_control: { type: "ephemeral" }
232
+ }
233
+ ]
234
+ });
235
+ } else {
236
+ out.push({ role: "user", content: msg.text });
237
+ }
222
238
  } else if (msg.role === "assistant") {
223
239
  const blocks = [];
224
240
  if (msg.text) {
225
- blocks.push({ type: "text", text: msg.text });
241
+ const textBlock = { type: "text", text: msg.text };
242
+ if (msg.cached && msg.toolCalls.length === 0) {
243
+ textBlock.cache_control = { type: "ephemeral" };
244
+ }
245
+ blocks.push(textBlock);
226
246
  }
227
247
  for (const tc of msg.toolCalls) {
228
248
  blocks.push({
@@ -574,6 +594,87 @@ var toolProviders = [
574
594
  function getToolProvider(id) {
575
595
  return toolProviders.find((p) => p.id === id);
576
596
  }
597
+
598
+ // src/server/history.ts
599
+ var DEFAULT_MAX_HISTORY_PAIRS = 20;
600
+ var DEFAULT_MAX_TEXT_CHARS = 4e3;
601
+ function historyToNormalizedMessages(rows, opts = {}) {
602
+ const maxPairs = opts.maxPairs ?? DEFAULT_MAX_HISTORY_PAIRS;
603
+ const maxTextChars = opts.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS;
604
+ const pairs = [];
605
+ let i = 0;
606
+ while (i < rows.length) {
607
+ const row = rows[i];
608
+ if (row.role !== "user" || !row.question) {
609
+ i += 1;
610
+ continue;
611
+ }
612
+ const next = rows[i + 1];
613
+ if (next?.role !== "assistant") {
614
+ i += 1;
615
+ continue;
616
+ }
617
+ const assistantText = truncate(
618
+ assistantMessageToText(next),
619
+ maxTextChars
620
+ );
621
+ if (assistantText) {
622
+ pairs.push([
623
+ { role: "user", text: truncate(row.question, maxTextChars) },
624
+ { role: "assistant", text: assistantText, toolCalls: [] }
625
+ ]);
626
+ }
627
+ i += 2;
628
+ }
629
+ const kept = maxPairs > 0 ? pairs.slice(-maxPairs) : pairs;
630
+ return kept.flat();
631
+ }
632
+ function truncate(text, max) {
633
+ if (max <= 0 || text.length <= max) return text;
634
+ return text.slice(0, max);
635
+ }
636
+ function assistantMessageToText(row) {
637
+ if (row.errorJson) return "";
638
+ const proseText = proseToText(row.prose);
639
+ if (proseText) return proseText;
640
+ const blockText = blocksToText(row.blocks);
641
+ return blockText;
642
+ }
643
+ function proseToText(prose) {
644
+ if (!prose || typeof prose !== "object") return "";
645
+ const entries = Object.entries(prose).map(([k, v]) => [Number(k), typeof v === "string" ? v : ""]).filter(([k, v]) => Number.isFinite(k) && v.length > 0).sort(([a], [b]) => a - b);
646
+ return entries.map(([, v]) => v).join("\n\n").trim();
647
+ }
648
+ function blocksToText(blocks) {
649
+ if (!Array.isArray(blocks)) return "";
650
+ const parts = [];
651
+ for (const raw of blocks) {
652
+ if (!raw || typeof raw !== "object") continue;
653
+ const b = raw;
654
+ switch (b.kind) {
655
+ case "paragraph_brief": {
656
+ const facts = (b.key_facts ?? []).filter((f) => f && f.trim());
657
+ if (b.topic) parts.push(b.topic);
658
+ if (facts.length) parts.push(facts.join("\n"));
659
+ break;
660
+ }
661
+ case "list": {
662
+ const items = (b.items ?? []).filter((s) => s && s.trim());
663
+ if (b.title) parts.push(b.title);
664
+ if (items.length) parts.push(items.map((s) => `- ${s}`).join("\n"));
665
+ break;
666
+ }
667
+ case "chart":
668
+ case "table":
669
+ if (b.title) parts.push(`[${b.title}]`);
670
+ break;
671
+ case "callout":
672
+ if (b.text) parts.push(b.text);
673
+ break;
674
+ }
675
+ }
676
+ return parts.join("\n\n").trim();
677
+ }
577
678
  function vertexHost2(location) {
578
679
  return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
579
680
  }
@@ -914,13 +1015,25 @@ function createAgentCustomRoutes(ctx) {
914
1015
  const rawChatSessionId = body?.chatSessionId;
915
1016
  const incomingChatSessionId = typeof rawChatSessionId === "number" && Number.isInteger(rawChatSessionId) ? rawChatSessionId : null;
916
1017
  const aiSettings = await persistence.getAiSettings();
1018
+ const effectiveProjectId = aiSettings.gcpProjectId ?? vertex.projectId;
917
1019
  let chatSessionId;
1020
+ let priorMessages = [];
918
1021
  if (incomingChatSessionId !== null) {
919
1022
  const owned = await persistence.getSession(incomingChatSessionId, userId);
920
1023
  if (!owned) {
921
1024
  return jsonError(404, "NOT_FOUND", "Chat session not found.");
922
1025
  }
923
1026
  chatSessionId = owned.id;
1027
+ const stored = await persistence.listMessagesForSession(chatSessionId, userId);
1028
+ priorMessages = historyToNormalizedMessages(stored);
1029
+ if (priorMessages.length > 0) {
1030
+ const last = priorMessages[priorMessages.length - 1];
1031
+ if (last.role === "assistant" && last.toolCalls.length === 0) {
1032
+ priorMessages[priorMessages.length - 1] = { ...last, cached: true };
1033
+ } else if (last.role === "user") {
1034
+ priorMessages[priorMessages.length - 1] = { ...last, cached: true };
1035
+ }
1036
+ }
924
1037
  } else {
925
1038
  const created = await persistence.createSession({
926
1039
  userId,
@@ -957,7 +1070,7 @@ function createAgentCustomRoutes(ctx) {
957
1070
  }
958
1071
  const provider = def.createProvider({
959
1072
  auth: vertex.auth,
960
- projectId: vertex.projectId,
1073
+ projectId: effectiveProjectId,
961
1074
  defaultLocation: vertex.defaultLocation,
962
1075
  modelIds: vertex.modelIds,
963
1076
  location: aiSettings.gcpLocation
@@ -1008,6 +1121,7 @@ data: ${JSON.stringify(data)}
1008
1121
  send("meta", { chatSessionId, scopeLabel });
1009
1122
  const agentResult = await runAgent({
1010
1123
  question,
1124
+ priorMessages,
1011
1125
  ctx: toolContext,
1012
1126
  tools: tools.tools,
1013
1127
  systemBlocks,
@@ -1035,7 +1149,7 @@ data: ${JSON.stringify(data)}
1035
1149
  }
1036
1150
  for await (const token of narratorFn({
1037
1151
  auth: vertex.auth,
1038
- projectId: vertex.projectId,
1152
+ projectId: effectiveProjectId,
1039
1153
  location: aiSettings.gcpLocation,
1040
1154
  modelId: narratorModelId,
1041
1155
  maxTokens: aiSettings.maxOutputTokens,
@@ -1226,7 +1340,17 @@ function createAgentVercelRoutes(ctx) {
1226
1340
  if (short) return short;
1227
1341
  }
1228
1342
  const body = await req.json().catch(() => null);
1229
- const question = typeof body?.question === "string" ? body.question.trim() : "";
1343
+ let question = typeof body?.question === "string" ? body.question.trim() : "";
1344
+ if (!question && Array.isArray(body?.messages)) {
1345
+ const msgs = body.messages;
1346
+ for (let i = msgs.length - 1; i >= 0; i -= 1) {
1347
+ const m = msgs[i];
1348
+ if (m && m.role === "user" && typeof m.content === "string") {
1349
+ question = m.content.trim();
1350
+ break;
1351
+ }
1352
+ }
1353
+ }
1230
1354
  if (!question) {
1231
1355
  return jsonError2(
1232
1356
  400,
@@ -1239,7 +1363,9 @@ function createAgentVercelRoutes(ctx) {
1239
1363
  const rawModel = body?.model;
1240
1364
  const requestedModel = typeof rawModel === "string" && VALID_MODELS.has(rawModel) ? rawModel : null;
1241
1365
  const aiSettings = await persistence.getAiSettings();
1366
+ const effectiveProjectId = aiSettings.gcpProjectId ?? vertex.projectId;
1242
1367
  let chatSessionId;
1368
+ let priorMessages = [];
1243
1369
  if (incomingChatSessionId !== null) {
1244
1370
  const owned = await persistence.getSession(
1245
1371
  incomingChatSessionId,
@@ -1249,6 +1375,11 @@ function createAgentVercelRoutes(ctx) {
1249
1375
  return jsonError2(404, "NOT_FOUND", "Chat session not found.");
1250
1376
  }
1251
1377
  chatSessionId = owned.id;
1378
+ const stored = await persistence.listMessagesForSession(
1379
+ chatSessionId,
1380
+ userId
1381
+ );
1382
+ priorMessages = historyToNormalizedMessages(stored);
1252
1383
  } else {
1253
1384
  const created = await persistence.createSession({
1254
1385
  userId,
@@ -1310,18 +1441,24 @@ function createAgentVercelRoutes(ctx) {
1310
1441
  });
1311
1442
  const system = systemBlocks.map((b) => b.text).join("\n\n");
1312
1443
  const model = provider === "claude" ? anthropic.createVertexAnthropic({
1313
- project: vertex.projectId,
1444
+ project: effectiveProjectId,
1314
1445
  location: vertex.defaultLocation,
1315
1446
  googleAuthOptions: {}
1316
1447
  })(vertex.modelIds.claude) : googleVertex.createVertex({
1317
- project: vertex.projectId,
1448
+ project: effectiveProjectId,
1318
1449
  location: aiSettings.gcpLocation,
1319
1450
  googleAuthOptions: {}
1320
1451
  })(vertex.modelIds.gemini);
1452
+ const priorCoreMessages = priorMessages.filter(
1453
+ (m) => m.role === "user" || m.role === "assistant"
1454
+ ).map((m) => ({ role: m.role, content: m.text }));
1321
1455
  const result = ai.streamText({
1322
1456
  model,
1323
1457
  system,
1324
- messages: [{ role: "user", content: question }],
1458
+ messages: [
1459
+ ...priorCoreMessages,
1460
+ { role: "user", content: question }
1461
+ ],
1325
1462
  tools: vercelTools,
1326
1463
  maxSteps: 12,
1327
1464
  maxTokens: aiSettings.maxOutputTokens,
@@ -1615,6 +1752,7 @@ function createChatSessionsRoutes(ctx) {
1615
1752
  var VALID_LOCATIONS = ["us-east5", "global"];
1616
1753
  var MIN_MAX_OUTPUT_TOKENS = 256;
1617
1754
  var MAX_MAX_OUTPUT_TOKENS = 64e3;
1755
+ var GCP_PROJECT_ID_REGEX = /^[a-z][-a-z0-9]{4,28}[a-z0-9]$/;
1618
1756
  function isStringRecord(v) {
1619
1757
  return typeof v === "object" && v !== null && !Array.isArray(v);
1620
1758
  }
@@ -1631,6 +1769,7 @@ function toWire(settings) {
1631
1769
  chat_interface: settings.chatInterface,
1632
1770
  max_output_tokens: settings.maxOutputTokens,
1633
1771
  role_prompt: settings.rolePrompt,
1772
+ gcp_project_id: settings.gcpProjectId,
1634
1773
  updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
1635
1774
  updated_by_user_id: settings.updatedByUserId
1636
1775
  };
@@ -1728,11 +1867,28 @@ function createAdminSettingsRoutes(ctx) {
1728
1867
  return jsonResponse({ error: "invalid_role_prompt" }, 400);
1729
1868
  }
1730
1869
  }
1731
- if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
1870
+ if ("gcp_project_id" in body) {
1871
+ const v = body.gcp_project_id;
1872
+ if (v === null) {
1873
+ patch.gcpProjectId = null;
1874
+ } else if (typeof v === "string") {
1875
+ const trimmed = v.trim();
1876
+ if (trimmed === "") {
1877
+ patch.gcpProjectId = null;
1878
+ } else if (trimmed.length > 64 || !GCP_PROJECT_ID_REGEX.test(trimmed)) {
1879
+ return jsonResponse({ error: "invalid_gcp_project_id" }, 400);
1880
+ } else {
1881
+ patch.gcpProjectId = trimmed;
1882
+ }
1883
+ } else {
1884
+ return jsonResponse({ error: "invalid_gcp_project_id" }, 400);
1885
+ }
1886
+ }
1887
+ if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch) && !("gcpProjectId" in patch)) {
1732
1888
  return jsonResponse(
1733
1889
  {
1734
1890
  error: "empty_patch",
1735
- message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
1891
+ message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt, gcp_project_id."
1736
1892
  },
1737
1893
  400
1738
1894
  );
@@ -1793,9 +1949,10 @@ function configureAiChat(opts) {
1793
1949
  `Unknown tool provider '${providerId ?? settings.toolProvider}'. Registered: ${toolProviders2.map((p) => p.id).join(", ")}.`
1794
1950
  );
1795
1951
  }
1952
+ const effectiveProjectId = settings.gcpProjectId ?? opts.vertex.projectId;
1796
1953
  const provider = def.createProvider({
1797
1954
  auth: opts.vertex.auth,
1798
- projectId: opts.vertex.projectId,
1955
+ projectId: effectiveProjectId,
1799
1956
  defaultLocation: opts.vertex.defaultLocation,
1800
1957
  modelIds: opts.vertex.modelIds,
1801
1958
  location: location ?? settings.gcpLocation