@maximem/synap-js-sdk 0.4.0 → 0.4.1
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/dist/anticipation-cache-ARYEEKMU.cjs +10 -0
- package/dist/{anticipation-cache-JBZHTJ3O.cjs.map → anticipation-cache-ARYEEKMU.cjs.map} +1 -1
- package/dist/{anticipation-cache-BgkqcTPJ.d.cts → anticipation-cache-BMVF5sXb.d.cts} +10 -0
- package/dist/{anticipation-cache-BgkqcTPJ.d.ts → anticipation-cache-BMVF5sXb.d.ts} +10 -0
- package/dist/{anticipation-cache-D5YHEPV3.js → anticipation-cache-H7HEVWQC.js} +2 -2
- package/dist/{chunk-TCEP5OVR.js → chunk-6LDUDKHS.js} +33 -1
- package/dist/{chunk-TCEP5OVR.js.map → chunk-6LDUDKHS.js.map} +1 -1
- package/dist/{chunk-NIDPTBNC.cjs → chunk-RB57I2IQ.cjs} +33 -1
- package/dist/chunk-RB57I2IQ.cjs.map +1 -0
- package/dist/grpc/index.cjs +1 -1
- package/dist/grpc/index.d.cts +1 -1
- package/dist/grpc/index.d.ts +1 -1
- package/dist/grpc/index.js +1 -1
- package/dist/index.cjs +626 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -12
- package/dist/index.d.ts +26 -12
- package/dist/index.js +614 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/anticipation-cache-JBZHTJ3O.cjs +0 -10
- package/dist/chunk-NIDPTBNC.cjs.map +0 -1
- /package/dist/{anticipation-cache-D5YHEPV3.js.map → anticipation-cache-H7HEVWQC.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
isRecallQuery,
|
|
33
33
|
stem,
|
|
34
34
|
tokenize
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-6LDUDKHS.js";
|
|
36
36
|
import {
|
|
37
37
|
getEnv
|
|
38
38
|
} from "./chunk-QBCVMPGK.js";
|
|
@@ -525,10 +525,19 @@ function buildFetchRequest(scope, options) {
|
|
|
525
525
|
}
|
|
526
526
|
return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
|
|
527
527
|
}
|
|
528
|
-
async function fetchContext(transport, scope, options = {}) {
|
|
528
|
+
async function fetchContext(transport, scope, options = {}, hooks = {}) {
|
|
529
|
+
const startedAt = Date.now();
|
|
530
|
+
const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
|
|
531
|
+
if (anticipated !== null) {
|
|
532
|
+
hooks.onServedFromCache?.(scope, options, anticipated);
|
|
533
|
+
hooks.onAssembled?.(scope, options, anticipated.response, startedAt);
|
|
534
|
+
return anticipated.response;
|
|
535
|
+
}
|
|
529
536
|
const { endpoint, body } = buildFetchRequest(scope, options);
|
|
530
537
|
const result = await transport.request(endpoint, { body });
|
|
531
|
-
|
|
538
|
+
const response = result?.["context"] ?? {};
|
|
539
|
+
hooks.onAssembled?.(scope, options, response, startedAt);
|
|
540
|
+
return response;
|
|
532
541
|
}
|
|
533
542
|
|
|
534
543
|
// src/context/normalize.ts
|
|
@@ -1031,6 +1040,8 @@ var CompactionSubscribers = class {
|
|
|
1031
1040
|
}
|
|
1032
1041
|
};
|
|
1033
1042
|
function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
|
|
1043
|
+
}, onTurn = () => {
|
|
1044
|
+
}, onFetched = () => {
|
|
1034
1045
|
}) {
|
|
1035
1046
|
const convId = (v) => {
|
|
1036
1047
|
const id = typeof v === "string" ? v : v.conversation_id ?? v.conversationId;
|
|
@@ -1042,6 +1053,12 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1042
1053
|
async record_message(options) {
|
|
1043
1054
|
const body = buildRecordMessageBody(options);
|
|
1044
1055
|
const result = await transport.request("conversations_messages", { body });
|
|
1056
|
+
onTurn({
|
|
1057
|
+
conversationId: String(body["conversation_id"] ?? ""),
|
|
1058
|
+
role: String(body["role"] ?? "user"),
|
|
1059
|
+
content: String(body["content"] ?? ""),
|
|
1060
|
+
timestamp: body["timestamp"]
|
|
1061
|
+
});
|
|
1045
1062
|
onWrite({
|
|
1046
1063
|
userId: body["user_id"],
|
|
1047
1064
|
customerId: body["customer_id"]
|
|
@@ -1056,6 +1073,14 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1056
1073
|
const result = await transport.request("conversations_messages_batch", {
|
|
1057
1074
|
body: { messages: bodies }
|
|
1058
1075
|
});
|
|
1076
|
+
for (const body of bodies) {
|
|
1077
|
+
onTurn({
|
|
1078
|
+
conversationId: String(body["conversation_id"] ?? ""),
|
|
1079
|
+
role: String(body["role"] ?? "user"),
|
|
1080
|
+
content: String(body["content"] ?? ""),
|
|
1081
|
+
timestamp: body["timestamp"]
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1059
1084
|
for (const body of bodies) {
|
|
1060
1085
|
onWrite({
|
|
1061
1086
|
userId: body["user_id"],
|
|
@@ -1075,10 +1100,15 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1075
1100
|
},
|
|
1076
1101
|
context: {
|
|
1077
1102
|
async fetch(options = {}) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1103
|
+
const id = options.conversation_id ?? options.conversationId;
|
|
1104
|
+
validateConversationId(id);
|
|
1105
|
+
const response = await fetchContext(transport, "conversation", options);
|
|
1106
|
+
onFetched(
|
|
1107
|
+
response,
|
|
1108
|
+
id,
|
|
1109
|
+
options.user_id ?? options.userId
|
|
1080
1110
|
);
|
|
1081
|
-
return
|
|
1111
|
+
return response;
|
|
1082
1112
|
},
|
|
1083
1113
|
get_context_for_prompt: async (options = {}) => {
|
|
1084
1114
|
const id = convId(options);
|
|
@@ -1134,7 +1164,7 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
|
|
|
1134
1164
|
}
|
|
1135
1165
|
|
|
1136
1166
|
// src/user/interface.ts
|
|
1137
|
-
function createUserNamespace(transport) {
|
|
1167
|
+
function createUserNamespace(transport, hooks = {}) {
|
|
1138
1168
|
return {
|
|
1139
1169
|
async get_profile(options) {
|
|
1140
1170
|
const userId = typeof options === "string" ? options : options.user_id ?? options.userId;
|
|
@@ -1148,7 +1178,7 @@ function createUserNamespace(transport) {
|
|
|
1148
1178
|
});
|
|
1149
1179
|
},
|
|
1150
1180
|
context: { async fetch(options = {}) {
|
|
1151
|
-
return fetchContext(transport, "user", options);
|
|
1181
|
+
return fetchContext(transport, "user", options, hooks);
|
|
1152
1182
|
} }
|
|
1153
1183
|
};
|
|
1154
1184
|
}
|
|
@@ -1212,13 +1242,24 @@ function envTls() {
|
|
|
1212
1242
|
if (raw === void 0 || raw === "") return void 0;
|
|
1213
1243
|
return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
|
|
1214
1244
|
}
|
|
1215
|
-
function createInstanceNamespace(credentials, cache) {
|
|
1245
|
+
function createInstanceNamespace(credentials, cache, onTurn) {
|
|
1216
1246
|
let client = null;
|
|
1217
1247
|
let dispatchCompaction = null;
|
|
1248
|
+
let applyCompaction = null;
|
|
1218
1249
|
const internals = {
|
|
1219
1250
|
onCompactionUpdate(conversationId, bundle) {
|
|
1251
|
+
applyCompaction?.(bundle);
|
|
1220
1252
|
dispatchCompaction?.(conversationId, bundle);
|
|
1221
1253
|
},
|
|
1254
|
+
setCompactionApplier(apply) {
|
|
1255
|
+
applyCompaction = apply;
|
|
1256
|
+
},
|
|
1257
|
+
sendContextUsed(event) {
|
|
1258
|
+
client?.sendContextUsed(event);
|
|
1259
|
+
},
|
|
1260
|
+
sendContextAssembled(event) {
|
|
1261
|
+
client?.sendContextAssembled(event);
|
|
1262
|
+
},
|
|
1222
1263
|
setCompactionDispatcher(dispatch) {
|
|
1223
1264
|
dispatchCompaction = dispatch;
|
|
1224
1265
|
},
|
|
@@ -1275,6 +1316,12 @@ function createInstanceNamespace(credentials, cache) {
|
|
|
1275
1316
|
if (options.content === void 0) {
|
|
1276
1317
|
throw new ListeningNotActiveError("content is required");
|
|
1277
1318
|
}
|
|
1319
|
+
const conversationId = options.conversation_id ?? options.conversationId ?? "";
|
|
1320
|
+
onTurn?.({
|
|
1321
|
+
conversationId,
|
|
1322
|
+
role: options.role ?? "user",
|
|
1323
|
+
content: options.content
|
|
1324
|
+
});
|
|
1278
1325
|
client.sendConversationEvent({
|
|
1279
1326
|
event_type: options.event_type ?? options.eventType ?? "user_message",
|
|
1280
1327
|
content: options.content,
|
|
@@ -1315,6 +1362,403 @@ function createInstanceNamespace(credentials, cache) {
|
|
|
1315
1362
|
// src/version.ts
|
|
1316
1363
|
var SDK_VERSION = "1.0.0-beta.0";
|
|
1317
1364
|
|
|
1365
|
+
// src/registry.ts
|
|
1366
|
+
var instances = /* @__PURE__ */ new Map();
|
|
1367
|
+
function buildRegistryKey(instanceId, apiKey) {
|
|
1368
|
+
if (instanceId) return instanceId;
|
|
1369
|
+
const key = apiKey ?? "";
|
|
1370
|
+
if (!key) return "";
|
|
1371
|
+
return `apikey:${digest(key)}`;
|
|
1372
|
+
}
|
|
1373
|
+
function digest(value) {
|
|
1374
|
+
const PRIME = 1099511628211n;
|
|
1375
|
+
const MASK = 0xffffffffffffffffn;
|
|
1376
|
+
let hash = 14695981039346656037n;
|
|
1377
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1378
|
+
hash ^= BigInt(value.charCodeAt(i));
|
|
1379
|
+
hash = hash * PRIME & MASK;
|
|
1380
|
+
}
|
|
1381
|
+
return hash.toString(16).padStart(16, "0");
|
|
1382
|
+
}
|
|
1383
|
+
function get(key) {
|
|
1384
|
+
return instances.get(key);
|
|
1385
|
+
}
|
|
1386
|
+
function registerIfAbsent(key, client) {
|
|
1387
|
+
const existing = instances.get(key);
|
|
1388
|
+
if (existing !== void 0) return existing;
|
|
1389
|
+
instances.set(key, client);
|
|
1390
|
+
return void 0;
|
|
1391
|
+
}
|
|
1392
|
+
function aliasIfAbsent(key, client) {
|
|
1393
|
+
if (!key || instances.has(key)) return false;
|
|
1394
|
+
instances.set(key, client);
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
function unregisterIfOwner(key, client) {
|
|
1398
|
+
if (instances.get(key) === client) instances.delete(key);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/cache/short-term-store.ts
|
|
1402
|
+
var DEFAULT_MAX_CONVERSATIONS = 100;
|
|
1403
|
+
var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
|
|
1404
|
+
function parseIso(value) {
|
|
1405
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
1406
|
+
if (value instanceof Date) return value.getTime();
|
|
1407
|
+
if (typeof value !== "string") return null;
|
|
1408
|
+
const parsed = Date.parse(value);
|
|
1409
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1410
|
+
}
|
|
1411
|
+
function isAfter(turnTimestamp, cutoffMs) {
|
|
1412
|
+
const parsed = parseIso(turnTimestamp);
|
|
1413
|
+
if (parsed === null) return true;
|
|
1414
|
+
return parsed > cutoffMs;
|
|
1415
|
+
}
|
|
1416
|
+
var ShortTermStore = class {
|
|
1417
|
+
// Insertion order IS the LRU order: re-inserting on touch moves an entry to
|
|
1418
|
+
// the end, so the first key Map yields is the least recently used.
|
|
1419
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1420
|
+
#maxConversations;
|
|
1421
|
+
#maxAgeMs;
|
|
1422
|
+
#now;
|
|
1423
|
+
constructor(options = {}) {
|
|
1424
|
+
this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
|
|
1425
|
+
this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
1426
|
+
this.#now = options.now ?? (() => Date.now());
|
|
1427
|
+
}
|
|
1428
|
+
/** The entry, or null. Age-evicts on read, as Python does. */
|
|
1429
|
+
get(conversationId) {
|
|
1430
|
+
if (!conversationId) return null;
|
|
1431
|
+
const entry = this.#entries.get(conversationId);
|
|
1432
|
+
if (entry === void 0) return null;
|
|
1433
|
+
if (this.#now() - entry.lastActivityAt > this.#maxAgeMs) {
|
|
1434
|
+
this.#entries.delete(conversationId);
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
this.#touch(conversationId, entry);
|
|
1438
|
+
return entry;
|
|
1439
|
+
}
|
|
1440
|
+
has(conversationId) {
|
|
1441
|
+
return this.get(conversationId) !== null;
|
|
1442
|
+
}
|
|
1443
|
+
get size() {
|
|
1444
|
+
return this.#entries.size;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Append a raw turn.
|
|
1448
|
+
*
|
|
1449
|
+
* Called from `record_message` and the stream's `send_message`. When no entry
|
|
1450
|
+
* exists yet (the first turn, before any compaction has arrived) a fresh one
|
|
1451
|
+
* is created with an empty summary; the next `compaction_update` fills the
|
|
1452
|
+
* summary fields in and prunes the turns it covers.
|
|
1453
|
+
*/
|
|
1454
|
+
appendTurn(conversationId, role, content, timestamp) {
|
|
1455
|
+
if (!conversationId) return;
|
|
1456
|
+
const stamp = timestamp ?? new Date(this.#now()).toISOString();
|
|
1457
|
+
const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
|
|
1458
|
+
entry.recentTurns.push({ role, content, timestamp: stamp });
|
|
1459
|
+
entry.lastActivityAt = parseIso(stamp) ?? this.#now();
|
|
1460
|
+
this.#touch(conversationId, entry);
|
|
1461
|
+
this.#evict();
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Apply a `compaction_update` bundle.
|
|
1465
|
+
*
|
|
1466
|
+
* Replaces the summary fields from the bundle's `conversation_context` and
|
|
1467
|
+
* prunes locally buffered turns at or before its `end_timestamp`. Turns that
|
|
1468
|
+
* arrived DURING the compaction window are kept by design: the server has not
|
|
1469
|
+
* seen them yet, which is the whole reason the local tail exists.
|
|
1470
|
+
*/
|
|
1471
|
+
applyCompaction(bundle) {
|
|
1472
|
+
if (typeof bundle !== "object" || bundle === null) return;
|
|
1473
|
+
const cc = bundle["conversation_context"] ?? {};
|
|
1474
|
+
const conversationId = String(
|
|
1475
|
+
cc["conversation_id"] ?? bundle["anticipation_conversation_id"] ?? bundle["conversation_id"] ?? ""
|
|
1476
|
+
);
|
|
1477
|
+
if (!conversationId) return;
|
|
1478
|
+
const endTs = parseIso(cc["end_timestamp"] ?? cc["compacted_at"]);
|
|
1479
|
+
const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
|
|
1480
|
+
entry.summary = cc["summary"] || entry.summary;
|
|
1481
|
+
entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
|
|
1482
|
+
entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
|
|
1483
|
+
entry.currentState = { ...cc["current_state"] ?? {} };
|
|
1484
|
+
entry.keyExtractions = { ...cc["key_extractions"] ?? {} };
|
|
1485
|
+
entry.compactionId = cc["compaction_id"] || entry.compactionId;
|
|
1486
|
+
entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
|
|
1487
|
+
entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
|
|
1488
|
+
if (endTs !== null && entry.recentTurns.length > 0) {
|
|
1489
|
+
entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
|
|
1490
|
+
}
|
|
1491
|
+
entry.lastActivityAt = this.#now();
|
|
1492
|
+
this.#touch(conversationId, entry);
|
|
1493
|
+
this.#evict();
|
|
1494
|
+
}
|
|
1495
|
+
invalidate(conversationId) {
|
|
1496
|
+
this.#entries.delete(conversationId);
|
|
1497
|
+
}
|
|
1498
|
+
clear() {
|
|
1499
|
+
this.#entries.clear();
|
|
1500
|
+
}
|
|
1501
|
+
#blank(conversationId) {
|
|
1502
|
+
return {
|
|
1503
|
+
conversationId,
|
|
1504
|
+
summary: null,
|
|
1505
|
+
factualParagraph: null,
|
|
1506
|
+
conversationalParagraph: null,
|
|
1507
|
+
currentState: {},
|
|
1508
|
+
keyExtractions: {},
|
|
1509
|
+
compactionId: null,
|
|
1510
|
+
compactedAt: null,
|
|
1511
|
+
endTimestamp: null,
|
|
1512
|
+
recentTurns: [],
|
|
1513
|
+
lastActivityAt: this.#now()
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
/** Re-insert so this key becomes the most recently used. */
|
|
1517
|
+
#touch(conversationId, entry) {
|
|
1518
|
+
this.#entries.delete(conversationId);
|
|
1519
|
+
this.#entries.set(conversationId, entry);
|
|
1520
|
+
}
|
|
1521
|
+
#evict() {
|
|
1522
|
+
while (this.#entries.size > this.#maxConversations) {
|
|
1523
|
+
const oldest = this.#entries.keys().next();
|
|
1524
|
+
if (oldest.done === true) break;
|
|
1525
|
+
this.#entries.delete(oldest.value);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// src/context/overlay.ts
|
|
1531
|
+
var MAX_TURNS = 20;
|
|
1532
|
+
var TOKEN_BUDGET = 2e3;
|
|
1533
|
+
function verbatimOverlayEnabled() {
|
|
1534
|
+
const raw = (getEnv("SYNAP_ST_VERBATIM_OVERLAY") ?? "").trim().toLowerCase();
|
|
1535
|
+
if (["0", "false", "no", "off"].includes(raw)) return false;
|
|
1536
|
+
if (["1", "true", "yes", "on"].includes(raw)) return true;
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
function estimateTokens(turn) {
|
|
1540
|
+
return Math.max(1, Math.floor(String(turn.content ?? "").length / 4));
|
|
1541
|
+
}
|
|
1542
|
+
function budgetRecentTurns(turns) {
|
|
1543
|
+
const windowed = turns.slice(-MAX_TURNS);
|
|
1544
|
+
let total = windowed.reduce((n2, t) => n2 + estimateTokens(t), 0);
|
|
1545
|
+
while (windowed.length > 0 && total > TOKEN_BUDGET) {
|
|
1546
|
+
const dropped = windowed.shift();
|
|
1547
|
+
total -= estimateTokens(dropped);
|
|
1548
|
+
}
|
|
1549
|
+
return windowed;
|
|
1550
|
+
}
|
|
1551
|
+
function overlayLocalRecentTurns(response, store, conversationId) {
|
|
1552
|
+
if (conversationId === void 0 || conversationId === "") return;
|
|
1553
|
+
try {
|
|
1554
|
+
const entry = store.get(conversationId);
|
|
1555
|
+
if (entry === null) return;
|
|
1556
|
+
const existing = response.conversation_context ?? null;
|
|
1557
|
+
if (existing !== null && !verbatimOverlayEnabled()) return;
|
|
1558
|
+
const localTurns = budgetRecentTurns(entry.recentTurns);
|
|
1559
|
+
const serverField = (name) => existing === null ? void 0 : existing[name];
|
|
1560
|
+
const orLocal = (name, local) => {
|
|
1561
|
+
const value = serverField(name);
|
|
1562
|
+
if (value === void 0 || value === null || value === "") return local;
|
|
1563
|
+
if (typeof value === "object" && Object.keys(value).length === 0) return local;
|
|
1564
|
+
return value;
|
|
1565
|
+
};
|
|
1566
|
+
response.conversation_context = {
|
|
1567
|
+
summary: orLocal("summary", entry.summary),
|
|
1568
|
+
current_state: orLocal("current_state", { ...entry.currentState }),
|
|
1569
|
+
key_extractions: orLocal("key_extractions", { ...entry.keyExtractions }),
|
|
1570
|
+
recent_turns: localTurns,
|
|
1571
|
+
compaction_id: orLocal("compaction_id", entry.compactionId),
|
|
1572
|
+
compacted_at: orLocal("compacted_at", entry.compactedAt),
|
|
1573
|
+
conversation_id: conversationId
|
|
1574
|
+
};
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/context/user-summary.ts
|
|
1580
|
+
var USER_SUMMARY_INTERVAL = 5;
|
|
1581
|
+
var MAX_SUMMARY_ITEMS = 3;
|
|
1582
|
+
var COLLECTIONS = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
|
|
1583
|
+
var TurnCounter = class {
|
|
1584
|
+
#counts = /* @__PURE__ */ new Map();
|
|
1585
|
+
/** Increment and return the new count. Python buckets a missing id under `_global`. */
|
|
1586
|
+
increment(conversationId) {
|
|
1587
|
+
const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
|
|
1588
|
+
const next = (this.#counts.get(key) ?? 0) + 1;
|
|
1589
|
+
this.#counts.set(key, next);
|
|
1590
|
+
return next;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* True on every Nth turn, and never on the first.
|
|
1594
|
+
*
|
|
1595
|
+
* Python: `count > 0 and count % interval == 0`. Turn 5 injects, turn 1 does
|
|
1596
|
+
* not, which matters because the first turn is usually the one that just
|
|
1597
|
+
* triggered a fresh retrieval anyway.
|
|
1598
|
+
*/
|
|
1599
|
+
shouldInject(conversationId) {
|
|
1600
|
+
const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
|
|
1601
|
+
const count = this.#counts.get(key) ?? 0;
|
|
1602
|
+
return count > 0 && count % USER_SUMMARY_INTERVAL === 0;
|
|
1603
|
+
}
|
|
1604
|
+
clear() {
|
|
1605
|
+
this.#counts.clear();
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
function itemId(item) {
|
|
1609
|
+
return String(item["item_id"] ?? item["id"] ?? "");
|
|
1610
|
+
}
|
|
1611
|
+
function pick2(item, ...names) {
|
|
1612
|
+
for (const name of names) {
|
|
1613
|
+
const value = item[name];
|
|
1614
|
+
if (value !== void 0 && value !== null) return value;
|
|
1615
|
+
}
|
|
1616
|
+
return void 0;
|
|
1617
|
+
}
|
|
1618
|
+
function normalise(item) {
|
|
1619
|
+
const content = pick2(item, "content") ?? "";
|
|
1620
|
+
const confidence = pick2(item, "confidence") ?? 0;
|
|
1621
|
+
const extractedAt = pick2(item, "extracted_at", "created_at") ?? null;
|
|
1622
|
+
return {
|
|
1623
|
+
id: itemId(item),
|
|
1624
|
+
content: String(content),
|
|
1625
|
+
confidence: Number(confidence),
|
|
1626
|
+
source: String(pick2(item, "source") ?? ""),
|
|
1627
|
+
category: String(pick2(item, "context_type", "category") ?? ""),
|
|
1628
|
+
emotion_type: String(pick2(item, "context_type", "emotion_type") ?? ""),
|
|
1629
|
+
strength: Number(pick2(item, "strength", "confidence") ?? 0),
|
|
1630
|
+
summary: String(pick2(item, "summary", "content") ?? ""),
|
|
1631
|
+
significance: Number(pick2(item, "significance", "confidence") ?? 0),
|
|
1632
|
+
intensity: Number(pick2(item, "intensity", "confidence") ?? 0),
|
|
1633
|
+
context: String(pick2(item, "context", "content") ?? ""),
|
|
1634
|
+
participants: pick2(item, "participants") ?? [],
|
|
1635
|
+
extracted_at: extractedAt,
|
|
1636
|
+
occurred_at: pick2(item, "occurred_at", "extracted_at", "created_at") ?? null,
|
|
1637
|
+
detected_at: pick2(item, "detected_at", "extracted_at", "created_at") ?? null,
|
|
1638
|
+
metadata: pick2(item, "metadata") ?? {},
|
|
1639
|
+
event_date: pick2(item, "event_date") ?? null,
|
|
1640
|
+
valid_until: pick2(item, "valid_until") ?? null,
|
|
1641
|
+
temporal_category: pick2(item, "temporal_category") ?? null,
|
|
1642
|
+
temporal_confidence: Number(pick2(item, "temporal_confidence") ?? 0)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function mergeUserSummary(response, bundle) {
|
|
1646
|
+
const itemsByType = bundle["items_by_type"] ?? {};
|
|
1647
|
+
for (const collection of COLLECTIONS) {
|
|
1648
|
+
const items = itemsByType[collection];
|
|
1649
|
+
if (!Array.isArray(items)) continue;
|
|
1650
|
+
const existing = response[collection] ?? [];
|
|
1651
|
+
const existingIds = new Set(existing.map((i) => String(i.id ?? i.item_id ?? "")));
|
|
1652
|
+
const fresh = items.filter((i) => typeof i === "object" && i !== null && !existingIds.has(itemId(i))).slice(0, MAX_SUMMARY_ITEMS).map(normalise);
|
|
1653
|
+
if (fresh.length === 0) continue;
|
|
1654
|
+
response[collection] = [...existing, ...fresh];
|
|
1655
|
+
}
|
|
1656
|
+
return response;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/context/anticipated.ts
|
|
1660
|
+
function scopeLookupParams(scope, params) {
|
|
1661
|
+
const { searchQuery, entityId, customerId, clientId, conversationId } = params;
|
|
1662
|
+
switch (scope) {
|
|
1663
|
+
case "conversation":
|
|
1664
|
+
return { searchQuery, entityId, customerId, clientId, conversationId };
|
|
1665
|
+
case "user":
|
|
1666
|
+
return { searchQuery, entityId, customerId, clientId };
|
|
1667
|
+
case "customer":
|
|
1668
|
+
return { searchQuery, entityId: null, customerId, clientId };
|
|
1669
|
+
case "client":
|
|
1670
|
+
return { searchQuery, entityId: null, customerId: null, clientId };
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function buildAnticipationResponse(hit) {
|
|
1674
|
+
const response = {};
|
|
1675
|
+
for (const [type, items] of Object.entries(hit.itemsByType)) {
|
|
1676
|
+
response[type] = items;
|
|
1677
|
+
}
|
|
1678
|
+
response.metadata = {
|
|
1679
|
+
source: "anticipation",
|
|
1680
|
+
ttl_seconds: 0,
|
|
1681
|
+
cache_hit: true,
|
|
1682
|
+
retrieved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1683
|
+
};
|
|
1684
|
+
return response;
|
|
1685
|
+
}
|
|
1686
|
+
function servedItemIds(hit) {
|
|
1687
|
+
const ids = [];
|
|
1688
|
+
for (const items of Object.values(hit.itemsByType)) {
|
|
1689
|
+
for (const item of items) {
|
|
1690
|
+
const id = item.item_id ?? item["id"];
|
|
1691
|
+
if (typeof id === "string" && id !== "") ids.push(id);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return ids;
|
|
1695
|
+
}
|
|
1696
|
+
function tryServeFromCache(cache, scope, params, maxItems) {
|
|
1697
|
+
try {
|
|
1698
|
+
const scoped = scopeLookupParams(scope, params);
|
|
1699
|
+
const hit = cache.lookup({
|
|
1700
|
+
searchQuery: scoped.searchQuery ?? null,
|
|
1701
|
+
entityId: scoped.entityId ?? null,
|
|
1702
|
+
customerId: scoped.customerId ?? null,
|
|
1703
|
+
clientId: scoped.clientId ?? null,
|
|
1704
|
+
conversationId: scoped.conversationId ?? null,
|
|
1705
|
+
maxItems
|
|
1706
|
+
});
|
|
1707
|
+
if (hit === null) return { response: null, hit: null };
|
|
1708
|
+
return { response: buildAnticipationResponse(hit), hit };
|
|
1709
|
+
} catch {
|
|
1710
|
+
return { response: null, hit: null };
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/context/assembled.ts
|
|
1715
|
+
var COLLECTIONS2 = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
|
|
1716
|
+
function finalItemIds(response) {
|
|
1717
|
+
const ids = [];
|
|
1718
|
+
for (const collection of COLLECTIONS2) {
|
|
1719
|
+
const items = response[collection];
|
|
1720
|
+
if (!Array.isArray(items)) continue;
|
|
1721
|
+
for (const item of items) {
|
|
1722
|
+
const id = item["id"] ?? item["item_id"];
|
|
1723
|
+
if (typeof id === "string" && id !== "") ids.push(id);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return ids;
|
|
1727
|
+
}
|
|
1728
|
+
function resolveSource(response, override) {
|
|
1729
|
+
const metadata = response.metadata ?? {};
|
|
1730
|
+
let source = override ?? String(metadata["source"] ?? "") ?? "";
|
|
1731
|
+
if (source === "") source = "cloud";
|
|
1732
|
+
if (source === "cache") source = "http_cache";
|
|
1733
|
+
const impliesHit = ["anticipation", "anticipation_cache", "http_cache", "sdk_authoritative"];
|
|
1734
|
+
const cacheHit = impliesHit.includes(source) ? true : Boolean(metadata["cache_hit"]);
|
|
1735
|
+
return { source, cacheHit };
|
|
1736
|
+
}
|
|
1737
|
+
function buildAssembledEvent(params) {
|
|
1738
|
+
const { response } = params;
|
|
1739
|
+
const { source, cacheHit } = resolveSource(response, params.sourceOverride);
|
|
1740
|
+
const conversationContext = response.conversation_context ?? {};
|
|
1741
|
+
const recentTurns = conversationContext["recent_turns"];
|
|
1742
|
+
const metadata = response.metadata ?? {};
|
|
1743
|
+
return {
|
|
1744
|
+
correlation_id: params.correlationId,
|
|
1745
|
+
conversation_id: params.conversationId ?? "",
|
|
1746
|
+
user_id: params.userId ?? "",
|
|
1747
|
+
customer_id: params.customerId ?? "",
|
|
1748
|
+
final_item_ids: finalItemIds(response),
|
|
1749
|
+
final_total_tokens: Number(metadata["total_tokens"] ?? 0),
|
|
1750
|
+
compaction_id: String(conversationContext["compaction_id"] ?? ""),
|
|
1751
|
+
recent_turn_count: Array.isArray(recentTurns) ? recentTurns.length : 0,
|
|
1752
|
+
compaction_end_timestamp: String(
|
|
1753
|
+
conversationContext["end_timestamp"] ?? conversationContext["compacted_at"] ?? ""
|
|
1754
|
+
),
|
|
1755
|
+
assembly_source: source,
|
|
1756
|
+
assembly_duration_ms: Math.max(0, Date.now() - params.startedAt),
|
|
1757
|
+
cache_hit: cacheHit,
|
|
1758
|
+
sdk_version: params.sdkVersion
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1318
1762
|
// src/context/unified.ts
|
|
1319
1763
|
var ITEM_COLLECTIONS = [
|
|
1320
1764
|
"facts",
|
|
@@ -1323,7 +1767,7 @@ var ITEM_COLLECTIONS = [
|
|
|
1323
1767
|
"emotions",
|
|
1324
1768
|
"temporal_events"
|
|
1325
1769
|
];
|
|
1326
|
-
function
|
|
1770
|
+
function itemId2(item) {
|
|
1327
1771
|
const id = item.id ?? item.item_id;
|
|
1328
1772
|
return typeof id === "string" && id !== "" ? id : void 0;
|
|
1329
1773
|
}
|
|
@@ -1359,7 +1803,7 @@ function mergeScopeResults(scopeResults) {
|
|
|
1359
1803
|
const items = response[collection];
|
|
1360
1804
|
if (!Array.isArray(items)) continue;
|
|
1361
1805
|
for (const item of items) {
|
|
1362
|
-
const id =
|
|
1806
|
+
const id = itemId2(item);
|
|
1363
1807
|
if (id !== void 0) {
|
|
1364
1808
|
if (seen.has(id)) continue;
|
|
1365
1809
|
seen.add(id);
|
|
@@ -1385,7 +1829,7 @@ function numberOf(value) {
|
|
|
1385
1829
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1386
1830
|
}
|
|
1387
1831
|
function scopeOf(ctx, item) {
|
|
1388
|
-
const id =
|
|
1832
|
+
const id = itemId2(item);
|
|
1389
1833
|
return (id !== void 0 ? ctx.scope_map[id] : void 0) ?? "unknown";
|
|
1390
1834
|
}
|
|
1391
1835
|
function bulletWithExtras(text, extras) {
|
|
@@ -1700,6 +2144,11 @@ var SynapClient = class {
|
|
|
1700
2144
|
// can still reach it. These two were public and made `client.transport
|
|
1701
2145
|
// .credentials.apiKey` and the raw cache's `now`/`fire`/`invalidateEntity`
|
|
1702
2146
|
// reachable, which is exactly what `client.cache` exists to prevent.
|
|
2147
|
+
// Definite-assignment assertions (`!`) because the constructor can RETURN an
|
|
2148
|
+
// existing client from the registry before reaching any of these. TypeScript
|
|
2149
|
+
// cannot follow a constructor return, so it sees a path where the fields are
|
|
2150
|
+
// unassigned. On that path the object is discarded and the incumbent is
|
|
2151
|
+
// handed back instead, so nothing observes them.
|
|
1703
2152
|
#transport;
|
|
1704
2153
|
#anticipationCache;
|
|
1705
2154
|
user;
|
|
@@ -1712,10 +2161,28 @@ var SynapClient = class {
|
|
|
1712
2161
|
instance;
|
|
1713
2162
|
#closed = false;
|
|
1714
2163
|
#initialized = false;
|
|
2164
|
+
#registryKeys;
|
|
1715
2165
|
#compactionSubscribers;
|
|
2166
|
+
#shortTerm;
|
|
2167
|
+
#turns;
|
|
2168
|
+
#fetchHooks;
|
|
1716
2169
|
constructor(options = {}) {
|
|
1717
2170
|
const credentials = resolveCredentials(options);
|
|
1718
2171
|
validateInstanceId(credentials.instanceId);
|
|
2172
|
+
const registryKey = buildRegistryKey(credentials.instanceId, credentials.apiKey);
|
|
2173
|
+
if (options._force_new !== true) {
|
|
2174
|
+
const existing = get(registryKey);
|
|
2175
|
+
if (existing !== void 0) {
|
|
2176
|
+
const incumbent = existing.#transport.currentCredentials().apiKey;
|
|
2177
|
+
if (credentials.apiKey && incumbent && credentials.apiKey !== incumbent) {
|
|
2178
|
+
existing.#warn(
|
|
2179
|
+
`Instance ${registryKey} is already running in this process on a different API key. The key passed here is ignored; this client will authenticate as the existing one. Rotating a key this way has no effect. Construct with apiKey and no instanceId to get a client on your own credential.`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
return existing;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
this.#registryKeys = options._force_new === true ? [] : [registryKey];
|
|
1719
2186
|
this.#transport = new HttpTransport({
|
|
1720
2187
|
credentials,
|
|
1721
2188
|
...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
|
|
@@ -1727,10 +2194,41 @@ var SynapClient = class {
|
|
|
1727
2194
|
});
|
|
1728
2195
|
if (options.heartbeat) this.#transport.startHeartbeat();
|
|
1729
2196
|
this.#anticipationCache = new AnticipationCache();
|
|
1730
|
-
this
|
|
1731
|
-
|
|
1732
|
-
|
|
2197
|
+
this.#fetchHooks = {
|
|
2198
|
+
beforeFetch: (scope, options2) => {
|
|
2199
|
+
const o2 = options2;
|
|
2200
|
+
const attempt = tryServeFromCache(
|
|
2201
|
+
this.#anticipationCache,
|
|
2202
|
+
scope,
|
|
2203
|
+
{
|
|
2204
|
+
searchQuery: o2["search_query"] ?? o2["searchQuery"],
|
|
2205
|
+
entityId: o2["user_id"] ?? o2["userId"],
|
|
2206
|
+
customerId: o2["customer_id"] ?? o2["customerId"],
|
|
2207
|
+
clientId: this.#transport.currentCredentials().clientId || null,
|
|
2208
|
+
conversationId: o2["conversation_id"] ?? o2["conversationId"]
|
|
2209
|
+
},
|
|
2210
|
+
Number(o2["max_results"] ?? o2["maxResults"] ?? 10)
|
|
2211
|
+
);
|
|
2212
|
+
if (attempt.response === null || attempt.hit === null) return null;
|
|
2213
|
+
return {
|
|
2214
|
+
response: attempt.response,
|
|
2215
|
+
servedItemIds: servedItemIds(attempt.hit),
|
|
2216
|
+
bundleId: attempt.hit.bundleIds[0] ?? ""
|
|
2217
|
+
};
|
|
2218
|
+
},
|
|
2219
|
+
onAssembled: (scope, options2, response, startedAt) => {
|
|
2220
|
+
this.#emitContextAssembled(scope, options2, response, startedAt);
|
|
2221
|
+
},
|
|
2222
|
+
onServedFromCache: (_scope, options2, served) => {
|
|
2223
|
+
this.#emitContextUsed(options2, _scope, served);
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
this.user = createUserNamespace(this.#transport, this.#fetchHooks);
|
|
2227
|
+
this.customer = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "customer", o2, this.#fetchHooks) } };
|
|
2228
|
+
this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
|
|
1733
2229
|
this.#compactionSubscribers = new CompactionSubscribers();
|
|
2230
|
+
this.#shortTerm = new ShortTermStore();
|
|
2231
|
+
this.#turns = new TurnCounter();
|
|
1734
2232
|
this.conversation = createConversationNamespace(
|
|
1735
2233
|
this.#transport,
|
|
1736
2234
|
this.#compactionSubscribers,
|
|
@@ -1743,6 +2241,15 @@ var SynapClient = class {
|
|
|
1743
2241
|
for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
|
|
1744
2242
|
if (id) this.#anticipationCache.invalidateEntity(id);
|
|
1745
2243
|
}
|
|
2244
|
+
},
|
|
2245
|
+
// Buffer the turn locally so the next fetch sees it, ahead of the server
|
|
2246
|
+
// having persisted and compacted it.
|
|
2247
|
+
({ conversationId, role, content, timestamp }) => {
|
|
2248
|
+
this.#shortTerm.appendTurn(conversationId, role, content, timestamp);
|
|
2249
|
+
},
|
|
2250
|
+
(response, conversationId, userId) => {
|
|
2251
|
+
overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
|
|
2252
|
+
this.#injectUserSummary(response, conversationId, userId);
|
|
1746
2253
|
}
|
|
1747
2254
|
);
|
|
1748
2255
|
this.memories = createMemoriesInterface(this.#transport);
|
|
@@ -1750,8 +2257,18 @@ var SynapClient = class {
|
|
|
1750
2257
|
this.cache = createCacheNamespace(this.#anticipationCache);
|
|
1751
2258
|
this.instance = createInstanceNamespace(
|
|
1752
2259
|
() => this.#transport.currentCredentials(),
|
|
1753
|
-
this.#anticipationCache
|
|
2260
|
+
this.#anticipationCache,
|
|
2261
|
+
({ conversationId, role, content }) => {
|
|
2262
|
+
this.#shortTerm.appendTurn(conversationId, role, content);
|
|
2263
|
+
}
|
|
1754
2264
|
);
|
|
2265
|
+
this.instance._internal.setCompactionApplier((bundle) => {
|
|
2266
|
+
this.#shortTerm.applyCompaction(bundle);
|
|
2267
|
+
});
|
|
2268
|
+
if (options._force_new !== true) {
|
|
2269
|
+
const incumbent = registerIfAbsent(registryKey, this);
|
|
2270
|
+
if (incumbent !== void 0) return incumbent;
|
|
2271
|
+
}
|
|
1755
2272
|
this.instance._internal.setCompactionDispatcher((conversationId, bundle) => {
|
|
1756
2273
|
this.#compactionSubscribers.dispatch(conversationId, bundle, (error) => {
|
|
1757
2274
|
this.#warn(`compaction subscriber threw: ${String(error)}`);
|
|
@@ -1789,10 +2306,10 @@ var SynapClient = class {
|
|
|
1789
2306
|
}
|
|
1790
2307
|
const context = await fetchContext(this.#transport, "user", {
|
|
1791
2308
|
user_id: userId,
|
|
1792
|
-
...
|
|
1793
|
-
...
|
|
2309
|
+
...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
|
|
2310
|
+
...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
|
|
1794
2311
|
search_query: [options.query],
|
|
1795
|
-
max_results:
|
|
2312
|
+
max_results: pick3(options, "max_results", "maxResults") ?? 10,
|
|
1796
2313
|
types: options.types ?? ["all"],
|
|
1797
2314
|
...options.mode !== void 0 ? { mode: options.mode } : {}
|
|
1798
2315
|
});
|
|
@@ -1809,10 +2326,10 @@ var SynapClient = class {
|
|
|
1809
2326
|
if (!userId) throw new InvalidInputError("user_id is required");
|
|
1810
2327
|
const context = await fetchContext(this.#transport, "user", {
|
|
1811
2328
|
user_id: userId,
|
|
1812
|
-
...
|
|
1813
|
-
...
|
|
2329
|
+
...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
|
|
2330
|
+
...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
|
|
1814
2331
|
search_query: [],
|
|
1815
|
-
max_results:
|
|
2332
|
+
max_results: pick3(options, "max_results", "maxResults") ?? 100,
|
|
1816
2333
|
types: options.types ?? ["all"],
|
|
1817
2334
|
...options.mode !== void 0 ? { mode: options.mode } : {}
|
|
1818
2335
|
});
|
|
@@ -1858,6 +2375,11 @@ var SynapClient = class {
|
|
|
1858
2375
|
...resolvedClientId !== "" && credentials.clientId === "" ? { clientId: resolvedClientId } : {},
|
|
1859
2376
|
...resolvedInstanceId !== "" && credentials.instanceId === "" ? { instanceId: resolvedInstanceId } : {}
|
|
1860
2377
|
});
|
|
2378
|
+
if (resolvedInstanceId !== "" && this.#registryKeys.length > 0) {
|
|
2379
|
+
if (aliasIfAbsent(resolvedInstanceId, this)) {
|
|
2380
|
+
this.#registryKeys.push(resolvedInstanceId);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
1861
2383
|
} catch {
|
|
1862
2384
|
}
|
|
1863
2385
|
}
|
|
@@ -1994,6 +2516,9 @@ var SynapClient = class {
|
|
|
1994
2516
|
this.#warn(`Scope '${label}' fetch failed in unified fetch (non-fatal): ${String(result.reason)}`);
|
|
1995
2517
|
}
|
|
1996
2518
|
const merged = mergeScopeResults(successful);
|
|
2519
|
+
for (const [, response] of successful) {
|
|
2520
|
+
overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
|
|
2521
|
+
}
|
|
1997
2522
|
if (includeConversationContext && conversationId !== void 0 && conversationId !== "") {
|
|
1998
2523
|
try {
|
|
1999
2524
|
merged.conversation_context = await this.conversation.context.get_context_for_prompt({
|
|
@@ -2026,6 +2551,69 @@ var SynapClient = class {
|
|
|
2026
2551
|
anticipation_cache_snapshot() {
|
|
2027
2552
|
return this.#anticipationCache.snapshot();
|
|
2028
2553
|
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Report a locally served fetch back to the server.
|
|
2556
|
+
*
|
|
2557
|
+
* Only while listening: the event rides the Listen stream, and there is
|
|
2558
|
+
* nowhere to put it otherwise. Never throws, because a telemetry failure
|
|
2559
|
+
* must not fail a retrieval that already succeeded.
|
|
2560
|
+
*/
|
|
2561
|
+
#emitContextUsed(options, scope, served) {
|
|
2562
|
+
try {
|
|
2563
|
+
if (!this.instance.is_listening) return;
|
|
2564
|
+
this.instance._internal.sendContextUsed({
|
|
2565
|
+
bundle_id: served.bundleId,
|
|
2566
|
+
conversation_id: String(options["conversation_id"] ?? options["conversationId"] ?? ""),
|
|
2567
|
+
user_id: String(options["user_id"] ?? options["userId"] ?? ""),
|
|
2568
|
+
customer_id: String(options["customer_id"] ?? options["customerId"] ?? ""),
|
|
2569
|
+
served_item_ids: served.servedItemIds,
|
|
2570
|
+
scope,
|
|
2571
|
+
source_bundle_ids: served.bundleId ? [served.bundleId] : [],
|
|
2572
|
+
timestamp_ms: Date.now()
|
|
2573
|
+
});
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Report what was actually composed for this turn.
|
|
2579
|
+
*
|
|
2580
|
+
* Fires whether the fetch was served from the cache or the network, because
|
|
2581
|
+
* the audit needs to distinguish them. Like the other stream events it is
|
|
2582
|
+
* fire-and-forget and only goes anywhere while listening.
|
|
2583
|
+
*/
|
|
2584
|
+
#emitContextAssembled(scope, options, response, startedAt) {
|
|
2585
|
+
try {
|
|
2586
|
+
if (!this.instance.is_listening) return;
|
|
2587
|
+
this.instance._internal.sendContextAssembled({
|
|
2588
|
+
...buildAssembledEvent({
|
|
2589
|
+
correlationId: newCorrelationId(),
|
|
2590
|
+
response,
|
|
2591
|
+
conversationId: options["conversation_id"] ?? options["conversationId"],
|
|
2592
|
+
userId: options["user_id"] ?? options["userId"],
|
|
2593
|
+
customerId: options["customer_id"] ?? options["customerId"],
|
|
2594
|
+
startedAt,
|
|
2595
|
+
sdkVersion: SDK_VERSION
|
|
2596
|
+
}),
|
|
2597
|
+
scope
|
|
2598
|
+
});
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Fold a cached user summary in, every Nth turn.
|
|
2604
|
+
*
|
|
2605
|
+
* Only when the caller supplied a user_id. Without one the summary lookup
|
|
2606
|
+
* cannot be scoped, and an unscoped lookup is how user A's summary ends up in
|
|
2607
|
+
* user B's response.
|
|
2608
|
+
*/
|
|
2609
|
+
#injectUserSummary(response, conversationId, userId) {
|
|
2610
|
+
this.#turns.increment(conversationId);
|
|
2611
|
+
if (userId === void 0 || userId === "") return;
|
|
2612
|
+
if (!this.#turns.shouldInject(conversationId)) return;
|
|
2613
|
+
const summary = this.#anticipationCache.lookupUserSummary(userId);
|
|
2614
|
+
if (summary === null) return;
|
|
2615
|
+
mergeUserSummary(response, summary);
|
|
2616
|
+
}
|
|
2029
2617
|
/** Python logs via `logger.warning`; here it is one overridable seam. */
|
|
2030
2618
|
#warn(message) {
|
|
2031
2619
|
console.warn(`[synap] ${message}`);
|
|
@@ -2034,13 +2622,16 @@ var SynapClient = class {
|
|
|
2034
2622
|
async shutdown() {
|
|
2035
2623
|
if (this.#closed) return;
|
|
2036
2624
|
this.#closed = true;
|
|
2625
|
+
this.#shortTerm.clear();
|
|
2626
|
+
this.#turns.clear();
|
|
2627
|
+
for (const key of this.#registryKeys) unregisterIfOwner(key, this);
|
|
2037
2628
|
await this.instance.stop_listening();
|
|
2038
2629
|
this.#compactionSubscribers.removeAll();
|
|
2039
2630
|
this.#transport.close();
|
|
2040
2631
|
this.#anticipationCache.clear();
|
|
2041
2632
|
}
|
|
2042
2633
|
};
|
|
2043
|
-
function
|
|
2634
|
+
function pick3(o2, snake, camel) {
|
|
2044
2635
|
const a = o2[snake];
|
|
2045
2636
|
return a !== void 0 ? a : o2[camel];
|
|
2046
2637
|
}
|