@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.
- package/.turbo/turbo-build.log +5 -0
- package/dist/index.js +32777 -0
- package/package.json +26 -0
- package/src/index.ts +29 -0
- package/src/semantic-recall.ts +119 -0
- package/src/vector-store.ts +118 -0
- package/src/working-memory.ts +189 -0
- package/test/index.test.ts +55 -0
- package/test/semantic-recall.test.ts +121 -0
- package/test/vector-store.test.ts +82 -0
- package/test/working-memory.test.ts +101 -0
- package/tsconfig.json +7 -0
|
@@ -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
|
+
})
|