@pentatonic-ai/ai-agent-sdk 0.4.8 → 0.5.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 (39) hide show
  1. package/README.md +59 -0
  2. package/bin/cli.js +70 -9
  3. package/dist/index.cjs +25 -3
  4. package/dist/index.js +25 -3
  5. package/package.json +4 -2
  6. package/packages/doctor/README.md +106 -0
  7. package/packages/doctor/__tests__/checks.test.js +187 -0
  8. package/packages/doctor/__tests__/detect.test.js +101 -0
  9. package/packages/doctor/__tests__/output.test.js +92 -0
  10. package/packages/doctor/__tests__/plugins.test.js +111 -0
  11. package/packages/doctor/__tests__/runner.test.js +131 -0
  12. package/packages/doctor/package.json +6 -0
  13. package/packages/doctor/src/checks/hosted-tes.js +109 -0
  14. package/packages/doctor/src/checks/local-memory.js +290 -0
  15. package/packages/doctor/src/checks/platform.js +170 -0
  16. package/packages/doctor/src/checks/universal.js +121 -0
  17. package/packages/doctor/src/detect.js +102 -0
  18. package/packages/doctor/src/index.js +33 -0
  19. package/packages/doctor/src/output.js +55 -0
  20. package/packages/doctor/src/plugins.js +81 -0
  21. package/packages/doctor/src/runner.js +136 -0
  22. package/packages/memory/migrations/005-atomic-memories.sql +16 -0
  23. package/packages/memory/migrations/006-fix-vector-dim.sql +97 -0
  24. package/packages/memory/openclaw-plugin/__tests__/chat-turn.test.js +208 -0
  25. package/packages/memory/openclaw-plugin/__tests__/indicator.test.js +142 -0
  26. package/packages/memory/openclaw-plugin/__tests__/version-check.test.js +136 -0
  27. package/packages/memory/openclaw-plugin/index.js +369 -58
  28. package/packages/memory/openclaw-plugin/openclaw.plugin.json +11 -1
  29. package/packages/memory/openclaw-plugin/package.json +1 -1
  30. package/packages/memory/src/__tests__/distill.test.js +175 -0
  31. package/packages/memory/src/__tests__/openclaw-chat-turn.test.js +289 -0
  32. package/packages/memory/src/distill.js +162 -0
  33. package/packages/memory/src/index.js +1 -0
  34. package/packages/memory/src/ingest.js +10 -0
  35. package/packages/memory/src/openclaw/index.js +280 -23
  36. package/packages/memory/src/openclaw/package.json +1 -1
  37. package/packages/memory/src/server.js +59 -5
  38. package/src/normalizer.js +16 -0
  39. package/src/session.js +21 -2
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Memory-used indicator tests.
3
+ *
4
+ * When the assemble hook injects memories into the system prompt, the
5
+ * plugin appends an instruction telling the LLM to add a visible footer
6
+ * to its reply so the end user sees when Pentatonic Memory was used.
7
+ *
8
+ * Opt out with show_memory_indicator: false in plugin config.
9
+ */
10
+
11
+ import plugin from "../index.js";
12
+
13
+ const realFetch = globalThis.fetch;
14
+
15
+ function mockFetch(searchResults) {
16
+ globalThis.fetch = async (url, init) => {
17
+ const body = init?.body ? JSON.parse(init.body) : null;
18
+ const query = body?.query || "";
19
+ // Return search results for hosted search; empty for anything else.
20
+ if (query.includes("semanticSearchMemories")) {
21
+ return {
22
+ ok: true,
23
+ status: 200,
24
+ json: async () => ({
25
+ data: { semanticSearchMemories: searchResults },
26
+ }),
27
+ };
28
+ }
29
+ if (url.endsWith("/search")) {
30
+ return {
31
+ ok: true,
32
+ status: 200,
33
+ json: async () => ({ results: searchResults }),
34
+ };
35
+ }
36
+ return {
37
+ ok: true,
38
+ status: 200,
39
+ json: async () => ({ data: {} }),
40
+ };
41
+ };
42
+ }
43
+
44
+ function makeEngine(extraConfig = {}) {
45
+ let factory;
46
+ plugin.register({
47
+ pluginConfig: {
48
+ tes_endpoint: "https://x.test",
49
+ tes_client_id: "c",
50
+ tes_api_key: "tes_c_xyz",
51
+ ...extraConfig,
52
+ },
53
+ registerTool: () => {},
54
+ registerContextEngine: (_name, fn) => {
55
+ factory = fn;
56
+ },
57
+ });
58
+ if (!factory) throw new Error("plugin did not register a context engine");
59
+ return factory();
60
+ }
61
+
62
+ afterEach(() => {
63
+ globalThis.fetch = realFetch;
64
+ });
65
+
66
+ describe("memory-used indicator — hosted mode", () => {
67
+ it("injects a footer instruction into systemPromptAddition when memories are found", async () => {
68
+ mockFetch([
69
+ { id: "m1", content: "Phil likes cheese", similarity: 0.9 },
70
+ { id: "m2", content: "Phil drinks cortado", similarity: 0.8 },
71
+ ]);
72
+ const engine = makeEngine();
73
+
74
+ const result = await engine.assemble({
75
+ sessionId: "s",
76
+ messages: [{ role: "user", content: "what do I like?" }],
77
+ });
78
+
79
+ expect(result.systemPromptAddition).toMatch(/🧠/);
80
+ expect(result.systemPromptAddition).toMatch(/Used 2 memories from Pentatonic Memory/);
81
+ expect(result.systemPromptAddition).toMatch(/append exactly this footer/);
82
+ });
83
+
84
+ it("pluralises correctly for a single memory", async () => {
85
+ mockFetch([{ id: "m1", content: "only one", similarity: 0.9 }]);
86
+ const engine = makeEngine();
87
+
88
+ const result = await engine.assemble({
89
+ sessionId: "s",
90
+ messages: [{ role: "user", content: "query" }],
91
+ });
92
+
93
+ expect(result.systemPromptAddition).toMatch(/Used 1 memory from Pentatonic Memory/);
94
+ expect(result.systemPromptAddition).not.toMatch(/Used 1 memories/);
95
+ });
96
+
97
+ it("omits the indicator instruction when show_memory_indicator is false", async () => {
98
+ mockFetch([{ id: "m1", content: "fact", similarity: 0.9 }]);
99
+ const engine = makeEngine({ show_memory_indicator: false });
100
+
101
+ const result = await engine.assemble({
102
+ sessionId: "s",
103
+ messages: [{ role: "user", content: "query" }],
104
+ });
105
+
106
+ expect(result.systemPromptAddition).toBeDefined();
107
+ expect(result.systemPromptAddition).not.toMatch(/🧠/);
108
+ expect(result.systemPromptAddition).not.toMatch(/Pentatonic Memory_/);
109
+ // Still contains the memory content itself
110
+ expect(result.systemPromptAddition).toMatch(/fact/);
111
+ });
112
+
113
+ it("does not inject anything when no memories are found", async () => {
114
+ mockFetch([]);
115
+ const engine = makeEngine();
116
+
117
+ const result = await engine.assemble({
118
+ sessionId: "s",
119
+ messages: [{ role: "user", content: "query" }],
120
+ });
121
+
122
+ // No memories → no systemPromptAddition at all (so nothing to indicate)
123
+ expect(result.systemPromptAddition).toBeUndefined();
124
+ });
125
+
126
+ it("instructs the LLM to omit the footer if memories weren't relevant", async () => {
127
+ // The prompt tells the model to skip the footer when the memories
128
+ // didn't influence the reply, so unrelated questions don't get a
129
+ // spurious "used 3 memories" footer.
130
+ mockFetch([{ id: "m1", content: "Phil likes cheese", similarity: 0.4 }]);
131
+ const engine = makeEngine();
132
+
133
+ const result = await engine.assemble({
134
+ sessionId: "s",
135
+ messages: [{ role: "user", content: "query" }],
136
+ });
137
+
138
+ expect(result.systemPromptAddition).toMatch(
139
+ /If the memories above were not relevant to your reply, omit the footer/
140
+ );
141
+ });
142
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Server-version mismatch warning — plugin checks the local memory
3
+ * server's /health payload and warns loudly (stderr) when the server
4
+ * is older than MIN_SERVER_VERSION. Catches the common footgun of
5
+ * updating the plugin without re-running `npx ... memory` to rebuild
6
+ * the Docker stack.
7
+ */
8
+
9
+ import plugin from "../index.js";
10
+
11
+ const realFetch = globalThis.fetch;
12
+
13
+ // Capture console.error so tests can assert on warnings.
14
+ function captureWarnings() {
15
+ const warnings = [];
16
+ const orig = console.error;
17
+ console.error = (...args) => warnings.push(args.join(" "));
18
+ return {
19
+ warnings,
20
+ restore: () => {
21
+ console.error = orig;
22
+ },
23
+ };
24
+ }
25
+
26
+ function mockHealth(body, ok = true) {
27
+ globalThis.fetch = async (url) => {
28
+ if (url.endsWith("/health")) {
29
+ return {
30
+ ok,
31
+ status: ok ? 200 : 500,
32
+ json: async () => body,
33
+ };
34
+ }
35
+ return { ok: true, status: 200, json: async () => ({}) };
36
+ };
37
+ }
38
+
39
+ function makeEngine(extraConfig = {}) {
40
+ let factory;
41
+ plugin.register({
42
+ pluginConfig: {
43
+ memory_url: "http://localhost:3333",
44
+ ...extraConfig,
45
+ },
46
+ registerTool: () => {},
47
+ registerContextEngine: (_n, fn) => {
48
+ factory = fn;
49
+ },
50
+ });
51
+ return factory();
52
+ }
53
+
54
+ afterEach(() => {
55
+ globalThis.fetch = realFetch;
56
+ });
57
+
58
+ describe("memory server version check", () => {
59
+ it("warns when server is older than MIN_SERVER_VERSION", async () => {
60
+ mockHealth({ status: "ok", version: "0.4.5" });
61
+ const cap = captureWarnings();
62
+ const engine = makeEngine();
63
+ // register schedules a localHealth() call asynchronously — await it
64
+ await new Promise((resolve) => setTimeout(resolve, 50));
65
+ cap.restore();
66
+
67
+ const warning = cap.warnings.find((w) =>
68
+ w.includes("memory server is 0.4.5")
69
+ );
70
+ expect(warning).toBeDefined();
71
+ expect(warning).toMatch(/npx @pentatonic-ai\/ai-agent-sdk@latest memory/);
72
+ });
73
+
74
+ it("does NOT warn when server is at MIN_SERVER_VERSION", async () => {
75
+ mockHealth({ status: "ok", version: "0.5.0" });
76
+ const cap = captureWarnings();
77
+ makeEngine();
78
+ await new Promise((resolve) => setTimeout(resolve, 50));
79
+ cap.restore();
80
+
81
+ const warning = cap.warnings.find((w) => w.includes("memory server is"));
82
+ expect(warning).toBeUndefined();
83
+ });
84
+
85
+ it("does NOT warn when server is newer than MIN_SERVER_VERSION", async () => {
86
+ mockHealth({ status: "ok", version: "1.2.3" });
87
+ const cap = captureWarnings();
88
+ makeEngine();
89
+ await new Promise((resolve) => setTimeout(resolve, 50));
90
+ cap.restore();
91
+
92
+ const warning = cap.warnings.find((w) => w.includes("memory server is"));
93
+ expect(warning).toBeUndefined();
94
+ });
95
+
96
+ it("does NOT warn when health payload lacks a version field", async () => {
97
+ // Older servers that predate the version field in /health — silent,
98
+ // don't spam warnings on something the user can't diagnose.
99
+ mockHealth({ status: "ok" });
100
+ const cap = captureWarnings();
101
+ makeEngine();
102
+ await new Promise((resolve) => setTimeout(resolve, 50));
103
+ cap.restore();
104
+
105
+ const warning = cap.warnings.find((w) => w.includes("memory server is"));
106
+ expect(warning).toBeUndefined();
107
+ });
108
+
109
+ it("does NOT warn when server is unreachable", async () => {
110
+ globalThis.fetch = async () => {
111
+ throw new Error("connection refused");
112
+ };
113
+ const cap = captureWarnings();
114
+ makeEngine();
115
+ await new Promise((resolve) => setTimeout(resolve, 50));
116
+ cap.restore();
117
+
118
+ const warning = cap.warnings.find((w) => w.includes("memory server is"));
119
+ expect(warning).toBeUndefined();
120
+ });
121
+
122
+ it("only warns once per server version (deduplicates)", async () => {
123
+ mockHealth({ status: "ok", version: "0.4.0" });
124
+ const cap = captureWarnings();
125
+ // Register twice — simulates multiple context-engine factory calls.
126
+ makeEngine();
127
+ makeEngine();
128
+ await new Promise((resolve) => setTimeout(resolve, 100));
129
+ cap.restore();
130
+
131
+ const warnings = cap.warnings.filter((w) =>
132
+ w.includes("memory server is 0.4.0")
133
+ );
134
+ expect(warnings).toHaveLength(1);
135
+ });
136
+ });