@maximem/synap-js-sdk 0.5.8 → 0.5.10
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 +1357 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +277 -11
- package/dist/index.d.ts +277 -11
- package/dist/index.js +1309 -80
- 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 },
|
|
@@ -264,6 +267,17 @@ var HttpTransport = class {
|
|
|
264
267
|
* deployment older than the field, and must mean "enforce nothing".
|
|
265
268
|
*/
|
|
266
269
|
userContextIsolation = void 0;
|
|
270
|
+
/**
|
|
271
|
+
* Set by the client to its identity resolution (`initialize()`'s whoami).
|
|
272
|
+
* Python refuses every call before `initialize()`; here the first request
|
|
273
|
+
* runs it instead, so a caller who skips it gets the same identity-dependent
|
|
274
|
+
* behaviour (client-scope cache hits, the B2C check) rather than an error.
|
|
275
|
+
*/
|
|
276
|
+
identityResolver = void 0;
|
|
277
|
+
/** Resolve identity once, before the first request. Never throws. */
|
|
278
|
+
async ready() {
|
|
279
|
+
if (this.identityResolver !== void 0) await this.identityResolver();
|
|
280
|
+
}
|
|
267
281
|
/** Refuse a customer_id on a B2C instance, at the call site. */
|
|
268
282
|
checkCustomerId(customerId, where) {
|
|
269
283
|
checkCustomerId(
|
|
@@ -295,6 +309,7 @@ var HttpTransport = class {
|
|
|
295
309
|
const spec = ENDPOINTS[name];
|
|
296
310
|
const correlationId = options.correlationId ?? newCorrelationId();
|
|
297
311
|
const idempotent = options.idempotent ?? spec.idempotent;
|
|
312
|
+
if (name !== "whoami") await this.ready();
|
|
298
313
|
let attempt = 0;
|
|
299
314
|
let last = null;
|
|
300
315
|
while (attempt < this.retryPolicy.maxAttempts) {
|
|
@@ -517,6 +532,86 @@ function sleep(ms, signal) {
|
|
|
517
532
|
});
|
|
518
533
|
}
|
|
519
534
|
|
|
535
|
+
// src/context/ranked.ts
|
|
536
|
+
var RESPONSE_SHAPES = ["by_type", "ranked"];
|
|
537
|
+
var MEMORY_TYPES = [
|
|
538
|
+
"facts",
|
|
539
|
+
"preferences",
|
|
540
|
+
"episodes",
|
|
541
|
+
"emotions",
|
|
542
|
+
"temporal_events"
|
|
543
|
+
];
|
|
544
|
+
var EXCERPT_CATEGORY = "excerpts";
|
|
545
|
+
function wireRankScore(item) {
|
|
546
|
+
const md = item.metadata;
|
|
547
|
+
let raw = md !== null && typeof md === "object" ? md["rank_score"] : void 0;
|
|
548
|
+
if (raw === void 0 || raw === null) raw = item["rank_score"];
|
|
549
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
550
|
+
}
|
|
551
|
+
function wireCategory(item, fallback = "") {
|
|
552
|
+
if (item.source === "chunks") return EXCERPT_CATEGORY;
|
|
553
|
+
const category = item["category"];
|
|
554
|
+
if (typeof category === "string" && category !== "") return category;
|
|
555
|
+
const contextType = item["context_type"];
|
|
556
|
+
if (typeof contextType === "string" && contextType !== "") return contextType;
|
|
557
|
+
return fallback;
|
|
558
|
+
}
|
|
559
|
+
function byRankScore(a, b) {
|
|
560
|
+
const sa = wireRankScore(a);
|
|
561
|
+
const sb = wireRankScore(b);
|
|
562
|
+
if (sa === null && sb === null) return 0;
|
|
563
|
+
if (sa === null) return 1;
|
|
564
|
+
if (sb === null) return -1;
|
|
565
|
+
return sb - sa;
|
|
566
|
+
}
|
|
567
|
+
function rankItems(byType) {
|
|
568
|
+
const flat = [];
|
|
569
|
+
for (const type of MEMORY_TYPES) {
|
|
570
|
+
const list = byType[type];
|
|
571
|
+
if (!Array.isArray(list)) continue;
|
|
572
|
+
for (const it of list) {
|
|
573
|
+
if (it === null || typeof it !== "object") continue;
|
|
574
|
+
flat.push({
|
|
575
|
+
...it,
|
|
576
|
+
category: wireCategory(it, type),
|
|
577
|
+
id: it.id || it.item_id || ""
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
flat.sort(byRankScore);
|
|
582
|
+
return flat.map((it, n2) => ({ ...it, rank: n2 + 1 }));
|
|
583
|
+
}
|
|
584
|
+
function applyResponseShape(raw, shape = "by_type") {
|
|
585
|
+
const wire = raw["items"];
|
|
586
|
+
if (shape !== "ranked" && !Array.isArray(wire)) return raw;
|
|
587
|
+
const source = Array.isArray(wire) ? wire : rankItems(raw);
|
|
588
|
+
const items = source.map((it) => ({
|
|
589
|
+
...it,
|
|
590
|
+
id: it.id || it.item_id || "",
|
|
591
|
+
category: wireCategory(it)
|
|
592
|
+
}));
|
|
593
|
+
const out = { ...raw, items };
|
|
594
|
+
for (const type of MEMORY_TYPES) {
|
|
595
|
+
out[type] = items.filter((it) => it["category"] === type);
|
|
596
|
+
}
|
|
597
|
+
out["excerpts"] = items.filter((it) => it["category"] === EXCERPT_CATEGORY);
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
var RANKED_LABELS = {
|
|
601
|
+
facts: "fact",
|
|
602
|
+
preferences: "preference",
|
|
603
|
+
episodes: "episode",
|
|
604
|
+
emotions: "emotion",
|
|
605
|
+
temporal_events: "temporal event",
|
|
606
|
+
[EXCERPT_CATEGORY]: "excerpt"
|
|
607
|
+
};
|
|
608
|
+
function rankedLabel(category) {
|
|
609
|
+
return RANKED_LABELS[category] ?? (category !== "" ? category : "item");
|
|
610
|
+
}
|
|
611
|
+
function sortByRankScore(items) {
|
|
612
|
+
return [...items].sort(byRankScore);
|
|
613
|
+
}
|
|
614
|
+
|
|
520
615
|
// src/context/fetch.ts
|
|
521
616
|
var ENDPOINT_BY_SCOPE = {
|
|
522
617
|
user: "context_fetch_user",
|
|
@@ -536,10 +631,10 @@ function buildFetchRequest(scope, options) {
|
|
|
536
631
|
const conversationId = opt(options, "conversation_id", "conversationId");
|
|
537
632
|
const searchQuery = opt(options, "search_query", "searchQuery");
|
|
538
633
|
const maxResults = opt(options, "max_results", "maxResults") ?? 10;
|
|
539
|
-
const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
|
|
540
634
|
const queryDate = opt(options, "query_date", "queryDate");
|
|
541
635
|
const perQuery = opt(options, "max_results_per_query", "maxResultsPerQuery");
|
|
542
|
-
const
|
|
636
|
+
const responseShape = responseShapeOf(options);
|
|
637
|
+
const { mode, precisionLevel } = retrievalOptionsOf(options);
|
|
543
638
|
if (searchQuery !== void 0 && !Array.isArray(searchQuery)) {
|
|
544
639
|
throw new InvalidInputError("search_query must be an array when provided");
|
|
545
640
|
}
|
|
@@ -556,16 +651,19 @@ function buildFetchRequest(scope, options) {
|
|
|
556
651
|
if (precisionLevel !== "high") body["precision_level"] = precisionLevel;
|
|
557
652
|
if (queryDate) body["query_date"] = queryDate;
|
|
558
653
|
if (perQuery) body["max_results_per_query"] = perQuery;
|
|
654
|
+
if (responseShape !== "by_type") body["response_shape"] = responseShape;
|
|
559
655
|
if (options.include_conversation_context === false) {
|
|
560
656
|
body["include_conversation_context"] = false;
|
|
561
657
|
}
|
|
658
|
+
const scopePath = opt(options, "scope_path", "scopePath");
|
|
659
|
+
if (scopePath !== void 0 && scopePath !== null && Object.keys(scopePath).length > 0) {
|
|
660
|
+
body["scope"] = scopePath;
|
|
661
|
+
}
|
|
562
662
|
switch (scope) {
|
|
563
663
|
case "user":
|
|
564
664
|
{
|
|
565
665
|
if (!userId) throw new InvalidInputError("user_id is required");
|
|
566
666
|
body["user_id"] = userId;
|
|
567
|
-
const scopePath = opt(options, "scope_path", "scopePath");
|
|
568
|
-
if (scopePath !== void 0) body["scope"] = scopePath;
|
|
569
667
|
const contextMode = opt(options, "context_mode", "contextMode") ?? "in-conversation";
|
|
570
668
|
if (!VALID_CONTEXT_MODES.includes(contextMode)) {
|
|
571
669
|
throw new InvalidInputError(
|
|
@@ -603,21 +701,67 @@ function buildFetchRequest(scope, options) {
|
|
|
603
701
|
return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
|
|
604
702
|
}
|
|
605
703
|
var VALID_CONTEXT_MODES = ["in-conversation", "conversation-summary"];
|
|
704
|
+
function responseShapeOf(options) {
|
|
705
|
+
const shape = opt(options, "response_shape", "responseShape") ?? "by_type";
|
|
706
|
+
if (!RESPONSE_SHAPES.includes(shape)) {
|
|
707
|
+
throw new InvalidInputError(
|
|
708
|
+
`Invalid response_shape '${shape}'. Must be one of: ${RESPONSE_SHAPES.join(", ")}`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return shape;
|
|
712
|
+
}
|
|
713
|
+
var VALID_MODES = ["fast", "accurate"];
|
|
714
|
+
var VALID_PRECISION = ["high", "medium"];
|
|
715
|
+
function retrievalOptionsOf(options) {
|
|
716
|
+
const mode = options.mode ?? "fast";
|
|
717
|
+
if (!VALID_MODES.includes(mode)) {
|
|
718
|
+
throw new InvalidInputError(
|
|
719
|
+
`Invalid mode '${mode}'. Must be one of: ${VALID_MODES.join(", ")}`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
|
|
723
|
+
if (!VALID_PRECISION.includes(precisionLevel)) {
|
|
724
|
+
throw new InvalidInputError(
|
|
725
|
+
`Invalid precision_level '${precisionLevel}'. Must be one of: ${VALID_PRECISION.join(", ")}`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return { mode, precisionLevel };
|
|
729
|
+
}
|
|
606
730
|
async function fetchContext(transport, scope, options = {}, hooks = {}) {
|
|
607
731
|
const startedAt = Date.now();
|
|
732
|
+
await transport.ready?.();
|
|
608
733
|
transport.checkCustomerId(
|
|
609
734
|
opt(options, "customer_id", "customerId"),
|
|
610
735
|
`${scope}.context.fetch`
|
|
611
736
|
);
|
|
612
|
-
const
|
|
737
|
+
const responseShape = responseShapeOf(options);
|
|
738
|
+
retrievalOptionsOf(options);
|
|
739
|
+
const network = async () => {
|
|
740
|
+
const { endpoint, body } = buildFetchRequest(scope, options);
|
|
741
|
+
const result = await transport.request(endpoint, { body });
|
|
742
|
+
return applyResponseShape(result?.["context"] ?? {}, responseShape);
|
|
743
|
+
};
|
|
744
|
+
const anticipated = await hooks.beforeFetch?.(scope, options) ?? null;
|
|
745
|
+
if (anticipated !== null && anticipated.borderline) {
|
|
746
|
+
const pending = network();
|
|
747
|
+
pending.catch(() => void 0);
|
|
748
|
+
let timer;
|
|
749
|
+
const late = new Promise((resolve) => {
|
|
750
|
+
timer = setTimeout(() => resolve(null), anticipated.raceDeadlineMs ?? 250);
|
|
751
|
+
});
|
|
752
|
+
const won = await Promise.race([pending.then((r) => r, () => null), late]);
|
|
753
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
754
|
+
if (won !== null) {
|
|
755
|
+
hooks.onAssembled?.(scope, options, won, startedAt);
|
|
756
|
+
return won;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
613
759
|
if (anticipated !== null) {
|
|
614
760
|
hooks.onServedFromCache?.(scope, options, anticipated);
|
|
615
761
|
hooks.onAssembled?.(scope, options, anticipated.response, startedAt);
|
|
616
|
-
return anticipated.response;
|
|
762
|
+
return applyResponseShape(anticipated.response, responseShape);
|
|
617
763
|
}
|
|
618
|
-
const
|
|
619
|
-
const result = await transport.request(endpoint, { body });
|
|
620
|
-
const response = result?.["context"] ?? {};
|
|
764
|
+
const response = await network();
|
|
621
765
|
hooks.onAssembled?.(scope, options, response, startedAt);
|
|
622
766
|
return response;
|
|
623
767
|
}
|
|
@@ -738,6 +882,17 @@ function normalizeConversationContext(value) {
|
|
|
738
882
|
conversationId: pick(value["conversationId"], value.conversation_id, null)
|
|
739
883
|
};
|
|
740
884
|
}
|
|
885
|
+
function normalizeRetrievalHealth(raw) {
|
|
886
|
+
if (!raw || typeof raw !== "object") return null;
|
|
887
|
+
const r = raw;
|
|
888
|
+
const warnings = r["warnings"];
|
|
889
|
+
return {
|
|
890
|
+
degraded: r["degraded"] === void 0 ? true : Boolean(r["degraded"]),
|
|
891
|
+
level: r["level"] ?? null,
|
|
892
|
+
code: r["code"] ?? null,
|
|
893
|
+
warnings: Array.isArray(warnings) ? warnings.map((w) => String(w)) : []
|
|
894
|
+
};
|
|
895
|
+
}
|
|
741
896
|
function normalizeContextResponse(result = {}) {
|
|
742
897
|
const context = result["context"] ?? result;
|
|
743
898
|
return {
|
|
@@ -754,7 +909,15 @@ function normalizeContextResponse(result = {}) {
|
|
|
754
909
|
)
|
|
755
910
|
),
|
|
756
911
|
metadata: normalizeContextMetadata(obj(context.metadata)),
|
|
757
|
-
|
|
912
|
+
// `retrieval` is a SIBLING of `context` in the body, so it is read off the
|
|
913
|
+
// envelope first and only then off a bare context. Reading it from
|
|
914
|
+
// `context` alone is how the Python SDK dropped it.
|
|
915
|
+
retrieval: normalizeRetrievalHealth(
|
|
916
|
+
pick(result["retrieval"], context["retrieval"], null)
|
|
917
|
+
),
|
|
918
|
+
rawResponse: pick(context["rawResponse"], context["raw_response"], {}),
|
|
919
|
+
// Only for a ranked fetch, so a by-type result has exactly the keys it had.
|
|
920
|
+
...Array.isArray(context.items) ? { items: context.items, excerpts: Array.isArray(context.excerpts) ? context.excerpts : [] } : {}
|
|
758
921
|
};
|
|
759
922
|
}
|
|
760
923
|
|
|
@@ -1069,6 +1232,27 @@ function validateInstanceId(instanceId) {
|
|
|
1069
1232
|
}
|
|
1070
1233
|
}
|
|
1071
1234
|
|
|
1235
|
+
// src/agent-model.ts
|
|
1236
|
+
var AGENT_MODEL_KEY = "agent_model";
|
|
1237
|
+
var AGENT_CONTEXT_WINDOW_KEY = "agent_context_window";
|
|
1238
|
+
function agentModelOf(options) {
|
|
1239
|
+
const o2 = options ?? {};
|
|
1240
|
+
const window = o2["context_window"] ?? o2["contextWindow"];
|
|
1241
|
+
return {
|
|
1242
|
+
model: typeof o2["model"] === "string" ? o2["model"] : void 0,
|
|
1243
|
+
contextWindow: typeof window === "number" ? window : void 0
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function withAgentModel(metadata, perCall, defaults, asStrings = false) {
|
|
1247
|
+
const model = perCall.model || defaults.model;
|
|
1248
|
+
const window = perCall.contextWindow || defaults.contextWindow;
|
|
1249
|
+
if (!model && !window) return metadata;
|
|
1250
|
+
const out = { ...metadata ?? {} };
|
|
1251
|
+
if (model) out[AGENT_MODEL_KEY] = String(model);
|
|
1252
|
+
if (window) out[AGENT_CONTEXT_WINDOW_KEY] = asStrings ? String(Math.trunc(window)) : Math.trunc(window);
|
|
1253
|
+
return out;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1072
1256
|
// src/conversation/interface.ts
|
|
1073
1257
|
var COMPACTION_LEVELS = [
|
|
1074
1258
|
"low",
|
|
@@ -1197,10 +1381,50 @@ var CompactionSubscribers = class {
|
|
|
1197
1381
|
}
|
|
1198
1382
|
}
|
|
1199
1383
|
};
|
|
1384
|
+
function storedBatchRows(bodies, result) {
|
|
1385
|
+
const r = result ?? {};
|
|
1386
|
+
const results = Array.isArray(r["results"]) ? r["results"] : [];
|
|
1387
|
+
const errors = Array.isArray(r["errors"]) ? r["errors"] : [];
|
|
1388
|
+
const failed = new Set(errors.map((e) => e?.["index"]));
|
|
1389
|
+
const stored = bodies.filter((_, index) => !failed.has(index));
|
|
1390
|
+
if (stored.length !== results.length) return [];
|
|
1391
|
+
const pairs = [];
|
|
1392
|
+
stored.forEach((body, i) => {
|
|
1393
|
+
const row = results[i];
|
|
1394
|
+
if (row && typeof row === "object" && row["message_id"]) pairs.push([body, row]);
|
|
1395
|
+
});
|
|
1396
|
+
return pairs;
|
|
1397
|
+
}
|
|
1200
1398
|
function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
|
|
1201
1399
|
}, onTurn = () => {
|
|
1202
1400
|
}, onFetched = () => {
|
|
1203
|
-
}, localShortTerm = { prompt: () => null, compacted: () => null }) {
|
|
1401
|
+
}, localShortTerm = { prompt: () => null, compacted: () => null }, agentModel = () => ({})) {
|
|
1402
|
+
const writesInFlight = /* @__PURE__ */ new Map();
|
|
1403
|
+
const withTurnModel = (body, options) => {
|
|
1404
|
+
const metadata = withAgentModel(
|
|
1405
|
+
body["metadata"],
|
|
1406
|
+
agentModelOf(options),
|
|
1407
|
+
agentModel()
|
|
1408
|
+
);
|
|
1409
|
+
return metadata === void 0 ? body : { ...body, metadata };
|
|
1410
|
+
};
|
|
1411
|
+
const writeStarted = (conversationId) => {
|
|
1412
|
+
writesInFlight.set(conversationId, (writesInFlight.get(conversationId) ?? 0) + 1);
|
|
1413
|
+
};
|
|
1414
|
+
const writeLanded = (conversationId) => {
|
|
1415
|
+
const remaining = (writesInFlight.get(conversationId) ?? 0) - 1;
|
|
1416
|
+
if (remaining > 0) writesInFlight.set(conversationId, remaining);
|
|
1417
|
+
else writesInFlight.delete(conversationId);
|
|
1418
|
+
};
|
|
1419
|
+
const confirmation = (result, conversationId) => {
|
|
1420
|
+
const r = result ?? {};
|
|
1421
|
+
const count = r["conversation_message_count"];
|
|
1422
|
+
return {
|
|
1423
|
+
...typeof r["message_id"] === "string" ? { messageId: r["message_id"] } : {},
|
|
1424
|
+
serverMessageCount: typeof count === "number" ? count : null,
|
|
1425
|
+
inFlight: writesInFlight.get(conversationId) ?? 0
|
|
1426
|
+
};
|
|
1427
|
+
};
|
|
1204
1428
|
const convId = (v) => {
|
|
1205
1429
|
const id = typeof v === "string" ? v : v.conversation_id ?? v.conversationId;
|
|
1206
1430
|
if (!id) throw new InvalidInputError("conversation_id is required");
|
|
@@ -1213,13 +1437,21 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1213
1437
|
options["customer_id"] ?? options["customerId"],
|
|
1214
1438
|
"conversation.record_message"
|
|
1215
1439
|
);
|
|
1216
|
-
const body = buildRecordMessageBody(options);
|
|
1217
|
-
const
|
|
1440
|
+
const body = withTurnModel(buildRecordMessageBody(options), options);
|
|
1441
|
+
const conversationId = String(body["conversation_id"] ?? "");
|
|
1442
|
+
writeStarted(conversationId);
|
|
1443
|
+
let result;
|
|
1444
|
+
try {
|
|
1445
|
+
result = await transport.request("conversations_messages", { body });
|
|
1446
|
+
} finally {
|
|
1447
|
+
writeLanded(conversationId);
|
|
1448
|
+
}
|
|
1218
1449
|
onTurn({
|
|
1219
|
-
conversationId
|
|
1450
|
+
conversationId,
|
|
1220
1451
|
role: String(body["role"] ?? "user"),
|
|
1221
1452
|
content: String(body["content"] ?? ""),
|
|
1222
|
-
timestamp: body["timestamp"]
|
|
1453
|
+
timestamp: body["timestamp"],
|
|
1454
|
+
...confirmation(result, conversationId)
|
|
1223
1455
|
});
|
|
1224
1456
|
onWrite({
|
|
1225
1457
|
userId: body["user_id"],
|
|
@@ -1238,16 +1470,18 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1238
1470
|
"conversation.record_messages_batch"
|
|
1239
1471
|
);
|
|
1240
1472
|
}
|
|
1241
|
-
const bodies = messages.map(buildRecordMessageBody);
|
|
1473
|
+
const bodies = messages.map((m) => withTurnModel(buildRecordMessageBody(m), m));
|
|
1242
1474
|
const result = await transport.request("conversations_messages_batch", {
|
|
1243
1475
|
body: { messages: bodies }
|
|
1244
1476
|
});
|
|
1245
|
-
for (const body of bodies) {
|
|
1477
|
+
for (const [body, row] of storedBatchRows(bodies, result)) {
|
|
1478
|
+
const conversationId = String(body["conversation_id"] ?? "");
|
|
1246
1479
|
onTurn({
|
|
1247
|
-
conversationId
|
|
1480
|
+
conversationId,
|
|
1248
1481
|
role: String(body["role"] ?? "user"),
|
|
1249
1482
|
content: String(body["content"] ?? ""),
|
|
1250
|
-
timestamp: body["timestamp"]
|
|
1483
|
+
timestamp: body["timestamp"],
|
|
1484
|
+
...confirmation(row, conversationId)
|
|
1251
1485
|
});
|
|
1252
1486
|
}
|
|
1253
1487
|
for (const body of bodies) {
|
|
@@ -1282,13 +1516,24 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1282
1516
|
},
|
|
1283
1517
|
get_context_for_prompt: async (options = {}) => {
|
|
1284
1518
|
const id = convId(options);
|
|
1519
|
+
const search = (options.search_history ?? options.searchHistory ?? true) && localShortTerm.historyExcerpts;
|
|
1520
|
+
const withHistory = async (response2) => {
|
|
1521
|
+
if (!search) return response2;
|
|
1522
|
+
try {
|
|
1523
|
+
return await localShortTerm.historyExcerpts(id, response2, options.query);
|
|
1524
|
+
} catch {
|
|
1525
|
+
return response2;
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1285
1528
|
const local = localShortTerm.prompt(id, options.style ?? "structured");
|
|
1286
|
-
if (local !== null) return local;
|
|
1529
|
+
if (local !== null) return withHistory(local);
|
|
1287
1530
|
const result = await transport.request("context_for_prompt", {
|
|
1288
1531
|
pathParams: { conversation_id: id },
|
|
1289
1532
|
query: { style: options.style ?? "structured" }
|
|
1290
1533
|
});
|
|
1291
|
-
|
|
1534
|
+
const response = result?.["context_for_prompt"] ?? result ?? {};
|
|
1535
|
+
localShortTerm.reseed?.(id, response);
|
|
1536
|
+
return withHistory(response);
|
|
1292
1537
|
},
|
|
1293
1538
|
async compact(options) {
|
|
1294
1539
|
return transport.request("conversations_compact", { body: buildCompactBody(options) });
|
|
@@ -1471,11 +1716,13 @@ function jsonOrText(value) {
|
|
|
1471
1716
|
}
|
|
1472
1717
|
}
|
|
1473
1718
|
}
|
|
1474
|
-
function createInstanceNamespace(credentials, cache, onTurn) {
|
|
1719
|
+
function createInstanceNamespace(credentials, cache, onTurn, agentModel = () => ({}), onToolResult) {
|
|
1475
1720
|
let client = null;
|
|
1476
1721
|
const openSessions = /* @__PURE__ */ new Map();
|
|
1477
1722
|
let dispatchCompaction = null;
|
|
1478
1723
|
let applyCompaction = null;
|
|
1724
|
+
let onListen = null;
|
|
1725
|
+
let onConversationState = null;
|
|
1479
1726
|
const internals = {
|
|
1480
1727
|
onCompactionUpdate(conversationId, bundle) {
|
|
1481
1728
|
applyCompaction?.(bundle);
|
|
@@ -1484,6 +1731,12 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1484
1731
|
setCompactionApplier(apply) {
|
|
1485
1732
|
applyCompaction = apply;
|
|
1486
1733
|
},
|
|
1734
|
+
setOnListen(handle) {
|
|
1735
|
+
onListen = handle;
|
|
1736
|
+
},
|
|
1737
|
+
setConversationStateHandler(handle) {
|
|
1738
|
+
onConversationState = handle;
|
|
1739
|
+
},
|
|
1487
1740
|
sendContextUsed(event) {
|
|
1488
1741
|
client?.sendContextUsed(event);
|
|
1489
1742
|
},
|
|
@@ -1518,7 +1771,11 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1518
1771
|
if (client !== null) {
|
|
1519
1772
|
throw new ListeningAlreadyActiveError("Listening is already active on this instance");
|
|
1520
1773
|
}
|
|
1521
|
-
|
|
1774
|
+
try {
|
|
1775
|
+
onListen?.();
|
|
1776
|
+
} catch {
|
|
1777
|
+
}
|
|
1778
|
+
const { GrpcStreamClient } = await import("./stream-client-3WHXGW4U.js");
|
|
1522
1779
|
const onContext = options.on_context ?? options.onContext;
|
|
1523
1780
|
const onDisconnect = options.on_disconnect ?? options.onDisconnect;
|
|
1524
1781
|
const onReconnect = options.on_reconnect ?? options.onReconnect;
|
|
@@ -1532,6 +1789,9 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1532
1789
|
onCompactionUpdate: (conversationId, bundle) => {
|
|
1533
1790
|
internals.onCompactionUpdate(conversationId, bundle);
|
|
1534
1791
|
},
|
|
1792
|
+
onConversationState: (conversationId, messageCount) => {
|
|
1793
|
+
onConversationState?.(conversationId, messageCount);
|
|
1794
|
+
},
|
|
1535
1795
|
onStateChange: (state) => {
|
|
1536
1796
|
if (state !== "connected") openSessions.clear();
|
|
1537
1797
|
if (state === "reconnecting") {
|
|
@@ -1610,7 +1870,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1610
1870
|
user_id: options.user_id ?? options.userId ?? "",
|
|
1611
1871
|
customer_id: options.customer_id ?? options.customerId ?? "",
|
|
1612
1872
|
session_id: options.session_id ?? options.sessionId ?? openSessions.get(conversationId) ?? "",
|
|
1613
|
-
metadata: options.metadata ?? {},
|
|
1873
|
+
metadata: withAgentModel(options.metadata, agentModelOf(options), agentModel(), true) ?? {},
|
|
1614
1874
|
timestamp_ms: Date.now(),
|
|
1615
1875
|
tool_name: options.tool_name ?? options.toolName ?? "",
|
|
1616
1876
|
// Python serialises tool_args with json.dumps into tool_args_json.
|
|
@@ -1640,6 +1900,10 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1640
1900
|
});
|
|
1641
1901
|
},
|
|
1642
1902
|
async record_tool_result(options) {
|
|
1903
|
+
const conversationId = options.conversation_id ?? options.conversationId ?? "";
|
|
1904
|
+
if (conversationId) {
|
|
1905
|
+
onToolResult?.({ conversationId, result: options.result, toolName: options.tool_name ?? options.toolName ?? "" });
|
|
1906
|
+
}
|
|
1643
1907
|
await namespace.send_message({
|
|
1644
1908
|
content: "",
|
|
1645
1909
|
role: "tool",
|
|
@@ -1690,7 +1954,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
|
|
|
1690
1954
|
}
|
|
1691
1955
|
|
|
1692
1956
|
// src/version.ts
|
|
1693
|
-
var SDK_VERSION = "0.5.
|
|
1957
|
+
var SDK_VERSION = "0.5.10";
|
|
1694
1958
|
|
|
1695
1959
|
// src/registry.ts
|
|
1696
1960
|
var instances = /* @__PURE__ */ new Map();
|
|
@@ -1731,6 +1995,34 @@ function unregisterIfOwner(key, client) {
|
|
|
1731
1995
|
// src/cache/short-term-store.ts
|
|
1732
1996
|
var DEFAULT_MAX_CONVERSATIONS = 100;
|
|
1733
1997
|
var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
|
|
1998
|
+
var CONTEXT_BLOCK_PREFIX = "## Conversation so far (compacted";
|
|
1999
|
+
function contextBlockOf(cc) {
|
|
2000
|
+
const block = cc["context_block"];
|
|
2001
|
+
if (typeof block === "string" && block) return block;
|
|
2002
|
+
const summary = cc["summary"];
|
|
2003
|
+
if (typeof summary === "string" && summary.startsWith(CONTEXT_BLOCK_PREFIX)) return summary;
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
function localMessageCount(entry) {
|
|
2007
|
+
return entry.compactedMessageCount + entry.recentTurns.length;
|
|
2008
|
+
}
|
|
2009
|
+
function isComplete(entry) {
|
|
2010
|
+
return entry.participant && entry.synced && !entry.shared && entry.serverMessageCount !== null && entry.serverMessageCount <= localMessageCount(entry);
|
|
2011
|
+
}
|
|
2012
|
+
function heldIn(turn, turns) {
|
|
2013
|
+
return turns.some(
|
|
2014
|
+
(held) => turn.message_id && held.message_id ? held.message_id === turn.message_id : held.role === turn.role && held.content === turn.content
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
function turnsAfter(localTurns, serverTurns) {
|
|
2018
|
+
const newer = [];
|
|
2019
|
+
for (let i = localTurns.length - 1; i >= 0; i -= 1) {
|
|
2020
|
+
const turn = localTurns[i];
|
|
2021
|
+
if (heldIn(turn, serverTurns)) break;
|
|
2022
|
+
newer.unshift(turn);
|
|
2023
|
+
}
|
|
2024
|
+
return newer;
|
|
2025
|
+
}
|
|
1734
2026
|
function parseIso(value) {
|
|
1735
2027
|
if (value === null || value === void 0 || value === "") return null;
|
|
1736
2028
|
if (value instanceof Date) return value.getTime();
|
|
@@ -1750,11 +2042,23 @@ var ShortTermStore = class {
|
|
|
1750
2042
|
#maxConversations;
|
|
1751
2043
|
#maxAgeMs;
|
|
1752
2044
|
#now;
|
|
2045
|
+
/**
|
|
2046
|
+
* Every turn appended here is also kept in the archive (a TurnStore), which
|
|
2047
|
+
* a compaction does not prune: the turns it covers stay searchable locally
|
|
2048
|
+
* (history-search.ts). Python: `ShortTermContextStore.attach_archive`.
|
|
2049
|
+
*/
|
|
2050
|
+
#archive = null;
|
|
1753
2051
|
constructor(options = {}) {
|
|
1754
2052
|
this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
|
|
1755
2053
|
this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
1756
2054
|
this.#now = options.now ?? (() => Date.now());
|
|
1757
2055
|
}
|
|
2056
|
+
attachArchive(archive) {
|
|
2057
|
+
this.#archive = archive;
|
|
2058
|
+
}
|
|
2059
|
+
get archive() {
|
|
2060
|
+
return this.#archive;
|
|
2061
|
+
}
|
|
1758
2062
|
/** The entry, or null. Age-evicts on read, as Python does. */
|
|
1759
2063
|
get(conversationId) {
|
|
1760
2064
|
if (!conversationId) return null;
|
|
@@ -1780,23 +2084,88 @@ var ShortTermStore = class {
|
|
|
1780
2084
|
* exists yet (the first turn, before any compaction has arrived) a fresh one
|
|
1781
2085
|
* is created with an empty summary; the next `compaction_update` fills the
|
|
1782
2086
|
* summary fields in and prunes the turns it covers.
|
|
2087
|
+
*
|
|
2088
|
+
* `confirmation` is what the write learned from the server. See
|
|
2089
|
+
* `TurnConfirmation` and `noteServerCount`.
|
|
1783
2090
|
*/
|
|
1784
|
-
appendTurn(conversationId, role, content, timestamp) {
|
|
2091
|
+
appendTurn(conversationId, role, content, timestamp, confirmation = {}) {
|
|
1785
2092
|
if (!conversationId) return;
|
|
1786
2093
|
const stamp = timestamp ?? new Date(this.#now()).toISOString();
|
|
2094
|
+
if (this.#archive !== null) {
|
|
2095
|
+
try {
|
|
2096
|
+
this.#archive.add(conversationId, role, content, stamp, { messageId: confirmation.messageId ?? null });
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
1787
2100
|
const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
|
|
1788
|
-
|
|
2101
|
+
const messageId = confirmation.messageId;
|
|
2102
|
+
const alreadyHeld = Boolean(messageId) && entry.recentTurns.some((t) => t.message_id === messageId);
|
|
2103
|
+
if (!alreadyHeld) {
|
|
2104
|
+
const turn = { role, content, timestamp: stamp };
|
|
2105
|
+
if (messageId) turn.message_id = messageId;
|
|
2106
|
+
entry.recentTurns.push(turn);
|
|
2107
|
+
}
|
|
2108
|
+
const count = confirmation.serverMessageCount;
|
|
2109
|
+
if (count !== null && count !== void 0) {
|
|
2110
|
+
this.#noteCount(entry, Number(count), confirmation.inFlight ?? 0);
|
|
2111
|
+
}
|
|
1789
2112
|
entry.lastActivityAt = parseIso(stamp) ?? this.#now();
|
|
1790
2113
|
this.#touch(conversationId, entry);
|
|
1791
2114
|
this.#evict();
|
|
1792
2115
|
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Record the server's message count for a conversation.
|
|
2118
|
+
*
|
|
2119
|
+
* The streamed write path's half of `appendTurn`'s confirmation: a streamed
|
|
2120
|
+
* turn is appended when it is sent, and the count arrives later, once the
|
|
2121
|
+
* server has written it. A count for a conversation this process holds
|
|
2122
|
+
* nothing for is ignored.
|
|
2123
|
+
*/
|
|
2124
|
+
noteServerCount(conversationId, count) {
|
|
2125
|
+
if (!conversationId) return;
|
|
2126
|
+
const entry = this.#entries.get(conversationId);
|
|
2127
|
+
if (entry !== void 0) this.#noteCount(entry, Number(count), 0);
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Compare the server's count with what the entry holds.
|
|
2131
|
+
*
|
|
2132
|
+
* The count is taken after one of this process's own writes, so it proves
|
|
2133
|
+
* the process takes part in the conversation.
|
|
2134
|
+
*
|
|
2135
|
+
* Server ahead of an entry that was in sync: something else wrote, and the
|
|
2136
|
+
* entry is marked shared for good. Server ahead of an entry that was never
|
|
2137
|
+
* in sync: this process joined late (a restart, an eviction), and `reseed`
|
|
2138
|
+
* will fix it. `inFlight` covers this process's own writes that the server
|
|
2139
|
+
* has counted and the entry has not appended yet, so two concurrent writes
|
|
2140
|
+
* do not look like a second writer.
|
|
2141
|
+
*/
|
|
2142
|
+
#noteCount(entry, count, inFlight) {
|
|
2143
|
+
entry.participant = true;
|
|
2144
|
+
if (count > localMessageCount(entry) + inFlight) {
|
|
2145
|
+
if (entry.synced) entry.shared = true;
|
|
2146
|
+
} else if (entry.serverMessageCount === null) {
|
|
2147
|
+
entry.synced = true;
|
|
2148
|
+
}
|
|
2149
|
+
entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, count);
|
|
2150
|
+
}
|
|
1793
2151
|
/**
|
|
1794
2152
|
* Apply a `compaction_update` bundle.
|
|
1795
2153
|
*
|
|
1796
2154
|
* Replaces the summary fields from the bundle's `conversation_context` and
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
2155
|
+
* drops the buffered turns the new summary covers.
|
|
2156
|
+
*
|
|
2157
|
+
* Which turns those are is decided by count, not by clock. The bundle says
|
|
2158
|
+
* how many messages the summary covers and how many the conversation held
|
|
2159
|
+
* when the bundle was built, so the last `total - compacted` of its
|
|
2160
|
+
* `recent_turns` are exactly the uncovered messages. Turns this process
|
|
2161
|
+
* appended after the server took that count are kept behind them. Timestamps
|
|
2162
|
+
* cannot do this job: the REST write returns the time of the response and
|
|
2163
|
+
* not of the stored row, a caller may supply its own message time, and a
|
|
2164
|
+
* streamed turn is stamped when a background write runs.
|
|
2165
|
+
*
|
|
2166
|
+
* A bundle from a server that sends no counts falls back to the older
|
|
2167
|
+
* timestamp prune, and the entry is left out of sync so it is never used to
|
|
2168
|
+
* answer on its own.
|
|
1800
2169
|
*/
|
|
1801
2170
|
applyCompaction(bundle) {
|
|
1802
2171
|
if (typeof bundle !== "object" || bundle === null) return;
|
|
@@ -1806,22 +2175,84 @@ var ShortTermStore = class {
|
|
|
1806
2175
|
);
|
|
1807
2176
|
if (!conversationId) return;
|
|
1808
2177
|
const endTs = parseIso(cc["end_timestamp"] ?? cc["compacted_at"]);
|
|
2178
|
+
const compactedAt = parseIso(cc["compacted_at"]);
|
|
2179
|
+
const compactedCount = Number(cc["compacted_message_count"] ?? 0) || 0;
|
|
2180
|
+
const totalCount = Number(cc["total_message_count"] ?? 0) || 0;
|
|
1809
2181
|
const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2182
|
+
const heldAt = parseIso(entry.compactedAt);
|
|
2183
|
+
if (compactedAt !== null && heldAt !== null && compactedAt < heldAt) return;
|
|
2184
|
+
const block = contextBlockOf(cc);
|
|
2185
|
+
if (block) {
|
|
2186
|
+
entry.contextBlock = block;
|
|
2187
|
+
entry.summary = cc["summary"] || block;
|
|
2188
|
+
entry.factualParagraph = null;
|
|
2189
|
+
entry.conversationalParagraph = null;
|
|
2190
|
+
} else {
|
|
2191
|
+
entry.summary = cc["summary"] || entry.summary;
|
|
2192
|
+
entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
|
|
2193
|
+
entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
|
|
2194
|
+
}
|
|
1813
2195
|
entry.currentState = { ...cc["current_state"] ?? {} };
|
|
1814
2196
|
entry.keyExtractions = { ...cc["key_extractions"] ?? {} };
|
|
1815
2197
|
entry.compactionId = cc["compaction_id"] || entry.compactionId;
|
|
1816
2198
|
entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
|
|
1817
2199
|
entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
|
|
1818
|
-
if (
|
|
1819
|
-
|
|
2200
|
+
if (compactedCount > 0 && compactedCount <= totalCount) {
|
|
2201
|
+
const uncovered = totalCount - compactedCount;
|
|
2202
|
+
const serverTurns = Array.isArray(cc["recent_turns"]) ? cc["recent_turns"] : [];
|
|
2203
|
+
const tail = uncovered > 0 ? serverTurns.slice(-uncovered).map((t) => ({ ...t })) : [];
|
|
2204
|
+
const ahead = localMessageCount(entry) - totalCount;
|
|
2205
|
+
const newer = entry.synced && ahead > 0 ? entry.recentTurns.slice(-ahead).filter((t) => !heldIn(t, tail)) : [];
|
|
2206
|
+
entry.recentTurns = [...tail, ...newer];
|
|
2207
|
+
entry.compactedMessageCount = compactedCount;
|
|
2208
|
+
entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, totalCount);
|
|
2209
|
+
entry.synced = tail.length === uncovered;
|
|
2210
|
+
} else {
|
|
2211
|
+
if (endTs !== null && entry.recentTurns.length > 0) {
|
|
2212
|
+
entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
|
|
2213
|
+
}
|
|
2214
|
+
entry.synced = false;
|
|
1820
2215
|
}
|
|
1821
2216
|
entry.lastActivityAt = this.#now();
|
|
1822
2217
|
this.#touch(conversationId, entry);
|
|
1823
2218
|
this.#evict();
|
|
1824
2219
|
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Bring an entry back in step from a `/context-for-prompt` response.
|
|
2222
|
+
*
|
|
2223
|
+
* Called after the server was asked because the entry was not complete. The
|
|
2224
|
+
* response lists every message the server's summary does not cover, which is
|
|
2225
|
+
* what `recentTurns` is meant to hold.
|
|
2226
|
+
*
|
|
2227
|
+
* Only an entry that already exists is reseeded. Creating one here would let
|
|
2228
|
+
* a process that never writes to the conversation answer from a snapshot it
|
|
2229
|
+
* has no way to refresh.
|
|
2230
|
+
*
|
|
2231
|
+
* If the server's summary is not the one cached here, the entry cannot
|
|
2232
|
+
* render it, so it stays out of sync until the next bundle brings the
|
|
2233
|
+
* summary itself.
|
|
2234
|
+
*
|
|
2235
|
+
* Turns held locally and absent from the response are kept when they come
|
|
2236
|
+
* after everything the response has: they were written or sent after the
|
|
2237
|
+
* server read its messages.
|
|
2238
|
+
*/
|
|
2239
|
+
reseed(conversationId, state) {
|
|
2240
|
+
if (!conversationId) return;
|
|
2241
|
+
const entry = this.#entries.get(conversationId);
|
|
2242
|
+
if (entry === void 0 || entry.shared) return;
|
|
2243
|
+
entry.serverMessageCount = Math.max(
|
|
2244
|
+
entry.serverMessageCount ?? 0,
|
|
2245
|
+
Number(state.totalMessageCount) || 0
|
|
2246
|
+
);
|
|
2247
|
+
if ((state.compactionId || null) !== entry.compactionId) {
|
|
2248
|
+
entry.synced = false;
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const serverTurns = state.recentMessages.map((m) => ({ ...m }));
|
|
2252
|
+
entry.recentTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
|
|
2253
|
+
entry.compactedMessageCount = Number(state.compactedMessageCount) || 0;
|
|
2254
|
+
entry.synced = true;
|
|
2255
|
+
}
|
|
1825
2256
|
invalidate(conversationId) {
|
|
1826
2257
|
this.#entries.delete(conversationId);
|
|
1827
2258
|
}
|
|
@@ -1832,6 +2263,7 @@ var ShortTermStore = class {
|
|
|
1832
2263
|
return {
|
|
1833
2264
|
conversationId,
|
|
1834
2265
|
summary: null,
|
|
2266
|
+
contextBlock: null,
|
|
1835
2267
|
factualParagraph: null,
|
|
1836
2268
|
conversationalParagraph: null,
|
|
1837
2269
|
currentState: {},
|
|
@@ -1840,7 +2272,12 @@ var ShortTermStore = class {
|
|
|
1840
2272
|
compactedAt: null,
|
|
1841
2273
|
endTimestamp: null,
|
|
1842
2274
|
recentTurns: [],
|
|
1843
|
-
lastActivityAt: this.#now()
|
|
2275
|
+
lastActivityAt: this.#now(),
|
|
2276
|
+
compactedMessageCount: 0,
|
|
2277
|
+
serverMessageCount: null,
|
|
2278
|
+
participant: false,
|
|
2279
|
+
synced: false,
|
|
2280
|
+
shared: false
|
|
1844
2281
|
};
|
|
1845
2282
|
}
|
|
1846
2283
|
/** Re-insert so this key becomes the most recently used. */
|
|
@@ -1857,6 +2294,520 @@ var ShortTermStore = class {
|
|
|
1857
2294
|
}
|
|
1858
2295
|
};
|
|
1859
2296
|
|
|
2297
|
+
// src/cache/turn-store.ts
|
|
2298
|
+
var SWEEP_EVERY_WRITES = 50;
|
|
2299
|
+
function bytesOf(text) {
|
|
2300
|
+
return new TextEncoder().encode(text).length;
|
|
2301
|
+
}
|
|
2302
|
+
var TurnStore = class {
|
|
2303
|
+
limits;
|
|
2304
|
+
onDisk = false;
|
|
2305
|
+
#now;
|
|
2306
|
+
#seq = 0;
|
|
2307
|
+
#writes = 0;
|
|
2308
|
+
#conversations = /* @__PURE__ */ new Map();
|
|
2309
|
+
#vectors = /* @__PURE__ */ new Map();
|
|
2310
|
+
constructor(options = {}) {
|
|
2311
|
+
this.limits = options.limits ?? { ...HISTORY_SEARCH.store_limits };
|
|
2312
|
+
this.#now = options.now ?? (() => Date.now() / 1e3);
|
|
2313
|
+
}
|
|
2314
|
+
add(conversationId, role, content, timestamp, options = {}) {
|
|
2315
|
+
this.addMany(conversationId, [
|
|
2316
|
+
{ role, content, timestamp: timestamp ?? "", message_id: options.messageId ?? null, position: options.position ?? null }
|
|
2317
|
+
]);
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Insert turns not held yet; returns how many were new. A turn held without
|
|
2321
|
+
* an id or position (a streamed write) gets them when the same role and
|
|
2322
|
+
* text come back from the server.
|
|
2323
|
+
*/
|
|
2324
|
+
addMany(conversationId, turns) {
|
|
2325
|
+
if (!conversationId) return 0;
|
|
2326
|
+
const conv = this.#conversations.get(conversationId) ?? { lastActivity: 0, turns: [] };
|
|
2327
|
+
let added = 0;
|
|
2328
|
+
for (const turn of turns) {
|
|
2329
|
+
const role = String(turn.role || "user");
|
|
2330
|
+
const content = String(turn.content ?? "");
|
|
2331
|
+
if (!content) continue;
|
|
2332
|
+
const messageId = turn.message_id ? String(turn.message_id) : null;
|
|
2333
|
+
const raw = turn.position;
|
|
2334
|
+
const position = typeof raw === "number" && Number.isInteger(raw) ? raw : /^\d+$/.test(String(raw ?? "")) ? Number(raw) : null;
|
|
2335
|
+
const ts = turn.timestamp instanceof Date ? turn.timestamp.toISOString() : String(turn.timestamp ?? "");
|
|
2336
|
+
if (messageId !== null) {
|
|
2337
|
+
const held = conv.turns.find((t) => t.message_id === messageId);
|
|
2338
|
+
if (held !== void 0) {
|
|
2339
|
+
if (position !== null) held.position = position;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
const same = conv.turns.find(
|
|
2344
|
+
(t) => t.role === role && t.content === content && (t.message_id === null || t.position === null)
|
|
2345
|
+
);
|
|
2346
|
+
if (same !== void 0) {
|
|
2347
|
+
same.message_id = same.message_id ?? messageId;
|
|
2348
|
+
same.position = position ?? same.position;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
conv.turns.push({ seq: ++this.#seq, role, content, timestamp: ts, message_id: messageId, position });
|
|
2352
|
+
added += 1;
|
|
2353
|
+
}
|
|
2354
|
+
conv.lastActivity = this.#now();
|
|
2355
|
+
this.#conversations.set(conversationId, conv);
|
|
2356
|
+
this.#trim(conv);
|
|
2357
|
+
this.#writes += 1;
|
|
2358
|
+
if (this.#writes % SWEEP_EVERY_WRITES === 0) this.sweep();
|
|
2359
|
+
return added;
|
|
2360
|
+
}
|
|
2361
|
+
#trim(conv) {
|
|
2362
|
+
const limits = this.limits;
|
|
2363
|
+
let size = conv.turns.reduce((n2, t) => n2 + bytesOf(t.content), 0);
|
|
2364
|
+
while (conv.turns.length > limits.max_turns_per_conversation || size > limits.max_bytes_per_conversation) {
|
|
2365
|
+
const excess = Math.max(conv.turns.length - limits.max_turns_per_conversation, 1);
|
|
2366
|
+
const order = [...conv.turns].sort(
|
|
2367
|
+
(a, b) => (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || a.seq - b.seq
|
|
2368
|
+
);
|
|
2369
|
+
const gone = new Set(order.slice(0, excess).map((t) => t.seq));
|
|
2370
|
+
if (gone.size === 0) break;
|
|
2371
|
+
for (const t of conv.turns) if (gone.has(t.seq)) size -= bytesOf(t.content);
|
|
2372
|
+
conv.turns = conv.turns.filter((t) => !gone.has(t.seq));
|
|
2373
|
+
for (const vectors of this.#vectors.values()) for (const seq of gone) vectors.delete(seq);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
/** Idle conversations, then the least recently used beyond the count, then more until the bytes fit. */
|
|
2377
|
+
sweep() {
|
|
2378
|
+
const limits = this.limits;
|
|
2379
|
+
const now = this.#now();
|
|
2380
|
+
for (const [id, conv] of this.#conversations) {
|
|
2381
|
+
if (conv.lastActivity < now - limits.idle_ttl_seconds) this.drop(id);
|
|
2382
|
+
}
|
|
2383
|
+
const byRecent = [...this.#conversations.entries()].sort((a, b) => b[1].lastActivity - a[1].lastActivity);
|
|
2384
|
+
for (const [id] of byRecent.slice(limits.max_conversations)) this.drop(id);
|
|
2385
|
+
while (this.sizeBytes() > limits.max_file_bytes) {
|
|
2386
|
+
const oldest = this.conversations()[0];
|
|
2387
|
+
if (oldest === void 0) break;
|
|
2388
|
+
this.drop(oldest);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
drop(conversationId) {
|
|
2392
|
+
const conv = this.#conversations.get(conversationId);
|
|
2393
|
+
if (conv === void 0) return;
|
|
2394
|
+
for (const vectors of this.#vectors.values()) for (const t of conv.turns) vectors.delete(t.seq);
|
|
2395
|
+
this.#conversations.delete(conversationId);
|
|
2396
|
+
}
|
|
2397
|
+
clear() {
|
|
2398
|
+
this.#conversations.clear();
|
|
2399
|
+
this.#vectors.clear();
|
|
2400
|
+
}
|
|
2401
|
+
close() {
|
|
2402
|
+
}
|
|
2403
|
+
/** In conversation order: by server position where known, else as written here. */
|
|
2404
|
+
turns(conversationId) {
|
|
2405
|
+
const conv = this.#conversations.get(conversationId);
|
|
2406
|
+
if (conv === void 0) return [];
|
|
2407
|
+
const bySeq = [...conv.turns].sort((a, b) => a.seq - b.seq);
|
|
2408
|
+
const key = /* @__PURE__ */ new Map();
|
|
2409
|
+
let top = 0;
|
|
2410
|
+
let step = 0;
|
|
2411
|
+
for (const t of bySeq) {
|
|
2412
|
+
if (t.position !== null) {
|
|
2413
|
+
key.set(t.seq, t.position);
|
|
2414
|
+
top = Math.max(top, t.position);
|
|
2415
|
+
step = 0;
|
|
2416
|
+
} else {
|
|
2417
|
+
step += 1;
|
|
2418
|
+
key.set(t.seq, top + step * 1e-6);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return bySeq.sort((a, b) => (key.get(a.seq) ?? 0) - (key.get(b.seq) ?? 0) || a.seq - b.seq).map((t) => ({ ...t }));
|
|
2422
|
+
}
|
|
2423
|
+
/** Turns the server also holds: archived tool results are local only. Python: `count`. */
|
|
2424
|
+
count(conversationId) {
|
|
2425
|
+
return (this.#conversations.get(conversationId)?.turns ?? []).filter((t) => t.role !== "tool").length;
|
|
2426
|
+
}
|
|
2427
|
+
sizeBytes() {
|
|
2428
|
+
let n2 = 0;
|
|
2429
|
+
for (const conv of this.#conversations.values()) for (const t of conv.turns) n2 += bytesOf(t.content);
|
|
2430
|
+
return n2;
|
|
2431
|
+
}
|
|
2432
|
+
/** Conversation ids, least recently active first. */
|
|
2433
|
+
conversations() {
|
|
2434
|
+
return [...this.#conversations.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity).map(([id]) => id);
|
|
2435
|
+
}
|
|
2436
|
+
vectors(seqs, model) {
|
|
2437
|
+
const held = this.#vectors.get(model);
|
|
2438
|
+
const out = /* @__PURE__ */ new Map();
|
|
2439
|
+
if (held === void 0) return out;
|
|
2440
|
+
for (const seq of seqs) {
|
|
2441
|
+
const v = held.get(seq);
|
|
2442
|
+
if (v !== void 0) out.set(seq, v);
|
|
2443
|
+
}
|
|
2444
|
+
return out;
|
|
2445
|
+
}
|
|
2446
|
+
putVectors(model, items) {
|
|
2447
|
+
const held = this.#vectors.get(model) ?? /* @__PURE__ */ new Map();
|
|
2448
|
+
for (const [seq, vec] of items) held.set(seq, vec);
|
|
2449
|
+
this.#vectors.set(model, held);
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
// src/cache/history-search.ts
|
|
2454
|
+
var WORD_RE = /[\p{L}\p{N}\p{M}]+/gu;
|
|
2455
|
+
var FORMS = HISTORY_SEARCH.irregular_forms ?? {};
|
|
2456
|
+
function tokenize2(text) {
|
|
2457
|
+
const out = [];
|
|
2458
|
+
for (const word of (text || "").toLowerCase().match(WORD_RE) ?? []) {
|
|
2459
|
+
if (/^[\x00-\x7f]+$/.test(word)) out.push(...tokenize(FORMS[word] ?? word));
|
|
2460
|
+
else out.push(word);
|
|
2461
|
+
}
|
|
2462
|
+
return out;
|
|
2463
|
+
}
|
|
2464
|
+
var LITERAL_SOURCE = String.raw`"([^"]{3,})"|\b([A-Z0-9][A-Z0-9-]{3,})\b|(\d[\d,.:/-]*\d|\d)`;
|
|
2465
|
+
var SENTENCE_RE = /(?<=[.!?])\s+/;
|
|
2466
|
+
function literalsOf(query) {
|
|
2467
|
+
const out = [];
|
|
2468
|
+
for (const m of (query || "").matchAll(new RegExp(LITERAL_SOURCE, "g"))) {
|
|
2469
|
+
const quoted = m[1];
|
|
2470
|
+
const value = quoted || m[2] || m[3];
|
|
2471
|
+
if (value && (quoted || /\d/.test(value))) out.push(value.toLowerCase());
|
|
2472
|
+
}
|
|
2473
|
+
return out;
|
|
2474
|
+
}
|
|
2475
|
+
function cosine(a, b) {
|
|
2476
|
+
let dot = 0;
|
|
2477
|
+
let na = 0;
|
|
2478
|
+
let nb = 0;
|
|
2479
|
+
const n2 = Math.min(a.length, b.length);
|
|
2480
|
+
for (let i = 0; i < n2; i++) {
|
|
2481
|
+
dot += a[i] * b[i];
|
|
2482
|
+
na += a[i] * a[i];
|
|
2483
|
+
nb += b[i] * b[i];
|
|
2484
|
+
}
|
|
2485
|
+
return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
2486
|
+
}
|
|
2487
|
+
function words(text) {
|
|
2488
|
+
return text.split(/\s+/).filter(Boolean);
|
|
2489
|
+
}
|
|
2490
|
+
function excerptOf(content, terms, literals, maxWords) {
|
|
2491
|
+
let sentences = content.trim().split(SENTENCE_RE).filter((s2) => s2.trim());
|
|
2492
|
+
if (sentences.length === 0) sentences = [content.trim()];
|
|
2493
|
+
const score = (sentence) => {
|
|
2494
|
+
const low = sentence.toLowerCase();
|
|
2495
|
+
const held = new Set(tokenize2(sentence));
|
|
2496
|
+
let n2 = 0;
|
|
2497
|
+
for (const t of terms) if (held.has(t)) n2 += 1;
|
|
2498
|
+
for (const lit of literals) if (low.includes(lit)) n2 += 2;
|
|
2499
|
+
return n2;
|
|
2500
|
+
};
|
|
2501
|
+
let best = 0;
|
|
2502
|
+
for (let i = 1; i < sentences.length; i++) if (score(sentences[i]) > score(sentences[best])) best = i;
|
|
2503
|
+
let lo = best;
|
|
2504
|
+
let hi = best;
|
|
2505
|
+
let count = words(sentences[best]).length;
|
|
2506
|
+
for (; ; ) {
|
|
2507
|
+
let grew = false;
|
|
2508
|
+
for (const j of [hi + 1, lo - 1]) {
|
|
2509
|
+
if (j >= 0 && j < sentences.length && count + words(sentences[j]).length <= maxWords) {
|
|
2510
|
+
count += words(sentences[j]).length;
|
|
2511
|
+
lo = Math.min(lo, j);
|
|
2512
|
+
hi = Math.max(hi, j);
|
|
2513
|
+
grew = true;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if (!grew) break;
|
|
2517
|
+
}
|
|
2518
|
+
let text = sentences.slice(lo, hi + 1).join(" ");
|
|
2519
|
+
if (words(text).length > maxWords) text = words(text).slice(0, maxWords).join(" ") + " ...";
|
|
2520
|
+
return (lo > 0 ? "... " : "") + text + (hi < sentences.length - 1 && !text.endsWith("...") ? " ..." : "");
|
|
2521
|
+
}
|
|
2522
|
+
var FENCE = /^\s*(?:```|~~~)/;
|
|
2523
|
+
var TABLE_RULE = /^\s*\|?\s*:?-{2,}/;
|
|
2524
|
+
var SENTENCE = /(?<=[.!?])\s+/;
|
|
2525
|
+
function splitPassages(content, maxWords) {
|
|
2526
|
+
const text = (content ?? "").trim();
|
|
2527
|
+
if (words(text).length <= maxWords) return text ? [text] : [];
|
|
2528
|
+
const out = [];
|
|
2529
|
+
const lines = text.split("\n");
|
|
2530
|
+
let i = 0;
|
|
2531
|
+
while (i < lines.length) {
|
|
2532
|
+
const line = lines[i];
|
|
2533
|
+
if (FENCE.test(line)) {
|
|
2534
|
+
let j2 = i + 1;
|
|
2535
|
+
while (j2 < lines.length && !FENCE.test(lines[j2])) j2 += 1;
|
|
2536
|
+
out.push(...windowsOf(lines.slice(i + 1, j2), maxWords));
|
|
2537
|
+
i = j2 + 1;
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (line.trim().startsWith("|")) {
|
|
2541
|
+
let j2 = i;
|
|
2542
|
+
while (j2 < lines.length && lines[j2].trim().startsWith("|")) j2 += 1;
|
|
2543
|
+
const rows = lines.slice(i, j2).filter((r) => !TABLE_RULE.test(r)).map((r) => r.trim());
|
|
2544
|
+
const [header, body] = rows.length > 1 ? [rows[0], rows.slice(1)] : ["", rows];
|
|
2545
|
+
for (const row of body) {
|
|
2546
|
+
const both = `${header} ${row}`.trim();
|
|
2547
|
+
out.push(words(both).length <= Math.trunc(maxWords * 3 / 2) ? both : row);
|
|
2548
|
+
}
|
|
2549
|
+
i = j2;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
let j = i;
|
|
2553
|
+
while (j < lines.length && lines[j].trim() && !FENCE.test(lines[j]) && !lines[j].trim().startsWith("|")) j += 1;
|
|
2554
|
+
if (j === i) {
|
|
2555
|
+
i += 1;
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const block = lines.slice(i, j);
|
|
2559
|
+
if (block.length > 1 && block.every((b) => words(b).length <= 25)) {
|
|
2560
|
+
out.push(...windowsOf(block, maxWords));
|
|
2561
|
+
} else {
|
|
2562
|
+
const sentences = block.map((b) => b.trim()).join(" ").split(SENTENCE).filter((x) => x.trim());
|
|
2563
|
+
out.push(...windowsOf(sentences, maxWords));
|
|
2564
|
+
}
|
|
2565
|
+
i = j;
|
|
2566
|
+
}
|
|
2567
|
+
const kept = out.filter((x) => x.trim());
|
|
2568
|
+
return kept.length ? kept : [text];
|
|
2569
|
+
}
|
|
2570
|
+
function windowsOf(pieces, maxWords) {
|
|
2571
|
+
const out = [];
|
|
2572
|
+
let cur = [];
|
|
2573
|
+
let used = 0;
|
|
2574
|
+
for (const raw of pieces) {
|
|
2575
|
+
const piece = raw.trim();
|
|
2576
|
+
if (!piece) continue;
|
|
2577
|
+
const n2 = words(piece).length;
|
|
2578
|
+
if (cur.length && used + n2 > maxWords) {
|
|
2579
|
+
out.push(cur.join(" "));
|
|
2580
|
+
cur = [];
|
|
2581
|
+
used = 0;
|
|
2582
|
+
}
|
|
2583
|
+
if (n2 > maxWords) {
|
|
2584
|
+
const w = words(piece);
|
|
2585
|
+
for (let k = 0; k < w.length; k += maxWords) out.push(w.slice(k, k + maxWords).join(" "));
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
cur.push(piece);
|
|
2589
|
+
used += n2;
|
|
2590
|
+
}
|
|
2591
|
+
if (cur.length) out.push(cur.join(" "));
|
|
2592
|
+
return out;
|
|
2593
|
+
}
|
|
2594
|
+
function askedKinds(query) {
|
|
2595
|
+
const out = [];
|
|
2596
|
+
for (const [name, spec] of Object.entries(HISTORY_SEARCH.asked_kinds ?? {})) {
|
|
2597
|
+
if (name.startsWith("$") || typeof spec !== "object") continue;
|
|
2598
|
+
if (new RegExp(spec.query, "i").test(query ?? "")) out.push(new RegExp(spec.value));
|
|
2599
|
+
}
|
|
2600
|
+
return out;
|
|
2601
|
+
}
|
|
2602
|
+
function asksHowItStarted(query) {
|
|
2603
|
+
const spec = HISTORY_SEARCH.early_query;
|
|
2604
|
+
if (!spec?.pattern) return false;
|
|
2605
|
+
return new RegExp(spec.pattern, (spec.flags ?? "").includes("i") ? "i" : "").test(query ?? "");
|
|
2606
|
+
}
|
|
2607
|
+
async function searchTurns(turns, query, options = {}) {
|
|
2608
|
+
const params = HISTORY_SEARCH;
|
|
2609
|
+
const k = params.max_excerpts;
|
|
2610
|
+
const skip = new Set(options.exclude ?? []);
|
|
2611
|
+
const pool = turns.filter((t) => t.content && !skip.has(t.content) && !skip.has(t.message_id ?? ""));
|
|
2612
|
+
const terms = new Set(tokenize2(query || ""));
|
|
2613
|
+
const literals = literalsOf(query);
|
|
2614
|
+
if (pool.length === 0 || terms.size === 0 && literals.length === 0 && !options.encoder) return [];
|
|
2615
|
+
const maxWords = params.max_words_per_excerpt;
|
|
2616
|
+
const units = [];
|
|
2617
|
+
pool.forEach((t, ti) => {
|
|
2618
|
+
if (!params.passages) {
|
|
2619
|
+
units.push({ seq: t.seq, content: t.content, turn: ti, first: true, last: true, part: -1 });
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const parts = splitPassages(t.content, maxWords);
|
|
2623
|
+
parts.forEach((part, pi) => {
|
|
2624
|
+
units.push({
|
|
2625
|
+
seq: t.seq,
|
|
2626
|
+
content: part,
|
|
2627
|
+
turn: ti,
|
|
2628
|
+
first: pi === 0,
|
|
2629
|
+
last: pi === parts.length - 1,
|
|
2630
|
+
part: parts.length > 1 ? pi : -1
|
|
2631
|
+
});
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
const docs = units.map((u) => tokenize2(u.content));
|
|
2635
|
+
const bm = new BM25(docs);
|
|
2636
|
+
const termList = [...terms];
|
|
2637
|
+
const rows = units.map((turn, i) => {
|
|
2638
|
+
const held = new Set(docs[i]);
|
|
2639
|
+
let matched = 0;
|
|
2640
|
+
for (const t of terms) if (held.has(t)) matched += 1;
|
|
2641
|
+
const low = turn.content.toLowerCase();
|
|
2642
|
+
let literal = 0;
|
|
2643
|
+
for (const lit of literals) if (low.includes(lit)) literal += 1;
|
|
2644
|
+
return { i, bm25: matched ? bm.score(termList, i) : 0, matched, literal };
|
|
2645
|
+
});
|
|
2646
|
+
const need = Math.min(params.min_matched_terms, Math.max(1, terms.size));
|
|
2647
|
+
const rareCap = Math.max(1, Math.trunc((params.rare_term_max_share ?? 0) * units.length));
|
|
2648
|
+
const rare = /* @__PURE__ */ new Set();
|
|
2649
|
+
for (const t of terms) {
|
|
2650
|
+
let n3 = 0;
|
|
2651
|
+
for (const doc of docs) if (doc.includes(t)) n3 += 1;
|
|
2652
|
+
if (n3 > 0 && n3 <= rareCap) rare.add(t);
|
|
2653
|
+
}
|
|
2654
|
+
let eligible;
|
|
2655
|
+
if (params.passages) {
|
|
2656
|
+
const turnTerms = /* @__PURE__ */ new Map();
|
|
2657
|
+
for (const r of rows) {
|
|
2658
|
+
const held = turnTerms.get(units[r.i].turn) ?? /* @__PURE__ */ new Set();
|
|
2659
|
+
for (const t of docs[r.i]) if (terms.has(t)) held.add(t);
|
|
2660
|
+
turnTerms.set(units[r.i].turn, held);
|
|
2661
|
+
}
|
|
2662
|
+
eligible = rows.filter(
|
|
2663
|
+
(r) => r.literal > 0 || r.matched > 0 && (turnTerms.get(units[r.i].turn).size >= need || docs[r.i].some((t) => rare.has(t)))
|
|
2664
|
+
);
|
|
2665
|
+
} else {
|
|
2666
|
+
eligible = rows.filter(
|
|
2667
|
+
(r) => r.literal > 0 || r.matched >= need || r.matched > 0 && docs[r.i].some((t) => rare.has(t))
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
const rrf = params.rrf_k;
|
|
2671
|
+
const fused = /* @__PURE__ */ new Map();
|
|
2672
|
+
const via = /* @__PURE__ */ new Map();
|
|
2673
|
+
for (const r of eligible) fused.set(r.i, 0);
|
|
2674
|
+
const add = (i, rank, how) => {
|
|
2675
|
+
fused.set(i, (fused.get(i) ?? 0) + 1 / (rrf + rank + 1));
|
|
2676
|
+
if (!via.has(i)) via.set(i, how);
|
|
2677
|
+
};
|
|
2678
|
+
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"));
|
|
2679
|
+
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"));
|
|
2680
|
+
const kinds = askedKinds(query);
|
|
2681
|
+
if (kinds.length) {
|
|
2682
|
+
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"));
|
|
2683
|
+
}
|
|
2684
|
+
if (options.encoder) {
|
|
2685
|
+
const ranked2 = await embeddingRanks(units, query, options.encoder, options.store ?? null, params.min_cosine ?? 0.4);
|
|
2686
|
+
ranked2.forEach(([i], rank) => add(i, rank, "embedding"));
|
|
2687
|
+
}
|
|
2688
|
+
if (fused.size === 0) return [];
|
|
2689
|
+
const n2 = Math.max(pool.length - 1, 1);
|
|
2690
|
+
const weight = asksHowItStarted(query) ? 0 : params.recency_weight;
|
|
2691
|
+
const top = /* @__PURE__ */ new Map();
|
|
2692
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
2693
|
+
for (const [u, v] of fused) {
|
|
2694
|
+
const ti = units[u].turn;
|
|
2695
|
+
const score = v + weight * (ti / n2) / (rrf + 1);
|
|
2696
|
+
ranked.set(ti, [...ranked.get(ti) ?? [], [score, u]]);
|
|
2697
|
+
const held = top.get(ti);
|
|
2698
|
+
if (!held || score > held[0]) top.set(ti, [score, u]);
|
|
2699
|
+
}
|
|
2700
|
+
const best = [...top.keys()].sort((a, b) => top.get(b)[0] - top.get(a)[0] || a - b).slice(0, k);
|
|
2701
|
+
const perTurn = params.passages_per_turn ?? 1;
|
|
2702
|
+
const shown = (ti) => {
|
|
2703
|
+
const unit = units[top.get(ti)[1]];
|
|
2704
|
+
if (!params.passages || unit.first && unit.last) return excerptOf(pool[ti].content, terms, literals, maxWords);
|
|
2705
|
+
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);
|
|
2706
|
+
const parts = [];
|
|
2707
|
+
for (const u of picked) parts.push((units[u].first || parts.length ? "" : "... ") + units[u].content);
|
|
2708
|
+
return parts.join(" ... ") + (units[picked[picked.length - 1]].last ? "" : " ...");
|
|
2709
|
+
};
|
|
2710
|
+
const found = /* @__PURE__ */ new Map();
|
|
2711
|
+
for (const ti of best) {
|
|
2712
|
+
found.set(ti, {
|
|
2713
|
+
position: pool[ti].position,
|
|
2714
|
+
role: pool[ti].role,
|
|
2715
|
+
timestamp: pool[ti].timestamp,
|
|
2716
|
+
score: top.get(ti)[0],
|
|
2717
|
+
via: via.get(top.get(ti)[1]) ?? "",
|
|
2718
|
+
text: shown(ti)
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
const before2 = params.previous_turns ?? 0;
|
|
2722
|
+
const tail = params.previous_turn_words ?? 40;
|
|
2723
|
+
for (const i of best) {
|
|
2724
|
+
for (let j = i - before2; j < i; j++) {
|
|
2725
|
+
if (j < 0 || found.has(j)) continue;
|
|
2726
|
+
const hit = pool[i].position;
|
|
2727
|
+
const prev = pool[j].position;
|
|
2728
|
+
if (hit && prev && hit - prev > before2) continue;
|
|
2729
|
+
const w = words(pool[j].content);
|
|
2730
|
+
found.set(j, {
|
|
2731
|
+
position: prev,
|
|
2732
|
+
role: pool[j].role,
|
|
2733
|
+
timestamp: pool[j].timestamp,
|
|
2734
|
+
score: 0,
|
|
2735
|
+
via: "previous",
|
|
2736
|
+
text: w.length <= tail ? w.join(" ") : "... " + w.slice(-tail).join(" ")
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return [...found.keys()].sort((a, b) => a - b).map((i) => found.get(i));
|
|
2741
|
+
}
|
|
2742
|
+
async function embeddingRanks(pool, query, encoder, store, minCosine) {
|
|
2743
|
+
const keyOf = (u) => (u.part ?? -1) < 0 ? encoder.model : `${encoder.model}#p${u.part}`;
|
|
2744
|
+
const vectors = /* @__PURE__ */ new Map();
|
|
2745
|
+
if (store !== null) {
|
|
2746
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2747
|
+
pool.forEach((u, i) => byKey.set(keyOf(u), [...byKey.get(keyOf(u)) ?? [], i]));
|
|
2748
|
+
for (const [key, idx] of byKey) {
|
|
2749
|
+
const held = store.vectors(idx.map((i) => pool[i].seq), key);
|
|
2750
|
+
for (const i of idx) {
|
|
2751
|
+
const v = held.get(pool[i].seq);
|
|
2752
|
+
if (v) vectors.set(i, v);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
const missing = pool.map((_, i) => i).filter((i) => !vectors.has(i));
|
|
2757
|
+
if (missing.length > 0) {
|
|
2758
|
+
const fresh = await encoder.encode(missing.map((i) => pool[i].content));
|
|
2759
|
+
const put = /* @__PURE__ */ new Map();
|
|
2760
|
+
missing.forEach((i, j) => {
|
|
2761
|
+
const v = Float32Array.from(fresh[j] ?? []);
|
|
2762
|
+
vectors.set(i, v);
|
|
2763
|
+
const key = keyOf(pool[i]);
|
|
2764
|
+
put.set(key, (put.get(key) ?? /* @__PURE__ */ new Map()).set(pool[i].seq, v));
|
|
2765
|
+
});
|
|
2766
|
+
if (store) for (const [key, items] of put) store.putVectors(key, items);
|
|
2767
|
+
}
|
|
2768
|
+
const q = (await (encoder.encodeQuery ?? encoder.encode).call(encoder, [query]))[0] ?? [];
|
|
2769
|
+
return pool.map((_, i) => [i, cosine(q, vectors.get(i) ?? [])]).filter(([, c]) => c >= minCosine).sort((a, b) => b[1] - a[1]);
|
|
2770
|
+
}
|
|
2771
|
+
function toolResultText(result, toolName) {
|
|
2772
|
+
let text = typeof result === "string" ? result : JSON.stringify(result) ?? String(result);
|
|
2773
|
+
const limit = HISTORY_SEARCH.tool_result_max_chars ?? 16e3;
|
|
2774
|
+
if (text.length > limit) text = text.slice(0, limit) + " ...[cut]";
|
|
2775
|
+
return `[tool result: ${toolName || "tool"}] ${text}`;
|
|
2776
|
+
}
|
|
2777
|
+
function renderExcerpts(excerpts, latest = "") {
|
|
2778
|
+
if (excerpts.length === 0) return "";
|
|
2779
|
+
const lines = [HISTORY_SEARCH.heading];
|
|
2780
|
+
for (const e of excerpts) {
|
|
2781
|
+
let where = e.position ? `turn ${e.position}, ${e.role}` : `${e.role}, earlier`;
|
|
2782
|
+
if (e.timestamp) {
|
|
2783
|
+
const ago = before(e.timestamp, latest);
|
|
2784
|
+
where += `, ${e.timestamp}` + (ago ? ` (${ago})` : "");
|
|
2785
|
+
}
|
|
2786
|
+
lines.push(`- ${where}: "${e.text}"`);
|
|
2787
|
+
}
|
|
2788
|
+
return lines.join("\n");
|
|
2789
|
+
}
|
|
2790
|
+
var ZONED = /(?:Z|[+-]\d{2}:\d{2})$/;
|
|
2791
|
+
function instant(stamp) {
|
|
2792
|
+
const text = (stamp ?? "").trim();
|
|
2793
|
+
if (!ZONED.test(text)) return null;
|
|
2794
|
+
const ms = Date.parse(text);
|
|
2795
|
+
return Number.isNaN(ms) ? null : ms / 1e3;
|
|
2796
|
+
}
|
|
2797
|
+
function before(stamp, latest) {
|
|
2798
|
+
const at = instant(stamp);
|
|
2799
|
+
const now = instant(latest);
|
|
2800
|
+
if (at === null || now === null || now < at) return "";
|
|
2801
|
+
const seconds = Math.trunc(now - at);
|
|
2802
|
+
for (const [unit, size] of [["day", 86400], ["hour", 3600], ["minute", 60]]) {
|
|
2803
|
+
if (seconds >= size) {
|
|
2804
|
+
const n2 = Math.trunc(seconds / size);
|
|
2805
|
+
return `${n2} ${unit}${n2 !== 1 ? "s" : ""} before the latest message`;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return "less than a minute before the latest message";
|
|
2809
|
+
}
|
|
2810
|
+
|
|
1860
2811
|
// src/context/overlay.ts
|
|
1861
2812
|
var MAX_TURNS = 20;
|
|
1862
2813
|
var TOKEN_BUDGET = 2e3;
|
|
@@ -1885,7 +2836,23 @@ function overlayLocalRecentTurns(response, store, conversationId, configured) {
|
|
|
1885
2836
|
if (entry === null) return;
|
|
1886
2837
|
const existing = response.conversation_context ?? null;
|
|
1887
2838
|
if (existing !== null && !verbatimOverlayEnabled(configured)) return;
|
|
1888
|
-
const
|
|
2839
|
+
const serverCompaction = existing === null ? null : existing["compaction_id"];
|
|
2840
|
+
const localIsWhole = isComplete(entry) && (!serverCompaction || serverCompaction === entry.compactionId);
|
|
2841
|
+
let sourceTurns = entry.recentTurns;
|
|
2842
|
+
if (existing !== null && !localIsWhole) {
|
|
2843
|
+
const raw = Array.isArray(existing["recent_turns"]) ? existing["recent_turns"] : [];
|
|
2844
|
+
const serverTurns = raw.map((t) => ({
|
|
2845
|
+
role: String(t["role"] ?? "user"),
|
|
2846
|
+
content: String(t["content"] ?? ""),
|
|
2847
|
+
timestamp: String(t["timestamp"] ?? "")
|
|
2848
|
+
}));
|
|
2849
|
+
sourceTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];
|
|
2850
|
+
}
|
|
2851
|
+
const localTurns = budgetRecentTurns(sourceTurns).map((t) => ({
|
|
2852
|
+
role: t.role,
|
|
2853
|
+
content: t.content,
|
|
2854
|
+
timestamp: t.timestamp
|
|
2855
|
+
}));
|
|
1889
2856
|
const serverField = (name) => existing === null ? void 0 : existing[name];
|
|
1890
2857
|
const orLocal = (name, local) => {
|
|
1891
2858
|
const value = serverField(name);
|
|
@@ -1940,14 +2907,28 @@ function buildRecentMessages(turns) {
|
|
|
1940
2907
|
role: t.role || "user",
|
|
1941
2908
|
content: t.content ?? "",
|
|
1942
2909
|
timestamp: t.timestamp,
|
|
1943
|
-
message_id: `local-${idx}`
|
|
2910
|
+
message_id: t.message_id || `local-${idx}`
|
|
1944
2911
|
}));
|
|
1945
2912
|
}
|
|
1946
2913
|
function compactedMessageCount(_entry) {
|
|
1947
2914
|
return 0;
|
|
1948
2915
|
}
|
|
2916
|
+
function blockOf(entry) {
|
|
2917
|
+
if (entry.contextBlock) return entry.contextBlock.trim();
|
|
2918
|
+
if (entry.summary && entry.summary.startsWith(CONTEXT_BLOCK_PREFIX)) return entry.summary.trim();
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
1949
2921
|
function formatStructured(entry, recent) {
|
|
1950
2922
|
const parts = [];
|
|
2923
|
+
const block = blockOf(entry);
|
|
2924
|
+
if (block) {
|
|
2925
|
+
parts.push(block);
|
|
2926
|
+
if (recent.length > 0) {
|
|
2927
|
+
parts.push(`## Recent Turns (${recent.length})`);
|
|
2928
|
+
for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);
|
|
2929
|
+
}
|
|
2930
|
+
return parts.filter(Boolean).join("\n\n").trim();
|
|
2931
|
+
}
|
|
1951
2932
|
if (entry.summary) {
|
|
1952
2933
|
parts.push("## Summary");
|
|
1953
2934
|
parts.push(entry.summary.trim());
|
|
@@ -1982,9 +2963,11 @@ function formatStructured(entry, recent) {
|
|
|
1982
2963
|
}
|
|
1983
2964
|
function formatNarrative(entry, recent) {
|
|
1984
2965
|
const parts = [];
|
|
1985
|
-
|
|
1986
|
-
if (
|
|
1987
|
-
else if (entry.
|
|
2966
|
+
const block = blockOf(entry);
|
|
2967
|
+
if (block) parts.push(block);
|
|
2968
|
+
else if (entry.summary) parts.push(entry.summary.trim());
|
|
2969
|
+
if (!block && entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());
|
|
2970
|
+
else if (!block && entry.factualParagraph) parts.push(entry.factualParagraph.trim());
|
|
1988
2971
|
if (recent.length > 0) {
|
|
1989
2972
|
const tail = recent.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
1990
2973
|
parts.push(`Recent exchanges:
|
|
@@ -1994,6 +2977,10 @@ ${tail}`);
|
|
|
1994
2977
|
}
|
|
1995
2978
|
function formatBullets(entry, recent) {
|
|
1996
2979
|
const parts = [];
|
|
2980
|
+
const block = blockOf(entry);
|
|
2981
|
+
if (block) {
|
|
2982
|
+
return [block, ...recent.map((m) => `- ${m.role}: ${m.content}`)].join("\n").trim();
|
|
2983
|
+
}
|
|
1997
2984
|
if (entry.summary) parts.push(`- Summary: ${entry.summary.trim()}`);
|
|
1998
2985
|
for (const [k, v] of Object.entries(entry.currentState ?? {})) {
|
|
1999
2986
|
parts.push(`- ${k}: ${stringify(v)}`);
|
|
@@ -2031,7 +3018,9 @@ function renderForPrompt(entry, style = DEFAULT_STYLE, now = Date.now) {
|
|
|
2031
3018
|
recent_messages: recent,
|
|
2032
3019
|
recent_message_count: recent.length,
|
|
2033
3020
|
compacted_message_count: compacted,
|
|
2034
|
-
total_message_count: recent.length + compacted
|
|
3021
|
+
total_message_count: recent.length + compacted,
|
|
3022
|
+
context_block: blockOf(entry),
|
|
3023
|
+
compaction_id: entry.compactionId
|
|
2035
3024
|
};
|
|
2036
3025
|
}
|
|
2037
3026
|
function renderCompacted(entry, format = "structured", correlationId = "") {
|
|
@@ -2176,10 +3165,10 @@ function servedItemIds(hit) {
|
|
|
2176
3165
|
}
|
|
2177
3166
|
return ids;
|
|
2178
3167
|
}
|
|
2179
|
-
function tryServeFromCache(cache, scope, params, maxItems) {
|
|
3168
|
+
async function tryServeFromCache(cache, scope, params, maxItems) {
|
|
2180
3169
|
try {
|
|
2181
3170
|
const scoped = scopeLookupParams(scope, params);
|
|
2182
|
-
const hit = cache.
|
|
3171
|
+
const hit = await cache.lookupAsync({
|
|
2183
3172
|
searchQuery: scoped.searchQuery ?? null,
|
|
2184
3173
|
entityId: scoped.entityId ?? null,
|
|
2185
3174
|
customerId: scoped.customerId ?? null,
|
|
@@ -2267,6 +3256,8 @@ function mergeScopeResults(scopeResults) {
|
|
|
2267
3256
|
episodes: [],
|
|
2268
3257
|
emotions: [],
|
|
2269
3258
|
temporal_events: [],
|
|
3259
|
+
items: [],
|
|
3260
|
+
excerpts: [],
|
|
2270
3261
|
scope_map: {},
|
|
2271
3262
|
conversation_context: null,
|
|
2272
3263
|
profile: null,
|
|
@@ -2276,6 +3267,8 @@ function mergeScopeResults(scopeResults) {
|
|
|
2276
3267
|
total_items: 0,
|
|
2277
3268
|
metadata: null
|
|
2278
3269
|
};
|
|
3270
|
+
const rankedSeen = /* @__PURE__ */ new Set();
|
|
3271
|
+
const rankedItems = [];
|
|
2279
3272
|
for (const [scopeName, response] of scopeResults) {
|
|
2280
3273
|
out.scopes_queried.push(scopeName);
|
|
2281
3274
|
if (out.metadata === null && response.metadata !== void 0) {
|
|
@@ -2300,8 +3293,32 @@ function mergeScopeResults(scopeResults) {
|
|
|
2300
3293
|
out[collection].push(item);
|
|
2301
3294
|
}
|
|
2302
3295
|
}
|
|
3296
|
+
const excerpts = response["excerpts"];
|
|
3297
|
+
if (Array.isArray(excerpts)) {
|
|
3298
|
+
for (const item of excerpts) {
|
|
3299
|
+
const id = itemId2(item);
|
|
3300
|
+
if (id !== void 0) {
|
|
3301
|
+
if (seen.has(id)) continue;
|
|
3302
|
+
seen.add(id);
|
|
3303
|
+
out.scope_map[id] = scopeName;
|
|
3304
|
+
}
|
|
3305
|
+
out.excerpts.push(item);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
const ranked = response["items"];
|
|
3309
|
+
if (Array.isArray(ranked)) {
|
|
3310
|
+
for (const item of ranked) {
|
|
3311
|
+
const id = itemId2(item);
|
|
3312
|
+
if (id !== void 0) {
|
|
3313
|
+
if (rankedSeen.has(id)) continue;
|
|
3314
|
+
rankedSeen.add(id);
|
|
3315
|
+
}
|
|
3316
|
+
rankedItems.push(item);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
2303
3319
|
}
|
|
2304
|
-
out.
|
|
3320
|
+
out.items = sortByRankScore(rankedItems).map((item, n2) => ({ ...item, rank: n2 + 1 }));
|
|
3321
|
+
out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0) + out.excerpts.length;
|
|
2305
3322
|
return out;
|
|
2306
3323
|
}
|
|
2307
3324
|
function oneDecimal(value) {
|
|
@@ -2327,7 +3344,18 @@ function formatForPrompt(ctx, options = {}) {
|
|
|
2327
3344
|
const includeScope = options.includeScope ?? false;
|
|
2328
3345
|
const includeConversationContext = options.includeConversationContext ?? true;
|
|
2329
3346
|
const sections = [];
|
|
2330
|
-
|
|
3347
|
+
const ranked = ctx.items ?? [];
|
|
3348
|
+
if (ranked.length > 0) {
|
|
3349
|
+
const lines = ranked.map((it, n2) => {
|
|
3350
|
+
let label = rankedLabel(String(it["category"] ?? ""));
|
|
3351
|
+
if (includeScope) label += `, scope: ${scopeOf(ctx, it)}`;
|
|
3352
|
+
return `${n2 + 1}. [${label}] ${String(it.content ?? "")}`;
|
|
3353
|
+
});
|
|
3354
|
+
sections.push(`### Memories
|
|
3355
|
+
${lines.join("\n")}`);
|
|
3356
|
+
}
|
|
3357
|
+
const typed = ranked.length === 0;
|
|
3358
|
+
if (typed && ctx.facts.length > 0) {
|
|
2331
3359
|
const lines = ctx.facts.map((f) => {
|
|
2332
3360
|
let line = `- ${String(f.content ?? "")}`;
|
|
2333
3361
|
if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;
|
|
@@ -2336,7 +3364,7 @@ function formatForPrompt(ctx, options = {}) {
|
|
|
2336
3364
|
sections.push(`### Facts
|
|
2337
3365
|
${lines.join("\n")}`);
|
|
2338
3366
|
}
|
|
2339
|
-
if (ctx.preferences.length > 0) {
|
|
3367
|
+
if (typed && ctx.preferences.length > 0) {
|
|
2340
3368
|
const lines = ctx.preferences.map((p) => {
|
|
2341
3369
|
const extras = [];
|
|
2342
3370
|
if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);
|
|
@@ -2349,7 +3377,7 @@ ${lines.join("\n")}`);
|
|
|
2349
3377
|
sections.push(`### Preferences
|
|
2350
3378
|
${lines.join("\n")}`);
|
|
2351
3379
|
}
|
|
2352
|
-
if (ctx.episodes.length > 0) {
|
|
3380
|
+
if (typed && ctx.episodes.length > 0) {
|
|
2353
3381
|
const lines = ctx.episodes.map((e) => {
|
|
2354
3382
|
let line = `- ${String(e["summary"] ?? "")}`;
|
|
2355
3383
|
if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;
|
|
@@ -2358,7 +3386,7 @@ ${lines.join("\n")}`);
|
|
|
2358
3386
|
sections.push(`### Episodes
|
|
2359
3387
|
${lines.join("\n")}`);
|
|
2360
3388
|
}
|
|
2361
|
-
if (ctx.emotions.length > 0) {
|
|
3389
|
+
if (typed && ctx.emotions.length > 0) {
|
|
2362
3390
|
const lines = ctx.emotions.map((em) => {
|
|
2363
3391
|
const extras = [];
|
|
2364
3392
|
if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);
|
|
@@ -2372,7 +3400,7 @@ ${lines.join("\n")}`);
|
|
|
2372
3400
|
sections.push(`### Emotions
|
|
2373
3401
|
${lines.join("\n")}`);
|
|
2374
3402
|
}
|
|
2375
|
-
if (ctx.temporal_events.length > 0) {
|
|
3403
|
+
if (typed && ctx.temporal_events.length > 0) {
|
|
2376
3404
|
const lines = ctx.temporal_events.map((te) => {
|
|
2377
3405
|
const extras = [];
|
|
2378
3406
|
if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);
|
|
@@ -2626,7 +3654,10 @@ function buildTool(target, options = {}, onWarning = () => {
|
|
|
2626
3654
|
}
|
|
2627
3655
|
|
|
2628
3656
|
// src/client.ts
|
|
2629
|
-
|
|
3657
|
+
function localModelOff() {
|
|
3658
|
+
return ["off", "0", "false", "no", "none"].includes((getEnv("SYNAP_SDK_LOCAL_MODEL") ?? "").trim().toLowerCase());
|
|
3659
|
+
}
|
|
3660
|
+
var SynapClient = class _SynapClient {
|
|
2630
3661
|
// Native-private (`#`), not TypeScript `private`. TS `private` is erased at
|
|
2631
3662
|
// compile time, so the field stays enumerable at runtime and a JS consumer
|
|
2632
3663
|
// can still reach it. These two were public and made `client.transport
|
|
@@ -2649,10 +3680,20 @@ var SynapClient = class {
|
|
|
2649
3680
|
instance;
|
|
2650
3681
|
#closed = false;
|
|
2651
3682
|
#initialized = false;
|
|
3683
|
+
#identity;
|
|
2652
3684
|
#registryKeys;
|
|
2653
3685
|
#compactionSubscribers;
|
|
2654
3686
|
#shortTerm;
|
|
3687
|
+
/** Every raw turn, past compaction, for local history search. */
|
|
3688
|
+
#turnStore;
|
|
3689
|
+
/** A local embedding model for history search; null = keywords only. */
|
|
3690
|
+
#localEncoder = null;
|
|
3691
|
+
/** `history_search_model`: "auto", an "onnx:..." name, or null for none. */
|
|
3692
|
+
#localModelName = "auto";
|
|
3693
|
+
#localModelStarted = false;
|
|
3694
|
+
#backfillAfter = /* @__PURE__ */ new Map();
|
|
2655
3695
|
#stVerbatimOverlay;
|
|
3696
|
+
#agentModel = {};
|
|
2656
3697
|
#logger;
|
|
2657
3698
|
#stAuthoritative;
|
|
2658
3699
|
#turns;
|
|
@@ -2690,13 +3731,14 @@ var SynapClient = class {
|
|
|
2690
3731
|
userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`
|
|
2691
3732
|
});
|
|
2692
3733
|
if (options.heartbeat) this.#transport.startHeartbeat();
|
|
3734
|
+
this.#transport.identityResolver = () => this.#resolveIdentity();
|
|
2693
3735
|
this.#anticipationCache = new AnticipationCache();
|
|
2694
3736
|
this.#fetchHooks = {
|
|
2695
|
-
beforeFetch: (scope, options2) => {
|
|
3737
|
+
beforeFetch: async (scope, options2) => {
|
|
2696
3738
|
const o2 = options2;
|
|
2697
3739
|
if (o2["query_date"] ?? o2["queryDate"]) return null;
|
|
2698
3740
|
if (o2["max_results_per_query"] ?? o2["maxResultsPerQuery"]) return null;
|
|
2699
|
-
const attempt = tryServeFromCache(
|
|
3741
|
+
const attempt = await tryServeFromCache(
|
|
2700
3742
|
this.#anticipationCache,
|
|
2701
3743
|
scope,
|
|
2702
3744
|
{
|
|
@@ -2720,7 +3762,9 @@ var SynapClient = class {
|
|
|
2720
3762
|
return {
|
|
2721
3763
|
response: attempt.response,
|
|
2722
3764
|
servedItemIds: servedItemIds(attempt.hit),
|
|
2723
|
-
bundleId: attempt.hit.bundleIds[0] ?? ""
|
|
3765
|
+
bundleId: attempt.hit.bundleIds[0] ?? "",
|
|
3766
|
+
borderline: attempt.hit.band === "borderline",
|
|
3767
|
+
raceDeadlineMs: SEMANTIC_MATCH.race_deadline_ms
|
|
2724
3768
|
};
|
|
2725
3769
|
},
|
|
2726
3770
|
onAssembled: (scope, options2, response, startedAt) => {
|
|
@@ -2735,9 +3779,19 @@ var SynapClient = class {
|
|
|
2735
3779
|
this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
|
|
2736
3780
|
this.#compactionSubscribers = new CompactionSubscribers();
|
|
2737
3781
|
this.#shortTerm = new ShortTermStore();
|
|
3782
|
+
this.#turnStore = new TurnStore();
|
|
3783
|
+
this.#shortTerm.attachArchive(this.#turnStore);
|
|
2738
3784
|
this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;
|
|
3785
|
+
this.#agentModel = {
|
|
3786
|
+
model: options.agent_model ?? options.agentModel,
|
|
3787
|
+
contextWindow: options.agent_context_window ?? options.agentContextWindow
|
|
3788
|
+
};
|
|
2739
3789
|
this.#logger = options.logger;
|
|
2740
3790
|
this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;
|
|
3791
|
+
this.#localEncoder = options.history_search_encoder ?? options.historySearchEncoder ?? null;
|
|
3792
|
+
const modelOption = options.history_search_model !== void 0 ? options.history_search_model : options.historySearchModel;
|
|
3793
|
+
this.#localModelName = modelOption === void 0 ? "auto" : modelOption;
|
|
3794
|
+
if (this.#localEncoder?.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(this.#localEncoder);
|
|
2741
3795
|
this.#turns = new TurnCounter();
|
|
2742
3796
|
this.conversation = createConversationNamespace(
|
|
2743
3797
|
this.#transport,
|
|
@@ -2754,8 +3808,12 @@ var SynapClient = class {
|
|
|
2754
3808
|
},
|
|
2755
3809
|
// Buffer the turn locally so the next fetch sees it, ahead of the server
|
|
2756
3810
|
// having persisted and compacted it.
|
|
2757
|
-
({ conversationId, role, content, timestamp }) => {
|
|
2758
|
-
this.#shortTerm.appendTurn(conversationId, role, content, timestamp
|
|
3811
|
+
({ conversationId, role, content, timestamp, messageId, serverMessageCount, inFlight }) => {
|
|
3812
|
+
this.#shortTerm.appendTurn(conversationId, role, content, timestamp, {
|
|
3813
|
+
messageId,
|
|
3814
|
+
serverMessageCount,
|
|
3815
|
+
inFlight
|
|
3816
|
+
});
|
|
2759
3817
|
},
|
|
2760
3818
|
(response, conversationId, userId, scopePath) => {
|
|
2761
3819
|
overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);
|
|
@@ -2763,8 +3821,11 @@ var SynapClient = class {
|
|
|
2763
3821
|
},
|
|
2764
3822
|
{
|
|
2765
3823
|
prompt: (conversationId, style) => this.#localPrompt(conversationId, style),
|
|
2766
|
-
compacted: (conversationId, format) => this.#localCompacted(conversationId, format)
|
|
2767
|
-
|
|
3824
|
+
compacted: (conversationId, format) => this.#localCompacted(conversationId, format),
|
|
3825
|
+
reseed: (conversationId, response) => this.#reseedShortTerm(conversationId, response),
|
|
3826
|
+
historyExcerpts: (conversationId, response, query) => this.#withHistoryExcerpts(conversationId, response, query)
|
|
3827
|
+
},
|
|
3828
|
+
() => this.#agentModel
|
|
2768
3829
|
);
|
|
2769
3830
|
this.memories = createMemoriesInterface(this.#transport, ({ userId, customerId }) => {
|
|
2770
3831
|
for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
|
|
@@ -2778,11 +3839,17 @@ var SynapClient = class {
|
|
|
2778
3839
|
this.#anticipationCache,
|
|
2779
3840
|
({ conversationId, role, content }) => {
|
|
2780
3841
|
this.#shortTerm.appendTurn(conversationId, role, content);
|
|
2781
|
-
}
|
|
3842
|
+
},
|
|
3843
|
+
() => this.#agentModel,
|
|
3844
|
+
({ conversationId, result, toolName }) => this.archive_tool_result(conversationId, result, { tool_name: toolName })
|
|
2782
3845
|
);
|
|
3846
|
+
this.instance._internal.setOnListen(() => this.#startLocalModel());
|
|
2783
3847
|
this.instance._internal.setCompactionApplier((bundle) => {
|
|
2784
3848
|
this.#shortTerm.applyCompaction(bundle);
|
|
2785
3849
|
});
|
|
3850
|
+
this.instance._internal.setConversationStateHandler((conversationId, messageCount) => {
|
|
3851
|
+
this.#shortTerm.noteServerCount(conversationId, messageCount);
|
|
3852
|
+
});
|
|
2786
3853
|
if (options._force_new !== true) {
|
|
2787
3854
|
const incumbent = registerIfAbsent(registryKey, this);
|
|
2788
3855
|
if (incumbent !== void 0) return incumbent;
|
|
@@ -2870,19 +3937,30 @@ var SynapClient = class {
|
|
|
2870
3937
|
/**
|
|
2871
3938
|
* Resolve `client_id` and `instance_id` from the API key.
|
|
2872
3939
|
*
|
|
2873
|
-
* Mirrors Python's `initialize()`.
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
2876
|
-
*
|
|
2877
|
-
*
|
|
2878
|
-
*
|
|
3940
|
+
* Mirrors Python's `initialize()`. Python raises "SDK not initialized" on
|
|
3941
|
+
* any call made before it; here the first request runs the same resolution
|
|
3942
|
+
* instead (`identityResolver` on the transport), so skipping this call costs
|
|
3943
|
+
* nothing and breaks no existing caller. It matters for the anticipation
|
|
3944
|
+
* cache: a bundle shared at client scope is filtered on `client_id`, and an
|
|
3945
|
+
* empty one fails that filter for every bundle.
|
|
3946
|
+
*
|
|
3947
|
+
* Calling it explicitly still locks `configure()`, as in Python. Resolving
|
|
3948
|
+
* on first use does not, because a caller who never called it could always
|
|
3949
|
+
* configure after a request, and that keeps working.
|
|
2879
3950
|
*
|
|
2880
3951
|
* Best-effort and idempotent. A whoami failure leaves whatever the
|
|
2881
3952
|
* constructor resolved and is not fatal.
|
|
2882
3953
|
*/
|
|
2883
3954
|
async initialize() {
|
|
2884
|
-
if (this.#initialized) return;
|
|
2885
3955
|
this.#initialized = true;
|
|
3956
|
+
await this.#resolveIdentity();
|
|
3957
|
+
}
|
|
3958
|
+
/** Runs whoami at most once, however many calls arrive together. */
|
|
3959
|
+
#resolveIdentity() {
|
|
3960
|
+
this.#identity ??= this.#whoami();
|
|
3961
|
+
return this.#identity;
|
|
3962
|
+
}
|
|
3963
|
+
async #whoami() {
|
|
2886
3964
|
const credentials = this.#transport.currentCredentials();
|
|
2887
3965
|
if (credentials.clientId !== "" && credentials.instanceId !== "") return;
|
|
2888
3966
|
try {
|
|
@@ -2942,6 +4020,14 @@ var SynapClient = class {
|
|
|
2942
4020
|
this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);
|
|
2943
4021
|
}
|
|
2944
4022
|
if (options.logger !== void 0) this.#logger = options.logger;
|
|
4023
|
+
const model = options.agent_model ?? options.agentModel;
|
|
4024
|
+
const window = options.agent_context_window ?? options.agentContextWindow;
|
|
4025
|
+
if (model !== void 0 || window !== void 0) {
|
|
4026
|
+
this.#agentModel = {
|
|
4027
|
+
model: model ?? this.#agentModel.model,
|
|
4028
|
+
contextWindow: window ?? this.#agentModel.contextWindow
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
2945
4031
|
}
|
|
2946
4032
|
/**
|
|
2947
4033
|
* Fetch and merge context across every scope with an identifier, in one call.
|
|
@@ -2975,9 +4061,11 @@ var SynapClient = class {
|
|
|
2975
4061
|
include_scope_labels: includeScopeLabels = false,
|
|
2976
4062
|
context_mode: contextMode = "in-conversation",
|
|
2977
4063
|
include_profile: includeProfile = true,
|
|
2978
|
-
last_n_conversations: lastNConversations = 1
|
|
4064
|
+
last_n_conversations: lastNConversations = 1,
|
|
4065
|
+
response_shape: responseShape = "by_type"
|
|
2979
4066
|
} = options;
|
|
2980
4067
|
validateConversationId(conversationId);
|
|
4068
|
+
responseShapeOf({ response_shape: responseShape });
|
|
2981
4069
|
const wants = (scope) => scopes === void 0 || scopes.includes(scope);
|
|
2982
4070
|
const shared = {
|
|
2983
4071
|
search_query: searchQuery,
|
|
@@ -2989,7 +4077,8 @@ var SynapClient = class {
|
|
|
2989
4077
|
// threaded three call sites by hand and missed the client branch, which
|
|
2990
4078
|
// sits one indent deeper; spreading removes that whole class of miss.
|
|
2991
4079
|
query_date: queryDate,
|
|
2992
|
-
max_results_per_query: maxResultsPerQuery
|
|
4080
|
+
max_results_per_query: maxResultsPerQuery,
|
|
4081
|
+
response_shape: responseShape
|
|
2993
4082
|
};
|
|
2994
4083
|
const labels = [];
|
|
2995
4084
|
const tasks = [];
|
|
@@ -3152,25 +4241,139 @@ var SynapClient = class {
|
|
|
3152
4241
|
/**
|
|
3153
4242
|
* Whether the SDK is authoritative for short-term context.
|
|
3154
4243
|
*
|
|
3155
|
-
* Resolution order matches Python's `_is_st_authoritative`:
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
4244
|
+
* Resolution order matches Python's `_is_st_authoritative`:
|
|
4245
|
+
* `SYNAP_SDK_ST_AUTHORITATIVE` first, as an explicit override in either
|
|
4246
|
+
* direction, then the option. Off by default. Being enabled does not mean
|
|
4247
|
+
* the local store is used: that also takes an entry that holds the whole
|
|
4248
|
+
* conversation, see `isComplete`.
|
|
3158
4249
|
*/
|
|
3159
4250
|
#isStAuthoritative() {
|
|
3160
|
-
if (this.#stAuthoritative === true) return true;
|
|
3161
4251
|
const raw = (getEnv("SYNAP_SDK_ST_AUTHORITATIVE") ?? "").trim().toLowerCase();
|
|
3162
|
-
|
|
4252
|
+
if (["0", "false", "no", "off"].includes(raw)) return false;
|
|
4253
|
+
if (["1", "true", "yes", "on"].includes(raw)) return true;
|
|
4254
|
+
return this.#stAuthoritative === true;
|
|
3163
4255
|
}
|
|
3164
4256
|
/**
|
|
3165
4257
|
* Render `get_context_for_prompt` from the local store, or null to fall
|
|
3166
|
-
* through to the network.
|
|
3167
|
-
*
|
|
3168
|
-
*
|
|
4258
|
+
* through to the network.
|
|
4259
|
+
*
|
|
4260
|
+
* The store answers only when it holds the whole conversation: the server
|
|
4261
|
+
* has confirmed a count the entry accounts for. An entry that is merely
|
|
4262
|
+
* present is not enough. After a restart or an eviction it holds one turn,
|
|
4263
|
+
* and rendering that as the conversation drops everything said before it.
|
|
3169
4264
|
*/
|
|
4265
|
+
/**
|
|
4266
|
+
* `response` with the earlier turns that best match `query` (by default the
|
|
4267
|
+
* latest user message) appended, when a compaction exists. The search runs
|
|
4268
|
+
* here, over the turn archive. Python: `_with_history_excerpts`.
|
|
4269
|
+
*/
|
|
4270
|
+
async #withHistoryExcerpts(conversationId, response, query) {
|
|
4271
|
+
const r = response ?? {};
|
|
4272
|
+
const store = this.#turnStore;
|
|
4273
|
+
const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
|
|
4274
|
+
store.addMany(
|
|
4275
|
+
conversationId,
|
|
4276
|
+
recent.map((m) => ({
|
|
4277
|
+
role: String(m["role"] ?? "user"),
|
|
4278
|
+
content: String(m["content"] ?? ""),
|
|
4279
|
+
timestamp: String(m["timestamp"] ?? ""),
|
|
4280
|
+
message_id: typeof m["message_id"] === "string" ? m["message_id"] : null
|
|
4281
|
+
}))
|
|
4282
|
+
);
|
|
4283
|
+
const entry = this.#shortTerm.get(conversationId);
|
|
4284
|
+
const total = Math.max(Number(r["total_message_count"] ?? 0) || 0, Number(entry?.serverMessageCount ?? 0) || 0);
|
|
4285
|
+
this.#maybeBackfillTurns(conversationId, total);
|
|
4286
|
+
if (!r["context_block"]) return response;
|
|
4287
|
+
const turns = store.turns(conversationId);
|
|
4288
|
+
let q = query;
|
|
4289
|
+
if (!q) {
|
|
4290
|
+
const latest = [...recent].reverse().find((m) => m["role"] === "user");
|
|
4291
|
+
q = latest ? String(latest["content"] ?? "") : [...turns].reverse().find((t) => t.role === "user")?.content ?? "";
|
|
4292
|
+
}
|
|
4293
|
+
if (!q) return response;
|
|
4294
|
+
const exclude = /* @__PURE__ */ new Set();
|
|
4295
|
+
for (const m of recent) {
|
|
4296
|
+
if (typeof m["message_id"] === "string" && m["message_id"]) exclude.add(m["message_id"]);
|
|
4297
|
+
exclude.add(String(m["content"] ?? ""));
|
|
4298
|
+
}
|
|
4299
|
+
this.#startLocalModel();
|
|
4300
|
+
const hits = await searchTurns(turns, q, { exclude, encoder: this.#localEncoder, store });
|
|
4301
|
+
if (hits.length === 0) return response;
|
|
4302
|
+
const block = renderExcerpts(hits, turns.length ? turns[turns.length - 1].timestamp : "");
|
|
4303
|
+
return {
|
|
4304
|
+
...r,
|
|
4305
|
+
history_excerpts: hits.map((h) => ({ position: h.position, role: h.role, text: h.text })),
|
|
4306
|
+
history_block: block,
|
|
4307
|
+
formatted_context: (String(r["formatted_context"] ?? "").trimEnd() + "\n\n" + block).trim()
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
/**
|
|
4311
|
+
* Keep what a tool returned in this process's turn archive, so history search
|
|
4312
|
+
* can find it after a compaction. Never sent anywhere: the server's
|
|
4313
|
+
* conversation holds what people said, and a compaction is made from that.
|
|
4314
|
+
* `instance.record_tool_result` calls this too. Python: `archive_tool_result`.
|
|
4315
|
+
*/
|
|
4316
|
+
archive_tool_result(conversationId, result, options = {}) {
|
|
4317
|
+
if (!conversationId || result === null || result === void 0) return;
|
|
4318
|
+
try {
|
|
4319
|
+
this.#turnStore.add(
|
|
4320
|
+
conversationId,
|
|
4321
|
+
"tool",
|
|
4322
|
+
toolResultText(result, options.tool_name ?? options.toolName),
|
|
4323
|
+
options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4324
|
+
);
|
|
4325
|
+
} catch {
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
static BACKFILL_COOLDOWN_MS = 6e4;
|
|
4329
|
+
static BACKFILL_PAGE = 500;
|
|
4330
|
+
/** Fetch, in the background, turns the server holds and the archive does not. */
|
|
4331
|
+
/**
|
|
4332
|
+
* Load the local embedding model in the background, once. Searches and
|
|
4333
|
+
* lookups before it is ready match by words. Python: `_start_history_encoder`.
|
|
4334
|
+
*/
|
|
4335
|
+
#startLocalModel() {
|
|
4336
|
+
if (this.#localModelStarted) return;
|
|
4337
|
+
this.#localModelStarted = true;
|
|
4338
|
+
if (localModelOff()) return;
|
|
4339
|
+
const name = this.#localModelName === "auto" ? SEMANTIC_MATCH.model : this.#localModelName;
|
|
4340
|
+
if (!name) return;
|
|
4341
|
+
const callerEncoder = this.#localEncoder;
|
|
4342
|
+
void import("./onnx-encoder-NP35KH4H.js").then(({ canRunLocalModel, loadOnnxEncoder }) => canRunLocalModel() ? loadOnnxEncoder(name) : null).then((encoder) => {
|
|
4343
|
+
if (encoder === null) return;
|
|
4344
|
+
if (callerEncoder === null) this.#localEncoder = encoder;
|
|
4345
|
+
if (encoder.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(encoder);
|
|
4346
|
+
}).catch(() => {
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
#maybeBackfillTurns(conversationId, total) {
|
|
4350
|
+
if (total <= this.#turnStore.count(conversationId)) return;
|
|
4351
|
+
const now = Date.now();
|
|
4352
|
+
if (now < (this.#backfillAfter.get(conversationId) ?? 0)) return;
|
|
4353
|
+
this.#backfillAfter.set(conversationId, now + _SynapClient.BACKFILL_COOLDOWN_MS);
|
|
4354
|
+
void this.#backfillTurns(conversationId, total);
|
|
4355
|
+
}
|
|
4356
|
+
async #backfillTurns(conversationId, total) {
|
|
4357
|
+
try {
|
|
4358
|
+
let start = 1;
|
|
4359
|
+
while (start <= total) {
|
|
4360
|
+
const result = await this.#transport.request("conversation_turns", {
|
|
4361
|
+
pathParams: { conversation_id: conversationId },
|
|
4362
|
+
query: { start, limit: _SynapClient.BACKFILL_PAGE }
|
|
4363
|
+
});
|
|
4364
|
+
const page = (result ?? {})["turns"] ?? [];
|
|
4365
|
+
this.#turnStore.addMany(conversationId, page);
|
|
4366
|
+
if (page.length < _SynapClient.BACKFILL_PAGE) break;
|
|
4367
|
+
start += _SynapClient.BACKFILL_PAGE;
|
|
4368
|
+
}
|
|
4369
|
+
} catch (error) {
|
|
4370
|
+
this.#warn(`Turn backfill for ${conversationId} failed: ${String(error)}`);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
3170
4373
|
#localPrompt(conversationId, style) {
|
|
3171
4374
|
if (!this.#isStAuthoritative()) return null;
|
|
3172
4375
|
const entry = this.#shortTerm.get(conversationId);
|
|
3173
|
-
if (entry === null) return null;
|
|
4376
|
+
if (entry === null || !isComplete(entry)) return null;
|
|
3174
4377
|
if (entry.compactionId === null && entry.recentTurns.length === 0) return null;
|
|
3175
4378
|
return renderForPrompt(entry, style);
|
|
3176
4379
|
}
|
|
@@ -3178,9 +4381,29 @@ var SynapClient = class {
|
|
|
3178
4381
|
#localCompacted(conversationId, format) {
|
|
3179
4382
|
if (!this.#isStAuthoritative()) return null;
|
|
3180
4383
|
const entry = this.#shortTerm.get(conversationId);
|
|
3181
|
-
if (entry === null || entry.compactionId === null) return null;
|
|
4384
|
+
if (entry === null || entry.compactionId === null || !isComplete(entry)) return null;
|
|
3182
4385
|
return renderCompacted(entry, format);
|
|
3183
4386
|
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Bring the local store back in step from the server's answer.
|
|
4389
|
+
*
|
|
4390
|
+
* Only for a conversation this process writes to. The server's answer lists
|
|
4391
|
+
* every message its summary does not cover, which is exactly what the entry
|
|
4392
|
+
* was missing.
|
|
4393
|
+
*/
|
|
4394
|
+
#reseedShortTerm(conversationId, response) {
|
|
4395
|
+
if (!this.#isStAuthoritative()) return;
|
|
4396
|
+
const entry = this.#shortTerm.get(conversationId);
|
|
4397
|
+
if (entry === null || !entry.participant) return;
|
|
4398
|
+
const r = response ?? {};
|
|
4399
|
+
const recent = Array.isArray(r["recent_messages"]) ? r["recent_messages"] : [];
|
|
4400
|
+
this.#shortTerm.reseed(conversationId, {
|
|
4401
|
+
compactionId: typeof r["compaction_id"] === "string" ? r["compaction_id"] : null,
|
|
4402
|
+
recentMessages: recent,
|
|
4403
|
+
compactedMessageCount: Number(r["compacted_message_count"] ?? 0) || 0,
|
|
4404
|
+
totalMessageCount: Number(r["total_message_count"] ?? 0) || 0
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
3184
4407
|
/** Every diagnostic the SDK emits passes through here. */
|
|
3185
4408
|
#warn(message) {
|
|
3186
4409
|
if (this.#logger !== void 0) {
|
|
@@ -3243,6 +4466,7 @@ export {
|
|
|
3243
4466
|
DEFAULT_TIMEOUTS,
|
|
3244
4467
|
DOCUMENT_TYPES,
|
|
3245
4468
|
ENDPOINTS,
|
|
4469
|
+
EXCERPT_CATEGORY,
|
|
3246
4470
|
FORGET_MODES,
|
|
3247
4471
|
HttpTransport,
|
|
3248
4472
|
INGEST_MODES,
|
|
@@ -3253,8 +4477,10 @@ export {
|
|
|
3253
4477
|
InvalidInstanceIdError,
|
|
3254
4478
|
ListeningAlreadyActiveError,
|
|
3255
4479
|
ListeningNotActiveError,
|
|
4480
|
+
MEMORY_TYPES,
|
|
3256
4481
|
NetworkTimeoutError,
|
|
3257
4482
|
PermanentError,
|
|
4483
|
+
RESPONSE_SHAPES,
|
|
3258
4484
|
RateLimitError,
|
|
3259
4485
|
SDKError,
|
|
3260
4486
|
SDK_VERSION,
|
|
@@ -3267,15 +4493,18 @@ export {
|
|
|
3267
4493
|
THRESHOLDS,
|
|
3268
4494
|
TranscriptConflictError,
|
|
3269
4495
|
TransientError,
|
|
4496
|
+
applyResponseShape,
|
|
3270
4497
|
flattenContextItems,
|
|
3271
4498
|
formatForPrompt,
|
|
3272
4499
|
isOutcomeUnknown,
|
|
3273
4500
|
isRecallQuery,
|
|
3274
4501
|
isSynapError,
|
|
3275
4502
|
mergeScopeResults,
|
|
4503
|
+
rankItems,
|
|
3276
4504
|
resolvePath,
|
|
3277
4505
|
shouldRetry,
|
|
3278
4506
|
stem,
|
|
3279
|
-
tokenize
|
|
4507
|
+
tokenize,
|
|
4508
|
+
wireRankScore
|
|
3280
4509
|
};
|
|
3281
4510
|
//# sourceMappingURL=index.js.map
|