@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.
- package/README.md +5 -1
- package/dist/anticipation-cache-LPNKMRBH.cjs +20 -0
- package/dist/anticipation-cache-LPNKMRBH.cjs.map +1 -0
- package/dist/anticipation-cache-RWKF55J4.js +20 -0
- package/dist/{chunk-FXGLT6AT.js → chunk-I7TWR4S6.js} +59 -3
- package/dist/chunk-I7TWR4S6.js.map +1 -0
- package/dist/{chunk-T6U5PVAD.cjs → chunk-ICUFS3B5.cjs} +449 -12
- package/dist/chunk-ICUFS3B5.cjs.map +1 -0
- package/dist/{chunk-ODVOQOSO.js → chunk-R72YL6Q4.js} +445 -8
- package/dist/chunk-R72YL6Q4.js.map +1 -0
- package/dist/{chunk-HCULAVJC.cjs → chunk-RE4DDIWO.cjs} +66 -10
- package/dist/chunk-RE4DDIWO.cjs.map +1 -0
- package/dist/chunk-T372OL3Q.cjs.map +1 -1
- package/dist/chunk-ZKOJDUN4.cjs.map +1 -1
- package/dist/env-55ULHNJZ.cjs.map +1 -1
- package/dist/{errors-Bl3vY1eb.d.cts → errors-Bdayqlzy.d.cts} +88 -2
- package/dist/{errors-Bl3vY1eb.d.ts → errors-Bdayqlzy.d.ts} +88 -2
- package/dist/grpc/index.cjs +4 -4
- package/dist/grpc/index.cjs.map +1 -1
- package/dist/grpc/index.d.cts +8 -3
- package/dist/grpc/index.d.ts +8 -3
- package/dist/grpc/index.js +3 -3
- package/dist/index.cjs +1300 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -5
- package/dist/index.d.ts +258 -5
- package/dist/index.js +1253 -69
- package/dist/index.js.map +1 -1
- package/dist/onnx-encoder-4RSL6ID4.cjs +103 -0
- package/dist/onnx-encoder-4RSL6ID4.cjs.map +1 -0
- package/dist/onnx-encoder-NP35KH4H.js +103 -0
- package/dist/onnx-encoder-NP35KH4H.js.map +1 -0
- package/dist/{stream-client-VWKINE47.js → stream-client-3WHXGW4U.js} +2 -2
- package/dist/stream-client-L6OQPZN7.cjs +16 -0
- package/dist/stream-client-L6OQPZN7.cjs.map +1 -0
- package/package.json +4 -2
- package/dist/anticipation-cache-LRE3TLK4.cjs +0 -14
- package/dist/anticipation-cache-LRE3TLK4.cjs.map +0 -1
- package/dist/anticipation-cache-WLU4NMD5.js +0 -14
- package/dist/chunk-FXGLT6AT.js.map +0 -1
- package/dist/chunk-HCULAVJC.cjs.map +0 -1
- package/dist/chunk-ODVOQOSO.js.map +0 -1
- package/dist/chunk-T6U5PVAD.cjs.map +0 -1
- package/dist/stream-client-N46JBT5A.cjs +0 -16
- package/dist/stream-client-N46JBT5A.cjs.map +0 -1
- /package/dist/{anticipation-cache-WLU4NMD5.js.map → anticipation-cache-RWKF55J4.js.map} +0 -0
- /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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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 (
|
|
1819
|
-
|
|
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
|
|
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
|
-
|
|
1986
|
-
if (
|
|
1987
|
-
else if (entry.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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`:
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
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
|
-
|
|
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.
|
|
3167
|
-
*
|
|
3168
|
-
*
|
|
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
|