@batonfx/memory 0.1.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.
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect } from "effect"
3
+ import { Memory } from "@batonfx/core"
4
+ import { VectorStore } from "../src/index"
5
+
6
+ const key: Memory.Key = { agent: "agent-a", subject: "subject-a" }
7
+ const otherSubject: Memory.Key = { agent: "agent-a", subject: "subject-b" }
8
+ const otherAgent: Memory.Key = { agent: "agent-b", subject: "subject-a" }
9
+
10
+ const document = (
11
+ id: string,
12
+ memoryKey: Memory.Key,
13
+ text: string,
14
+ embedding: ReadonlyArray<number>,
15
+ ): VectorStore.Embedded => ({
16
+ id,
17
+ key: memoryKey,
18
+ text,
19
+ embedding,
20
+ })
21
+
22
+ describe("VectorStore", () => {
23
+ it.effect("ranks matching-key documents by cosine score", () =>
24
+ Effect.gen(function* () {
25
+ const store = yield* VectorStore.VectorStore
26
+
27
+ yield* store.upsert([
28
+ document("near", key, "near", [1, 0]),
29
+ document("far", key, "far", [0, 1]),
30
+ document("also-near", key, "also-near", [0.8, 0.2]),
31
+ ])
32
+
33
+ const matches = yield* store.query({ key, embedding: [1, 0], limit: 2 })
34
+
35
+ expect(matches.map((match) => match.document.text)).toEqual(["near", "also-near"])
36
+ expect(matches[0]?.score).toBeGreaterThan(matches[1]?.score ?? 0)
37
+ }).pipe(Effect.provide(VectorStore.memoryLayer)),
38
+ )
39
+
40
+ it.effect("enforces exact agent and subject key isolation", () =>
41
+ Effect.gen(function* () {
42
+ const store = yield* VectorStore.VectorStore
43
+
44
+ yield* store.upsert([
45
+ document("same-id", key, "visible", [1, 0]),
46
+ document("same-id", otherSubject, "other subject", [1, 0]),
47
+ document("same-id", otherAgent, "other agent", [1, 0]),
48
+ ])
49
+
50
+ const matches = yield* store.query({ key, embedding: [1, 0], limit: 10 })
51
+
52
+ expect(matches.map((match) => match.document.text)).toEqual(["visible"])
53
+ }).pipe(Effect.provide(VectorStore.memoryLayer)),
54
+ )
55
+
56
+ it.effect("applies limit and minScore", () =>
57
+ Effect.gen(function* () {
58
+ const store = yield* VectorStore.VectorStore
59
+
60
+ yield* store.upsert([document("near", key, "near", [1, 0]), document("also-near", key, "also-near", [0.8, 0.2])])
61
+
62
+ const limited = yield* store.query({ key, embedding: [1, 0], limit: 1 })
63
+ const thresholded = yield* store.query({ key, embedding: [1, 0], limit: 10, minScore: 0.99 })
64
+
65
+ expect(limited.map((match) => match.document.text)).toEqual(["near"])
66
+ expect(thresholded.map((match) => match.document.text)).toEqual(["near"])
67
+ }).pipe(Effect.provide(VectorStore.memoryLayer)),
68
+ )
69
+
70
+ it.effect("fails loudly on matching-key dimension mismatch", () =>
71
+ Effect.gen(function* () {
72
+ const store = yield* VectorStore.VectorStore
73
+
74
+ yield* store.upsert([document("bad", key, "bad", [1, 0, 0])])
75
+
76
+ const failure = yield* Effect.flip(store.query({ key, embedding: [1, 0], limit: 1 }))
77
+
78
+ expect(failure._tag).toBe("@batonfx/memory/VectorStoreError")
79
+ expect(failure.message).toContain("dimension")
80
+ }).pipe(Effect.provide(VectorStore.memoryLayer)),
81
+ )
82
+ })
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect, Layer, Stream } from "effect"
3
+ import * as Ai from "effect/unstable/ai"
4
+ import { Memory } from "@batonfx/core"
5
+ import { WorkingMemory } from "../src/index"
6
+
7
+ const key: Memory.Key = { agent: "memory-agent", subject: "subject-a" }
8
+ const otherKey: Memory.Key = { agent: "memory-agent", subject: "subject-b" }
9
+
10
+ const textPart = (text: string) => Ai.Prompt.makePart("text", { text })
11
+ const user = (text: string) => Ai.Prompt.makeMessage("user", { content: [textPart(text)] })
12
+ const assistant = (text: string) => Ai.Prompt.makeMessage("assistant", { content: [textPart(text)] })
13
+ const prompt = (...messages: ReadonlyArray<Ai.Prompt.Message>) => Ai.Prompt.fromMessages(messages)
14
+
15
+ const itemText = (item: Memory.Item): string =>
16
+ item.parts
17
+ .filter((part): part is Ai.Prompt.TextPart => part.type === "text")
18
+ .map((part) => part.text)
19
+ .join("")
20
+
21
+ describe("WorkingMemory", () => {
22
+ it.effect("keeps a bounded recent tail", () =>
23
+ Effect.gen(function* () {
24
+ const memory = yield* Memory.Memory
25
+
26
+ yield* memory.remember({
27
+ key,
28
+ turn: 0,
29
+ terminal: true,
30
+ transcript: prompt(user("one"), assistant("two"), user("three")),
31
+ })
32
+
33
+ const recalled = yield* memory.recall({ key, turn: 0, prompt: prompt(user("current")) })
34
+
35
+ expect(recalled.map(itemText)).toEqual(["Assistant: two", "User: three"])
36
+ }).pipe(Effect.provide(WorkingMemory.layer({ maxMessages: 2 }))),
37
+ )
38
+
39
+ it.effect("summarizes overflow once and recalls summary before the recent tail", () => {
40
+ let summaryCalls = 0
41
+ let summaryPrompt = ""
42
+ const summaryModel = Layer.effect(
43
+ Ai.LanguageModel.LanguageModel,
44
+ Ai.LanguageModel.make({
45
+ generateText: (options) =>
46
+ Effect.sync(() => {
47
+ summaryCalls += 1
48
+ summaryPrompt = JSON.stringify(options.prompt.content)
49
+ return [{ type: "text", text: "summary" }]
50
+ }),
51
+ streamText: () => Stream.empty,
52
+ }),
53
+ )
54
+
55
+ return Effect.gen(function* () {
56
+ const memory = yield* Memory.Memory
57
+
58
+ yield* memory.remember({
59
+ key,
60
+ turn: 0,
61
+ terminal: true,
62
+ transcript: prompt(user("one"), assistant("two"), user("three"), assistant("four")),
63
+ })
64
+
65
+ const recalled = yield* memory.recall({ key, turn: 0, prompt: prompt(user("current")) })
66
+
67
+ expect(summaryCalls).toBe(1)
68
+ expect(summaryPrompt).toContain("one")
69
+ expect(summaryPrompt).toContain("two")
70
+ expect(recalled.map(itemText)).toEqual([
71
+ "<working-memory-summary>\nsummary\n</working-memory-summary>",
72
+ "User: three",
73
+ "Assistant: four",
74
+ ])
75
+ }).pipe(
76
+ Effect.provide(
77
+ WorkingMemory.layer({
78
+ maxMessages: 2,
79
+ summarize: { model: summaryModel },
80
+ }),
81
+ ),
82
+ )
83
+ })
84
+
85
+ it.effect("isolates state by memory key", () =>
86
+ Effect.gen(function* () {
87
+ const memory = yield* Memory.Memory
88
+
89
+ yield* memory.remember({
90
+ key,
91
+ turn: 0,
92
+ terminal: true,
93
+ transcript: prompt(user("one"), assistant("two")),
94
+ })
95
+
96
+ const recalled = yield* memory.recall({ key: otherKey, turn: 0, prompt: prompt(user("current")) })
97
+
98
+ expect(recalled).toEqual([])
99
+ }).pipe(Effect.provide(WorkingMemory.layer({ maxMessages: 2 }))),
100
+ )
101
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["src/**/*.ts", "test/**/*.ts"]
7
+ }