@batonfx/memory 0.4.4 → 0.6.1

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 CHANGED
@@ -1,5 +1,87 @@
1
1
  # `@batonfx/memory`
2
2
 
3
- Non-durable memory implementations for Baton agents.
3
+ Focused composition guide for Baton's non-durable memory implementations.
4
4
 
5
- See the [Baton documentation](https://github.com/In-Time-Tec/batonfx#readme) for installation, examples, and API guidance.
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add effect @batonfx/core @batonfx/memory
9
+ ```
10
+
11
+ ## Imports
12
+
13
+ ```ts
14
+ import { Memory } from "@batonfx/core"
15
+ import { WorkingMemory, VectorStore } from "@batonfx/memory"
16
+ ```
17
+
18
+ ## Layer graph
19
+
20
+ ```text
21
+ WorkingMemory.layer({ maxMessages: 4 })
22
+ └─ provides Memory.Memory
23
+ ├─ remember transcript by Memory.Key
24
+ └─ recall bounded recent messages
25
+ ```
26
+
27
+ ## Runnable program
28
+
29
+ Checked source: [`../../examples/package-composition-guides/src/memory.ts`](../../examples/package-composition-guides/src/memory.ts)
30
+
31
+ ```ts
32
+ import { Console, Effect } from "effect"
33
+ import { Prompt } from "effect/unstable/ai"
34
+ import { Memory } from "@batonfx/core"
35
+ import { WorkingMemory } from "@batonfx/memory"
36
+
37
+ const key: Memory.Key = { agent: "assistant", subject: "user-42" }
38
+ const text = (value: string) => Prompt.makePart("text", { text: value })
39
+ const message = (role: "user" | "assistant", value: string) => Prompt.makeMessage(role, { content: [text(value)] })
40
+
41
+ const program = Memory.Memory.use((memory) =>
42
+ Effect.gen(function* () {
43
+ yield* memory.remember({
44
+ key,
45
+ turn: 0,
46
+ terminal: true,
47
+ transcript: Prompt.fromMessages([message("user", "My name is Ada."), message("assistant", "Hello Ada.")]),
48
+ })
49
+ const recalled = yield* memory.recall({
50
+ key,
51
+ turn: 0,
52
+ prompt: Prompt.fromMessages([message("user", "What do you remember?")]),
53
+ })
54
+ yield* Console.log(`recalled ${recalled.length} messages`)
55
+ }),
56
+ ).pipe(Effect.provide(WorkingMemory.layer({ maxMessages: 4 })))
57
+
58
+ await Effect.runPromise(program)
59
+ ```
60
+
61
+ Run `bun examples/package-composition-guides/src/memory.ts`.
62
+
63
+ ## Errors, requirements, and resources
64
+
65
+ Before provisioning, the program requires `Memory.Memory` and can fail with schema-backed `MemoryError`; `WorkingMemory.layer` discharges the requirement, while retaining that declared production error channel. It succeeds with `void`. The in-process store owns no external resource and bounds each key's recent tail to four messages. Semantic memory additionally uses schema-backed `VectorStoreError`; embedding/vector failures map to `MemoryError`.
66
+
67
+ ## More
68
+
69
+ - Current behavior: [Memory](../../docs/features/memory.md)
70
+ - Deeper example: [memory chat](../../examples/memory-chat/)
71
+ - `VectorStore.memoryLayer` remains an exact deprecated alias of canonical `VectorStore.layerMemory` through the stated pre-1.0 deprecation window; `combinedLayer` is unchanged.
72
+
73
+ ### Working-memory summaries
74
+
75
+ Provide a dedicated summary model through Effect layer composition:
76
+
77
+ ```ts
78
+ import { Layer } from "effect"
79
+ import { WorkingMemory } from "@batonfx/memory"
80
+
81
+ const memoryLayer = WorkingMemory.layer({
82
+ maxMessages: 20,
83
+ summarize: {},
84
+ }).pipe(Layer.provide(WorkingMemory.summaryModelLayer.pipe(Layer.provide(modelLayer))))
85
+ ```
86
+
87
+ The summary model is acquired once in `memoryLayer`'s owning scope and reused across overflows. The former `summarize: { model: modelLayer }` option remains supported but is deprecated; migrate by composing the model through `summaryModelLayer` as shown above.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Layer } from "effect";
2
2
  import { EmbeddingModel } from "effect/unstable/ai";
3
3
  import { Memory } from "@batonfx/core";
4
4
  import { VectorStore } from "./vector-store.js";
5
+ import { SummaryModel } from "./working-memory.js";
5
6
  export * as SemanticRecall from "./semantic-recall.js";
6
7
  export * as VectorStore from "./vector-store.js";
7
8
  export * as WorkingMemory from "./working-memory.js";
@@ -11,4 +12,11 @@ export interface CombinedOptions {
11
12
  readonly semantic?: import("./semantic-recall.js").Options;
12
13
  }
13
14
  /** @experimental */
14
- export declare const combinedLayer: (options?: CombinedOptions) => Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel>;
15
+ export interface CombinedComposedOptions {
16
+ readonly working: import("./working-memory.js").ComposedOptions;
17
+ readonly semantic?: import("./semantic-recall.js").Options;
18
+ }
19
+ /** @experimental */
20
+ export declare function combinedLayer(options: CombinedComposedOptions): Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel | SummaryModel>;
21
+ /** @experimental */
22
+ export declare function combinedLayer(options?: CombinedOptions): Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel>;
package/dist/index.js CHANGED
@@ -3,13 +3,14 @@ import { EmbeddingModel } from "effect/unstable/ai";
3
3
  import { Memory } from "@batonfx/core";
4
4
  import { makeSemanticRecall } from "./semantic-recall.js";
5
5
  import { VectorStore } from "./vector-store.js";
6
- import { makeWorkingMemory } from "./working-memory.js";
6
+ import { makeWorkingMemory, SummaryModel } from "./working-memory.js";
7
7
  export * as SemanticRecall from "./semantic-recall.js";
8
8
  export * as VectorStore from "./vector-store.js";
9
9
  export * as WorkingMemory from "./working-memory.js";
10
- /** @experimental */
11
- export const combinedLayer = (options = {}) => Layer.effect(Memory.Memory, Effect.gen(function* () {
12
- const working = yield* makeWorkingMemory(options.working);
13
- const semantic = yield* makeSemanticRecall(options.semantic);
14
- return Memory.Memory.of(Memory.merge(working, semantic));
15
- }));
10
+ export function combinedLayer(options = {}) {
11
+ return Layer.effect(Memory.Memory, Effect.gen(function* () {
12
+ const working = options.working === undefined ? yield* makeWorkingMemory() : yield* makeWorkingMemory(options.working);
13
+ const semantic = yield* makeSemanticRecall(options.semantic);
14
+ return Memory.Memory.of(Memory.merge(working, semantic));
15
+ }));
16
+ }
@@ -43,7 +43,7 @@ const finalExchangeText = (prompt) => {
43
43
  };
44
44
  const itemFromMatch = (match) => ({
45
45
  id: match.document.id,
46
- parts: [textPart(match.document.text)],
46
+ content: [textPart(match.document.text)],
47
47
  metadata: { ...match.document.metadata, score: match.score },
48
48
  });
49
49
  /** @experimental */
@@ -44,8 +44,13 @@ declare const VectorStore_base: Context.ServiceClass<VectorStore, "@batonfx/memo
44
44
  /** @experimental */
45
45
  export declare class VectorStore extends VectorStore_base {
46
46
  }
47
- /** @experimental */
48
- export declare const memoryLayer: Layer.Layer<VectorStore>;
47
+ /** @experimental Ref-backed non-durable vector store. */
48
+ export declare const layerMemory: Layer.Layer<VectorStore>;
49
+ /**
50
+ * @experimental
51
+ * @deprecated Use {@link layerMemory}. This alias will not be removed before 1.0.0 and only in a separately planned major release.
52
+ */
53
+ export declare const memoryLayer: typeof layerMemory;
49
54
  /** @experimental */
50
55
  export declare const testLayer: (implementation: Interface) => Layer.Layer<VectorStore>;
51
56
  export {};
@@ -71,7 +71,12 @@ const make = Ref.make(HashMap.empty()).pipe(Effect.map((documents) => ({
71
71
  return input.id === undefined ? false : document.id !== input.id;
72
72
  })),
73
73
  })));
74
- /** @experimental */
75
- export const memoryLayer = Layer.effect(VectorStore, make.pipe(Effect.map(VectorStore.of)));
74
+ /** @experimental Ref-backed non-durable vector store. */
75
+ export const layerMemory = Layer.effect(VectorStore, make.pipe(Effect.map(VectorStore.of)));
76
+ /**
77
+ * @experimental
78
+ * @deprecated Use {@link layerMemory}. This alias will not be removed before 1.0.0 and only in a separately planned major release.
79
+ */
80
+ export const memoryLayer = layerMemory;
76
81
  /** @experimental */
77
82
  export const testLayer = (implementation) => Layer.succeed(VectorStore, VectorStore.of(implementation));
@@ -1,8 +1,9 @@
1
- import { Effect, Layer } from "effect";
1
+ import { Context, Effect, Layer, Scope } from "effect";
2
2
  import { LanguageModel } from "effect/unstable/ai";
3
3
  import { Memory } from "@batonfx/core";
4
4
  /** @experimental */
5
5
  export interface SummarizeOptions {
6
+ /** @deprecated Provide `SummaryModel` through Effect composition instead. */
6
7
  readonly model: Layer.Layer<LanguageModel.LanguageModel>;
7
8
  readonly prompt?: string;
8
9
  }
@@ -12,8 +13,34 @@ export interface Options {
12
13
  readonly summarize?: SummarizeOptions;
13
14
  }
14
15
  /** @experimental */
15
- export declare const make: (options?: Options) => Effect.Effect<Memory.Interface>;
16
+ export interface ComposedOptions {
17
+ readonly maxMessages?: number;
18
+ readonly summarize: {
19
+ readonly model?: never;
20
+ readonly prompt?: string;
21
+ };
22
+ }
23
+ declare const SummaryModel_base: Context.ServiceClass<SummaryModel, "@batonfx/memory/working-memory/SummaryModel", LanguageModel.Service>;
24
+ /** @experimental */
25
+ export declare class SummaryModel extends SummaryModel_base {
26
+ }
27
+ /** @experimental */
28
+ export declare const summaryModelLayer: Layer.Layer<SummaryModel, never, LanguageModel.LanguageModel>;
29
+ type WithoutSummaryOptions = Options & {
30
+ readonly summarize?: undefined;
31
+ };
32
+ /** @experimental */
33
+ export declare function make(options: ComposedOptions): Effect.Effect<Memory.Interface, never, SummaryModel>;
34
+ /** @experimental */
35
+ export declare function make(options?: WithoutSummaryOptions): Effect.Effect<Memory.Interface>;
36
+ /** @experimental */
37
+ export declare function make(options: Options): Effect.Effect<Memory.Interface, never, Scope.Scope>;
38
+ /** @experimental */
39
+ export declare function make(options: Options | ComposedOptions): Effect.Effect<Memory.Interface, never, SummaryModel | Scope.Scope>;
16
40
  /** @experimental */
17
41
  export declare const makeWorkingMemory: typeof make;
18
42
  /** @experimental */
19
- export declare const layer: (options?: Options) => Layer.Layer<Memory.Memory>;
43
+ export declare function layer(options: ComposedOptions): Layer.Layer<Memory.Memory, never, SummaryModel>;
44
+ /** @experimental */
45
+ export declare function layer(options?: Options): Layer.Layer<Memory.Memory>;
46
+ export {};
@@ -1,6 +1,11 @@
1
- import { Effect, HashMap, Layer, Ref } from "effect";
1
+ import { Context, Effect, HashMap, Layer, Scope, SynchronizedRef } from "effect";
2
2
  import { LanguageModel, Prompt, Toolkit } from "effect/unstable/ai";
3
3
  import { Memory } from "@batonfx/core";
4
+ /** @experimental */
5
+ export class SummaryModel extends Context.Service()("@batonfx/memory/working-memory/SummaryModel") {
6
+ }
7
+ /** @experimental */
8
+ export const summaryModelLayer = Layer.effect(SummaryModel, LanguageModel.LanguageModel);
4
9
  const emptyState = {
5
10
  recent: [],
6
11
  counter: 0,
@@ -59,85 +64,93 @@ const renderSummaryPrompt = (prompt, summary, overflow) => [
59
64
  `New messages:\n${overflow.map(formatItem).join("\n")}`,
60
65
  "Return only the updated summary.",
61
66
  ].join("\n\n");
62
- const summarizeOverflow = (options, summary, overflow) => Effect.scoped(Layer.build(options.model).pipe(Effect.flatMap((context) => Effect.gen(function* () {
63
- const model = yield* LanguageModel.LanguageModel;
64
- const response = yield* model.generateText({
65
- prompt: renderSummaryPrompt(options.prompt ?? defaultSummaryPrompt, summary, overflow),
66
- toolkit: Toolkit.empty,
67
- toolChoice: "none",
68
- });
67
+ const summarizeOverflow = (model, prompt, summary, overflow) => model
68
+ .generateText({
69
+ prompt: renderSummaryPrompt(prompt ?? defaultSummaryPrompt, summary, overflow),
70
+ toolkit: Toolkit.empty,
71
+ toolChoice: "none",
72
+ })
73
+ .pipe(Effect.map((response) => {
69
74
  const text = response.text.trim();
70
75
  return text.length === 0 ? summary : text;
71
- }).pipe(Effect.provide(context))), Effect.mapError(memoryError)));
76
+ }), Effect.mapError(memoryError));
77
+ const resolveSummaryModel = (options) => options.summarize === undefined
78
+ ? Effect.void
79
+ : "model" in options.summarize
80
+ ? Layer.build(options.summarize.model).pipe(Effect.map((context) => Context.get(context, LanguageModel.LanguageModel)))
81
+ : SummaryModel;
72
82
  const recallItems = (state) => [
73
83
  ...(state.summary === undefined
74
84
  ? []
75
85
  : [
76
86
  {
77
87
  id: "working-summary",
78
- parts: [textPart(`<working-memory-summary>\n${state.summary}\n</working-memory-summary>`)],
88
+ content: [textPart(`<working-memory-summary>\n${state.summary}\n</working-memory-summary>`)],
79
89
  },
80
90
  ]),
81
- ...state.recent.map((item) => ({ id: item.id, parts: [textPart(formatItem(item))] })),
91
+ ...state.recent.map((item) => ({ id: item.id, content: [textPart(formatItem(item))] })),
82
92
  ];
83
- /** @experimental */
84
- export const make = (options = {}) => Ref.make(HashMap.empty()).pipe(Effect.map((states) => {
85
- const maxMessages = Math.max(0, Math.floor(options.maxMessages ?? 20));
86
- return {
87
- recall: (input) => Ref.get(states).pipe(Effect.map((current) => HashMap.get(current, keyId(input.key))), Effect.map((state) => (state._tag === "Some" ? recallItems(state.value) : []))),
88
- remember: (input) => {
89
- const incoming = normalize(input.transcript);
90
- if (incoming.length === 0)
91
- return Effect.void;
92
- return Effect.gen(function* () {
93
- const current = yield* Ref.get(states);
94
- const id = keyId(input.key);
95
- const existing = HashMap.get(current, id).pipe((option) => option._tag === "Some" ? option.value : emptyState);
96
- const start = appendStart(existing.recent, incoming);
97
- const appended = incoming.slice(start);
98
- if (appended.length === 0)
99
- return;
100
- let counter = existing.counter;
101
- const stored = appended.map((item) => {
102
- counter += 1;
103
- return { id: `working-${counter}`, role: item.role, text: item.text };
104
- });
105
- const combined = [...existing.recent, ...stored];
106
- const trimmed = trimToWindow(combined, maxMessages);
107
- const summary = trimmed.overflow.length === 0
108
- ? existing.summary
109
- : options.summarize === undefined
93
+ export function make(options = {}) {
94
+ return Effect.gen(function* () {
95
+ const summaryModel = yield* resolveSummaryModel(options);
96
+ const states = yield* SynchronizedRef.make(HashMap.empty());
97
+ const maxMessages = Math.max(0, Math.floor(options.maxMessages ?? 20));
98
+ return {
99
+ recall: (input) => SynchronizedRef.get(states).pipe(Effect.map((current) => HashMap.get(current, keyId(input.key))), Effect.map((state) => (state._tag === "Some" ? recallItems(state.value) : []))),
100
+ remember: (input) => {
101
+ const incoming = normalize(input.transcript);
102
+ if (incoming.length === 0)
103
+ return Effect.void;
104
+ return SynchronizedRef.updateEffect(states, (current) => Effect.gen(function* () {
105
+ const id = keyId(input.key);
106
+ const existing = HashMap.get(current, id).pipe((option) => option._tag === "Some" ? option.value : emptyState);
107
+ const start = appendStart(existing.recent, incoming);
108
+ const appended = incoming.slice(start);
109
+ if (appended.length === 0)
110
+ return current;
111
+ let counter = existing.counter;
112
+ const stored = appended.map((item) => {
113
+ counter += 1;
114
+ return { id: `working-${counter}`, role: item.role, text: item.text };
115
+ });
116
+ const combined = [...existing.recent, ...stored];
117
+ const trimmed = trimToWindow(combined, maxMessages);
118
+ const summary = trimmed.overflow.length === 0
110
119
  ? existing.summary
111
- : yield* summarizeOverflow(options.summarize, existing.summary, trimmed.overflow);
120
+ : summaryModel === undefined
121
+ ? existing.summary
122
+ : yield* summarizeOverflow(summaryModel, options.summarize?.prompt, existing.summary, trimmed.overflow);
123
+ const nextState = {
124
+ recent: trimmed.recent,
125
+ counter,
126
+ ...(summary === undefined ? {} : { summary }),
127
+ };
128
+ return HashMap.set(current, id, nextState);
129
+ }));
130
+ },
131
+ forget: (input) => SynchronizedRef.update(states, (current) => {
132
+ const id = keyId(input.key);
133
+ if (input.id === undefined)
134
+ return HashMap.remove(current, id);
135
+ const existing = HashMap.get(current, id);
136
+ if (existing._tag === "None")
137
+ return current;
138
+ const summary = input.id === "working-summary" ? undefined : existing.value.summary;
139
+ const recent = existing.value.recent.filter((item) => item.id !== input.id);
140
+ if (summary === undefined && recent.length === 0)
141
+ return HashMap.remove(current, id);
112
142
  const nextState = {
113
- recent: trimmed.recent,
114
- counter,
143
+ recent,
144
+ counter: existing.value.counter,
115
145
  ...(summary === undefined ? {} : { summary }),
116
146
  };
117
- yield* Ref.update(states, HashMap.set(id, nextState));
118
- });
119
- },
120
- forget: (input) => Ref.update(states, (current) => {
121
- const id = keyId(input.key);
122
- if (input.id === undefined)
123
- return HashMap.remove(current, id);
124
- const existing = HashMap.get(current, id);
125
- if (existing._tag === "None")
126
- return current;
127
- const summary = input.id === "working-summary" ? undefined : existing.value.summary;
128
- const recent = existing.value.recent.filter((item) => item.id !== input.id);
129
- if (summary === undefined && recent.length === 0)
130
- return HashMap.remove(current, id);
131
- const nextState = {
132
- recent,
133
- counter: existing.value.counter,
134
- ...(summary === undefined ? {} : { summary }),
135
- };
136
- return HashMap.set(current, id, nextState);
137
- }),
138
- };
139
- }));
147
+ return HashMap.set(current, id, nextState);
148
+ }),
149
+ };
150
+ });
151
+ }
140
152
  /** @experimental */
141
153
  export const makeWorkingMemory = make;
142
- /** @experimental */
143
- export const layer = (options = {}) => Layer.effect(Memory.Memory, make(options).pipe(Effect.map(Memory.Memory.of)));
154
+ export function layer(options = {}) {
155
+ return Layer.effect(Memory.Memory, make(options).pipe(Effect.map(Memory.Memory.of)));
156
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@batonfx/memory",
4
- "version": "0.4.4",
4
+ "version": "0.6.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -17,11 +17,11 @@
17
17
  "typecheck": "bun tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@batonfx/core": "0.4.4",
21
- "effect": "4.0.0-beta.93"
20
+ "@batonfx/core": "0.6.1",
21
+ "effect": "4.0.0-beta.98"
22
22
  },
23
23
  "devDependencies": {
24
- "@effect/vitest": "4.0.0-beta.93",
24
+ "@effect/vitest": "4.0.0-beta.98",
25
25
  "@types/bun": "1.3.13",
26
26
  "typescript": "7.0.2",
27
27
  "vitest": "4.0.16"