@pentatonic-ai/ai-agent-sdk 0.6.0 → 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.
- package/README.md +170 -69
- package/bin/__tests__/callback-server.test.js +4 -1
- package/bin/cli.js +41 -164
- package/bin/commands/config.js +251 -0
- package/package.json +2 -1
- package/packages/doctor/__tests__/detect.test.js +2 -6
- package/packages/doctor/src/checks/local-memory.js +164 -196
- package/packages/doctor/src/detect.js +11 -3
- package/packages/memory/src/corpus/adapters.js +104 -0
- package/packages/memory/src/corpus/cli.js +72 -7
- package/packages/memory/src/corpus/index.js +1 -1
- package/packages/memory-engine/.env.example +13 -0
- package/packages/memory-engine/README.md +131 -0
- package/packages/memory-engine/bench/README.md +99 -0
- package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
- package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
- package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
- package/packages/memory-engine/compat/Dockerfile +11 -0
- package/packages/memory-engine/compat/server.py +680 -0
- package/packages/memory-engine/docker-compose.yml +243 -0
- package/packages/memory-engine/docs/MIGRATION.md +178 -0
- package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
- package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
- package/packages/memory-engine/engine/README.md +52 -0
- package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
- package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
- package/packages/memory-engine/engine/l6-document-store.py +1018 -0
- package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
- package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
- package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
- package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
- package/packages/memory-engine/engine/services/l4/server.py +235 -0
- package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
- package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
- package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
- package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
- package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
- package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
- package/packages/memory-engine/pme_memory/__init__.py +0 -0
- package/packages/memory-engine/pme_memory/__main__.py +129 -0
- package/packages/memory-engine/pme_memory/artifacts.py +95 -0
- package/packages/memory-engine/pme_memory/embed.py +74 -0
- package/packages/memory-engine/pme_memory/health.py +36 -0
- package/packages/memory-engine/pme_memory/hygiene.py +159 -0
- package/packages/memory-engine/pme_memory/indexer.py +200 -0
- package/packages/memory-engine/pme_memory/needs.py +55 -0
- package/packages/memory-engine/pme_memory/provenance.py +80 -0
- package/packages/memory-engine/pme_memory/scoring.py +168 -0
- package/packages/memory-engine/pme_memory/search.py +52 -0
- package/packages/memory-engine/pme_memory/store.py +86 -0
- package/packages/memory-engine/pme_memory/synthesis.py +114 -0
- package/packages/memory-engine/pyproject.toml +65 -0
- package/packages/memory-engine/scripts/kg-extractor.py +557 -0
- package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
- package/packages/memory-engine/tests/test_api_contract.sh +57 -0
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local
|
|
2
|
+
* Local memory engine checks.
|
|
3
3
|
*
|
|
4
|
-
* Targets the stack started by
|
|
5
|
-
* -
|
|
6
|
-
* - Ollama (or any OpenAI-compatible embedding + chat endpoint)
|
|
7
|
-
* - Memory MCP server on PORT (default 3333)
|
|
4
|
+
* Targets the engine stack started by:
|
|
5
|
+
* cd packages/memory-engine && docker compose up -d
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The engine exposes a compat HTTP shim on port 8099 (or whatever
|
|
8
|
+
* memory_url is set to in the user's plugin config). All checks are
|
|
9
|
+
* just HTTP calls + plugin-config parsing — no direct Postgres/pg_vector
|
|
10
|
+
* probing. The legacy Postgres+Ollama checks were removed when the
|
|
11
|
+
* v0.5.x in-process memory server was deprecated.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
14
18
|
import { SEVERITY } from "../index.js";
|
|
15
19
|
|
|
20
|
+
const DEFAULT_ENGINE_URL = "http://localhost:8099";
|
|
21
|
+
|
|
16
22
|
async function fetchWithTimeout(url, opts = {}, timeoutMs = 5000) {
|
|
17
23
|
return await fetch(url, {
|
|
18
24
|
...opts,
|
|
@@ -20,177 +26,149 @@ async function fetchWithTimeout(url, opts = {}, timeoutMs = 5000) {
|
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
function
|
|
29
|
+
function parseFrontmatter(content) {
|
|
30
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const line of m[1].split("\n")) {
|
|
34
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
35
|
+
if (kv) out[kv[1]] = kv[2].trim();
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findPluginConfig() {
|
|
41
|
+
const candidates = [
|
|
42
|
+
process.env.CLAUDE_CONFIG_DIR,
|
|
43
|
+
join(homedir(), ".claude-pentatonic"),
|
|
44
|
+
join(homedir(), ".claude"),
|
|
45
|
+
].filter(Boolean);
|
|
46
|
+
for (const dir of candidates) {
|
|
47
|
+
const p = join(dir, "tes-memory.local.md");
|
|
48
|
+
if (existsSync(p)) return p;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveEngineUrl() {
|
|
54
|
+
// 1. env var override
|
|
55
|
+
if (process.env.MEMORY_ENGINE_URL) return process.env.MEMORY_ENGINE_URL;
|
|
56
|
+
// 2. plugin config
|
|
57
|
+
const cfgPath = findPluginConfig();
|
|
58
|
+
if (cfgPath) {
|
|
59
|
+
try {
|
|
60
|
+
const fm = parseFrontmatter(readFileSync(cfgPath, "utf-8"));
|
|
61
|
+
if (fm?.mode === "local" && fm.memory_url) return fm.memory_url;
|
|
62
|
+
} catch {
|
|
63
|
+
// fall through
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 3. default
|
|
67
|
+
return DEFAULT_ENGINE_URL;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkPluginConfig() {
|
|
24
71
|
return {
|
|
25
|
-
name: "
|
|
26
|
-
severity: SEVERITY.
|
|
72
|
+
name: "plugin config (tes-memory.local.md)",
|
|
73
|
+
severity: SEVERITY.WARNING,
|
|
27
74
|
run: async () => {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
75
|
+
const cfgPath = findPluginConfig();
|
|
76
|
+
if (!cfgPath) {
|
|
30
77
|
return {
|
|
31
78
|
ok: false,
|
|
32
|
-
msg: "
|
|
79
|
+
msg: "no tes-memory.local.md found — run `npx @pentatonic-ai/ai-agent-sdk config local`",
|
|
33
80
|
};
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
let pg;
|
|
82
|
+
let fm;
|
|
37
83
|
try {
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
84
|
+
fm = parseFrontmatter(readFileSync(cfgPath, "utf-8"));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { ok: false, msg: `${cfgPath}: ${err.message}` };
|
|
87
|
+
}
|
|
88
|
+
if (!fm) {
|
|
89
|
+
return { ok: false, msg: `${cfgPath}: no parseable frontmatter` };
|
|
90
|
+
}
|
|
91
|
+
if (fm.mode !== "local") {
|
|
40
92
|
return {
|
|
41
93
|
ok: false,
|
|
42
|
-
msg: "
|
|
94
|
+
msg: `${cfgPath}: mode is "${fm.mode || "(unset)"}" — expected "local"`,
|
|
95
|
+
detail: { mode: fm.mode, path: cfgPath },
|
|
43
96
|
};
|
|
44
97
|
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await client.connect();
|
|
48
|
-
const v = await client.query("SELECT version()");
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
msg: v.rows[0].version.split(",")[0],
|
|
52
|
-
detail: { version: v.rows[0].version },
|
|
53
|
-
};
|
|
54
|
-
} catch (err) {
|
|
98
|
+
if (!fm.memory_url) {
|
|
55
99
|
return {
|
|
56
100
|
ok: false,
|
|
57
|
-
msg:
|
|
101
|
+
msg: `${cfgPath}: memory_url not set`,
|
|
102
|
+
detail: { path: cfgPath },
|
|
58
103
|
};
|
|
59
|
-
} finally {
|
|
60
|
-
await client.end().catch(() => {});
|
|
61
104
|
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
msg: `${fm.memory_url} (${cfgPath})`,
|
|
108
|
+
detail: { memory_url: fm.memory_url, path: cfgPath },
|
|
109
|
+
};
|
|
62
110
|
},
|
|
63
111
|
};
|
|
64
112
|
}
|
|
65
113
|
|
|
66
|
-
function
|
|
114
|
+
function checkEngineHealth() {
|
|
67
115
|
return {
|
|
68
|
-
name: "
|
|
116
|
+
name: "engine /health",
|
|
69
117
|
severity: SEVERITY.CRITICAL,
|
|
70
118
|
run: async () => {
|
|
71
|
-
const
|
|
72
|
-
if (!dsn) return { ok: false, msg: "DATABASE_URL not set" };
|
|
73
|
-
let pg;
|
|
119
|
+
const url = resolveEngineUrl();
|
|
74
120
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
const client = new pg.Client({ connectionString: dsn });
|
|
80
|
-
try {
|
|
81
|
-
await client.connect();
|
|
82
|
-
const r = await client.query(
|
|
83
|
-
"SELECT extversion FROM pg_extension WHERE extname='vector'"
|
|
84
|
-
);
|
|
85
|
-
if (!r.rowCount) {
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
msg: "pgvector not installed — run CREATE EXTENSION vector",
|
|
89
|
-
};
|
|
121
|
+
const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
return { ok: false, msg: `HTTP ${res.status} from ${url}/health` };
|
|
90
124
|
}
|
|
125
|
+
const data = await res.json();
|
|
91
126
|
return {
|
|
92
127
|
ok: true,
|
|
93
|
-
msg:
|
|
94
|
-
detail:
|
|
128
|
+
msg: `${data.engine || "engine"} v${data.version || "?"} (${data.status})`,
|
|
129
|
+
detail: data,
|
|
95
130
|
};
|
|
96
131
|
} catch (err) {
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
msg: `${url}/health unreachable: ${err.message}`,
|
|
135
|
+
detail: { url },
|
|
136
|
+
};
|
|
100
137
|
}
|
|
101
138
|
},
|
|
102
139
|
};
|
|
103
140
|
}
|
|
104
141
|
|
|
105
|
-
function
|
|
142
|
+
function checkEngineLayers() {
|
|
106
143
|
return {
|
|
107
|
-
name: "
|
|
144
|
+
name: "engine layers (L0–L6)",
|
|
108
145
|
severity: SEVERITY.WARNING,
|
|
109
146
|
run: async () => {
|
|
110
|
-
const
|
|
111
|
-
if (!dsn) return { ok: false, msg: "DATABASE_URL not set" };
|
|
112
|
-
let pg;
|
|
113
|
-
try {
|
|
114
|
-
pg = (await import("pg")).default;
|
|
115
|
-
} catch {
|
|
116
|
-
return { ok: false, msg: "'pg' not installed" };
|
|
117
|
-
}
|
|
118
|
-
const client = new pg.Client({ connectionString: dsn });
|
|
147
|
+
const url = resolveEngineUrl();
|
|
119
148
|
try {
|
|
120
|
-
await
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
149
|
+
const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
|
|
150
|
+
if (!res.ok) return { ok: false, msg: `HTTP ${res.status}` };
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
const layers = data.layers || {};
|
|
153
|
+
const expected = ["l0", "l1", "l2", "l3", "l4", "l5", "l6"];
|
|
154
|
+
const okList = [];
|
|
155
|
+
const degradedList = [];
|
|
156
|
+
for (const k of expected) {
|
|
157
|
+
const status = layers[k];
|
|
158
|
+
if (status === "ok") okList.push(k);
|
|
159
|
+
else if (status) degradedList.push(`${k}=${status}`);
|
|
131
160
|
}
|
|
132
|
-
|
|
133
|
-
} catch (err) {
|
|
134
|
-
if (/relation .* does not exist/.test(err.message)) {
|
|
161
|
+
if (degradedList.length === 0) {
|
|
135
162
|
return {
|
|
136
|
-
ok:
|
|
137
|
-
msg:
|
|
163
|
+
ok: true,
|
|
164
|
+
msg: `${okList.length}/7 ok`,
|
|
165
|
+
detail: { layers },
|
|
138
166
|
};
|
|
139
167
|
}
|
|
140
|
-
return { ok: false, msg: err.message };
|
|
141
|
-
} finally {
|
|
142
|
-
await client.end().catch(() => {});
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function checkEmbeddingEndpoint() {
|
|
149
|
-
return {
|
|
150
|
-
name: "embedding endpoint",
|
|
151
|
-
severity: SEVERITY.CRITICAL,
|
|
152
|
-
run: async () => {
|
|
153
|
-
const url = process.env.EMBEDDING_URL;
|
|
154
|
-
const model = process.env.EMBEDDING_MODEL;
|
|
155
|
-
if (!url || !model) {
|
|
156
168
|
return {
|
|
157
169
|
ok: false,
|
|
158
|
-
msg:
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
// Probe with a 1-token embed call against the OpenAI-compatible API.
|
|
162
|
-
try {
|
|
163
|
-
const headers = { "Content-Type": "application/json" };
|
|
164
|
-
if (process.env.API_KEY) {
|
|
165
|
-
headers.Authorization = `Bearer ${process.env.API_KEY}`;
|
|
166
|
-
}
|
|
167
|
-
const res = await fetchWithTimeout(
|
|
168
|
-
`${url.replace(/\/$/, "")}/embeddings`,
|
|
169
|
-
{
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers,
|
|
172
|
-
body: JSON.stringify({ model, input: "ping" }),
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
if (!res.ok) {
|
|
176
|
-
const body = await res.text().catch(() => "");
|
|
177
|
-
return {
|
|
178
|
-
ok: false,
|
|
179
|
-
msg: `HTTP ${res.status}: ${body.slice(0, 120)}`,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const data = await res.json();
|
|
183
|
-
const dim = data.data?.[0]?.embedding?.length;
|
|
184
|
-
if (!dim) {
|
|
185
|
-
return {
|
|
186
|
-
ok: false,
|
|
187
|
-
msg: "endpoint responded but returned no embedding",
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
ok: true,
|
|
192
|
-
msg: `${model} ok (${dim}-dim)`,
|
|
193
|
-
detail: { url, model, dim },
|
|
170
|
+
msg: `${okList.length}/7 ok; degraded: ${degradedList.join(", ")}`,
|
|
171
|
+
detail: { layers },
|
|
194
172
|
};
|
|
195
173
|
} catch (err) {
|
|
196
174
|
return { ok: false, msg: err.message };
|
|
@@ -199,45 +177,33 @@ function checkEmbeddingEndpoint() {
|
|
|
199
177
|
};
|
|
200
178
|
}
|
|
201
179
|
|
|
202
|
-
function
|
|
180
|
+
function checkEmbeddingPath() {
|
|
181
|
+
// Surfaces the engine's view of nv_embed (the URL it points at). If
|
|
182
|
+
// that's "unreachable" or "http 4xx/5xx", L4/L5/L6 indexing won't work.
|
|
183
|
+
// The engine reports this in /health under layers.nv_embed.
|
|
203
184
|
return {
|
|
204
|
-
name: "
|
|
205
|
-
severity: SEVERITY.
|
|
185
|
+
name: "embedding endpoint (engine→external)",
|
|
186
|
+
severity: SEVERITY.WARNING,
|
|
206
187
|
run: async () => {
|
|
207
|
-
const url =
|
|
208
|
-
const model = process.env.LLM_MODEL;
|
|
209
|
-
if (!url || !model) {
|
|
210
|
-
return {
|
|
211
|
-
ok: false,
|
|
212
|
-
msg: "LLM_URL or LLM_MODEL not set",
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
// /models is cheap and present on every OpenAI-compatible server.
|
|
188
|
+
const url = resolveEngineUrl();
|
|
216
189
|
try {
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
headers.Authorization = `Bearer ${process.env.API_KEY}`;
|
|
220
|
-
}
|
|
221
|
-
const res = await fetchWithTimeout(
|
|
222
|
-
`${url.replace(/\/$/, "")}/models`,
|
|
223
|
-
{ headers }
|
|
224
|
-
);
|
|
225
|
-
if (!res.ok) {
|
|
226
|
-
return { ok: false, msg: `HTTP ${res.status}` };
|
|
227
|
-
}
|
|
190
|
+
const res = await fetchWithTimeout(`${url.replace(/\/$/, "")}/health`);
|
|
191
|
+
if (!res.ok) return { ok: false, msg: `engine /health HTTP ${res.status}` };
|
|
228
192
|
const data = await res.json();
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
193
|
+
const nv = data.layers?.nv_embed;
|
|
194
|
+
if (nv === "ok") {
|
|
195
|
+
return { ok: true, msg: "reachable" };
|
|
196
|
+
}
|
|
197
|
+
if (!nv) {
|
|
231
198
|
return {
|
|
232
199
|
ok: false,
|
|
233
|
-
msg:
|
|
234
|
-
detail: { url, requested: model, available: ids },
|
|
200
|
+
msg: "engine /health did not report nv_embed status",
|
|
235
201
|
};
|
|
236
202
|
}
|
|
237
203
|
return {
|
|
238
|
-
ok:
|
|
239
|
-
msg:
|
|
240
|
-
detail: {
|
|
204
|
+
ok: false,
|
|
205
|
+
msg: `nv_embed=${nv} — L4/L5/L6 indexing will fail. Check NV_EMBED_URL in packages/memory-engine/.env`,
|
|
206
|
+
detail: { nv_embed: nv },
|
|
241
207
|
};
|
|
242
208
|
} catch (err) {
|
|
243
209
|
return { ok: false, msg: err.message };
|
|
@@ -246,45 +212,47 @@ function checkLlmEndpoint() {
|
|
|
246
212
|
};
|
|
247
213
|
}
|
|
248
214
|
|
|
249
|
-
function
|
|
215
|
+
function checkOllamaBindIfPresent() {
|
|
216
|
+
// If the user is using Ollama as their embedding backend AND running
|
|
217
|
+
// the engine in Docker, Ollama needs to be bound on all interfaces so
|
|
218
|
+
// containers can reach it via host.docker.internal. Common gotcha.
|
|
219
|
+
// We probe by checking whether Ollama is listening on something other
|
|
220
|
+
// than 127.0.0.1 — we can't directly read systemd config, so we fall
|
|
221
|
+
// back to probing 0.0.0.0:11434 from the host (which always works if
|
|
222
|
+
// any interface is listening) vs trying to detect the bind address.
|
|
223
|
+
//
|
|
224
|
+
// Approach: hit /api/tags. If reachable on 127.0.0.1, Ollama is up;
|
|
225
|
+
// we then warn if the user is in a Docker context (engine reachable)
|
|
226
|
+
// because that's the configuration where the bind matters.
|
|
250
227
|
return {
|
|
251
|
-
name: "
|
|
252
|
-
severity: SEVERITY.
|
|
228
|
+
name: "Ollama bind config (if used)",
|
|
229
|
+
severity: SEVERITY.INFO,
|
|
253
230
|
run: async () => {
|
|
254
|
-
// The MCP server uses stdio by default but exposes PORT for HTTP/SSE.
|
|
255
|
-
// If PORT isn't bound we can't probe — that's not an error, just info.
|
|
256
|
-
const port = process.env.PORT || "3333";
|
|
257
|
-
const url = `http://127.0.0.1:${port}/`;
|
|
258
231
|
try {
|
|
259
|
-
const res = await fetchWithTimeout(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
};
|
|
265
|
-
} catch
|
|
266
|
-
|
|
267
|
-
// via stdio only — surface as info so users aren't alarmed.
|
|
268
|
-
if (/ECONNREFUSED|fetch failed/.test(err.message)) {
|
|
269
|
-
return {
|
|
270
|
-
ok: true,
|
|
271
|
-
msg: `port ${port} not bound (running via stdio? — skipped)`,
|
|
272
|
-
detail: { url },
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
return { ok: false, msg: err.message };
|
|
232
|
+
const res = await fetchWithTimeout(
|
|
233
|
+
"http://127.0.0.1:11434/api/tags",
|
|
234
|
+
{},
|
|
235
|
+
1500
|
|
236
|
+
);
|
|
237
|
+
if (!res.ok) return { ok: true, msg: "Ollama not reachable on 127.0.0.1 — skipping (not used?)" };
|
|
238
|
+
} catch {
|
|
239
|
+
return { ok: true, msg: "Ollama not running locally — skipping" };
|
|
276
240
|
}
|
|
241
|
+
// Ollama IS running on host. Warn the user about the bind config.
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
msg: "Ollama running on host. If engine is containerised, ensure OLLAMA_HOST=0.0.0.0:11434 (see README)",
|
|
245
|
+
};
|
|
277
246
|
},
|
|
278
247
|
};
|
|
279
248
|
}
|
|
280
249
|
|
|
281
250
|
export function localMemoryChecks() {
|
|
282
251
|
return [
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
checkMemoryServer(),
|
|
252
|
+
checkPluginConfig(),
|
|
253
|
+
checkEngineHealth(),
|
|
254
|
+
checkEngineLayers(),
|
|
255
|
+
checkEmbeddingPath(),
|
|
256
|
+
checkOllamaBindIfPresent(),
|
|
289
257
|
];
|
|
290
258
|
}
|
|
@@ -69,10 +69,18 @@ export function detectPaths(opts = {}) {
|
|
|
69
69
|
found.add(PATHS.PLATFORM);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// Local engine: detected via plugin config (claude-pentatonic preferred,
|
|
73
|
+
// fall back to claude), or explicit MEMORY_ENGINE_URL env override.
|
|
74
|
+
const localConfigPaths = [
|
|
75
|
+
env.CLAUDE_CONFIG_DIR
|
|
76
|
+
? join(env.CLAUDE_CONFIG_DIR, "tes-memory.local.md")
|
|
77
|
+
: null,
|
|
78
|
+
join(home, ".claude-pentatonic", "tes-memory.local.md"),
|
|
79
|
+
join(home, ".claude", "tes-memory.local.md"),
|
|
80
|
+
].filter(Boolean);
|
|
73
81
|
const looksLocal =
|
|
74
|
-
(env.
|
|
75
|
-
fileExists(
|
|
82
|
+
Boolean(env.MEMORY_ENGINE_URL) ||
|
|
83
|
+
localConfigPaths.some((p) => fileExists(p));
|
|
76
84
|
if (looksLocal) {
|
|
77
85
|
found.add(PATHS.LOCAL);
|
|
78
86
|
}
|
|
@@ -292,3 +292,107 @@ export function hostedAdapter(config, opts = {}) {
|
|
|
292
292
|
},
|
|
293
293
|
};
|
|
294
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
|
+
}
|