@pentatonic-ai/ai-agent-sdk 0.5.11 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +345 -174
  2. package/bin/__tests__/callback-server.test.js +70 -0
  3. package/bin/__tests__/credentials.test.js +58 -0
  4. package/bin/__tests__/login.test.js +210 -0
  5. package/bin/__tests__/pkce.test.js +39 -0
  6. package/bin/__tests__/whoami.test.js +77 -0
  7. package/bin/cli.js +109 -440
  8. package/bin/commands/config.js +251 -0
  9. package/bin/commands/login.js +219 -0
  10. package/bin/commands/whoami.js +41 -0
  11. package/bin/lib/callback-server.js +137 -0
  12. package/bin/lib/credentials.js +100 -0
  13. package/bin/lib/pkce.js +26 -0
  14. package/package.json +4 -2
  15. package/packages/doctor/__tests__/detect.test.js +2 -6
  16. package/packages/doctor/src/checks/local-memory.js +164 -196
  17. package/packages/doctor/src/detect.js +11 -3
  18. package/packages/memory/src/__tests__/corpus-chunkers.test.js +143 -0
  19. package/packages/memory/src/__tests__/corpus-discover.test.js +175 -0
  20. package/packages/memory/src/__tests__/corpus-ingest.test.js +236 -0
  21. package/packages/memory/src/__tests__/corpus-signatures.test.js +175 -0
  22. package/packages/memory/src/__tests__/corpus-state.test.js +161 -0
  23. package/packages/memory/src/__tests__/ingest-corpus-opts.test.js +129 -0
  24. package/packages/memory/src/__tests__/search-kind.test.js +108 -0
  25. package/packages/memory/src/corpus/adapters.js +398 -0
  26. package/packages/memory/src/corpus/chunkers.js +328 -0
  27. package/packages/memory/src/corpus/cli.js +613 -0
  28. package/packages/memory/src/corpus/discover.js +379 -0
  29. package/packages/memory/src/corpus/index.js +68 -0
  30. package/packages/memory/src/corpus/ingest.js +356 -0
  31. package/packages/memory/src/corpus/signatures.js +280 -0
  32. package/packages/memory/src/corpus/state.js +134 -0
  33. package/packages/memory/src/index.js +18 -0
  34. package/packages/memory/src/ingest.js +20 -11
  35. package/packages/memory/src/openclaw/index.js +39 -1
  36. package/packages/memory/src/search.js +30 -7
  37. package/packages/memory-engine/.env.example +13 -0
  38. package/packages/memory-engine/README.md +131 -0
  39. package/packages/memory-engine/bench/README.md +99 -0
  40. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
  41. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
  42. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
  43. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
  44. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
  45. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
  46. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
  47. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
  48. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
  49. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
  50. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
  51. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
  52. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
  53. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
  54. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
  55. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
  56. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
  57. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
  58. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
  59. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
  60. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
  61. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
  62. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
  63. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
  64. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
  65. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
  66. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
  67. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
  68. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
  69. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
  70. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
  71. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
  72. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
  73. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
  74. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
  75. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
  76. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
  77. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
  78. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
  79. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
  80. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
  81. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
  82. package/packages/memory-engine/compat/Dockerfile +11 -0
  83. package/packages/memory-engine/compat/server.py +680 -0
  84. package/packages/memory-engine/docker-compose.yml +243 -0
  85. package/packages/memory-engine/docs/MIGRATION.md +178 -0
  86. package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
  87. package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
  88. package/packages/memory-engine/engine/README.md +52 -0
  89. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
  90. package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
  91. package/packages/memory-engine/engine/l6-document-store.py +1018 -0
  92. package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
  93. package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
  94. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
  95. package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
  96. package/packages/memory-engine/engine/services/l4/server.py +235 -0
  97. package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
  98. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
  99. package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
  100. package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
  101. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
  102. package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
  103. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  104. package/packages/memory-engine/pme_memory/__main__.py +129 -0
  105. package/packages/memory-engine/pme_memory/artifacts.py +95 -0
  106. package/packages/memory-engine/pme_memory/embed.py +74 -0
  107. package/packages/memory-engine/pme_memory/health.py +36 -0
  108. package/packages/memory-engine/pme_memory/hygiene.py +159 -0
  109. package/packages/memory-engine/pme_memory/indexer.py +200 -0
  110. package/packages/memory-engine/pme_memory/needs.py +55 -0
  111. package/packages/memory-engine/pme_memory/provenance.py +80 -0
  112. package/packages/memory-engine/pme_memory/scoring.py +168 -0
  113. package/packages/memory-engine/pme_memory/search.py +52 -0
  114. package/packages/memory-engine/pme_memory/store.py +86 -0
  115. package/packages/memory-engine/pme_memory/synthesis.py +114 -0
  116. package/packages/memory-engine/pyproject.toml +65 -0
  117. package/packages/memory-engine/scripts/kg-extractor.py +557 -0
  118. package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
  119. package/packages/memory-engine/tests/test_api_contract.sh +57 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Corpus ingest adapters — concrete implementations of the
3
+ * { ingestChunk, deleteByCorpusFile }
4
+ * contract that the corpus pipeline depends on.
5
+ *
6
+ * `localAdapter` — writes via the existing memory.ingest() against a pg
7
+ * pool (or any query function). Stores corpus chunks
8
+ * in the `semantic` layer with metadata.source_file
9
+ * and metadata.corpus_file_key.
10
+ *
11
+ * `hostedAdapter` — calls the existing `createMemory` GraphQL mutation
12
+ * on deep-memory directly. This bypasses the
13
+ * STORE_MEMORY event queue (which has max_batch_size=1
14
+ * and would be the wrong shape for bulk ingest) and
15
+ * writes synchronously into the explicit `semantic`
16
+ * layer with full metadata.
17
+ *
18
+ * Companion TES PR required: the createMemory resolver
19
+ * currently hardcodes layerType: "episodic" even when
20
+ * layerId is supplied — see the audit notes in
21
+ * specs/01-onboarding-repo-ingest.md §12. Until that
22
+ * ships, chunks will land in episodic and may decay.
23
+ */
24
+
25
+ import { ingest } from "../ingest.js";
26
+ import { buildHostedHeaders } from "../hosted.js";
27
+
28
+ const CREATE_MEMORY_MUTATION = `
29
+ mutation CreateMemory($clientId: String!, $layerId: String!, $content: String!, $metadata: JSON) {
30
+ createMemory(clientId: $clientId, layerId: $layerId, content: $content, metadata: $metadata) {
31
+ id
32
+ layer_id
33
+ }
34
+ }
35
+ `;
36
+
37
+ const MEMORY_LAYERS_QUERY = `
38
+ query MemoryLayers($clientId: String!) {
39
+ memoryLayers(clientId: $clientId) {
40
+ id
41
+ name
42
+ layer_type
43
+ is_active
44
+ }
45
+ }
46
+ `;
47
+
48
+ const DELETE_BY_METADATA_MUTATION = `
49
+ mutation DeleteMemoryNodesByMetadata($clientId: String!, $metadataKey: String!, $metadataValue: String!) {
50
+ deleteMemoryNodesByMetadata(clientId: $clientId, metadataKey: $metadataKey, metadataValue: $metadataValue)
51
+ }
52
+ `;
53
+
54
+ const DEFAULT_LAYER = "semantic";
55
+
56
+ /**
57
+ * Build a local adapter against a memory system instance.
58
+ *
59
+ * @param {object} memory - createMemorySystem() result OR raw deps
60
+ * @param {object} opts
61
+ * @param {string} opts.clientId - Required, used as memory_layers.client_id
62
+ * @param {string} [opts.userId]
63
+ * @param {string} [opts.layer="semantic"]
64
+ * @returns {{ingestChunk, deleteByCorpusFile, init}}
65
+ */
66
+ export function localAdapter(memory, opts = {}) {
67
+ if (!opts.clientId) throw new Error("localAdapter: clientId is required");
68
+
69
+ const layer = opts.layer || DEFAULT_LAYER;
70
+
71
+ return {
72
+ /**
73
+ * One-time setup — ensure the target layer exists.
74
+ */
75
+ async init() {
76
+ // memory.ensureLayers takes a single clientId and creates all four
77
+ // default layers idempotently. Safe to call repeatedly.
78
+ if (typeof memory.ensureLayers === "function") {
79
+ await memory.ensureLayers(opts.clientId);
80
+ }
81
+ },
82
+
83
+ async ingestChunk(content, metadata) {
84
+ // Code references aren't conversational user-stated facts, so the
85
+ // distill step (which runs an "extract atomic facts from a
86
+ // conversation" prompt) is at best wasted compute and at worst
87
+ // hallucinates "user" facts from code structure that pollute the
88
+ // semantic layer. Skip distillation for corpus ingest.
89
+ const ingestOpts = {
90
+ clientId: opts.clientId,
91
+ userId: opts.userId,
92
+ layerType: layer,
93
+ metadata,
94
+ distill: false,
95
+ };
96
+ // Use memory.ingest if we have the high-level API, otherwise fall
97
+ // back to the lower-level ingest() function with explicit deps.
98
+ if (typeof memory.ingest === "function") {
99
+ const result = await memory.ingest(content, ingestOpts);
100
+ return { id: result.id };
101
+ }
102
+ // Direct ingest() form — caller passed { db, ai, llm }
103
+ const result = await ingest(
104
+ memory.db,
105
+ memory.ai,
106
+ memory.llm,
107
+ content,
108
+ ingestOpts
109
+ );
110
+ return { id: result.id };
111
+ },
112
+
113
+ /**
114
+ * Delete all memory_nodes whose metadata.corpus_file_key matches
115
+ * `${repoAbs}::${relPath}`. Used when re-ingesting a changed file
116
+ * or removing a vanished one.
117
+ *
118
+ * Returns the number of rows deleted.
119
+ */
120
+ async deleteByCorpusFile(repoAbs, relPath) {
121
+ if (!memory.db) {
122
+ throw new Error(
123
+ "localAdapter: deleteByCorpusFile requires { db } on the memory deps"
124
+ );
125
+ }
126
+ const key = `${repoAbs}::${relPath}`;
127
+ const res = await memory.db(
128
+ `DELETE FROM memory_nodes
129
+ WHERE client_id = $1
130
+ AND metadata->>'corpus_file_key' = $2
131
+ RETURNING id`,
132
+ [opts.clientId, key]
133
+ );
134
+ return (res.rows || []).length;
135
+ },
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Hosted adapter — calls the deep-memory `createMemory` GraphQL
141
+ * mutation directly so corpus chunks land in the chosen layer
142
+ * (semantic by default) with embedding + HyDE applied server-side.
143
+ *
144
+ * Why not events?
145
+ * The STORE_MEMORY event path is fine for chat-turn fan-out (one
146
+ * event per conversation turn) but is the wrong shape for bulk
147
+ * corpus ingest. The Cloudflare consumer queue is configured with
148
+ * max_batch_size=1 and max_concurrency=30 (see
149
+ * thing-event-system/workers/wrangler.epic.toml), so 12,000 chunks
150
+ * would mean 12,000 individual consumer invocations — slow, and
151
+ * the existing consumer also hardcodes layer routing to "episodic"
152
+ * (which would decay code chunks within days). Calling the GraphQL
153
+ * mutation directly is faster, cheaper, and lets us specify the
154
+ * layer explicitly.
155
+ *
156
+ * Required TES companion fix:
157
+ * modules/deep-memory/graphql/memory/resolvers.js#createMemory
158
+ * accepts a layerId argument and validates the row exists, but
159
+ * then ignores it and writes to "episodic" anyway. A one-line
160
+ * change to read layer_type from the validated row makes it honor
161
+ * the request. Until that lands, chunks ingested via this adapter
162
+ * will land in episodic; once it lands, they go to whatever layer
163
+ * was requested (default: semantic).
164
+ *
165
+ * Deletion:
166
+ * No `deleteMemoriesByMetadata` mutation exists today. The adapter
167
+ * logs the intent locally but cannot drop server-side chunks for
168
+ * removed files — those will accumulate. Documented as a follow-up.
169
+ *
170
+ * @param {object} config - { endpoint, clientId, apiKey } (or tes_* legacy)
171
+ * @param {object} [opts]
172
+ * @param {number} [opts.timeoutMs=15000]
173
+ * @param {string} [opts.layerName="semantic"] - Target layer name; resolved to layerId via memoryLayers query on first call
174
+ */
175
+ export function hostedAdapter(config, opts = {}) {
176
+ const timeoutMs = opts.timeoutMs ?? 15000;
177
+ const layerName = opts.layerName || DEFAULT_LAYER;
178
+
179
+ const endpoint = config.endpoint || config.tes_endpoint;
180
+ const clientId = config.clientId || config.tes_client_id;
181
+ const apiKey = config.apiKey || config.tes_api_key;
182
+ if (!endpoint || !clientId || !apiKey) {
183
+ throw new Error(
184
+ "hostedAdapter: requires { endpoint, clientId, apiKey } (tes_* keys also accepted)"
185
+ );
186
+ }
187
+ const cfg = { endpoint, clientId, apiKey };
188
+ const headers = buildHostedHeaders(cfg);
189
+
190
+ let layerId = null;
191
+ let layerLookupPromise = null;
192
+
193
+ async function graphql(query, variables) {
194
+ const controller = new AbortController();
195
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
196
+ try {
197
+ const res = await fetch(`${cfg.endpoint}/api/graphql`, {
198
+ method: "POST",
199
+ headers,
200
+ body: JSON.stringify({ query, variables }),
201
+ signal: controller.signal,
202
+ });
203
+ clearTimeout(timer);
204
+ if (!res.ok) {
205
+ return { error: `tes_http_${res.status}` };
206
+ }
207
+ const body = await res.json();
208
+ if (body?.errors?.length) {
209
+ return {
210
+ error: `tes_graphql_error:${body.errors[0]?.message}`,
211
+ errors: body.errors,
212
+ };
213
+ }
214
+ return { data: body.data };
215
+ } catch (err) {
216
+ clearTimeout(timer);
217
+ return {
218
+ error: err.name === "AbortError" ? "tes_timeout" : "tes_unreachable",
219
+ };
220
+ }
221
+ }
222
+
223
+ async function resolveLayerId() {
224
+ if (layerId) return layerId;
225
+ if (layerLookupPromise) return layerLookupPromise;
226
+
227
+ layerLookupPromise = (async () => {
228
+ const result = await graphql(MEMORY_LAYERS_QUERY, { clientId });
229
+ if (result.error) {
230
+ throw new Error(`hostedAdapter: layer lookup failed (${result.error})`);
231
+ }
232
+ const layers = result.data?.memoryLayers || [];
233
+ const match = layers.find(
234
+ (l) => l.is_active && (l.name === layerName || l.layer_type === layerName)
235
+ );
236
+ if (!match) {
237
+ throw new Error(
238
+ `hostedAdapter: no active layer named "${layerName}" for client ${clientId}. ` +
239
+ `Available: ${layers.map((l) => l.name).join(", ") || "<none>"}. ` +
240
+ `Ensure the deep-memory module is enabled in the TES dashboard.`
241
+ );
242
+ }
243
+ layerId = match.id;
244
+ return layerId;
245
+ })();
246
+ return layerLookupPromise;
247
+ }
248
+
249
+ return {
250
+ async init() {
251
+ // Resolve the target layerId now so we fail fast if the module
252
+ // isn't enabled or the layer doesn't exist.
253
+ await resolveLayerId();
254
+ },
255
+
256
+ async ingestChunk(content, metadata) {
257
+ // Note: distillation happens (or not) server-side. We can't pass
258
+ // distill:false through createMemory the way localAdapter does.
259
+ // metadata.kind = "code_reference" is the signal downstream
260
+ // consumers should branch on to skip the conversation-shaped
261
+ // distiller. Tracked as a follow-up TES change.
262
+ const lid = await resolveLayerId();
263
+ const result = await graphql(CREATE_MEMORY_MUTATION, {
264
+ clientId,
265
+ layerId: lid,
266
+ content,
267
+ metadata,
268
+ });
269
+ if (result.error) return { skipped: result.error };
270
+ return { id: result.data?.createMemory?.id };
271
+ },
272
+
273
+ async deleteByCorpusFile(repoAbs, relPath) {
274
+ // Calls the deleteMemoryNodesByMetadata mutation added in TES
275
+ // PR #245. Server-side returns the rowcount; we propagate it.
276
+ // If the mutation isn't deployed yet (older TES tenant), the
277
+ // GraphQL error is swallowed as a skipped delete — the SDK's
278
+ // local state still drops the entry on its side, and orphaned
279
+ // server-side chunks accumulate until the TES PR lands.
280
+ const key = `${repoAbs}::${relPath}`;
281
+ const result = await graphql(DELETE_BY_METADATA_MUTATION, {
282
+ clientId,
283
+ metadataKey: "corpus_file_key",
284
+ metadataValue: key,
285
+ });
286
+ if (result.error) {
287
+ // Older TES tenants (pre-PR-245) will reject the unknown
288
+ // mutation; treat as zero deletions rather than throwing.
289
+ return 0;
290
+ }
291
+ return result.data?.deleteMemoryNodesByMetadata || 0;
292
+ },
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Engine adapter — talks directly to the memory engine's HTTP API
298
+ * (`/store`, `/store-batch`, `/forget`) at `engineUrl`. Used for the
299
+ * local-OSS path where no TES is involved, and for any case where the
300
+ * caller wants to ingest straight into a Pentatonic-managed engine
301
+ * without going through the TES GraphQL surface.
302
+ *
303
+ * Wire format matches the engine's compat shim. References-mode
304
+ * metadata (kind: "code_reference") and arbitrary metadata pass
305
+ * through as JSONB on the engine side.
306
+ *
307
+ * @param {object} config
308
+ * @param {string} config.engineUrl - e.g. "http://localhost:8099"
309
+ * @param {string} [config.arena] - tenant scope; defaults to "default"
310
+ * @param {string} [config.apiKey] - optional Authorization: Bearer
311
+ * @param {object} [opts]
312
+ * @param {number} [opts.timeoutMs=30000]
313
+ * @returns {{ingestChunk, deleteByCorpusFile, init}}
314
+ */
315
+ export function engineAdapter(config, opts = {}) {
316
+ const engineUrl = (config.engineUrl || "").replace(/\/$/, "");
317
+ if (!engineUrl) {
318
+ throw new Error("engineAdapter: engineUrl is required");
319
+ }
320
+ const arena = config.arena || "default";
321
+ const apiKey = config.apiKey || null;
322
+ const timeoutMs = opts.timeoutMs ?? 30000;
323
+
324
+ function headers() {
325
+ const h = { "content-type": "application/json" };
326
+ if (apiKey) h["authorization"] = `Bearer ${apiKey}`;
327
+ return h;
328
+ }
329
+
330
+ async function http(path, body) {
331
+ const controller = new AbortController();
332
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
333
+ try {
334
+ const res = await fetch(`${engineUrl}${path}`, {
335
+ method: "POST",
336
+ headers: headers(),
337
+ body: JSON.stringify(body),
338
+ signal: controller.signal,
339
+ });
340
+ clearTimeout(timer);
341
+ if (!res.ok) return { error: `engine_http_${res.status}` };
342
+ return { data: await res.json() };
343
+ } catch (err) {
344
+ clearTimeout(timer);
345
+ return {
346
+ error: err.name === "AbortError" ? "engine_timeout" : "engine_unreachable",
347
+ };
348
+ }
349
+ }
350
+
351
+ return {
352
+ /**
353
+ * Verify the engine is reachable before kicking off ingest.
354
+ * Engine /health returns 200 even when individual layers are
355
+ * "degraded"; we just check the HTTP path works.
356
+ */
357
+ async init() {
358
+ const controller = new AbortController();
359
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
360
+ try {
361
+ const res = await fetch(`${engineUrl}/health`, {
362
+ headers: headers(),
363
+ signal: controller.signal,
364
+ });
365
+ clearTimeout(timer);
366
+ if (!res.ok) {
367
+ throw new Error(`engineAdapter: /health returned ${res.status}`);
368
+ }
369
+ } catch (err) {
370
+ clearTimeout(timer);
371
+ throw new Error(
372
+ `engineAdapter: engine at ${engineUrl} unreachable (${err.message})`
373
+ );
374
+ }
375
+ },
376
+
377
+ async ingestChunk(content, metadata) {
378
+ // Engine ingests via /store; one chunk per call. The corpus
379
+ // pipeline batches at the file level, but each chunk is its own
380
+ // /store call so we get a per-chunk id back. /store-batch is
381
+ // available for future bulk ingest if/when the pipeline rewires.
382
+ const body = { content, metadata: { ...metadata, arena } };
383
+ const result = await http("/store", body);
384
+ if (result.error) return { skipped: result.error };
385
+ return { id: result.data?.id };
386
+ },
387
+
388
+ async deleteByCorpusFile(repoAbs, relPath) {
389
+ const key = `${repoAbs}::${relPath}`;
390
+ const result = await http("/forget", {
391
+ metadata_contains: { corpus_file_key: key },
392
+ arena,
393
+ });
394
+ if (result.error) return 0;
395
+ return result.data?.deleted ?? 0;
396
+ },
397
+ };
398
+ }