@anteros/core 0.0.41 → 0.0.44

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 (133) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +724 -58
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1567 -0
  11. package/lib/agentMemory.ts +689 -0
  12. package/lib/agents.ts +180 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/mcpClient.ts +128 -0
  25. package/lib/messages.ts +299 -0
  26. package/lib/providers.ts +970 -0
  27. package/lib/routes.ts +14 -1
  28. package/lib/scriptWorker.ts +4 -1
  29. package/lib/scripts.ts +6 -1
  30. package/lib/skills.ts +481 -0
  31. package/lib/sockets.ts +3 -1
  32. package/lib/workflow.ts +209 -37
  33. package/lib/zip.ts +109 -0
  34. package/package.json +1 -1
  35. package/server/access.ts +96 -0
  36. package/server/agents.ts +472 -0
  37. package/server/api.ts +80 -65
  38. package/server/boot.ts +11 -0
  39. package/server/hono.ts +8 -3
  40. package/server/mcp.ts +17 -63
  41. package/server/routes.ts +19 -24
  42. package/tests/agent-mcp.test.ts +116 -0
  43. package/tests/agent-memory.test.ts +617 -0
  44. package/tests/agent-provider.test.ts +288 -0
  45. package/tests/agent-runtime.test.ts +369 -0
  46. package/tests/agent-skills.test.ts +177 -0
  47. package/tests/agent-summarize.test.ts +124 -0
  48. package/tests/agent-working-memory.test.ts +404 -0
  49. package/tests/agent.test.ts +1241 -0
  50. package/tests/agents-api.test.ts +593 -0
  51. package/tests/api-private-fields.test.ts +196 -0
  52. package/tests/api.test.ts +51 -0
  53. package/tests/attachment.test.ts +241 -0
  54. package/tests/audit-operations.test.ts +1 -1
  55. package/tests/audit-redact.test.ts +29 -3
  56. package/tests/audit-run-action.test.ts +1 -1
  57. package/tests/endpoints.test.ts +80 -0
  58. package/tests/file-collection-replication.test.ts +2 -0
  59. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  60. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  61. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  62. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  63. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  64. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  65. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  66. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  67. package/tests/fixtures/attachment/data.csv +2 -0
  68. package/tests/fixtures/fake-provider.ts +128 -0
  69. package/tests/fixtures/repl-only/app.ts +3 -0
  70. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  71. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  72. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  73. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  74. package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
  75. package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
  76. package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
  77. package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
  78. package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
  79. package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
  80. package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
  81. package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
  82. package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
  83. package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
  84. package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
  85. package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
  86. package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
  87. package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
  88. package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
  89. package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
  90. package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
  91. package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
  92. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  93. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  94. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  95. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  96. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  97. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  98. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  99. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  100. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  101. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  102. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  103. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  104. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  105. package/tests/hono.test.ts +6 -3
  106. package/tests/locks.test.ts +120 -0
  107. package/tests/mcp.test.ts +11 -11
  108. package/tests/messages.test.ts +258 -0
  109. package/tests/replication-change-stream.test.ts +240 -0
  110. package/tests/replication-control.test.ts +4 -2
  111. package/tests/replication-initial-sync.test.ts +2 -0
  112. package/tests/replication-meta.test.ts +246 -0
  113. package/tests/replication.test.ts +2 -0
  114. package/tests/upload.test.ts +44 -12
  115. package/tests/vars-api.test.ts +1 -1
  116. package/tests/vars-replication.test.ts +5 -2
  117. package/tests/workflow-engine.test.ts +340 -0
  118. package/tests/workflow-retention.test.ts +103 -0
  119. package/tests/workflow.test.ts +186 -0
  120. package/types/agent.d.ts +1067 -0
  121. package/types/config.d.ts +16 -0
  122. package/types/file.d.ts +7 -7
  123. package/types/hook.d.ts +6 -0
  124. package/types/lifecycle.d.ts +6 -0
  125. package/types/mcp.d.ts +11 -1
  126. package/types/replication.d.ts +70 -4
  127. package/types/route.d.ts +7 -1
  128. package/types/scripts.d.ts +6 -0
  129. package/types/service.d.ts +7 -1
  130. package/types/tenant.d.ts +13 -0
  131. package/types/vars.d.ts +2 -2
  132. package/types/websocket.d.ts +6 -0
  133. package/types/workflow.d.ts +50 -3
@@ -0,0 +1,404 @@
1
+ import { describe, it, expect, afterAll } from "bun:test";
2
+ import { Agent, define, v } from "../index";
3
+ import { InMemoryAgentMemory } from "../lib/agent";
4
+ import { agents } from "../lib/agents";
5
+ import type { AgentMemoryProcessor } from "../types/agent";
6
+ import {
7
+ anthropicText,
8
+ at,
9
+ fakeProvider,
10
+ openaiText,
11
+ openaiTool,
12
+ request,
13
+ type Fake,
14
+ } from "./fixtures/fake-provider";
15
+
16
+ /**
17
+ * Working memory, history window and memory processors — the three additions
18
+ * that come from the Mastra memory model (see the Memory page).
19
+ */
20
+
21
+ const servers: Fake[] = [];
22
+ function track(fake: Fake): Fake {
23
+ servers.push(fake);
24
+ return fake;
25
+ }
26
+
27
+ afterAll(() => {
28
+ for (const server of servers) server.stop();
29
+ });
30
+
31
+ function makeAgent(overrides: Record<string, any> = {}, provider: Record<string, any> = {}) {
32
+ return new Agent({
33
+ id: "memo",
34
+ description: "An agent with a memory.",
35
+ instructions: "You are a test agent.",
36
+ provider: {
37
+ model: "test-model",
38
+ apiKey: "test-key",
39
+ protocol: "openai",
40
+ options: { retries: 0 },
41
+ ...provider,
42
+ },
43
+ ...overrides,
44
+ } as any);
45
+ }
46
+
47
+ /** The prompt the provider received, as one string. */
48
+ function sentSystem(provider: Fake, index = 0): string {
49
+ const messages = request(provider, index).body.messages;
50
+ return messages.filter((message: any) => message.role === "system")
51
+ .map((message: any) => message.content).join("\n");
52
+ }
53
+
54
+ describe("working memory — template", () => {
55
+ const template = "# Profile\n- Name:\n- Timezone:";
56
+
57
+ it("injects the block into the system prompt, with the update tool", async () => {
58
+ const provider = track(fakeProvider(() => openaiText("ok")));
59
+ const agent = makeAgent(
60
+ { memory: new InMemoryAgentMemory(), workingMemory: { template } },
61
+ { baseUrl: provider.url },
62
+ );
63
+
64
+ await agent.generate("Bonjour", { thread: "t1", resource: "u1" });
65
+
66
+ const system = sentSystem(provider);
67
+ expect(system).toContain("## Working memory");
68
+ expect(system).toContain("# Profile\n- Name:\n- Timezone:"); // the empty template
69
+ expect(system).toContain("updateWorkingMemory");
70
+
71
+ const tools = request(provider, 0).body.tools.map((tool: any) => tool.function.name);
72
+ expect(tools).toEqual(["updateWorkingMemory"]);
73
+ expect(request(provider, 0).body.tools[0].function.parameters.required).toEqual(["content"]);
74
+ });
75
+
76
+ it("stores what the model wrote, and replays it on the next run", async () => {
77
+ const provider = track(fakeProvider((_body, _req, hits) =>
78
+ hits === 1
79
+ ? openaiTool("updateWorkingMemory", { content: "# Profile\n- Name: Sam\n- Timezone: CET" })
80
+ : openaiText("Noted.")));
81
+ const memory = new InMemoryAgentMemory();
82
+ const agent = makeAgent({ memory, workingMemory: { template } }, { baseUrl: provider.url });
83
+
84
+ const result = await agent.generate("Je m'appelle Sam, je suis en CET", { thread: "t1", resource: "u1" });
85
+
86
+ expect(result.toolResults[0]!.name).toBe("updateWorkingMemory");
87
+ expect(result.toolResults[0]!.error).toBeUndefined();
88
+ expect(await agent.getWorkingMemory({ resource: "u1" })).toBe("# Profile\n- Name: Sam\n- Timezone: CET");
89
+
90
+ // The block is built **once per run**: the model call that follows the tool
91
+ // still holds the prompt it started with…
92
+ expect(sentSystem(provider, 1)).toContain("- Name:\n");
93
+
94
+ // …and the next run reads the stored profile back
95
+ await agent.generate("Et moi ?", { thread: "t1", resource: "u1" });
96
+ expect(sentSystem(provider, 2)).toContain("- Name: Sam");
97
+ });
98
+
99
+ it("is shared by every thread of a caller, and isolated between callers", async () => {
100
+ const memory = new InMemoryAgentMemory();
101
+ const agent = makeAgent({ memory, workingMemory: { template } }, { baseUrl: "http://127.0.0.1:1" });
102
+
103
+ await agent.setWorkingMemory("# Profile\n- Name: Sam", { resource: "u1", thread: "t1" });
104
+ // Another thread, same caller
105
+ expect(await agent.getWorkingMemory({ resource: "u1", thread: "t2" })).toContain("Sam");
106
+ // Another caller
107
+ expect(await agent.getWorkingMemory({ resource: "u2", thread: "t1" })).toBeUndefined();
108
+ });
109
+
110
+ it("scopes per thread when asked, and stays inert without the ids it needs", async () => {
111
+ const memory = new InMemoryAgentMemory();
112
+ const agent = makeAgent(
113
+ { memory, workingMemory: { template, scope: "thread" } },
114
+ { baseUrl: "http://127.0.0.1:1" },
115
+ );
116
+
117
+ await agent.setWorkingMemory("thread one", { resource: "u1", thread: "t1" });
118
+ expect(await agent.getWorkingMemory({ resource: "u1", thread: "t1" })).toBe("thread one");
119
+ expect(await agent.getWorkingMemory({ resource: "u1", thread: "t2" })).toBeUndefined();
120
+
121
+ // No `resource` and no `thread` → nothing to key it with, so nothing happens
122
+ const provider = track(fakeProvider(() => openaiText("ok")));
123
+ const loose = makeAgent(
124
+ { memory: new InMemoryAgentMemory(), workingMemory: { template } },
125
+ { baseUrl: provider.url },
126
+ );
127
+ await loose.generate("hi");
128
+ expect(sentSystem(provider)).not.toContain("## Working memory");
129
+ });
130
+
131
+ it("is read-only when the run is readOnly, and drops the tool when tools are narrowed", async () => {
132
+ const provider = track(fakeProvider(() => openaiText("ok")));
133
+ const agent = makeAgent(
134
+ { memory: new InMemoryAgentMemory(), workingMemory: { template } },
135
+ { baseUrl: provider.url },
136
+ );
137
+
138
+ await agent.generate("hi", { thread: "t1", resource: "u1", memory: { readOnly: true } });
139
+ expect(sentSystem(provider)).toContain("## Working memory");
140
+ expect(request(provider, 0).body.tools).toBeUndefined(); // no tool to write with
141
+
142
+ await agent.generate("hi", { thread: "t1", resource: "u1", tools: false });
143
+ expect(request(provider, 1).body.tools).toBeUndefined();
144
+
145
+ await agent.generate("hi", { thread: "t1", resource: "u1", tools: ["forecast"] });
146
+ expect(request(provider, 2).body.tools).toBeUndefined(); // an allow-list is respected
147
+ });
148
+
149
+ it("lets a tenant declare its own `updateWorkingMemory` tool", async () => {
150
+ const provider = track(fakeProvider(() => openaiText("ok")));
151
+ const agent = makeAgent({
152
+ memory: new InMemoryAgentMemory(),
153
+ workingMemory: { template },
154
+ tools: {
155
+ updateWorkingMemory: define.Tool({
156
+ id: "updateWorkingMemory",
157
+ description: "Ours.",
158
+ execute: () => "mine",
159
+ }),
160
+ },
161
+ }, { baseUrl: provider.url });
162
+
163
+ await agent.generate("hi", { thread: "t1", resource: "u1" });
164
+ expect(request(provider, 0).body.tools[0].function.description).toBe("Ours.");
165
+ });
166
+
167
+ it("refuses a declaration with both a template and a schema, or neither", () => {
168
+ const memory = new InMemoryAgentMemory();
169
+ expect(() => makeAgent({ memory, workingMemory: {} })).toThrow(/AGENT_WORKING_MEMORY_INVALID|template/);
170
+ expect(() => makeAgent({ memory, workingMemory: { template: "x", schema: v.object({}) } })).toThrow(/template/);
171
+ });
172
+
173
+ it("refuses to write without a memory, without a declaration, or without the scope id", async () => {
174
+ const noMemory = makeAgent({}, { baseUrl: "http://127.0.0.1:1" });
175
+ expect((await noMemory.setWorkingMemory("x", { resource: "u1" }).catch((err) => err)).code)
176
+ .toBe("AGENT_NO_MEMORY");
177
+
178
+ const declared = makeAgent({ memory: new InMemoryAgentMemory() }, { baseUrl: "http://127.0.0.1:1" });
179
+ expect((await declared.setWorkingMemory("x", { resource: "u1" }).catch((err) => err)).code)
180
+ .toBe("AGENT_WORKING_MEMORY_DISABLED");
181
+
182
+ const agent = makeAgent(
183
+ { memory: new InMemoryAgentMemory(), workingMemory: { template } },
184
+ { baseUrl: "http://127.0.0.1:1" },
185
+ );
186
+ expect((await agent.setWorkingMemory("x").catch((err) => err)).code)
187
+ .toBe("AGENT_WORKING_MEMORY_SCOPE_REQUIRED");
188
+
189
+ // A store that cannot keep a scratchpad says so
190
+ const poor = makeAgent({
191
+ memory: { get: () => [], save: () => {}, clear: () => {} } as any,
192
+ workingMemory: { template },
193
+ }, { baseUrl: "http://127.0.0.1:1" });
194
+ expect((await poor.setWorkingMemory("x", { resource: "u1" }).catch((err) => err)).code)
195
+ .toBe("AGENT_WORKING_MEMORY_UNSUPPORTED");
196
+ });
197
+ });
198
+
199
+ describe("working memory — schema", () => {
200
+ const schema = v.object({
201
+ name: v.string(),
202
+ timezone: v.string(),
203
+ goals: v.array().items(v.string()),
204
+ });
205
+
206
+ const profileAgent = (provider: string) => makeAgent({
207
+ memory: new InMemoryAgentMemory(),
208
+ workingMemory: { schema },
209
+ }, { baseUrl: provider });
210
+
211
+ it("hands the model a JSON block and a patch-shaped tool", async () => {
212
+ const provider = track(fakeProvider(() => openaiText("ok")));
213
+ await profileAgent(provider.url).generate("hi", { thread: "t1", resource: "u1" });
214
+
215
+ expect(sentSystem(provider)).toContain("```json");
216
+ expect(sentSystem(provider)).toContain("{}");
217
+ // Patch semantics: nothing is required, so the model sends only what it learned
218
+ expect(request(provider, 0).body.tools[0].function.parameters.required).toBeUndefined();
219
+ expect(Object.keys(request(provider, 0).body.tools[0].function.parameters.properties))
220
+ .toEqual(["name", "timezone", "goals"]);
221
+ });
222
+
223
+ it("deep merges an update, replaces arrays, and removes a field sent as null", async () => {
224
+ const agent = profileAgent("http://127.0.0.1:1");
225
+
226
+ expect(await agent.setWorkingMemory({ name: "Sam", timezone: "CET" }, { resource: "u1" }))
227
+ .toEqual({ name: "Sam", timezone: "CET" });
228
+ expect(await agent.setWorkingMemory({ goals: ["ship v1"] }, { resource: "u1" }))
229
+ .toEqual({ name: "Sam", timezone: "CET", goals: ["ship v1"] });
230
+ // An array replaces, it does not append
231
+ expect(await agent.setWorkingMemory({ goals: ["ship v2", "rest"] }, { resource: "u1" }))
232
+ .toEqual({ name: "Sam", timezone: "CET", goals: ["ship v2", "rest"] });
233
+ // `null` removes a key
234
+ expect(await agent.setWorkingMemory({ timezone: null }, { resource: "u1" }))
235
+ .toEqual({ name: "Sam", goals: ["ship v2", "rest"] });
236
+ });
237
+
238
+ it("refuses an update that breaks the schema", async () => {
239
+ const agent = profileAgent("http://127.0.0.1:1");
240
+ const error: any = await agent.setWorkingMemory({ name: 42 }, { resource: "u1" }).catch((err) => err);
241
+ expect(error).toBeInstanceOf(Error);
242
+ expect(await agent.getWorkingMemory({ resource: "u1" })).toBeUndefined();
243
+ });
244
+
245
+ it("reports a bad tool argument to the model instead of failing the run", async () => {
246
+ const provider = track(fakeProvider((_body, _req, hits) =>
247
+ hits === 1
248
+ ? openaiTool("updateWorkingMemory", { name: 42 })
249
+ : openaiText("sorry")));
250
+ const agent = profileAgent(provider.url);
251
+
252
+ const result = await agent.generate("call me 42", { thread: "t1", resource: "u1" });
253
+
254
+ expect(result.text).toBe("sorry");
255
+ expect(at(result.toolResults, 0).name).toBe("updateWorkingMemory");
256
+ expect(at(result.toolResults, 0).error).toContain("must be a string");
257
+ expect(await agent.getWorkingMemory({ resource: "u1" })).toBeUndefined();
258
+ });
259
+
260
+ it("clears the scratchpad", async () => {
261
+ const agent = profileAgent("http://127.0.0.1:1");
262
+ await agent.setWorkingMemory({ name: "Sam" }, { resource: "u1" });
263
+ await agent.clearWorkingMemory({ resource: "u1" });
264
+ expect(await agent.getWorkingMemory({ resource: "u1" })).toBeUndefined();
265
+ });
266
+ });
267
+
268
+ describe("history window — `lastMessages`", () => {
269
+ const seeded = async () => {
270
+ const memory = new InMemoryAgentMemory();
271
+ await memory.save("t1", [
272
+ { role: "user", content: "one" },
273
+ { role: "assistant", content: "1" },
274
+ { role: "user", content: "two" },
275
+ { role: "assistant", content: "2" },
276
+ { role: "user", content: "three" },
277
+ { role: "assistant", content: "3" },
278
+ ]);
279
+ return memory;
280
+ };
281
+
282
+ it("replays the last N messages, keeping the store untouched", async () => {
283
+ const provider = track(fakeProvider(() => openaiText("ok")));
284
+ const memory = await seeded();
285
+ const agent = makeAgent({ memory }, { baseUrl: provider.url });
286
+
287
+ await agent.generate("four", { thread: "t1", lastMessages: 2 });
288
+
289
+ expect(request(provider, 0).body.messages.map((m: any) => m.content)).toEqual([
290
+ "You are a test agent.", "three", "3", "four",
291
+ ]);
292
+ // The window is a prompt decision, not a write: the whole thread is still there
293
+ expect(await agent.getMessages("t1")).toHaveLength(8); // 6 + question + answer
294
+ });
295
+
296
+ it("takes the declaration default, and a call overrides it", async () => {
297
+ const provider = track(fakeProvider(() => openaiText("ok")));
298
+ const memory = await seeded();
299
+ const agent = makeAgent({ memory, lastMessages: 1 }, { baseUrl: provider.url });
300
+
301
+ await agent.generate("four", { thread: "t1" });
302
+ expect(request(provider, 0).body.messages.map((m: any) => m.content))
303
+ .toEqual(["You are a test agent.", "3", "four"]);
304
+
305
+ await agent.generate("five", { thread: "t1", lastMessages: 3 });
306
+ // The store kept everything (6 seeded + `four` + `ok`); the window replays its last 3
307
+ expect(request(provider, 1).body.messages.map((m: any) => m.content))
308
+ .toEqual(["You are a test agent.", "3", "four", "ok", "five"]);
309
+ });
310
+ });
311
+
312
+ describe("memory processors", () => {
313
+ it("`load` rewrites what the model sees", async () => {
314
+ const provider = track(fakeProvider(() => openaiText("ok")));
315
+ const memory = new InMemoryAgentMemory();
316
+ await memory.save("t1", [{ role: "user", content: "mon mot de passe est hunter2" }]);
317
+
318
+ const processors: AgentMemoryProcessor[] = [
319
+ // A redactor, a translator, your own recall… anything
320
+ {
321
+ id: "hide-history",
322
+ load: (messages) => messages.filter((message) => !String(message.content).includes("hunter2")),
323
+ },
324
+ ];
325
+
326
+ const agent = makeAgent({ memory, processors }, { baseUrl: provider.url });
327
+
328
+ await agent.generate("C'était quoi ?", { thread: "t1" });
329
+
330
+ const contents = request(provider, 0).body.messages.map((m: any) => m.content);
331
+ expect(contents.join("\n")).not.toContain("hunter2");
332
+ expect(contents.at(-1)).toBe("C'était quoi ?");
333
+ });
334
+
335
+ it("`save` rewrites what is stored", async () => {
336
+ const provider = track(fakeProvider(() => openaiText("SECRET")));
337
+ const memory = new InMemoryAgentMemory();
338
+ const processors: AgentMemoryProcessor[] = [
339
+ { save: (messages) => messages.map((message) => ({ ...message, content: "[redacted]" })) },
340
+ ];
341
+ const agent = makeAgent({ memory, processors }, { baseUrl: provider.url });
342
+
343
+ await agent.generate("hi", { thread: "t1" });
344
+
345
+ expect((await agent.getMessages("t1")).map((message) => message.content))
346
+ .toEqual(["[redacted]", "[redacted]"]);
347
+ // …without touching the answer the caller received
348
+ expect(request(provider, 0).body.messages.at(-1).content).toBe("hi");
349
+ });
350
+
351
+ it("a throwing `save` keeps the conversation out of the store — the answer still returns", async () => {
352
+ const provider = track(fakeProvider(() => openaiText("blocked")));
353
+ const processors: AgentMemoryProcessor[] = [{
354
+ save: (messages) => {
355
+ if (String(messages.at(-1)?.content).includes("blocked")) throw new Error("guardrail");
356
+ return messages;
357
+ },
358
+ }];
359
+ const agent = makeAgent({ memory: new InMemoryAgentMemory(), processors }, { baseUrl: provider.url });
360
+
361
+ const result = await agent.generate("this is blocked", { thread: "t1" });
362
+
363
+ expect(result.text).toBe("blocked");
364
+ expect(await agent.getMessages("t1")).toEqual([]);
365
+ });
366
+
367
+ it("runs in order, and a processor that returns nothing leaves the messages alone", async () => {
368
+ const provider = track(fakeProvider(() => openaiText("ok")));
369
+ const seen: string[] = [];
370
+ const processors: AgentMemoryProcessor[] = [
371
+ { id: "first", load: () => { seen.push("first"); } },
372
+ { id: "second", load: (messages) => { seen.push("second"); return [...messages, { role: "system", content: "added" }]; } },
373
+ ];
374
+ const agent = makeAgent({ memory: new InMemoryAgentMemory(), processors }, { baseUrl: provider.url });
375
+
376
+ await agent.generate("hi", { thread: "t1" });
377
+
378
+ expect(seen).toEqual(["first", "second"]);
379
+ expect(sentSystem(provider)).toContain("added");
380
+ });
381
+ });
382
+
383
+ describe("working memory — Anthropic", () => {
384
+ it("travels in the system prompt, out of band", async () => {
385
+ const provider = track(fakeProvider(() => anthropicText("ok")));
386
+ const agent = makeAgent(
387
+ { memory: new InMemoryAgentMemory(), workingMemory: { template: "# Memo\n- x:" } },
388
+ { protocol: "anthropic", baseUrl: provider.url },
389
+ );
390
+
391
+ await agent.generate("hi", { thread: "t1", resource: "u1" });
392
+
393
+ expect(request(provider, 0).body.system).toContain("## Working memory");
394
+ expect(request(provider, 0).body.tools[0].name).toBe("updateWorkingMemory");
395
+ });
396
+ });
397
+
398
+ describe("working memory — the registry exposes nothing new by accident", () => {
399
+ it("keeps `agents.memory` as the only store factory", () => {
400
+ // `summarize` is the one deliberate addition: a **processor** factory (it needs
401
+ // your model and your prompt), not a store
402
+ expect(Object.keys(agents.memory).sort()).toEqual(["InMemory", "Mongo", "Redis", "summarize"]);
403
+ });
404
+ });