@pentatonic-ai/ai-agent-sdk 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Hosted TES data-flow checks.
3
+ *
4
+ * The existing hosted-tes checks prove the TES server is up and the API
5
+ * key is accepted. They don't prove data is actually flowing end-to-end —
6
+ * you can have a green doctor pass while the Claude Code hook is silently
7
+ * dropping events, or while vector retrieval is returning nothing at the
8
+ * configured minScore.
9
+ *
10
+ * These checks close that gap with three real-data probes against the
11
+ * same GraphQL endpoint the SDK already uses at runtime:
12
+ *
13
+ * - "TES event stream has data" — events table has rows at all
14
+ * - "MEMORY_CREATED events present" — memory events exist for this client
15
+ * - "semantic search returns hits" — a broad probe query retrieves > 0
16
+ *
17
+ * All three are WARNINGs by default: a green liveness check + a "0 events"
18
+ * warning is more informative than pretending liveness implies correctness,
19
+ * but an empty stream on a fresh install is legitimate and shouldn't fail
20
+ * the overall doctor pass.
21
+ *
22
+ * GraphQL shapes match TES's deployed schema (verified against
23
+ * thing-event-system/functions/api/graphql/domains/event/schema.js and
24
+ * thing-event-system/modules/deep-memory/graphql/memory/schema.js):
25
+ *
26
+ * events(filter: EventFilterInput, limit: Int, offset: Int): EventPage!
27
+ * EventFilterInput { eventType: StringFilterInput, clientId: StringFilterInput, ... }
28
+ * EventPage { totalCount: Int!, ... }
29
+ *
30
+ * semanticSearchMemories(
31
+ * clientId: String!,
32
+ * query: String!,
33
+ * userId: String,
34
+ * limit: Int,
35
+ * minScore: Float
36
+ * ): [SemanticMemoryResult!]!
37
+ * SemanticMemoryResult { id: String!, similarity: Float!, ... }
38
+ */
39
+
40
+ import { SEVERITY } from "../index.js";
41
+
42
+ async function fetchWithTimeout(url, opts = {}, timeoutMs = 10_000) {
43
+ return await fetch(url, {
44
+ ...opts,
45
+ signal: AbortSignal.timeout(timeoutMs),
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Auth header: TES accepts `Authorization: Bearer tes_...` for end-user
51
+ * keys and `x-service-key: <key>` for internal/service keys. Mirrors the
52
+ * branching in hooks/scripts/shared.js so doctor authenticates the same
53
+ * way the SDK runtime does.
54
+ */
55
+ function authHeaders(apiKey, clientId) {
56
+ const headers = {
57
+ "Content-Type": "application/json",
58
+ "x-client-id": clientId,
59
+ };
60
+ if (apiKey?.startsWith("tes_")) {
61
+ headers["Authorization"] = `Bearer ${apiKey}`;
62
+ } else if (apiKey) {
63
+ headers["x-service-key"] = apiKey;
64
+ }
65
+ return headers;
66
+ }
67
+
68
+ async function graphql(endpoint, apiKey, clientId, query, variables) {
69
+ const res = await fetchWithTimeout(
70
+ `${endpoint.replace(/\/$/, "")}/api/graphql`,
71
+ {
72
+ method: "POST",
73
+ headers: authHeaders(apiKey, clientId),
74
+ body: JSON.stringify({ query, variables }),
75
+ }
76
+ );
77
+ if (!res.ok) {
78
+ const text = await res.text().catch(() => "");
79
+ throw new Error(`HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
80
+ }
81
+ const body = await res.json();
82
+ if (body.errors?.length) {
83
+ throw new Error(body.errors[0].message || "graphql error");
84
+ }
85
+ return body.data;
86
+ }
87
+
88
+ function requireHostedEnv() {
89
+ const endpoint = process.env.TES_ENDPOINT;
90
+ const apiKey = process.env.TES_API_KEY;
91
+ const clientId = process.env.TES_CLIENT_ID;
92
+ if (!endpoint || !apiKey || !clientId) {
93
+ return {
94
+ missing: true,
95
+ reason: "TES_ENDPOINT / TES_API_KEY / TES_CLIENT_ID required",
96
+ };
97
+ }
98
+ return { endpoint, apiKey, clientId };
99
+ }
100
+
101
+ function checkEventStreamHasData() {
102
+ return {
103
+ name: "TES event stream has data",
104
+ severity: SEVERITY.WARNING,
105
+ run: async () => {
106
+ const env = requireHostedEnv();
107
+ if (env.missing) return { ok: false, msg: env.reason };
108
+ try {
109
+ // `limit: 1` keeps the payload tiny — we only care about the total.
110
+ const data = await graphql(
111
+ env.endpoint,
112
+ env.apiKey,
113
+ env.clientId,
114
+ `query DoctorEventCount { events(limit: 1) { totalCount } }`
115
+ );
116
+ const total = data?.events?.totalCount ?? 0;
117
+ if (total > 0) {
118
+ return {
119
+ ok: true,
120
+ msg: `${total} event(s) in stream`,
121
+ detail: { totalCount: total },
122
+ };
123
+ }
124
+ return {
125
+ ok: false,
126
+ msg: "0 events yet — send one prompt to your agent and re-run",
127
+ detail: { totalCount: 0 },
128
+ };
129
+ } catch (err) {
130
+ return { ok: false, msg: err.message };
131
+ }
132
+ },
133
+ };
134
+ }
135
+
136
+ function checkMemoryCreatedForClient() {
137
+ return {
138
+ name: "MEMORY_CREATED events for client",
139
+ severity: SEVERITY.WARNING,
140
+ run: async () => {
141
+ const env = requireHostedEnv();
142
+ if (env.missing) return { ok: false, msg: env.reason };
143
+ try {
144
+ const data = await graphql(
145
+ env.endpoint,
146
+ env.apiKey,
147
+ env.clientId,
148
+ `query DoctorMemCount($eventType: String!, $client: String!) {
149
+ events(
150
+ limit: 1,
151
+ filter: {
152
+ eventType: { eq: $eventType }
153
+ clientId: { eq: $client }
154
+ }
155
+ ) {
156
+ totalCount
157
+ }
158
+ }`,
159
+ { eventType: "MEMORY_CREATED", client: env.clientId }
160
+ );
161
+ const total = data?.events?.totalCount ?? 0;
162
+ if (total > 0) {
163
+ return {
164
+ ok: true,
165
+ msg: `${total} MEMORY_CREATED event(s) for ${env.clientId}`,
166
+ detail: { totalCount: total, clientId: env.clientId },
167
+ };
168
+ }
169
+ return {
170
+ ok: false,
171
+ msg: `no MEMORY_CREATED events for ${env.clientId} yet — hook may not be writing memories`,
172
+ detail: { totalCount: 0, clientId: env.clientId },
173
+ };
174
+ } catch (err) {
175
+ return { ok: false, msg: err.message };
176
+ }
177
+ },
178
+ };
179
+ }
180
+
181
+ // Match TES's "Cannot query field 'X'" error wording precisely so a
182
+ // schema-arg mismatch doesn't masquerade as "deployment doesn't expose
183
+ // the field" — that would silently hide real errors.
184
+ const FIELD_NOT_FOUND_RE =
185
+ /cannot query field "?semanticSearchMemories"?/i;
186
+
187
+ function checkSemanticSearchReturnsHits() {
188
+ return {
189
+ name: "semanticSearchMemories returns hits",
190
+ severity: SEVERITY.WARNING,
191
+ run: async () => {
192
+ const env = requireHostedEnv();
193
+ if (env.missing) return { ok: false, msg: env.reason };
194
+ try {
195
+ // A broad probe query. Low minScore (0.1) because the point of this
196
+ // check is "does retrieval work at all", not "does retrieval rank
197
+ // well". A follow-up tuning warning can be a separate check later.
198
+ const query = process.env.PENTATONIC_DOCTOR_PROBE_QUERY || "heartbeat";
199
+ const minScore = 0.1;
200
+ const data = await graphql(
201
+ env.endpoint,
202
+ env.apiKey,
203
+ env.clientId,
204
+ `query DoctorSearch($clientId: String!, $q: String!, $minScore: Float!) {
205
+ semanticSearchMemories(
206
+ clientId: $clientId,
207
+ query: $q,
208
+ minScore: $minScore,
209
+ limit: 5
210
+ ) {
211
+ id
212
+ similarity
213
+ }
214
+ }`,
215
+ { clientId: env.clientId, q: query, minScore }
216
+ );
217
+ const hits = data?.semanticSearchMemories ?? [];
218
+ if (hits.length > 0) {
219
+ return {
220
+ ok: true,
221
+ msg: `${hits.length} hit(s) for "${query}" at minScore=${minScore}`,
222
+ detail: { query, minScore, hits: hits.length },
223
+ };
224
+ }
225
+ return {
226
+ ok: false,
227
+ msg: `0 hits for "${query}" at minScore=${minScore} — try lowering minScore or PENTATONIC_DOCTOR_PROBE_QUERY`,
228
+ detail: { query, minScore, hits: 0 },
229
+ };
230
+ } catch (err) {
231
+ // Only treat the precise "Cannot query field" error as
232
+ // "deployment doesn't expose this" — schema-arg mismatches and
233
+ // other graphql errors should surface, not be silently skipped.
234
+ if (FIELD_NOT_FOUND_RE.test(err.message)) {
235
+ return {
236
+ ok: true,
237
+ msg: "semanticSearchMemories not exposed by this deployment (skipped)",
238
+ };
239
+ }
240
+ return { ok: false, msg: err.message };
241
+ }
242
+ },
243
+ };
244
+ }
245
+
246
+ export function dataFlowChecks() {
247
+ return [
248
+ checkEventStreamHasData(),
249
+ checkMemoryCreatedForClient(),
250
+ checkSemanticSearchReturnsHits(),
251
+ ];
252
+ }
@@ -24,7 +24,9 @@ export { renderHuman, renderJson } from "./output.js";
24
24
  export { universalChecks } from "./checks/universal.js";
25
25
  export { localMemoryChecks } from "./checks/local-memory.js";
26
26
  export { hostedTesChecks } from "./checks/hosted-tes.js";
27
+ export { dataFlowChecks } from "./checks/data-flow.js";
27
28
  export { platformChecks } from "./checks/platform.js";
29
+ export { claudeCodeChecks } from "./checks/claude-code.js";
28
30
 
29
31
  export const SEVERITY = Object.freeze({
30
32
  CRITICAL: "critical",
@@ -21,7 +21,9 @@ import { detectPaths, PATHS } from "./detect.js";
21
21
  import { universalChecks } from "./checks/universal.js";
22
22
  import { localMemoryChecks } from "./checks/local-memory.js";
23
23
  import { hostedTesChecks } from "./checks/hosted-tes.js";
24
+ import { dataFlowChecks } from "./checks/data-flow.js";
24
25
  import { platformChecks } from "./checks/platform.js";
26
+ import { claudeCodeChecks } from "./checks/claude-code.js";
25
27
  import { loadPlugins } from "./plugins.js";
26
28
  import { SEVERITY } from "./index.js";
27
29
 
@@ -32,7 +34,8 @@ function pathChecks(path) {
32
34
  case PATHS.LOCAL:
33
35
  return localMemoryChecks();
34
36
  case PATHS.HOSTED:
35
- return hostedTesChecks();
37
+ // Liveness (hostedTesChecks) + end-to-end data-flow probes.
38
+ return [...hostedTesChecks(), ...dataFlowChecks()];
36
39
  case PATHS.PLATFORM:
37
40
  return platformChecks();
38
41
  default:
@@ -91,8 +94,9 @@ export async function runDoctor(opts = {}) {
91
94
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
92
95
  const paths = detectPaths(opts);
93
96
 
94
- // Universal checks always run.
95
- const checks = [...universalChecks()];
97
+ // Universal checks always run. claudeCodeChecks is also universal —
98
+ // the plugin may be present regardless of which install path is in use.
99
+ const checks = [...universalChecks(), ...claudeCodeChecks()];
96
100
  for (const p of paths) {
97
101
  checks.push(...pathChecks(p));
98
102
  }
@@ -581,3 +581,154 @@ describe("ingest options contract", () => {
581
581
  expect(registered.length).toBe(0);
582
582
  });
583
583
  });
584
+
585
+ // --- Ingest dedup ---
586
+
587
+ describe("ingest dedup option", () => {
588
+ function makeMockDb(state = {}) {
589
+ const calls = [];
590
+ const existing = state.existing || []; // [{ id, client_id, content }, ...]
591
+ const inserted = [];
592
+ const db = async (sql, params) => {
593
+ calls.push({ sql, params });
594
+ if (sql.includes("SELECT id FROM memory_layers")) {
595
+ return { rows: [{ id: "layer-1" }] };
596
+ }
597
+ // Dedup pre-check (raw + LIKE legacy form)
598
+ if (sql.includes("SELECT id FROM memory_nodes")) {
599
+ const [clientId, content] = params;
600
+ const match = existing.find(
601
+ (r) =>
602
+ r.client_id === clientId &&
603
+ (r.content === content ||
604
+ r.content.endsWith(`] ${content}`)) // legacy timestamp-prefixed
605
+ );
606
+ return { rows: match ? [{ id: match.id }] : [] };
607
+ }
608
+ // Insert path
609
+ if (sql.startsWith("INSERT INTO memory_nodes")) {
610
+ inserted.push({
611
+ id: params[0],
612
+ client_id: params[1],
613
+ content: params[3],
614
+ });
615
+ return { rows: [] };
616
+ }
617
+ return { rows: [] };
618
+ };
619
+ return { db, calls, inserted };
620
+ }
621
+
622
+ const mockAi = { embed: async () => null };
623
+ const mockLlm = { chat: async () => "[]" };
624
+
625
+ it("inserts a fresh row when no duplicate exists", async () => {
626
+ const { db, inserted } = makeMockDb({ existing: [] });
627
+
628
+ const out = await ingest(db, mockAi, mockLlm, "fresh content", {
629
+ clientId: "c",
630
+ dedup: true,
631
+ });
632
+
633
+ expect(out.deduped).toBeUndefined();
634
+ expect(out.id.startsWith("mem_")).toBe(true);
635
+ expect(inserted).toHaveLength(1);
636
+ expect(inserted[0].content).toBe("fresh content");
637
+ });
638
+
639
+ it("returns the existing row's id when raw content matches", async () => {
640
+ const { db, inserted } = makeMockDb({
641
+ existing: [
642
+ { id: "mem_existing", client_id: "c", content: "duplicate content" },
643
+ ],
644
+ });
645
+
646
+ const out = await ingest(db, mockAi, mockLlm, "duplicate content", {
647
+ clientId: "c",
648
+ dedup: true,
649
+ });
650
+
651
+ expect(out.deduped).toBe(true);
652
+ expect(out.id).toBe("mem_existing");
653
+ expect(out.content).toBe("duplicate content");
654
+ expect(inserted).toHaveLength(0); // no insert happened
655
+ });
656
+
657
+ it("matches legacy timestamp-prefixed rows (`[<iso>] <content>`)", async () => {
658
+ const { db, inserted } = makeMockDb({
659
+ existing: [
660
+ {
661
+ id: "mem_legacy",
662
+ client_id: "c",
663
+ content: "[2026-04-26T10:00:00Z] duplicate content",
664
+ },
665
+ ],
666
+ });
667
+
668
+ const out = await ingest(db, mockAi, mockLlm, "duplicate content", {
669
+ clientId: "c",
670
+ dedup: true,
671
+ });
672
+
673
+ expect(out.deduped).toBe(true);
674
+ expect(out.id).toBe("mem_legacy");
675
+ expect(inserted).toHaveLength(0);
676
+ });
677
+
678
+ it("dedup off (default) still inserts on duplicate content", async () => {
679
+ const { db, inserted } = makeMockDb({
680
+ existing: [
681
+ { id: "mem_existing", client_id: "c", content: "duplicate content" },
682
+ ],
683
+ });
684
+
685
+ const out = await ingest(db, mockAi, mockLlm, "duplicate content", {
686
+ clientId: "c",
687
+ // dedup omitted — defaults to false
688
+ });
689
+
690
+ expect(out.deduped).toBeUndefined();
691
+ expect(inserted).toHaveLength(1);
692
+ expect(inserted[0].id).not.toBe("mem_existing");
693
+ });
694
+
695
+ it("scopes dedup to the given clientId (cross-tenant collisions don't dedup)", async () => {
696
+ const { db, inserted } = makeMockDb({
697
+ existing: [
698
+ { id: "mem_other", client_id: "other", content: "duplicate content" },
699
+ ],
700
+ });
701
+
702
+ const out = await ingest(db, mockAi, mockLlm, "duplicate content", {
703
+ clientId: "c", // different tenant
704
+ dedup: true,
705
+ });
706
+
707
+ expect(out.deduped).toBeUndefined();
708
+ expect(inserted).toHaveLength(1);
709
+ expect(inserted[0].client_id).toBe("c");
710
+ });
711
+
712
+ it("dedup check failure falls through to insert (best-effort semantics)", async () => {
713
+ let dupCheckSql = null;
714
+ const flakyDb = async (sql, params) => {
715
+ if (sql.includes("SELECT id FROM memory_layers")) {
716
+ return { rows: [{ id: "layer-1" }] };
717
+ }
718
+ if (sql.includes("SELECT id FROM memory_nodes")) {
719
+ dupCheckSql = sql;
720
+ throw new Error("DB unreachable");
721
+ }
722
+ return { rows: [] };
723
+ };
724
+
725
+ const out = await ingest(flakyDb, mockAi, mockLlm, "content", {
726
+ clientId: "c",
727
+ dedup: true,
728
+ });
729
+
730
+ expect(dupCheckSql).toContain("memory_nodes");
731
+ expect(out.deduped).toBeUndefined();
732
+ expect(out.id.startsWith("mem_")).toBe(true);
733
+ });
734
+ });
@@ -370,3 +370,10 @@ function shortenReason(msg) {
370
370
  .replace(/[^a-z0-9]+/g, "_")
371
371
  .slice(0, 60);
372
372
  }
373
+
374
+ // Re-export the system-message injector so callers that import the
375
+ // hosted module get the full memory-augmentation surface in one place.
376
+ // Keeping the implementation in `./inject.js` lets non-hosted consumers
377
+ // (e.g. a future "augment a request body" helper that doesn't talk to
378
+ // TES) reuse it without pulling in the GraphQL surface.
379
+ export { injectMemories } from "./inject.js";
@@ -21,7 +21,17 @@ import { distill } from "./distill.js";
21
21
  * tasks (e.g. Cloudflare Worker ctx.waitUntil). If provided, the distill
22
22
  * background task is handed to it so the host keeps it alive past return.
23
23
  * Without it, distill is fire-and-forget (fine for Node/browser).
24
- * @returns {Promise<{id: string, content: string, layerId: string}>}
24
+ * @param {boolean} [opts.dedup=false] - Skip ingest if a memory_node with
25
+ * byte-equal content already exists for this `client_id`. Use for
26
+ * retry-safe pipelines where the same logical event may be processed
27
+ * twice (queue retries, consumer fan-out). Returns the existing row's
28
+ * id with `{deduped: true}` instead of inserting. Strict equality —
29
+ * not a semantic similarity match. Best-effort: if the SELECT itself
30
+ * fails, ingest proceeds (worst case: duplicate row, identical to
31
+ * `dedup:false` behaviour). The eventual structural fix is a
32
+ * `UNIQUE(client_id, content_hash)` constraint at the schema level;
33
+ * this option is the bridge.
34
+ * @returns {Promise<{id: string, content: string, layerId: string, deduped?: boolean}>}
25
35
  */
26
36
  export async function ingest(db, ai, llm, content, opts = {}) {
27
37
  const clientId = opts.clientId;
@@ -41,6 +51,35 @@ export async function ingest(db, ai, llm, content, opts = {}) {
41
51
  }
42
52
 
43
53
  const layerId = layerResult.rows[0].id;
54
+
55
+ // Optional dedup: skip the insert (and all the embedding/HyDE/distill
56
+ // work that would follow) if a row with byte-equal content already
57
+ // exists for this tenant. The OR-LIKE branch matches against the
58
+ // legacy `[<iso>] <content>` form so callers that wrote with a
59
+ // timestamp prefix dedup correctly until the legacy corpus ages out.
60
+ if (opts.dedup) {
61
+ try {
62
+ const dupCheck = await db(
63
+ `SELECT id FROM memory_nodes
64
+ WHERE client_id = $1
65
+ AND (content = $2 OR content LIKE '%] ' || $2)
66
+ LIMIT 1`,
67
+ [clientId, content]
68
+ );
69
+ if (dupCheck.rows?.length) {
70
+ log(`dedup: matched existing memory ${dupCheck.rows[0].id}`);
71
+ return {
72
+ id: dupCheck.rows[0].id,
73
+ content,
74
+ layerId,
75
+ deduped: true,
76
+ };
77
+ }
78
+ } catch (err) {
79
+ log(`dedup check failed (proceeding with insert): ${err.message}`);
80
+ }
81
+ }
82
+
44
83
  const memoryId = `mem_${crypto.randomUUID()}`;
45
84
 
46
85
  // Insert memory node
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Memory injection — formats retrieved memories as a system-message preamble
3
+ * and merges them into the upstream request body.
4
+ *
5
+ * Why a preamble (not a separate user-turn or tool-result):
6
+ * - Customer's existing system prompt is preserved verbatim, just appended.
7
+ * - Anthropic and OpenAI both treat system content as cache-friendly.
8
+ * - No conversation-history mutation — replays remain reproducible.
9
+ *
10
+ * Format:
11
+ * <tes:context>
12
+ * [1] (similarity 0.82) memory text...
13
+ * [2] (similarity 0.71) memory text...
14
+ * </tes:context>
15
+ *
16
+ * The XML-ish wrapper makes it trivial for the model to ignore on demand
17
+ * and trivial for an evaluator to strip when measuring quality deltas.
18
+ */
19
+
20
+ const MAX_CHARS_PER_MEMORY = 1200;
21
+
22
+ /**
23
+ * @param {object} body — upstream request body, mutated copy returned
24
+ * @param {Array<{id, content, similarity}>} memories
25
+ * @param {"anthropic"|"openai"} provider
26
+ * @returns {object} new body
27
+ */
28
+ export function injectMemories(body, memories, provider) {
29
+ if (!memories || memories.length === 0) return body;
30
+
31
+ const preamble = formatPreamble(memories);
32
+
33
+ if (provider === "anthropic") {
34
+ return injectAnthropic(body, preamble);
35
+ }
36
+ return injectOpenAI(body, preamble);
37
+ }
38
+
39
+ function formatPreamble(memories) {
40
+ const lines = ["<tes:context>"];
41
+ memories.forEach((m, i) => {
42
+ const sim =
43
+ typeof m.similarity === "number" ? m.similarity.toFixed(2) : "?";
44
+ const content = (m.content || "").slice(0, MAX_CHARS_PER_MEMORY);
45
+ lines.push(`[${i + 1}] (similarity ${sim}) ${content}`);
46
+ });
47
+ lines.push("</tes:context>");
48
+ return lines.join("\n");
49
+ }
50
+
51
+ function injectAnthropic(body, preamble) {
52
+ // Anthropic accepts `system` as either a string OR an array of content
53
+ // blocks. Preserve whichever shape the customer sent.
54
+ const next = { ...body };
55
+ if (typeof body.system === "string") {
56
+ next.system = `${preamble}\n\n${body.system}`;
57
+ } else if (Array.isArray(body.system)) {
58
+ next.system = [{ type: "text", text: preamble }, ...body.system];
59
+ } else {
60
+ next.system = preamble;
61
+ }
62
+ return next;
63
+ }
64
+
65
+ function injectOpenAI(body, preamble) {
66
+ // OpenAI carries the system prompt as the first message with role:'system'.
67
+ // If one exists we prepend; otherwise we insert a fresh one at index 0.
68
+ const messages = Array.isArray(body.messages) ? [...body.messages] : [];
69
+ if (messages.length > 0 && messages[0].role === "system") {
70
+ const existing = messages[0];
71
+ const existingContent =
72
+ typeof existing.content === "string"
73
+ ? existing.content
74
+ : JSON.stringify(existing.content);
75
+ messages[0] = {
76
+ ...existing,
77
+ content: `${preamble}\n\n${existingContent}`,
78
+ };
79
+ } else {
80
+ messages.unshift({ role: "system", content: preamble });
81
+ }
82
+ return { ...body, messages };
83
+ }
package/src/client.js CHANGED
@@ -56,8 +56,26 @@ export class TESClient {
56
56
  return new Session(this._config, opts);
57
57
  }
58
58
 
59
- wrap(client, { sessionId, userId, metadata, autoEmit = true, waitUntil } = {}) {
59
+ wrap(
60
+ client,
61
+ {
62
+ sessionId,
63
+ userId,
64
+ metadata,
65
+ autoEmit = true,
66
+ waitUntil,
67
+ memory,
68
+ memoryOpts,
69
+ } = {}
70
+ ) {
60
71
  const config = userId ? { ...this._config, userId } : this._config;
61
- return wrapClient(config, client, { sessionId, metadata, autoEmit, waitUntil });
72
+ return wrapClient(config, client, {
73
+ sessionId,
74
+ metadata,
75
+ autoEmit,
76
+ waitUntil,
77
+ memory,
78
+ memoryOpts,
79
+ });
62
80
  }
63
81
  }