@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 +84 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +8 -7
- package/dist/semantic-recall.js +1 -1
- package/dist/vector-store.d.ts +7 -2
- package/dist/vector-store.js +7 -2
- package/dist/working-memory.d.ts +30 -3
- package/dist/working-memory.js +79 -66
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# `@batonfx/memory`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Focused composition guide for Baton's non-durable memory implementations.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
}
|
package/dist/semantic-recall.js
CHANGED
|
@@ -43,7 +43,7 @@ const finalExchangeText = (prompt) => {
|
|
|
43
43
|
};
|
|
44
44
|
const itemFromMatch = (match) => ({
|
|
45
45
|
id: match.document.id,
|
|
46
|
-
|
|
46
|
+
content: [textPart(match.document.text)],
|
|
47
47
|
metadata: { ...match.document.metadata, score: match.score },
|
|
48
48
|
});
|
|
49
49
|
/** @experimental */
|
package/dist/vector-store.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/vector-store.js
CHANGED
|
@@ -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
|
|
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));
|
package/dist/working-memory.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {};
|
package/dist/working-memory.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { Effect, HashMap, Layer,
|
|
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 = (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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,
|
|
91
|
+
...state.recent.map((item) => ({ id: item.id, content: [textPart(formatItem(item))] })),
|
|
82
92
|
];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
:
|
|
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
|
|
114
|
-
counter,
|
|
143
|
+
recent,
|
|
144
|
+
counter: existing.value.counter,
|
|
115
145
|
...(summary === undefined ? {} : { summary }),
|
|
116
146
|
};
|
|
117
|
-
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
+
"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.
|
|
21
|
-
"effect": "4.0.0-beta.
|
|
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.
|
|
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"
|