@maximem/synap-js-sdk 0.5.8 → 0.5.9

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.
Files changed (47) hide show
  1. package/README.md +5 -1
  2. package/dist/anticipation-cache-LPNKMRBH.cjs +20 -0
  3. package/dist/anticipation-cache-LPNKMRBH.cjs.map +1 -0
  4. package/dist/anticipation-cache-RWKF55J4.js +20 -0
  5. package/dist/{chunk-FXGLT6AT.js → chunk-I7TWR4S6.js} +59 -3
  6. package/dist/chunk-I7TWR4S6.js.map +1 -0
  7. package/dist/{chunk-T6U5PVAD.cjs → chunk-ICUFS3B5.cjs} +449 -12
  8. package/dist/chunk-ICUFS3B5.cjs.map +1 -0
  9. package/dist/{chunk-ODVOQOSO.js → chunk-R72YL6Q4.js} +445 -8
  10. package/dist/chunk-R72YL6Q4.js.map +1 -0
  11. package/dist/{chunk-HCULAVJC.cjs → chunk-RE4DDIWO.cjs} +66 -10
  12. package/dist/chunk-RE4DDIWO.cjs.map +1 -0
  13. package/dist/chunk-T372OL3Q.cjs.map +1 -1
  14. package/dist/chunk-ZKOJDUN4.cjs.map +1 -1
  15. package/dist/env-55ULHNJZ.cjs.map +1 -1
  16. package/dist/{errors-Bl3vY1eb.d.cts → errors-Bdayqlzy.d.cts} +88 -2
  17. package/dist/{errors-Bl3vY1eb.d.ts → errors-Bdayqlzy.d.ts} +88 -2
  18. package/dist/grpc/index.cjs +4 -4
  19. package/dist/grpc/index.cjs.map +1 -1
  20. package/dist/grpc/index.d.cts +8 -3
  21. package/dist/grpc/index.d.ts +8 -3
  22. package/dist/grpc/index.js +3 -3
  23. package/dist/index.cjs +1300 -116
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +258 -5
  26. package/dist/index.d.ts +258 -5
  27. package/dist/index.js +1253 -69
  28. package/dist/index.js.map +1 -1
  29. package/dist/onnx-encoder-4RSL6ID4.cjs +103 -0
  30. package/dist/onnx-encoder-4RSL6ID4.cjs.map +1 -0
  31. package/dist/onnx-encoder-NP35KH4H.js +103 -0
  32. package/dist/onnx-encoder-NP35KH4H.js.map +1 -0
  33. package/dist/{stream-client-VWKINE47.js → stream-client-3WHXGW4U.js} +2 -2
  34. package/dist/stream-client-L6OQPZN7.cjs +16 -0
  35. package/dist/stream-client-L6OQPZN7.cjs.map +1 -0
  36. package/package.json +4 -2
  37. package/dist/anticipation-cache-LRE3TLK4.cjs +0 -14
  38. package/dist/anticipation-cache-LRE3TLK4.cjs.map +0 -1
  39. package/dist/anticipation-cache-WLU4NMD5.js +0 -14
  40. package/dist/chunk-FXGLT6AT.js.map +0 -1
  41. package/dist/chunk-HCULAVJC.cjs.map +0 -1
  42. package/dist/chunk-ODVOQOSO.js.map +0 -1
  43. package/dist/chunk-T6U5PVAD.cjs.map +0 -1
  44. package/dist/stream-client-N46JBT5A.cjs +0 -16
  45. package/dist/stream-client-N46JBT5A.cjs.map +0 -1
  46. /package/dist/{anticipation-cache-WLU4NMD5.js.map → anticipation-cache-RWKF55J4.js.map} +0 -0
  47. /package/dist/{stream-client-VWKINE47.js.map → stream-client-3WHXGW4U.js.map} +0 -0
package/dist/index.js CHANGED
@@ -3,12 +3,14 @@ import {
3
3
  AnticipationCache,
4
4
  BM25,
5
5
  CONTRACT,
6
+ HISTORY_SEARCH,
7
+ SEMANTIC_MATCH,
6
8
  THRESHOLDS,
7
9
  isRecallQuery,
8
10
  rungKey,
9
11
  stem,
10
12
  tokenize
11
- } from "./chunk-ODVOQOSO.js";
13
+ } from "./chunk-R72YL6Q4.js";
12
14
  import {
13
15
  getEnv
14
16
  } from "./chunk-QBCVMPGK.js";
@@ -55,6 +57,7 @@ var ENDPOINTS = {
55
57
  context_fetch_client: { method: "POST", path: "/v1/context/client/fetch", idempotent: true },
56
58
  context_fetch_conversation: { method: "POST", path: "/v1/context/conversation/fetch", idempotent: true },
57
59
  context_for_prompt: { method: "GET", path: "/v1/conversations/{conversation_id}/context-for-prompt", idempotent: true },
60
+ conversation_turns: { method: "GET", path: "/v1/conversations/{conversation_id}/messages", idempotent: true },
58
61
  conversations_ingest: { method: "POST", path: "/v1/conversations/ingest", idempotent: false },
59
62
  conversations_messages: { method: "POST", path: "/v1/conversations/messages", idempotent: false },
60
63
  conversations_messages_batch: { method: "POST", path: "/v1/conversations/messages/batch", idempotent: false },
@@ -517,6 +520,86 @@ function sleep(ms, signal) {
517
520
  });
518
521
  }
519
522
 
523
+ // src/context/ranked.ts
524
+ var RESPONSE_SHAPES = ["by_type", "ranked"];
525
+ var MEMORY_TYPES = [
526
+ "facts",
527
+ "preferences",
528
+ "episodes",
529
+ "emotions",
530
+ "temporal_events"
531
+ ];
532
+ var EXCERPT_CATEGORY = "excerpts";
533
+ function wireRankScore(item) {
534
+ const md = item.metadata;
535
+ let raw = md !== null && typeof md === "object" ? md["rank_score"] : void 0;
536
+ if (raw === void 0 || raw === null) raw = item["rank_score"];
537
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
538
+ }
539
+ function wireCategory(item, fallback = "") {
540
+ if (item.source === "chunks") return EXCERPT_CATEGORY;
541
+ const category = item["category"];
542
+ if (typeof category === "string" && category !== "") return category;
543
+ const contextType = item["context_type"];
544
+ if (typeof contextType === "string" && contextType !== "") return contextType;
545
+ return fallback;
546
+ }
547
+ function byRankScore(a, b) {
548
+ const sa = wireRankScore(a);
549
+ const sb = wireRankScore(b);
550
+ if (sa === null && sb === null) return 0;
551
+ if (sa === null) return 1;
552
+ if (sb === null) return -1;
553
+ return sb - sa;
554
+ }
555
+ function rankItems(byType) {
556
+ const flat = [];
557
+ for (const type of MEMORY_TYPES) {
558
+ const list = byType[type];
559
+ if (!Array.isArray(list)) continue;
560
+ for (const it of list) {
561
+ if (it === null || typeof it !== "object") continue;
562
+ flat.push({
563
+ ...it,
564
+ category: wireCategory(it, type),
565
+ id: it.id || it.item_id || ""
566
+ });
567
+ }
568
+ }
569
+ flat.sort(byRankScore);
570
+ return flat.map((it, n2) => ({ ...it, rank: n2 + 1 }));
571
+ }
572
+ function applyResponseShape(raw, shape = "by_type") {
573
+ const wire = raw["items"];
574
+ if (shape !== "ranked" && !Array.isArray(wire)) return raw;
575
+ const source = Array.isArray(wire) ? wire : rankItems(raw);
576
+ const items = source.map((it) => ({
577
+ ...it,
578
+ id: it.id || it.item_id || "",
579
+ category: wireCategory(it)
580
+ }));
581
+ const out = { ...raw, items };
582
+ for (const type of MEMORY_TYPES) {
583
+ out[type] = items.filter((it) => it["category"] === type);
584
+ }
585
+ out["excerpts"] = items.filter((it) => it["category"] === EXCERPT_CATEGORY);
586
+ return out;
587
+ }
588
+ var RANKED_LABELS = {
589
+ facts: "fact",
590
+ preferences: "preference",
591
+ episodes: "episode",
592
+ emotions: "emotion",
593
+ temporal_events: "temporal event",
594
+ [EXCERPT_CATEGORY]: "excerpt"
595
+ };
596
+ function rankedLabel(category) {
597
+ return RANKED_LABELS[category] ?? (category !== "" ? category : "item");
598
+ }
599
+ function sortByRankScore(items) {
600
+ return [...items].sort(byRankScore);
601
+ }
602
+
520
603
  // src/context/fetch.ts
521
604
  var ENDPOINT_BY_SCOPE = {
522
605
  user: "context_fetch_user",
@@ -539,6 +622,7 @@ function buildFetchRequest(scope, options) {
539
622
  const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
540
623
  const queryDate = opt(options, "query_date", "queryDate");
541
624
  const perQuery = opt(options, "max_results_per_query", "maxResultsPerQuery");
625
+ const responseShape = responseShapeOf(options);
542
626
  const mode = options.mode ?? "fast";
543
627
  if (searchQuery !== void 0 && !Array.isArray(searchQuery)) {
544
628
  throw new InvalidInputError("search_query must be an array when provided");
@@ -556,6 +640,7 @@ function buildFetchRequest(scope, options) {
556
640
  if (precisionLevel !== "high") body["precision_level"] = precisionLevel;
557
641
  if (queryDate) body["query_date"] = queryDate;
558
642
  if (perQuery) body["max_results_per_query"] = perQuery;
643
+ if (responseShape !== "by_type") body["response_shape"] = responseShape;
559
644
  if (options.include_conversation_context === false) {
560
645
  body["include_conversation_context"] = false;
561
646
  }
@@ -603,21 +688,48 @@ function buildFetchRequest(scope, options) {
603
688
  return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
604
689
  }
605
690
  var VALID_CONTEXT_MODES = ["in-conversation", "conversation-summary"];
691
+ function responseShapeOf(options) {
692
+ const shape = opt(options, "response_shape", "responseShape") ?? "by_type";
693
+ if (!RESPONSE_SHAPES.includes(shape)) {
694
+ throw new InvalidInputError(
695
+ `Invalid response_shape '${shape}'. Must be one of: ${RESPONSE_SHAPES.join(", ")}`
696
+ );
697
+ }
698
+ return shape;
699
+ }
606
700
  async function fetchContext(transport, scope, options = {}, hooks = {}) {
607
701
  const startedAt = Date.now();
608
702
  transport.checkCustomerId(
609
703
  opt(options, "customer_id", "customerId"),
610
704
  `${scope}.context.fetch`
611
705
  );
612
- const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
706
+ const responseShape = responseShapeOf(options);
707
+ const network = async () => {
708
+ const { endpoint, body } = buildFetchRequest(scope, options);
709
+ const result = await transport.request(endpoint, { body });
710
+ return applyResponseShape(result?.["context"] ?? {}, responseShape);
711
+ };
712
+ const anticipated = await hooks.beforeFetch?.(scope, options) ?? null;
713
+ if (anticipated !== null && anticipated.borderline) {
714
+ const pending = network();
715
+ pending.catch(() => void 0);
716
+ let timer;
717
+ const late = new Promise((resolve) => {
718
+ timer = setTimeout(() => resolve(null), anticipated.raceDeadlineMs ?? 250);
719
+ });
720
+ const won = await Promise.race([pending.then((r) => r, () => null), late]);
721
+ if (timer !== void 0) clearTimeout(timer);
722
+ if (won !== null) {
723
+ hooks.onAssembled?.(scope, options, won, startedAt);
724
+ return won;
725
+ }
726
+ }
613
727
  if (anticipated !== null) {
614
728
  hooks.onServedFromCache?.(scope, options, anticipated);
615
729
  hooks.onAssembled?.(scope, options, anticipated.response, startedAt);
616
- return anticipated.response;
730
+ return applyResponseShape(anticipated.response, responseShape);
617
731
  }
618
- const { endpoint, body } = buildFetchRequest(scope, options);
619
- const result = await transport.request(endpoint, { body });
620
- const response = result?.["context"] ?? {};
732
+ const response = await network();
621
733
  hooks.onAssembled?.(scope, options, response, startedAt);
622
734
  return response;
623
735
  }
@@ -738,6 +850,17 @@ function normalizeConversationContext(value) {
738
850
  conversationId: pick(value["conversationId"], value.conversation_id, null)
739
851
  };
740
852
  }
853
+ function normalizeRetrievalHealth(raw) {
854
+ if (!raw || typeof raw !== "object") return null;
855
+ const r = raw;
856
+ const warnings = r["warnings"];
857
+ return {
858
+ degraded: r["degraded"] === void 0 ? true : Boolean(r["degraded"]),
859
+ level: r["level"] ?? null,
860
+ code: r["code"] ?? null,
861
+ warnings: Array.isArray(warnings) ? warnings.map((w) => String(w)) : []
862
+ };
863
+ }
741
864
  function normalizeContextResponse(result = {}) {
742
865
  const context = result["context"] ?? result;
743
866
  return {
@@ -754,7 +877,15 @@ function normalizeContextResponse(result = {}) {
754
877
  )
755
878
  ),
756
879
  metadata: normalizeContextMetadata(obj(context.metadata)),
757
- rawResponse: pick(context["rawResponse"], context["raw_response"], {})
880
+ // `retrieval` is a SIBLING of `context` in the body, so it is read off the
881
+ // envelope first and only then off a bare context. Reading it from
882
+ // `context` alone is how the Python SDK dropped it.
883
+ retrieval: normalizeRetrievalHealth(
884
+ pick(result["retrieval"], context["retrieval"], null)
885
+ ),
886
+ rawResponse: pick(context["rawResponse"], context["raw_response"], {}),
887
+ // Only for a ranked fetch, so a by-type result has exactly the keys it had.
888
+ ...Array.isArray(context.items) ? { items: context.items, excerpts: Array.isArray(context.excerpts) ? context.excerpts : [] } : {}
758
889
  };
759
890
  }
760
891
 
@@ -1069,6 +1200,27 @@ function validateInstanceId(instanceId) {
1069
1200
  }
1070
1201
  }
1071
1202
 
1203
+ // src/agent-model.ts
1204
+ var AGENT_MODEL_KEY = "agent_model";
1205
+ var AGENT_CONTEXT_WINDOW_KEY = "agent_context_window";
1206
+ function agentModelOf(options) {
1207
+ const o2 = options ?? {};
1208
+ const window = o2["context_window"] ?? o2["contextWindow"];
1209
+ return {
1210
+ model: typeof o2["model"] === "string" ? o2["model"] : void 0,
1211
+ contextWindow: typeof window === "number" ? window : void 0
1212
+ };
1213
+ }
1214
+ function withAgentModel(metadata, perCall, defaults, asStrings = false) {
1215
+ const model = perCall.model || defaults.model;
1216
+ const window = perCall.contextWindow || defaults.contextWindow;
1217
+ if (!model && !window) return metadata;
1218
+ const out = { ...metadata ?? {} };
1219
+ if (model) out[AGENT_MODEL_KEY] = String(model);
1220
+ if (window) out[AGENT_CONTEXT_WINDOW_KEY] = asStrings ? String(Math.trunc(window)) : Math.trunc(window);
1221
+ return out;
1222
+ }
1223
+
1072
1224
  // src/conversation/interface.ts
1073
1225
  var COMPACTION_LEVELS = [
1074
1226
  "low",
@@ -1197,10 +1349,50 @@ var CompactionSubscribers = class {
1197
1349
  }
1198
1350
  }
1199
1351
  };
1352
+ function storedBatchRows(bodies, result) {
1353
+ const r = result ?? {};
1354
+ const results = Array.isArray(r["results"]) ? r["results"] : [];
1355
+ const errors = Array.isArray(r["errors"]) ? r["errors"] : [];
1356
+ const failed = new Set(errors.map((e) => e?.["index"]));
1357
+ const stored = bodies.filter((_, index) => !failed.has(index));
1358
+ if (stored.length !== results.length) return [];
1359
+ const pairs = [];
1360
+ stored.forEach((body, i) => {
1361
+ const row = results[i];
1362
+ if (row && typeof row === "object" && row["message_id"]) pairs.push([body, row]);
1363
+ });
1364
+ return pairs;
1365
+ }
1200
1366
  function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
1201
1367
  }, onTurn = () => {
1202
1368
  }, onFetched = () => {
1203
- }, localShortTerm = { prompt: () => null, compacted: () => null }) {
1369
+ }, localShortTerm = { prompt: () => null, compacted: () => null }, agentModel = () => ({})) {
1370
+ const writesInFlight = /* @__PURE__ */ new Map();
1371
+ const withTurnModel = (body, options) => {
1372
+ const metadata = withAgentModel(
1373
+ body["metadata"],
1374
+ agentModelOf(options),
1375
+ agentModel()
1376
+ );
1377
+ return metadata === void 0 ? body : { ...body, metadata };
1378
+ };
1379
+ const writeStarted = (conversationId) => {
1380
+ writesInFlight.set(conversationId, (writesInFlight.get(conversationId) ?? 0) + 1);
1381
+ };
1382
+ const writeLanded = (conversationId) => {
1383
+ const remaining = (writesInFlight.get(conversationId) ?? 0) - 1;
1384
+ if (remaining > 0) writesInFlight.set(conversationId, remaining);
1385
+ else writesInFlight.delete(conversationId);
1386
+ };
1387
+ const confirmation = (result, conversationId) => {
1388
+ const r = result ?? {};
1389
+ const count = r["conversation_message_count"];
1390
+ return {
1391
+ ...typeof r["message_id"] === "string" ? { messageId: r["message_id"] } : {},
1392
+ serverMessageCount: typeof count === "number" ? count : null,
1393
+ inFlight: writesInFlight.get(conversationId) ?? 0
1394
+ };
1395
+ };
1204
1396
  const convId = (v) => {
1205
1397
  const id = typeof v === "string" ? v : v.conversation_id ?? v.conversationId;
1206
1398
  if (!id) throw new InvalidInputError("conversation_id is required");
@@ -1213,13 +1405,21 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1213
1405
  options["customer_id"] ?? options["customerId"],
1214
1406
  "conversation.record_message"
1215
1407
  );
1216
- const body = buildRecordMessageBody(options);
1217
- const result = await transport.request("conversations_messages", { body });
1408
+ const body = withTurnModel(buildRecordMessageBody(options), options);
1409
+ const conversationId = String(body["conversation_id"] ?? "");
1410
+ writeStarted(conversationId);
1411
+ let result;
1412
+ try {
1413
+ result = await transport.request("conversations_messages", { body });
1414
+ } finally {
1415
+ writeLanded(conversationId);
1416
+ }
1218
1417
  onTurn({
1219
- conversationId: String(body["conversation_id"] ?? ""),
1418
+ conversationId,
1220
1419
  role: String(body["role"] ?? "user"),
1221
1420
  content: String(body["content"] ?? ""),
1222
- timestamp: body["timestamp"]
1421
+ timestamp: body["timestamp"],
1422
+ ...confirmation(result, conversationId)
1223
1423
  });
1224
1424
  onWrite({
1225
1425
  userId: body["user_id"],
@@ -1238,16 +1438,18 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1238
1438
  "conversation.record_messages_batch"
1239
1439
  );
1240
1440
  }
1241
- const bodies = messages.map(buildRecordMessageBody);
1441
+ const bodies = messages.map((m) => withTurnModel(buildRecordMessageBody(m), m));
1242
1442
  const result = await transport.request("conversations_messages_batch", {
1243
1443
  body: { messages: bodies }
1244
1444
  });
1245
- for (const body of bodies) {
1445
+ for (const [body, row] of storedBatchRows(bodies, result)) {
1446
+ const conversationId = String(body["conversation_id"] ?? "");
1246
1447
  onTurn({
1247
- conversationId: String(body["conversation_id"] ?? ""),
1448
+ conversationId,
1248
1449
  role: String(body["role"] ?? "user"),
1249
1450
  content: String(body["content"] ?? ""),
1250
- timestamp: body["timestamp"]
1451
+ timestamp: body["timestamp"],
1452
+ ...confirmation(row, conversationId)
1251
1453
  });
1252
1454
  }
1253
1455
  for (const body of bodies) {
@@ -1282,13 +1484,24 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1282
1484
  },
1283
1485
  get_context_for_prompt: async (options = {}) => {
1284
1486
  const id = convId(options);
1487
+ const search = (options.search_history ?? options.searchHistory ?? true) && localShortTerm.historyExcerpts;
1488
+ const withHistory = async (response2) => {
1489
+ if (!search) return response2;
1490
+ try {
1491
+ return await localShortTerm.historyExcerpts(id, response2, options.query);
1492
+ } catch {
1493
+ return response2;
1494
+ }
1495
+ };
1285
1496
  const local = localShortTerm.prompt(id, options.style ?? "structured");
1286
- if (local !== null) return local;
1497
+ if (local !== null) return withHistory(local);
1287
1498
  const result = await transport.request("context_for_prompt", {
1288
1499
  pathParams: { conversation_id: id },
1289
1500
  query: { style: options.style ?? "structured" }
1290
1501
  });
1291
- return result?.["context_for_prompt"] ?? result ?? {};
1502
+ const response = result?.["context_for_prompt"] ?? result ?? {};
1503
+ localShortTerm.reseed?.(id, response);
1504
+ return withHistory(response);
1292
1505
  },
1293
1506
  async compact(options) {
1294
1507
  return transport.request("conversations_compact", { body: buildCompactBody(options) });
@@ -1471,11 +1684,13 @@ function jsonOrText(value) {
1471
1684
  }
1472
1685
  }
1473
1686
  }
1474
- function createInstanceNamespace(credentials, cache, onTurn) {
1687
+ function createInstanceNamespace(credentials, cache, onTurn, agentModel = () => ({}), onToolResult) {
1475
1688
  let client = null;
1476
1689
  const openSessions = /* @__PURE__ */ new Map();
1477
1690
  let dispatchCompaction = null;
1478
1691
  let applyCompaction = null;
1692
+ let onListen = null;
1693
+ let onConversationState = null;
1479
1694
  const internals = {
1480
1695
  onCompactionUpdate(conversationId, bundle) {
1481
1696
  applyCompaction?.(bundle);
@@ -1484,6 +1699,12 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1484
1699
  setCompactionApplier(apply) {
1485
1700
  applyCompaction = apply;
1486
1701
  },
1702
+ setOnListen(handle) {
1703
+ onListen = handle;
1704
+ },
1705
+ setConversationStateHandler(handle) {
1706
+ onConversationState = handle;
1707
+ },
1487
1708
  sendContextUsed(event) {
1488
1709
  client?.sendContextUsed(event);
1489
1710
  },
@@ -1518,7 +1739,11 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1518
1739
  if (client !== null) {
1519
1740
  throw new ListeningAlreadyActiveError("Listening is already active on this instance");
1520
1741
  }
1521
- const { GrpcStreamClient } = await import("./stream-client-VWKINE47.js");
1742
+ try {
1743
+ onListen?.();
1744
+ } catch {
1745
+ }
1746
+ const { GrpcStreamClient } = await import("./stream-client-3WHXGW4U.js");
1522
1747
  const onContext = options.on_context ?? options.onContext;
1523
1748
  const onDisconnect = options.on_disconnect ?? options.onDisconnect;
1524
1749
  const onReconnect = options.on_reconnect ?? options.onReconnect;
@@ -1532,6 +1757,9 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1532
1757
  onCompactionUpdate: (conversationId, bundle) => {
1533
1758
  internals.onCompactionUpdate(conversationId, bundle);
1534
1759
  },
1760
+ onConversationState: (conversationId, messageCount) => {
1761
+ onConversationState?.(conversationId, messageCount);
1762
+ },
1535
1763
  onStateChange: (state) => {
1536
1764
  if (state !== "connected") openSessions.clear();
1537
1765
  if (state === "reconnecting") {
@@ -1610,7 +1838,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1610
1838
  user_id: options.user_id ?? options.userId ?? "",
1611
1839
  customer_id: options.customer_id ?? options.customerId ?? "",
1612
1840
  session_id: options.session_id ?? options.sessionId ?? openSessions.get(conversationId) ?? "",
1613
- metadata: options.metadata ?? {},
1841
+ metadata: withAgentModel(options.metadata, agentModelOf(options), agentModel(), true) ?? {},
1614
1842
  timestamp_ms: Date.now(),
1615
1843
  tool_name: options.tool_name ?? options.toolName ?? "",
1616
1844
  // Python serialises tool_args with json.dumps into tool_args_json.
@@ -1640,6 +1868,10 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1640
1868
  });
1641
1869
  },
1642
1870
  async record_tool_result(options) {
1871
+ const conversationId = options.conversation_id ?? options.conversationId ?? "";
1872
+ if (conversationId) {
1873
+ onToolResult?.({ conversationId, result: options.result, toolName: options.tool_name ?? options.toolName ?? "" });
1874
+ }
1643
1875
  await namespace.send_message({
1644
1876
  content: "",
1645
1877
  role: "tool",
@@ -1690,7 +1922,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1690
1922
  }
1691
1923
 
1692
1924
  // src/version.ts
1693
- var SDK_VERSION = "0.5.8";
1925
+ var SDK_VERSION = "0.5.9";
1694
1926
 
1695
1927
  // src/registry.ts
1696
1928
  var instances = /* @__PURE__ */ new Map();
@@ -1731,6 +1963,34 @@ function unregisterIfOwner(key, client) {
1731
1963
  // src/cache/short-term-store.ts
1732
1964
  var DEFAULT_MAX_CONVERSATIONS = 100;
1733
1965
  var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
1966
+ var CONTEXT_BLOCK_PREFIX = "## Conversation so far (compacted";
1967
+ function contextBlockOf(cc) {
1968
+ const block = cc["context_block"];
1969
+ if (typeof block === "string" && block) return block;
1970
+ const summary = cc["summary"];
1971
+ if (typeof summary === "string" && summary.startsWith(CONTEXT_BLOCK_PREFIX)) return summary;
1972
+ return null;
1973
+ }
1974
+ function localMessageCount(entry) {
1975
+ return entry.compactedMessageCount + entry.recentTurns.length;
1976
+ }
1977
+ function isComplete(entry) {
1978
+ return entry.participant && entry.synced && !entry.shared && entry.serverMessageCount !== null && entry.serverMessageCount <= localMessageCount(entry);
1979
+ }
1980
+ function heldIn(turn, turns) {
1981
+ return turns.some(
1982
+ (held) => turn.message_id && held.message_id ? held.message_id === turn.message_id : held.role === turn.role && held.content === turn.content
1983
+ );
1984
+ }
1985
+ function turnsAfter(localTurns, serverTurns) {
1986
+ const newer = [];
1987
+ for (let i = localTurns.length - 1; i >= 0; i -= 1) {
1988
+ const turn = localTurns[i];
1989
+ if (heldIn(turn, serverTurns)) break;
1990
+ newer.unshift(turn);
1991
+ }
1992
+ return newer;
1993
+ }
1734
1994
  function parseIso(value) {
1735
1995
  if (value === null || value === void 0 || value === "") return null;
1736
1996
  if (value instanceof Date) return value.getTime();
@@ -1750,11 +2010,23 @@ var ShortTermStore = class {
1750
2010
  #maxConversations;
1751
2011
  #maxAgeMs;
1752
2012
  #now;
2013
+ /**
2014
+ * Every turn appended here is also kept in the archive (a TurnStore), which
2015
+ * a compaction does not prune: the turns it covers stay searchable locally
2016
+ * (history-search.ts). Python: `ShortTermContextStore.attach_archive`.
2017
+ */
2018
+ #archive = null;
1753
2019
  constructor(options = {}) {
1754
2020
  this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
1755
2021
  this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
1756
2022
  this.#now = options.now ?? (() => Date.now());
1757
2023
  }
2024
+ attachArchive(archive) {
2025
+ this.#archive = archive;
2026
+ }
2027
+ get archive() {
2028
+ return this.#archive;
2029
+ }
1758
2030
  /** The entry, or null. Age-evicts on read, as Python does. */
1759
2031
  get(conversationId) {
1760
2032
  if (!conversationId) return null;
@@ -1780,23 +2052,88 @@ var ShortTermStore = class {
1780
2052
  * exists yet (the first turn, before any compaction has arrived) a fresh one
1781
2053
  * is created with an empty summary; the next `compaction_update` fills the
1782
2054
  * summary fields in and prunes the turns it covers.
2055
+ *
2056
+ * `confirmation` is what the write learned from the server. See
2057
+ * `TurnConfirmation` and `noteServerCount`.
1783
2058
  */
1784
- appendTurn(conversationId, role, content, timestamp) {
2059
+ appendTurn(conversationId, role, content, timestamp, confirmation = {}) {
1785
2060
  if (!conversationId) return;
1786
2061
  const stamp = timestamp ?? new Date(this.#now()).toISOString();
2062
+ if (this.#archive !== null) {
2063
+ try {
2064
+ this.#archive.add(conversationId, role, content, stamp, { messageId: confirmation.messageId ?? null });
2065
+ } catch {
2066
+ }
2067
+ }
1787
2068
  const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1788
- entry.recentTurns.push({ role, content, timestamp: stamp });
2069
+ const messageId = confirmation.messageId;
2070
+ const alreadyHeld = Boolean(messageId) && entry.recentTurns.some((t) => t.message_id === messageId);
2071
+ if (!alreadyHeld) {
2072
+ const turn = { role, content, timestamp: stamp };
2073
+ if (messageId) turn.message_id = messageId;
2074
+ entry.recentTurns.push(turn);
2075
+ }
2076
+ const count = confirmation.serverMessageCount;
2077
+ if (count !== null && count !== void 0) {
2078
+ this.#noteCount(entry, Number(count), confirmation.inFlight ?? 0);
2079
+ }
1789
2080
  entry.lastActivityAt = parseIso(stamp) ?? this.#now();
1790
2081
  this.#touch(conversationId, entry);
1791
2082
  this.#evict();
1792
2083
  }
2084
+ /**
2085
+ * Record the server's message count for a conversation.
2086
+ *
2087
+ * The streamed write path's half of `appendTurn`'s confirmation: a streamed
2088
+ * turn is appended when it is sent, and the count arrives later, once the
2089
+ * server has written it. A count for a conversation this process holds
2090
+ * nothing for is ignored.
2091
+ */
2092
+ noteServerCount(conversationId, count) {
2093
+ if (!conversationId) return;
2094
+ const entry = this.#entries.get(conversationId);
2095
+ if (entry !== void 0) this.#noteCount(entry, Number(count), 0);
2096
+ }
2097
+ /**
2098
+ * Compare the server's count with what the entry holds.
2099
+ *
2100
+ * The count is taken after one of this process's own writes, so it proves
2101
+ * the process takes part in the conversation.
2102
+ *
2103
+ * Server ahead of an entry that was in sync: something else wrote, and the
2104
+ * entry is marked shared for good. Server ahead of an entry that was never
2105
+ * in sync: this process joined late (a restart, an eviction), and `reseed`
2106
+ * will fix it. `inFlight` covers this process's own writes that the server
2107
+ * has counted and the entry has not appended yet, so two concurrent writes
2108
+ * do not look like a second writer.
2109
+ */
2110
+ #noteCount(entry, count, inFlight) {
2111
+ entry.participant = true;
2112
+ if (count > localMessageCount(entry) + inFlight) {
2113
+ if (entry.synced) entry.shared = true;
2114
+ } else if (entry.serverMessageCount === null) {
2115
+ entry.synced = true;
2116
+ }
2117
+ entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, count);
2118
+ }
1793
2119
  /**
1794
2120
  * Apply a `compaction_update` bundle.
1795
2121
  *
1796
2122
  * Replaces the summary fields from the bundle's `conversation_context` and
1797
- * prunes locally buffered turns at or before its `end_timestamp`. Turns that
1798
- * arrived DURING the compaction window are kept by design: the server has not
1799
- * seen them yet, which is the whole reason the local tail exists.
2123
+ * drops the buffered turns the new summary covers.
2124
+ *
2125
+ * Which turns those are is decided by count, not by clock. The bundle says
2126
+ * how many messages the summary covers and how many the conversation held
2127
+ * when the bundle was built, so the last `total - compacted` of its
2128
+ * `recent_turns` are exactly the uncovered messages. Turns this process
2129
+ * appended after the server took that count are kept behind them. Timestamps
2130
+ * cannot do this job: the REST write returns the time of the response and
2131
+ * not of the stored row, a caller may supply its own message time, and a
2132
+ * streamed turn is stamped when a background write runs.
2133
+ *
2134
+ * A bundle from a server that sends no counts falls back to the older
2135
+ * timestamp prune, and the entry is left out of sync so it is never used to
2136
+ * answer on its own.
1800
2137
  */
1801
2138
  applyCompaction(bundle) {
1802
2139
  if (typeof bundle !== "object" || bundle === null) return;
@@ -1806,22 +2143,84 @@ var ShortTermStore = class {
1806
2143
  );
1807
2144
  if (!conversationId) return;
1808
2145
  const endTs = parseIso(cc["end_timestamp"] ?? cc["compacted_at"]);
2146
+ const compactedAt = parseIso(cc["compacted_at"]);
2147
+ const compactedCount = Number(cc["compacted_message_count"] ?? 0) || 0;
2148
+ const totalCount = Number(cc["total_message_count"] ?? 0) || 0;
1809
2149
  const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1810
- entry.summary = cc["summary"] || entry.summary;
1811
- entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
1812
- entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
2150
+ const heldAt = parseIso(entry.compactedAt);
2151
+ if (compactedAt !== null && heldAt !== null && compactedAt < heldAt) return;
2152
+ const block = contextBlockOf(cc);
2153
+ if (block) {
2154
+ entry.contextBlock = block;
2155
+ entry.summary = cc["summary"] || block;
2156
+ entry.factualParagraph = null;
2157
+ entry.conversationalParagraph = null;
2158
+ } else {
2159
+ entry.summary = cc["summary"] || entry.summary;
2160
+ entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
2161
+ entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
2162
+ }
1813
2163
  entry.currentState = { ...cc["current_state"] ?? {} };
1814
2164
  entry.keyExtractions = { ...cc["key_extractions"] ?? {} };
1815
2165
  entry.compactionId = cc["compaction_id"] || entry.compactionId;
1816
2166
  entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
1817
2167
  entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
1818
- if (endTs !== null && entry.recentTurns.length > 0) {
1819
- entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
2168
+ if (compactedCount > 0 && compactedCount <= totalCount) {
2169
+ const uncovered = totalCount - compactedCount;
2170
+ const serverTurns = Array.isArray(cc["recent_turns"]) ? cc["recent_turns"] : [];
2171
+ const tail = uncovered > 0 ? serverTurns.slice(-uncovered).map((t) => ({ ...t })) : [];
2172
+ const ahead = localMessageCount(entry) - totalCount;
2173
+ const newer = entry.synced && ahead > 0 ? entry.recentTurns.slice(-ahead).filter((t) => !heldIn(t, tail)) : [];
2174
+ entry.recentTurns = [...tail, ...newer];
2175
+ entry.compactedMessageCount = compactedCount;
2176
+ entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, totalCount);
2177
+ entry.synced = tail.length === uncovered;
2178
+ } else {
2179
+ if (endTs !== null && entry.recentTurns.length > 0) {
2180
+ entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
2181
+ }
2182
+ entry.synced = false;
1820
2183
  }
1821
2184
  entry.lastActivityAt = this.#now();
1822
2185
  this.#touch(conversationId, entry);
1823
2186
  this.#evict();
1824
2187
  }
2188
+ /**
2189
+ * Bring an entry back in step from a `/context-for-prompt` response.
2190
+ *
2191
+ * Called after the server was asked because the entry was not complete. The
2192
+ * response lists every message the server's summary does not cover, which is
2193
+ * what `recentTurns` is meant to hold.
2194
+ *
2195
+ * Only an entry that already exists is reseeded. Creating one here would let
2196
+ * a process that never writes to the conversation answer from a snapshot it
2197
+ * has no way to refresh.
2198
+ *
2199
+ * If the server's summary is not the one cached here, the entry cannot
2200
+ * render it, so it stays out of sync until the next bundle brings the
2201
+ * summary itself.
2202
+ *
2203
+ * Turns held locally and absent from the response are kept when they come
2204
+ * after everything the response has: they were written or sent after the
2205
+ * server read its messages.
2206
+ */
2207
+ reseed(conversationId, state) {
2208
+ if (!conversationId) return;
2209
+ const entry = this.#entries.get(conversationId);
2210
+ if (entry === void 0 || entry.shared) return;
2211
+ entry.serverMessageCount = Math.max(
2212
+ entry.serverMessageCount ?? 0,
2213
+ Number(state.totalMessageCount) || 0
2214
+ );
2215
+ if ((state.compactionId || null) !== entry.compactionId) {
2216
+ entry.synced = false;
2217
+ return;
2218
+ }
2219
+ const serverTurns = state.recentMessages.map((m) => ({ ...m }));
2220
+ entry.recentTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
2221
+ entry.compactedMessageCount = Number(state.compactedMessageCount) || 0;
2222
+ entry.synced = true;
2223
+ }
1825
2224
  invalidate(conversationId) {
1826
2225
  this.#entries.delete(conversationId);
1827
2226
  }
@@ -1832,6 +2231,7 @@ var ShortTermStore = class {
1832
2231
  return {
1833
2232
  conversationId,
1834
2233
  summary: null,
2234
+ contextBlock: null,
1835
2235
  factualParagraph: null,
1836
2236
  conversationalParagraph: null,
1837
2237
  currentState: {},
@@ -1840,7 +2240,12 @@ var ShortTermStore = class {
1840
2240
  compactedAt: null,
1841
2241
  endTimestamp: null,
1842
2242
  recentTurns: [],
1843
- lastActivityAt: this.#now()
2243
+ lastActivityAt: this.#now(),
2244
+ compactedMessageCount: 0,
2245
+ serverMessageCount: null,
2246
+ participant: false,
2247
+ synced: false,
2248
+ shared: false
1844
2249
  };
1845
2250
  }
1846
2251
  /** Re-insert so this key becomes the most recently used. */
@@ -1857,6 +2262,520 @@ var ShortTermStore = class {
1857
2262
  }
1858
2263
  };
1859
2264
 
2265
+ // src/cache/turn-store.ts
2266
+ var SWEEP_EVERY_WRITES = 50;
2267
+ function bytesOf(text) {
2268
+ return new TextEncoder().encode(text).length;
2269
+ }
2270
+ var TurnStore = class {
2271
+ limits;
2272
+ onDisk = false;
2273
+ #now;
2274
+ #seq = 0;
2275
+ #writes = 0;
2276
+ #conversations = /* @__PURE__ */ new Map();
2277
+ #vectors = /* @__PURE__ */ new Map();
2278
+ constructor(options = {}) {
2279
+ this.limits = options.limits ?? { ...HISTORY_SEARCH.store_limits };
2280
+ this.#now = options.now ?? (() => Date.now() / 1e3);
2281
+ }
2282
+ add(conversationId, role, content, timestamp, options = {}) {
2283
+ this.addMany(conversationId, [
2284
+ { role, content, timestamp: timestamp ?? "", message_id: options.messageId ?? null, position: options.position ?? null }
2285
+ ]);
2286
+ }
2287
+ /**
2288
+ * Insert turns not held yet; returns how many were new. A turn held without
2289
+ * an id or position (a streamed write) gets them when the same role and
2290
+ * text come back from the server.
2291
+ */
2292
+ addMany(conversationId, turns) {
2293
+ if (!conversationId) return 0;
2294
+ const conv = this.#conversations.get(conversationId) ?? { lastActivity: 0, turns: [] };
2295
+ let added = 0;
2296
+ for (const turn of turns) {
2297
+ const role = String(turn.role || "user");
2298
+ const content = String(turn.content ?? "");
2299
+ if (!content) continue;
2300
+ const messageId = turn.message_id ? String(turn.message_id) : null;
2301
+ const raw = turn.position;
2302
+ const position = typeof raw === "number" && Number.isInteger(raw) ? raw : /^\d+$/.test(String(raw ?? "")) ? Number(raw) : null;
2303
+ const ts = turn.timestamp instanceof Date ? turn.timestamp.toISOString() : String(turn.timestamp ?? "");
2304
+ if (messageId !== null) {
2305
+ const held = conv.turns.find((t) => t.message_id === messageId);
2306
+ if (held !== void 0) {
2307
+ if (position !== null) held.position = position;
2308
+ continue;
2309
+ }
2310
+ }
2311
+ const same = conv.turns.find(
2312
+ (t) => t.role === role && t.content === content && (t.message_id === null || t.position === null)
2313
+ );
2314
+ if (same !== void 0) {
2315
+ same.message_id = same.message_id ?? messageId;
2316
+ same.position = position ?? same.position;
2317
+ continue;
2318
+ }
2319
+ conv.turns.push({ seq: ++this.#seq, role, content, timestamp: ts, message_id: messageId, position });
2320
+ added += 1;
2321
+ }
2322
+ conv.lastActivity = this.#now();
2323
+ this.#conversations.set(conversationId, conv);
2324
+ this.#trim(conv);
2325
+ this.#writes += 1;
2326
+ if (this.#writes % SWEEP_EVERY_WRITES === 0) this.sweep();
2327
+ return added;
2328
+ }
2329
+ #trim(conv) {
2330
+ const limits = this.limits;
2331
+ let size = conv.turns.reduce((n2, t) => n2 + bytesOf(t.content), 0);
2332
+ while (conv.turns.length > limits.max_turns_per_conversation || size > limits.max_bytes_per_conversation) {
2333
+ const excess = Math.max(conv.turns.length - limits.max_turns_per_conversation, 1);
2334
+ const order = [...conv.turns].sort(
2335
+ (a, b) => (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || a.seq - b.seq
2336
+ );
2337
+ const gone = new Set(order.slice(0, excess).map((t) => t.seq));
2338
+ if (gone.size === 0) break;
2339
+ for (const t of conv.turns) if (gone.has(t.seq)) size -= bytesOf(t.content);
2340
+ conv.turns = conv.turns.filter((t) => !gone.has(t.seq));
2341
+ for (const vectors of this.#vectors.values()) for (const seq of gone) vectors.delete(seq);
2342
+ }
2343
+ }
2344
+ /** Idle conversations, then the least recently used beyond the count, then more until the bytes fit. */
2345
+ sweep() {
2346
+ const limits = this.limits;
2347
+ const now = this.#now();
2348
+ for (const [id, conv] of this.#conversations) {
2349
+ if (conv.lastActivity < now - limits.idle_ttl_seconds) this.drop(id);
2350
+ }
2351
+ const byRecent = [...this.#conversations.entries()].sort((a, b) => b[1].lastActivity - a[1].lastActivity);
2352
+ for (const [id] of byRecent.slice(limits.max_conversations)) this.drop(id);
2353
+ while (this.sizeBytes() > limits.max_file_bytes) {
2354
+ const oldest = this.conversations()[0];
2355
+ if (oldest === void 0) break;
2356
+ this.drop(oldest);
2357
+ }
2358
+ }
2359
+ drop(conversationId) {
2360
+ const conv = this.#conversations.get(conversationId);
2361
+ if (conv === void 0) return;
2362
+ for (const vectors of this.#vectors.values()) for (const t of conv.turns) vectors.delete(t.seq);
2363
+ this.#conversations.delete(conversationId);
2364
+ }
2365
+ clear() {
2366
+ this.#conversations.clear();
2367
+ this.#vectors.clear();
2368
+ }
2369
+ close() {
2370
+ }
2371
+ /** In conversation order: by server position where known, else as written here. */
2372
+ turns(conversationId) {
2373
+ const conv = this.#conversations.get(conversationId);
2374
+ if (conv === void 0) return [];
2375
+ const bySeq = [...conv.turns].sort((a, b) => a.seq - b.seq);
2376
+ const key = /* @__PURE__ */ new Map();
2377
+ let top = 0;
2378
+ let step = 0;
2379
+ for (const t of bySeq) {
2380
+ if (t.position !== null) {
2381
+ key.set(t.seq, t.position);
2382
+ top = Math.max(top, t.position);
2383
+ step = 0;
2384
+ } else {
2385
+ step += 1;
2386
+ key.set(t.seq, top + step * 1e-6);
2387
+ }
2388
+ }
2389
+ return bySeq.sort((a, b) => (key.get(a.seq) ?? 0) - (key.get(b.seq) ?? 0) || a.seq - b.seq).map((t) => ({ ...t }));
2390
+ }
2391
+ /** Turns the server also holds: archived tool results are local only. Python: `count`. */
2392
+ count(conversationId) {
2393
+ return (this.#conversations.get(conversationId)?.turns ?? []).filter((t) => t.role !== "tool").length;
2394
+ }
2395
+ sizeBytes() {
2396
+ let n2 = 0;
2397
+ for (const conv of this.#conversations.values()) for (const t of conv.turns) n2 += bytesOf(t.content);
2398
+ return n2;
2399
+ }
2400
+ /** Conversation ids, least recently active first. */
2401
+ conversations() {
2402
+ return [...this.#conversations.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity).map(([id]) => id);
2403
+ }
2404
+ vectors(seqs, model) {
2405
+ const held = this.#vectors.get(model);
2406
+ const out = /* @__PURE__ */ new Map();
2407
+ if (held === void 0) return out;
2408
+ for (const seq of seqs) {
2409
+ const v = held.get(seq);
2410
+ if (v !== void 0) out.set(seq, v);
2411
+ }
2412
+ return out;
2413
+ }
2414
+ putVectors(model, items) {
2415
+ const held = this.#vectors.get(model) ?? /* @__PURE__ */ new Map();
2416
+ for (const [seq, vec] of items) held.set(seq, vec);
2417
+ this.#vectors.set(model, held);
2418
+ }
2419
+ };
2420
+
2421
+ // src/cache/history-search.ts
2422
+ var WORD_RE = /[\p{L}\p{N}\p{M}]+/gu;
2423
+ var FORMS = HISTORY_SEARCH.irregular_forms ?? {};
2424
+ function tokenize2(text) {
2425
+ const out = [];
2426
+ for (const word of (text || "").toLowerCase().match(WORD_RE) ?? []) {
2427
+ if (/^[\x00-\x7f]+$/.test(word)) out.push(...tokenize(FORMS[word] ?? word));
2428
+ else out.push(word);
2429
+ }
2430
+ return out;
2431
+ }
2432
+ var LITERAL_SOURCE = String.raw`"([^"]{3,})"|\b([A-Z0-9][A-Z0-9-]{3,})\b|(\d[\d,.:/-]*\d|\d)`;
2433
+ var SENTENCE_RE = /(?<=[.!?])\s+/;
2434
+ function literalsOf(query) {
2435
+ const out = [];
2436
+ for (const m of (query || "").matchAll(new RegExp(LITERAL_SOURCE, "g"))) {
2437
+ const quoted = m[1];
2438
+ const value = quoted || m[2] || m[3];
2439
+ if (value && (quoted || /\d/.test(value))) out.push(value.toLowerCase());
2440
+ }
2441
+ return out;
2442
+ }
2443
+ function cosine(a, b) {
2444
+ let dot = 0;
2445
+ let na = 0;
2446
+ let nb = 0;
2447
+ const n2 = Math.min(a.length, b.length);
2448
+ for (let i = 0; i < n2; i++) {
2449
+ dot += a[i] * b[i];
2450
+ na += a[i] * a[i];
2451
+ nb += b[i] * b[i];
2452
+ }
2453
+ return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
2454
+ }
2455
+ function words(text) {
2456
+ return text.split(/\s+/).filter(Boolean);
2457
+ }
2458
+ function excerptOf(content, terms, literals, maxWords) {
2459
+ let sentences = content.trim().split(SENTENCE_RE).filter((s2) => s2.trim());
2460
+ if (sentences.length === 0) sentences = [content.trim()];
2461
+ const score = (sentence) => {
2462
+ const low = sentence.toLowerCase();
2463
+ const held = new Set(tokenize2(sentence));
2464
+ let n2 = 0;
2465
+ for (const t of terms) if (held.has(t)) n2 += 1;
2466
+ for (const lit of literals) if (low.includes(lit)) n2 += 2;
2467
+ return n2;
2468
+ };
2469
+ let best = 0;
2470
+ for (let i = 1; i < sentences.length; i++) if (score(sentences[i]) > score(sentences[best])) best = i;
2471
+ let lo = best;
2472
+ let hi = best;
2473
+ let count = words(sentences[best]).length;
2474
+ for (; ; ) {
2475
+ let grew = false;
2476
+ for (const j of [hi + 1, lo - 1]) {
2477
+ if (j >= 0 && j < sentences.length && count + words(sentences[j]).length <= maxWords) {
2478
+ count += words(sentences[j]).length;
2479
+ lo = Math.min(lo, j);
2480
+ hi = Math.max(hi, j);
2481
+ grew = true;
2482
+ }
2483
+ }
2484
+ if (!grew) break;
2485
+ }
2486
+ let text = sentences.slice(lo, hi + 1).join(" ");
2487
+ if (words(text).length > maxWords) text = words(text).slice(0, maxWords).join(" ") + " ...";
2488
+ return (lo > 0 ? "... " : "") + text + (hi < sentences.length - 1 && !text.endsWith("...") ? " ..." : "");
2489
+ }
2490
+ var FENCE = /^\s*(?:```|~~~)/;
2491
+ var TABLE_RULE = /^\s*\|?\s*:?-{2,}/;
2492
+ var SENTENCE = /(?<=[.!?])\s+/;
2493
+ function splitPassages(content, maxWords) {
2494
+ const text = (content ?? "").trim();
2495
+ if (words(text).length <= maxWords) return text ? [text] : [];
2496
+ const out = [];
2497
+ const lines = text.split("\n");
2498
+ let i = 0;
2499
+ while (i < lines.length) {
2500
+ const line = lines[i];
2501
+ if (FENCE.test(line)) {
2502
+ let j2 = i + 1;
2503
+ while (j2 < lines.length && !FENCE.test(lines[j2])) j2 += 1;
2504
+ out.push(...windowsOf(lines.slice(i + 1, j2), maxWords));
2505
+ i = j2 + 1;
2506
+ continue;
2507
+ }
2508
+ if (line.trim().startsWith("|")) {
2509
+ let j2 = i;
2510
+ while (j2 < lines.length && lines[j2].trim().startsWith("|")) j2 += 1;
2511
+ const rows = lines.slice(i, j2).filter((r) => !TABLE_RULE.test(r)).map((r) => r.trim());
2512
+ const [header, body] = rows.length > 1 ? [rows[0], rows.slice(1)] : ["", rows];
2513
+ for (const row of body) {
2514
+ const both = `${header} ${row}`.trim();
2515
+ out.push(words(both).length <= Math.trunc(maxWords * 3 / 2) ? both : row);
2516
+ }
2517
+ i = j2;
2518
+ continue;
2519
+ }
2520
+ let j = i;
2521
+ while (j < lines.length && lines[j].trim() && !FENCE.test(lines[j]) && !lines[j].trim().startsWith("|")) j += 1;
2522
+ if (j === i) {
2523
+ i += 1;
2524
+ continue;
2525
+ }
2526
+ const block = lines.slice(i, j);
2527
+ if (block.length > 1 && block.every((b) => words(b).length <= 25)) {
2528
+ out.push(...windowsOf(block, maxWords));
2529
+ } else {
2530
+ const sentences = block.map((b) => b.trim()).join(" ").split(SENTENCE).filter((x) => x.trim());
2531
+ out.push(...windowsOf(sentences, maxWords));
2532
+ }
2533
+ i = j;
2534
+ }
2535
+ const kept = out.filter((x) => x.trim());
2536
+ return kept.length ? kept : [text];
2537
+ }
2538
+ function windowsOf(pieces, maxWords) {
2539
+ const out = [];
2540
+ let cur = [];
2541
+ let used = 0;
2542
+ for (const raw of pieces) {
2543
+ const piece = raw.trim();
2544
+ if (!piece) continue;
2545
+ const n2 = words(piece).length;
2546
+ if (cur.length && used + n2 > maxWords) {
2547
+ out.push(cur.join(" "));
2548
+ cur = [];
2549
+ used = 0;
2550
+ }
2551
+ if (n2 > maxWords) {
2552
+ const w = words(piece);
2553
+ for (let k = 0; k < w.length; k += maxWords) out.push(w.slice(k, k + maxWords).join(" "));
2554
+ continue;
2555
+ }
2556
+ cur.push(piece);
2557
+ used += n2;
2558
+ }
2559
+ if (cur.length) out.push(cur.join(" "));
2560
+ return out;
2561
+ }
2562
+ function askedKinds(query) {
2563
+ const out = [];
2564
+ for (const [name, spec] of Object.entries(HISTORY_SEARCH.asked_kinds ?? {})) {
2565
+ if (name.startsWith("$") || typeof spec !== "object") continue;
2566
+ if (new RegExp(spec.query, "i").test(query ?? "")) out.push(new RegExp(spec.value));
2567
+ }
2568
+ return out;
2569
+ }
2570
+ function asksHowItStarted(query) {
2571
+ const spec = HISTORY_SEARCH.early_query;
2572
+ if (!spec?.pattern) return false;
2573
+ return new RegExp(spec.pattern, (spec.flags ?? "").includes("i") ? "i" : "").test(query ?? "");
2574
+ }
2575
+ async function searchTurns(turns, query, options = {}) {
2576
+ const params = HISTORY_SEARCH;
2577
+ const k = params.max_excerpts;
2578
+ const skip = new Set(options.exclude ?? []);
2579
+ const pool = turns.filter((t) => t.content && !skip.has(t.content) && !skip.has(t.message_id ?? ""));
2580
+ const terms = new Set(tokenize2(query || ""));
2581
+ const literals = literalsOf(query);
2582
+ if (pool.length === 0 || terms.size === 0 && literals.length === 0 && !options.encoder) return [];
2583
+ const maxWords = params.max_words_per_excerpt;
2584
+ const units = [];
2585
+ pool.forEach((t, ti) => {
2586
+ if (!params.passages) {
2587
+ units.push({ seq: t.seq, content: t.content, turn: ti, first: true, last: true, part: -1 });
2588
+ return;
2589
+ }
2590
+ const parts = splitPassages(t.content, maxWords);
2591
+ parts.forEach((part, pi) => {
2592
+ units.push({
2593
+ seq: t.seq,
2594
+ content: part,
2595
+ turn: ti,
2596
+ first: pi === 0,
2597
+ last: pi === parts.length - 1,
2598
+ part: parts.length > 1 ? pi : -1
2599
+ });
2600
+ });
2601
+ });
2602
+ const docs = units.map((u) => tokenize2(u.content));
2603
+ const bm = new BM25(docs);
2604
+ const termList = [...terms];
2605
+ const rows = units.map((turn, i) => {
2606
+ const held = new Set(docs[i]);
2607
+ let matched = 0;
2608
+ for (const t of terms) if (held.has(t)) matched += 1;
2609
+ const low = turn.content.toLowerCase();
2610
+ let literal = 0;
2611
+ for (const lit of literals) if (low.includes(lit)) literal += 1;
2612
+ return { i, bm25: matched ? bm.score(termList, i) : 0, matched, literal };
2613
+ });
2614
+ const need = Math.min(params.min_matched_terms, Math.max(1, terms.size));
2615
+ const rareCap = Math.max(1, Math.trunc((params.rare_term_max_share ?? 0) * units.length));
2616
+ const rare = /* @__PURE__ */ new Set();
2617
+ for (const t of terms) {
2618
+ let n3 = 0;
2619
+ for (const doc of docs) if (doc.includes(t)) n3 += 1;
2620
+ if (n3 > 0 && n3 <= rareCap) rare.add(t);
2621
+ }
2622
+ let eligible;
2623
+ if (params.passages) {
2624
+ const turnTerms = /* @__PURE__ */ new Map();
2625
+ for (const r of rows) {
2626
+ const held = turnTerms.get(units[r.i].turn) ?? /* @__PURE__ */ new Set();
2627
+ for (const t of docs[r.i]) if (terms.has(t)) held.add(t);
2628
+ turnTerms.set(units[r.i].turn, held);
2629
+ }
2630
+ eligible = rows.filter(
2631
+ (r) => r.literal > 0 || r.matched > 0 && (turnTerms.get(units[r.i].turn).size >= need || docs[r.i].some((t) => rare.has(t)))
2632
+ );
2633
+ } else {
2634
+ eligible = rows.filter(
2635
+ (r) => r.literal > 0 || r.matched >= need || r.matched > 0 && docs[r.i].some((t) => rare.has(t))
2636
+ );
2637
+ }
2638
+ const rrf = params.rrf_k;
2639
+ const fused = /* @__PURE__ */ new Map();
2640
+ const via = /* @__PURE__ */ new Map();
2641
+ for (const r of eligible) fused.set(r.i, 0);
2642
+ const add = (i, rank, how) => {
2643
+ fused.set(i, (fused.get(i) ?? 0) + 1 / (rrf + rank + 1));
2644
+ if (!via.has(i)) via.set(i, how);
2645
+ };
2646
+ eligible.filter((r) => r.literal > 0).sort((a, b) => b.literal - a.literal || a.i - b.i).forEach((r, rank) => add(r.i, rank, "literal"));
2647
+ eligible.filter((r) => r.bm25 > 0).sort((a, b) => b.bm25 - a.bm25 || a.i - b.i).forEach((r, rank) => add(r.i, rank, "bm25"));
2648
+ const kinds = askedKinds(query);
2649
+ if (kinds.length) {
2650
+ rows.filter((r) => r.matched > 0 && kinds.some((k2) => k2.test(units[r.i].content))).sort((a, b) => b.bm25 - a.bm25 || a.i - b.i).forEach((r, rank) => add(r.i, rank, "kind"));
2651
+ }
2652
+ if (options.encoder) {
2653
+ const ranked2 = await embeddingRanks(units, query, options.encoder, options.store ?? null, params.min_cosine ?? 0.4);
2654
+ ranked2.forEach(([i], rank) => add(i, rank, "embedding"));
2655
+ }
2656
+ if (fused.size === 0) return [];
2657
+ const n2 = Math.max(pool.length - 1, 1);
2658
+ const weight = asksHowItStarted(query) ? 0 : params.recency_weight;
2659
+ const top = /* @__PURE__ */ new Map();
2660
+ const ranked = /* @__PURE__ */ new Map();
2661
+ for (const [u, v] of fused) {
2662
+ const ti = units[u].turn;
2663
+ const score = v + weight * (ti / n2) / (rrf + 1);
2664
+ ranked.set(ti, [...ranked.get(ti) ?? [], [score, u]]);
2665
+ const held = top.get(ti);
2666
+ if (!held || score > held[0]) top.set(ti, [score, u]);
2667
+ }
2668
+ const best = [...top.keys()].sort((a, b) => top.get(b)[0] - top.get(a)[0] || a - b).slice(0, k);
2669
+ const perTurn = params.passages_per_turn ?? 1;
2670
+ const shown = (ti) => {
2671
+ const unit = units[top.get(ti)[1]];
2672
+ if (!params.passages || unit.first && unit.last) return excerptOf(pool[ti].content, terms, literals, maxWords);
2673
+ const picked = [...ranked.get(ti)].sort((a, b) => b[0] - a[0] || b[1] - a[1]).slice(0, perTurn).map(([, u]) => u).sort((a, b) => a - b);
2674
+ const parts = [];
2675
+ for (const u of picked) parts.push((units[u].first || parts.length ? "" : "... ") + units[u].content);
2676
+ return parts.join(" ... ") + (units[picked[picked.length - 1]].last ? "" : " ...");
2677
+ };
2678
+ const found = /* @__PURE__ */ new Map();
2679
+ for (const ti of best) {
2680
+ found.set(ti, {
2681
+ position: pool[ti].position,
2682
+ role: pool[ti].role,
2683
+ timestamp: pool[ti].timestamp,
2684
+ score: top.get(ti)[0],
2685
+ via: via.get(top.get(ti)[1]) ?? "",
2686
+ text: shown(ti)
2687
+ });
2688
+ }
2689
+ const before2 = params.previous_turns ?? 0;
2690
+ const tail = params.previous_turn_words ?? 40;
2691
+ for (const i of best) {
2692
+ for (let j = i - before2; j < i; j++) {
2693
+ if (j < 0 || found.has(j)) continue;
2694
+ const hit = pool[i].position;
2695
+ const prev = pool[j].position;
2696
+ if (hit && prev && hit - prev > before2) continue;
2697
+ const w = words(pool[j].content);
2698
+ found.set(j, {
2699
+ position: prev,
2700
+ role: pool[j].role,
2701
+ timestamp: pool[j].timestamp,
2702
+ score: 0,
2703
+ via: "previous",
2704
+ text: w.length <= tail ? w.join(" ") : "... " + w.slice(-tail).join(" ")
2705
+ });
2706
+ }
2707
+ }
2708
+ return [...found.keys()].sort((a, b) => a - b).map((i) => found.get(i));
2709
+ }
2710
+ async function embeddingRanks(pool, query, encoder, store, minCosine) {
2711
+ const keyOf = (u) => (u.part ?? -1) < 0 ? encoder.model : `${encoder.model}#p${u.part}`;
2712
+ const vectors = /* @__PURE__ */ new Map();
2713
+ if (store !== null) {
2714
+ const byKey = /* @__PURE__ */ new Map();
2715
+ pool.forEach((u, i) => byKey.set(keyOf(u), [...byKey.get(keyOf(u)) ?? [], i]));
2716
+ for (const [key, idx] of byKey) {
2717
+ const held = store.vectors(idx.map((i) => pool[i].seq), key);
2718
+ for (const i of idx) {
2719
+ const v = held.get(pool[i].seq);
2720
+ if (v) vectors.set(i, v);
2721
+ }
2722
+ }
2723
+ }
2724
+ const missing = pool.map((_, i) => i).filter((i) => !vectors.has(i));
2725
+ if (missing.length > 0) {
2726
+ const fresh = await encoder.encode(missing.map((i) => pool[i].content));
2727
+ const put = /* @__PURE__ */ new Map();
2728
+ missing.forEach((i, j) => {
2729
+ const v = Float32Array.from(fresh[j] ?? []);
2730
+ vectors.set(i, v);
2731
+ const key = keyOf(pool[i]);
2732
+ put.set(key, (put.get(key) ?? /* @__PURE__ */ new Map()).set(pool[i].seq, v));
2733
+ });
2734
+ if (store) for (const [key, items] of put) store.putVectors(key, items);
2735
+ }
2736
+ const q = (await (encoder.encodeQuery ?? encoder.encode).call(encoder, [query]))[0] ?? [];
2737
+ return pool.map((_, i) => [i, cosine(q, vectors.get(i) ?? [])]).filter(([, c]) => c >= minCosine).sort((a, b) => b[1] - a[1]);
2738
+ }
2739
+ function toolResultText(result, toolName) {
2740
+ let text = typeof result === "string" ? result : JSON.stringify(result) ?? String(result);
2741
+ const limit = HISTORY_SEARCH.tool_result_max_chars ?? 16e3;
2742
+ if (text.length > limit) text = text.slice(0, limit) + " ...[cut]";
2743
+ return `[tool result: ${toolName || "tool"}] ${text}`;
2744
+ }
2745
+ function renderExcerpts(excerpts, latest = "") {
2746
+ if (excerpts.length === 0) return "";
2747
+ const lines = [HISTORY_SEARCH.heading];
2748
+ for (const e of excerpts) {
2749
+ let where = e.position ? `turn ${e.position}, ${e.role}` : `${e.role}, earlier`;
2750
+ if (e.timestamp) {
2751
+ const ago = before(e.timestamp, latest);
2752
+ where += `, ${e.timestamp}` + (ago ? ` (${ago})` : "");
2753
+ }
2754
+ lines.push(`- ${where}: "${e.text}"`);
2755
+ }
2756
+ return lines.join("\n");
2757
+ }
2758
+ var ZONED = /(?:Z|[+-]\d{2}:\d{2})$/;
2759
+ function instant(stamp) {
2760
+ const text = (stamp ?? "").trim();
2761
+ if (!ZONED.test(text)) return null;
2762
+ const ms = Date.parse(text);
2763
+ return Number.isNaN(ms) ? null : ms / 1e3;
2764
+ }
2765
+ function before(stamp, latest) {
2766
+ const at = instant(stamp);
2767
+ const now = instant(latest);
2768
+ if (at === null || now === null || now < at) return "";
2769
+ const seconds = Math.trunc(now - at);
2770
+ for (const [unit, size] of [["day", 86400], ["hour", 3600], ["minute", 60]]) {
2771
+ if (seconds >= size) {
2772
+ const n2 = Math.trunc(seconds / size);
2773
+ return `${n2} ${unit}${n2 !== 1 ? "s" : ""} before the latest message`;
2774
+ }
2775
+ }
2776
+ return "less than a minute before the latest message";
2777
+ }
2778
+
1860
2779
  // src/context/overlay.ts
1861
2780
  var MAX_TURNS = 20;
1862
2781
  var TOKEN_BUDGET = 2e3;
@@ -1885,7 +2804,23 @@ function overlayLocalRecentTurns(response, store, conversationId, configured) {
1885
2804
  if (entry === null) return;
1886
2805
  const existing = response.conversation_context ?? null;
1887
2806
  if (existing !== null && !verbatimOverlayEnabled(configured)) return;
1888
- const localTurns = budgetRecentTurns(entry.recentTurns);
2807
+ const serverCompaction = existing === null ? null : existing["compaction_id"];
2808
+ const localIsWhole = isComplete(entry) && (!serverCompaction || serverCompaction === entry.compactionId);
2809
+ let sourceTurns = entry.recentTurns;
2810
+ if (existing !== null && !localIsWhole) {
2811
+ const raw = Array.isArray(existing["recent_turns"]) ? existing["recent_turns"] : [];
2812
+ const serverTurns = raw.map((t) => ({
2813
+ role: String(t["role"] ?? "user"),
2814
+ content: String(t["content"] ?? ""),
2815
+ timestamp: String(t["timestamp"] ?? "")
2816
+ }));
2817
+ sourceTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
2818
+ }
2819
+ const localTurns = budgetRecentTurns(sourceTurns).map((t) => ({
2820
+ role: t.role,
2821
+ content: t.content,
2822
+ timestamp: t.timestamp
2823
+ }));
1889
2824
  const serverField = (name) => existing === null ? void 0 : existing[name];
1890
2825
  const orLocal = (name, local) => {
1891
2826
  const value = serverField(name);
@@ -1940,14 +2875,28 @@ function buildRecentMessages(turns) {
1940
2875
  role: t.role || "user",
1941
2876
  content: t.content ?? "",
1942
2877
  timestamp: t.timestamp,
1943
- message_id: `local-${idx}`
2878
+ message_id: t.message_id || `local-${idx}`
1944
2879
  }));
1945
2880
  }
1946
2881
  function compactedMessageCount(_entry) {
1947
2882
  return 0;
1948
2883
  }
2884
+ function blockOf(entry) {
2885
+ if (entry.contextBlock) return entry.contextBlock.trim();
2886
+ if (entry.summary && entry.summary.startsWith(CONTEXT_BLOCK_PREFIX)) return entry.summary.trim();
2887
+ return null;
2888
+ }
1949
2889
  function formatStructured(entry, recent) {
1950
2890
  const parts = [];
2891
+ const block = blockOf(entry);
2892
+ if (block) {
2893
+ parts.push(block);
2894
+ if (recent.length > 0) {
2895
+ parts.push(`## Recent Turns (${recent.length})`);
2896
+ for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);
2897
+ }
2898
+ return parts.filter(Boolean).join("\n\n").trim();
2899
+ }
1951
2900
  if (entry.summary) {
1952
2901
  parts.push("## Summary");
1953
2902
  parts.push(entry.summary.trim());
@@ -1982,9 +2931,11 @@ function formatStructured(entry, recent) {
1982
2931
  }
1983
2932
  function formatNarrative(entry, recent) {
1984
2933
  const parts = [];
1985
- if (entry.summary) parts.push(entry.summary.trim());
1986
- if (entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());
1987
- else if (entry.factualParagraph) parts.push(entry.factualParagraph.trim());
2934
+ const block = blockOf(entry);
2935
+ if (block) parts.push(block);
2936
+ else if (entry.summary) parts.push(entry.summary.trim());
2937
+ if (!block && entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());
2938
+ else if (!block && entry.factualParagraph) parts.push(entry.factualParagraph.trim());
1988
2939
  if (recent.length > 0) {
1989
2940
  const tail = recent.map((m) => `${m.role}: ${m.content}`).join("\n");
1990
2941
  parts.push(`Recent exchanges:
@@ -1994,6 +2945,10 @@ ${tail}`);
1994
2945
  }
1995
2946
  function formatBullets(entry, recent) {
1996
2947
  const parts = [];
2948
+ const block = blockOf(entry);
2949
+ if (block) {
2950
+ return [block, ...recent.map((m) => `- ${m.role}: ${m.content}`)].join("\n").trim();
2951
+ }
1997
2952
  if (entry.summary) parts.push(`- Summary: ${entry.summary.trim()}`);
1998
2953
  for (const [k, v] of Object.entries(entry.currentState ?? {})) {
1999
2954
  parts.push(`- ${k}: ${stringify(v)}`);
@@ -2031,7 +2986,9 @@ function renderForPrompt(entry, style = DEFAULT_STYLE, now = Date.now) {
2031
2986
  recent_messages: recent,
2032
2987
  recent_message_count: recent.length,
2033
2988
  compacted_message_count: compacted,
2034
- total_message_count: recent.length + compacted
2989
+ total_message_count: recent.length + compacted,
2990
+ context_block: blockOf(entry),
2991
+ compaction_id: entry.compactionId
2035
2992
  };
2036
2993
  }
2037
2994
  function renderCompacted(entry, format = "structured", correlationId = "") {
@@ -2176,10 +3133,10 @@ function servedItemIds(hit) {
2176
3133
  }
2177
3134
  return ids;
2178
3135
  }
2179
- function tryServeFromCache(cache, scope, params, maxItems) {
3136
+ async function tryServeFromCache(cache, scope, params, maxItems) {
2180
3137
  try {
2181
3138
  const scoped = scopeLookupParams(scope, params);
2182
- const hit = cache.lookup({
3139
+ const hit = await cache.lookupAsync({
2183
3140
  searchQuery: scoped.searchQuery ?? null,
2184
3141
  entityId: scoped.entityId ?? null,
2185
3142
  customerId: scoped.customerId ?? null,
@@ -2267,6 +3224,8 @@ function mergeScopeResults(scopeResults) {
2267
3224
  episodes: [],
2268
3225
  emotions: [],
2269
3226
  temporal_events: [],
3227
+ items: [],
3228
+ excerpts: [],
2270
3229
  scope_map: {},
2271
3230
  conversation_context: null,
2272
3231
  profile: null,
@@ -2276,6 +3235,8 @@ function mergeScopeResults(scopeResults) {
2276
3235
  total_items: 0,
2277
3236
  metadata: null
2278
3237
  };
3238
+ const rankedSeen = /* @__PURE__ */ new Set();
3239
+ const rankedItems = [];
2279
3240
  for (const [scopeName, response] of scopeResults) {
2280
3241
  out.scopes_queried.push(scopeName);
2281
3242
  if (out.metadata === null && response.metadata !== void 0) {
@@ -2300,8 +3261,32 @@ function mergeScopeResults(scopeResults) {
2300
3261
  out[collection].push(item);
2301
3262
  }
2302
3263
  }
3264
+ const excerpts = response["excerpts"];
3265
+ if (Array.isArray(excerpts)) {
3266
+ for (const item of excerpts) {
3267
+ const id = itemId2(item);
3268
+ if (id !== void 0) {
3269
+ if (seen.has(id)) continue;
3270
+ seen.add(id);
3271
+ out.scope_map[id] = scopeName;
3272
+ }
3273
+ out.excerpts.push(item);
3274
+ }
3275
+ }
3276
+ const ranked = response["items"];
3277
+ if (Array.isArray(ranked)) {
3278
+ for (const item of ranked) {
3279
+ const id = itemId2(item);
3280
+ if (id !== void 0) {
3281
+ if (rankedSeen.has(id)) continue;
3282
+ rankedSeen.add(id);
3283
+ }
3284
+ rankedItems.push(item);
3285
+ }
3286
+ }
2303
3287
  }
2304
- out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0);
3288
+ out.items = sortByRankScore(rankedItems).map((item, n2) => ({ ...item, rank: n2 + 1 }));
3289
+ out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0) + out.excerpts.length;
2305
3290
  return out;
2306
3291
  }
2307
3292
  function oneDecimal(value) {
@@ -2327,7 +3312,18 @@ function formatForPrompt(ctx, options = {}) {
2327
3312
  const includeScope = options.includeScope ?? false;
2328
3313
  const includeConversationContext = options.includeConversationContext ?? true;
2329
3314
  const sections = [];
2330
- if (ctx.facts.length > 0) {
3315
+ const ranked = ctx.items ?? [];
3316
+ if (ranked.length > 0) {
3317
+ const lines = ranked.map((it, n2) => {
3318
+ let label = rankedLabel(String(it["category"] ?? ""));
3319
+ if (includeScope) label += `, scope: ${scopeOf(ctx, it)}`;
3320
+ return `${n2 + 1}. [${label}] ${String(it.content ?? "")}`;
3321
+ });
3322
+ sections.push(`### Memories
3323
+ ${lines.join("\n")}`);
3324
+ }
3325
+ const typed = ranked.length === 0;
3326
+ if (typed && ctx.facts.length > 0) {
2331
3327
  const lines = ctx.facts.map((f) => {
2332
3328
  let line = `- ${String(f.content ?? "")}`;
2333
3329
  if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;
@@ -2336,7 +3332,7 @@ function formatForPrompt(ctx, options = {}) {
2336
3332
  sections.push(`### Facts
2337
3333
  ${lines.join("\n")}`);
2338
3334
  }
2339
- if (ctx.preferences.length > 0) {
3335
+ if (typed && ctx.preferences.length > 0) {
2340
3336
  const lines = ctx.preferences.map((p) => {
2341
3337
  const extras = [];
2342
3338
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);
@@ -2349,7 +3345,7 @@ ${lines.join("\n")}`);
2349
3345
  sections.push(`### Preferences
2350
3346
  ${lines.join("\n")}`);
2351
3347
  }
2352
- if (ctx.episodes.length > 0) {
3348
+ if (typed && ctx.episodes.length > 0) {
2353
3349
  const lines = ctx.episodes.map((e) => {
2354
3350
  let line = `- ${String(e["summary"] ?? "")}`;
2355
3351
  if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;
@@ -2358,7 +3354,7 @@ ${lines.join("\n")}`);
2358
3354
  sections.push(`### Episodes
2359
3355
  ${lines.join("\n")}`);
2360
3356
  }
2361
- if (ctx.emotions.length > 0) {
3357
+ if (typed && ctx.emotions.length > 0) {
2362
3358
  const lines = ctx.emotions.map((em) => {
2363
3359
  const extras = [];
2364
3360
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);
@@ -2372,7 +3368,7 @@ ${lines.join("\n")}`);
2372
3368
  sections.push(`### Emotions
2373
3369
  ${lines.join("\n")}`);
2374
3370
  }
2375
- if (ctx.temporal_events.length > 0) {
3371
+ if (typed && ctx.temporal_events.length > 0) {
2376
3372
  const lines = ctx.temporal_events.map((te) => {
2377
3373
  const extras = [];
2378
3374
  if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);
@@ -2626,7 +3622,10 @@ function buildTool(target, options = {}, onWarning = () => {
2626
3622
  }
2627
3623
 
2628
3624
  // src/client.ts
2629
- var SynapClient = class {
3625
+ function localModelOff() {
3626
+ return ["off", "0", "false", "no", "none"].includes((getEnv("SYNAP_SDK_LOCAL_MODEL") ?? "").trim().toLowerCase());
3627
+ }
3628
+ var SynapClient = class _SynapClient {
2630
3629
  // Native-private (`#`), not TypeScript `private`. TS `private` is erased at
2631
3630
  // compile time, so the field stays enumerable at runtime and a JS consumer
2632
3631
  // can still reach it. These two were public and made `client.transport
@@ -2652,7 +3651,16 @@ var SynapClient = class {
2652
3651
  #registryKeys;
2653
3652
  #compactionSubscribers;
2654
3653
  #shortTerm;
3654
+ /** Every raw turn, past compaction, for local history search. */
3655
+ #turnStore;
3656
+ /** A local embedding model for history search; null = keywords only. */
3657
+ #localEncoder = null;
3658
+ /** `history_search_model`: "auto", an "onnx:..." name, or null for none. */
3659
+ #localModelName = "auto";
3660
+ #localModelStarted = false;
3661
+ #backfillAfter = /* @__PURE__ */ new Map();
2655
3662
  #stVerbatimOverlay;
3663
+ #agentModel = {};
2656
3664
  #logger;
2657
3665
  #stAuthoritative;
2658
3666
  #turns;
@@ -2692,11 +3700,11 @@ var SynapClient = class {
2692
3700
  if (options.heartbeat) this.#transport.startHeartbeat();
2693
3701
  this.#anticipationCache = new AnticipationCache();
2694
3702
  this.#fetchHooks = {
2695
- beforeFetch: (scope, options2) => {
3703
+ beforeFetch: async (scope, options2) => {
2696
3704
  const o2 = options2;
2697
3705
  if (o2["query_date"] ?? o2["queryDate"]) return null;
2698
3706
  if (o2["max_results_per_query"] ?? o2["maxResultsPerQuery"]) return null;
2699
- const attempt = tryServeFromCache(
3707
+ const attempt = await tryServeFromCache(
2700
3708
  this.#anticipationCache,
2701
3709
  scope,
2702
3710
  {
@@ -2720,7 +3728,9 @@ var SynapClient = class {
2720
3728
  return {
2721
3729
  response: attempt.response,
2722
3730
  servedItemIds: servedItemIds(attempt.hit),
2723
- bundleId: attempt.hit.bundleIds[0] ?? ""
3731
+ bundleId: attempt.hit.bundleIds[0] ?? "",
3732
+ borderline: attempt.hit.band === "borderline",
3733
+ raceDeadlineMs: SEMANTIC_MATCH.race_deadline_ms
2724
3734
  };
2725
3735
  },
2726
3736
  onAssembled: (scope, options2, response, startedAt) => {
@@ -2735,9 +3745,19 @@ var SynapClient = class {
2735
3745
  this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
2736
3746
  this.#compactionSubscribers = new CompactionSubscribers();
2737
3747
  this.#shortTerm = new ShortTermStore();
3748
+ this.#turnStore = new TurnStore();
3749
+ this.#shortTerm.attachArchive(this.#turnStore);
2738
3750
  this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;
3751
+ this.#agentModel = {
3752
+ model: options.agent_model ?? options.agentModel,
3753
+ contextWindow: options.agent_context_window ?? options.agentContextWindow
3754
+ };
2739
3755
  this.#logger = options.logger;
2740
3756
  this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;
3757
+ this.#localEncoder = options.history_search_encoder ?? options.historySearchEncoder ?? null;
3758
+ const modelOption = options.history_search_model !== void 0 ? options.history_search_model : options.historySearchModel;
3759
+ this.#localModelName = modelOption === void 0 ? "auto" : modelOption;
3760
+ if (this.#localEncoder?.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(this.#localEncoder);
2741
3761
  this.#turns = new TurnCounter();
2742
3762
  this.conversation = createConversationNamespace(
2743
3763
  this.#transport,
@@ -2754,8 +3774,12 @@ var SynapClient = class {
2754
3774
  },
2755
3775
  // Buffer the turn locally so the next fetch sees it, ahead of the server
2756
3776
  // having persisted and compacted it.
2757
- ({ conversationId, role, content, timestamp }) => {
2758
- this.#shortTerm.appendTurn(conversationId, role, content, timestamp);
3777
+ ({ conversationId, role, content, timestamp, messageId, serverMessageCount, inFlight }) => {
3778
+ this.#shortTerm.appendTurn(conversationId, role, content, timestamp, {
3779
+ messageId,
3780
+ serverMessageCount,
3781
+ inFlight
3782
+ });
2759
3783
  },
2760
3784
  (response, conversationId, userId, scopePath) => {
2761
3785
  overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);
@@ -2763,8 +3787,11 @@ var SynapClient = class {
2763
3787
  },
2764
3788
  {
2765
3789
  prompt: (conversationId, style) => this.#localPrompt(conversationId, style),
2766
- compacted: (conversationId, format) => this.#localCompacted(conversationId, format)
2767
- }
3790
+ compacted: (conversationId, format) => this.#localCompacted(conversationId, format),
3791
+ reseed: (conversationId, response) => this.#reseedShortTerm(conversationId, response),
3792
+ historyExcerpts: (conversationId, response, query) => this.#withHistoryExcerpts(conversationId, response, query)
3793
+ },
3794
+ () => this.#agentModel
2768
3795
  );
2769
3796
  this.memories = createMemoriesInterface(this.#transport, ({ userId, customerId }) => {
2770
3797
  for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
@@ -2778,11 +3805,17 @@ var SynapClient = class {
2778
3805
  this.#anticipationCache,
2779
3806
  ({ conversationId, role, content }) => {
2780
3807
  this.#shortTerm.appendTurn(conversationId, role, content);
2781
- }
3808
+ },
3809
+ () => this.#agentModel,
3810
+ ({ conversationId, result, toolName }) => this.archive_tool_result(conversationId, result, { tool_name: toolName })
2782
3811
  );
3812
+ this.instance._internal.setOnListen(() => this.#startLocalModel());
2783
3813
  this.instance._internal.setCompactionApplier((bundle) => {
2784
3814
  this.#shortTerm.applyCompaction(bundle);
2785
3815
  });
3816
+ this.instance._internal.setConversationStateHandler((conversationId, messageCount) => {
3817
+ this.#shortTerm.noteServerCount(conversationId, messageCount);
3818
+ });
2786
3819
  if (options._force_new !== true) {
2787
3820
  const incumbent = registerIfAbsent(registryKey, this);
2788
3821
  if (incumbent !== void 0) return incumbent;
@@ -2942,6 +3975,14 @@ var SynapClient = class {
2942
3975
  this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);
2943
3976
  }
2944
3977
  if (options.logger !== void 0) this.#logger = options.logger;
3978
+ const model = options.agent_model ?? options.agentModel;
3979
+ const window = options.agent_context_window ?? options.agentContextWindow;
3980
+ if (model !== void 0 || window !== void 0) {
3981
+ this.#agentModel = {
3982
+ model: model ?? this.#agentModel.model,
3983
+ contextWindow: window ?? this.#agentModel.contextWindow
3984
+ };
3985
+ }
2945
3986
  }
2946
3987
  /**
2947
3988
  * Fetch and merge context across every scope with an identifier, in one call.
@@ -2975,9 +4016,11 @@ var SynapClient = class {
2975
4016
  include_scope_labels: includeScopeLabels = false,
2976
4017
  context_mode: contextMode = "in-conversation",
2977
4018
  include_profile: includeProfile = true,
2978
- last_n_conversations: lastNConversations = 1
4019
+ last_n_conversations: lastNConversations = 1,
4020
+ response_shape: responseShape = "by_type"
2979
4021
  } = options;
2980
4022
  validateConversationId(conversationId);
4023
+ responseShapeOf({ response_shape: responseShape });
2981
4024
  const wants = (scope) => scopes === void 0 || scopes.includes(scope);
2982
4025
  const shared = {
2983
4026
  search_query: searchQuery,
@@ -2989,7 +4032,8 @@ var SynapClient = class {
2989
4032
  // threaded three call sites by hand and missed the client branch, which
2990
4033
  // sits one indent deeper; spreading removes that whole class of miss.
2991
4034
  query_date: queryDate,
2992
- max_results_per_query: maxResultsPerQuery
4035
+ max_results_per_query: maxResultsPerQuery,
4036
+ response_shape: responseShape
2993
4037
  };
2994
4038
  const labels = [];
2995
4039
  const tasks = [];
@@ -3152,25 +4196,139 @@ var SynapClient = class {
3152
4196
  /**
3153
4197
  * Whether the SDK is authoritative for short-term context.
3154
4198
  *
3155
- * Resolution order matches Python's `_is_st_authoritative`: the explicit
3156
- * option first, then `SYNAP_SDK_ST_AUTHORITATIVE` (truthy strings only).
3157
- * Defaults to false, so nothing changes for a caller who never sets it.
4199
+ * Resolution order matches Python's `_is_st_authoritative`:
4200
+ * `SYNAP_SDK_ST_AUTHORITATIVE` first, as an explicit override in either
4201
+ * direction, then the option. Off by default. Being enabled does not mean
4202
+ * the local store is used: that also takes an entry that holds the whole
4203
+ * conversation, see `isComplete`.
3158
4204
  */
3159
4205
  #isStAuthoritative() {
3160
- if (this.#stAuthoritative === true) return true;
3161
4206
  const raw = (getEnv("SYNAP_SDK_ST_AUTHORITATIVE") ?? "").trim().toLowerCase();
3162
- return ["1", "true", "yes", "on"].includes(raw);
4207
+ if (["0", "false", "no", "off"].includes(raw)) return false;
4208
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
4209
+ return this.#stAuthoritative === true;
3163
4210
  }
3164
4211
  /**
3165
4212
  * Render `get_context_for_prompt` from the local store, or null to fall
3166
- * through to the network. Warm means a compaction landed OR raw turns are
3167
- * buffered: with neither there is nothing to render and the server's answer
3168
- * is the only real one.
4213
+ * through to the network.
4214
+ *
4215
+ * The store answers only when it holds the whole conversation: the server
4216
+ * has confirmed a count the entry accounts for. An entry that is merely
4217
+ * present is not enough. After a restart or an eviction it holds one turn,
4218
+ * and rendering that as the conversation drops everything said before it.
4219
+ */
4220
+ /**
4221
+ * `response` with the earlier turns that best match `query` (by default the
4222
+ * latest user message) appended, when a compaction exists. The search runs
4223
+ * here, over the turn archive. Python: `_with_history_excerpts`.
3169
4224
  */
4225
+ async #withHistoryExcerpts(conversationId, response, query) {
4226
+ const r = response ?? {};
4227
+ const store = this.#turnStore;
4228
+ const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
4229
+ store.addMany(
4230
+ conversationId,
4231
+ recent.map((m) => ({
4232
+ role: String(m["role"] ?? "user"),
4233
+ content: String(m["content"] ?? ""),
4234
+ timestamp: String(m["timestamp"] ?? ""),
4235
+ message_id: typeof m["message_id"] === "string" ? m["message_id"] : null
4236
+ }))
4237
+ );
4238
+ const entry = this.#shortTerm.get(conversationId);
4239
+ const total = Math.max(Number(r["total_message_count"] ?? 0) || 0, Number(entry?.serverMessageCount ?? 0) || 0);
4240
+ this.#maybeBackfillTurns(conversationId, total);
4241
+ if (!r["context_block"]) return response;
4242
+ const turns = store.turns(conversationId);
4243
+ let q = query;
4244
+ if (!q) {
4245
+ const latest = [...recent].reverse().find((m) => m["role"] === "user");
4246
+ q = latest ? String(latest["content"] ?? "") : [...turns].reverse().find((t) => t.role === "user")?.content ?? "";
4247
+ }
4248
+ if (!q) return response;
4249
+ const exclude = /* @__PURE__ */ new Set();
4250
+ for (const m of recent) {
4251
+ if (typeof m["message_id"] === "string" && m["message_id"]) exclude.add(m["message_id"]);
4252
+ exclude.add(String(m["content"] ?? ""));
4253
+ }
4254
+ this.#startLocalModel();
4255
+ const hits = await searchTurns(turns, q, { exclude, encoder: this.#localEncoder, store });
4256
+ if (hits.length === 0) return response;
4257
+ const block = renderExcerpts(hits, turns.length ? turns[turns.length - 1].timestamp : "");
4258
+ return {
4259
+ ...r,
4260
+ history_excerpts: hits.map((h) => ({ position: h.position, role: h.role, text: h.text })),
4261
+ history_block: block,
4262
+ formatted_context: (String(r["formatted_context"] ?? "").trimEnd() + "\n\n" + block).trim()
4263
+ };
4264
+ }
4265
+ /**
4266
+ * Keep what a tool returned in this process's turn archive, so history search
4267
+ * can find it after a compaction. Never sent anywhere: the server's
4268
+ * conversation holds what people said, and a compaction is made from that.
4269
+ * `instance.record_tool_result` calls this too. Python: `archive_tool_result`.
4270
+ */
4271
+ archive_tool_result(conversationId, result, options = {}) {
4272
+ if (!conversationId || result === null || result === void 0) return;
4273
+ try {
4274
+ this.#turnStore.add(
4275
+ conversationId,
4276
+ "tool",
4277
+ toolResultText(result, options.tool_name ?? options.toolName),
4278
+ options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
4279
+ );
4280
+ } catch {
4281
+ }
4282
+ }
4283
+ static BACKFILL_COOLDOWN_MS = 6e4;
4284
+ static BACKFILL_PAGE = 500;
4285
+ /** Fetch, in the background, turns the server holds and the archive does not. */
4286
+ /**
4287
+ * Load the local embedding model in the background, once. Searches and
4288
+ * lookups before it is ready match by words. Python: `_start_history_encoder`.
4289
+ */
4290
+ #startLocalModel() {
4291
+ if (this.#localModelStarted) return;
4292
+ this.#localModelStarted = true;
4293
+ if (localModelOff()) return;
4294
+ const name = this.#localModelName === "auto" ? SEMANTIC_MATCH.model : this.#localModelName;
4295
+ if (!name) return;
4296
+ const callerEncoder = this.#localEncoder;
4297
+ void import("./onnx-encoder-NP35KH4H.js").then(({ canRunLocalModel, loadOnnxEncoder }) => canRunLocalModel() ? loadOnnxEncoder(name) : null).then((encoder) => {
4298
+ if (encoder === null) return;
4299
+ if (callerEncoder === null) this.#localEncoder = encoder;
4300
+ if (encoder.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(encoder);
4301
+ }).catch(() => {
4302
+ });
4303
+ }
4304
+ #maybeBackfillTurns(conversationId, total) {
4305
+ if (total <= this.#turnStore.count(conversationId)) return;
4306
+ const now = Date.now();
4307
+ if (now < (this.#backfillAfter.get(conversationId) ?? 0)) return;
4308
+ this.#backfillAfter.set(conversationId, now + _SynapClient.BACKFILL_COOLDOWN_MS);
4309
+ void this.#backfillTurns(conversationId, total);
4310
+ }
4311
+ async #backfillTurns(conversationId, total) {
4312
+ try {
4313
+ let start = 1;
4314
+ while (start <= total) {
4315
+ const result = await this.#transport.request("conversation_turns", {
4316
+ pathParams: { conversation_id: conversationId },
4317
+ query: { start, limit: _SynapClient.BACKFILL_PAGE }
4318
+ });
4319
+ const page = (result ?? {})["turns"] ?? [];
4320
+ this.#turnStore.addMany(conversationId, page);
4321
+ if (page.length < _SynapClient.BACKFILL_PAGE) break;
4322
+ start += _SynapClient.BACKFILL_PAGE;
4323
+ }
4324
+ } catch (error) {
4325
+ this.#warn(`Turn backfill for ${conversationId} failed: ${String(error)}`);
4326
+ }
4327
+ }
3170
4328
  #localPrompt(conversationId, style) {
3171
4329
  if (!this.#isStAuthoritative()) return null;
3172
4330
  const entry = this.#shortTerm.get(conversationId);
3173
- if (entry === null) return null;
4331
+ if (entry === null || !isComplete(entry)) return null;
3174
4332
  if (entry.compactionId === null && entry.recentTurns.length === 0) return null;
3175
4333
  return renderForPrompt(entry, style);
3176
4334
  }
@@ -3178,9 +4336,29 @@ var SynapClient = class {
3178
4336
  #localCompacted(conversationId, format) {
3179
4337
  if (!this.#isStAuthoritative()) return null;
3180
4338
  const entry = this.#shortTerm.get(conversationId);
3181
- if (entry === null || entry.compactionId === null) return null;
4339
+ if (entry === null || entry.compactionId === null || !isComplete(entry)) return null;
3182
4340
  return renderCompacted(entry, format);
3183
4341
  }
4342
+ /**
4343
+ * Bring the local store back in step from the server's answer.
4344
+ *
4345
+ * Only for a conversation this process writes to. The server's answer lists
4346
+ * every message its summary does not cover, which is exactly what the entry
4347
+ * was missing.
4348
+ */
4349
+ #reseedShortTerm(conversationId, response) {
4350
+ if (!this.#isStAuthoritative()) return;
4351
+ const entry = this.#shortTerm.get(conversationId);
4352
+ if (entry === null || !entry.participant) return;
4353
+ const r = response ?? {};
4354
+ const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
4355
+ this.#shortTerm.reseed(conversationId, {
4356
+ compactionId: typeof r["compaction_id"] === "string" ? r["compaction_id"] : null,
4357
+ recentMessages: recent,
4358
+ compactedMessageCount: Number(r["compacted_message_count"] ?? 0) || 0,
4359
+ totalMessageCount: Number(r["total_message_count"] ?? 0) || 0
4360
+ });
4361
+ }
3184
4362
  /** Every diagnostic the SDK emits passes through here. */
3185
4363
  #warn(message) {
3186
4364
  if (this.#logger !== void 0) {
@@ -3243,6 +4421,7 @@ export {
3243
4421
  DEFAULT_TIMEOUTS,
3244
4422
  DOCUMENT_TYPES,
3245
4423
  ENDPOINTS,
4424
+ EXCERPT_CATEGORY,
3246
4425
  FORGET_MODES,
3247
4426
  HttpTransport,
3248
4427
  INGEST_MODES,
@@ -3253,8 +4432,10 @@ export {
3253
4432
  InvalidInstanceIdError,
3254
4433
  ListeningAlreadyActiveError,
3255
4434
  ListeningNotActiveError,
4435
+ MEMORY_TYPES,
3256
4436
  NetworkTimeoutError,
3257
4437
  PermanentError,
4438
+ RESPONSE_SHAPES,
3258
4439
  RateLimitError,
3259
4440
  SDKError,
3260
4441
  SDK_VERSION,
@@ -3267,15 +4448,18 @@ export {
3267
4448
  THRESHOLDS,
3268
4449
  TranscriptConflictError,
3269
4450
  TransientError,
4451
+ applyResponseShape,
3270
4452
  flattenContextItems,
3271
4453
  formatForPrompt,
3272
4454
  isOutcomeUnknown,
3273
4455
  isRecallQuery,
3274
4456
  isSynapError,
3275
4457
  mergeScopeResults,
4458
+ rankItems,
3276
4459
  resolvePath,
3277
4460
  shouldRetry,
3278
4461
  stem,
3279
- tokenize
4462
+ tokenize,
4463
+ wireRankScore
3280
4464
  };
3281
4465
  //# sourceMappingURL=index.js.map