@ixo/sqlite-saver 1.0.4

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 (35) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.js +4 -0
  4. package/.turbo/turbo-build.log +4 -0
  5. package/CHANGELOG.md +25 -0
  6. package/README.md +0 -0
  7. package/dist/index.d.ts +38 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +567 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/migrations/001_add_created_at_to_messages.d.ts +8 -0
  12. package/dist/migrations/001_add_created_at_to_messages.d.ts.map +1 -0
  13. package/dist/migrations/001_add_created_at_to_messages.js +32 -0
  14. package/dist/migrations/001_add_created_at_to_messages.js.map +1 -0
  15. package/dist/tests/agent-with-checkpoiner.test.d.ts +2 -0
  16. package/dist/tests/agent-with-checkpoiner.test.d.ts.map +1 -0
  17. package/dist/tests/agent-with-checkpoiner.test.js +206 -0
  18. package/dist/tests/agent-with-checkpoiner.test.js.map +1 -0
  19. package/dist/tests/checkpointer.test.d.ts +2 -0
  20. package/dist/tests/checkpointer.test.d.ts.map +1 -0
  21. package/dist/tests/checkpointer.test.js +426 -0
  22. package/dist/tests/checkpointer.test.js.map +1 -0
  23. package/dist/utils.d.ts +15 -0
  24. package/dist/utils.d.ts.map +1 -0
  25. package/dist/utils.js +284 -0
  26. package/dist/utils.js.map +1 -0
  27. package/jest.config.js +6 -0
  28. package/package.json +41 -0
  29. package/src/index.ts +929 -0
  30. package/src/migrations/001_add_created_at_to_messages.ts +48 -0
  31. package/src/tests/agent-with-checkpoiner.test.ts +264 -0
  32. package/src/tests/checkpointer.test.ts +628 -0
  33. package/src/utils.ts +358 -0
  34. package/tsconfig.json +11 -0
  35. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,48 @@
1
+ import { Database as DatabaseType } from 'better-sqlite3';
2
+
3
+ /**
4
+ * Migration 001: Add created_at column to messages table
5
+ *
6
+ * This migration adds the created_at column to the messages table
7
+ * if it doesn't already exist. It also creates the index that depends on it.
8
+ */
9
+ export default {
10
+ version: 1,
11
+ name: 'add_created_at_to_messages',
12
+ up: (db: DatabaseType) => {
13
+ // Check if column exists using PRAGMA
14
+ const columns = db
15
+ .prepare(`SELECT name FROM pragma_table_info('messages') WHERE name = ?`)
16
+ .all('created_at') as Array<{ name: string }>;
17
+
18
+ if (columns.length === 0) {
19
+ // Column doesn't exist, add it
20
+ db.exec(`
21
+ ALTER TABLE messages
22
+ ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;
23
+ `);
24
+
25
+ // Update any existing rows that might have NULL (safety measure)
26
+ db.exec(`
27
+ UPDATE messages
28
+ SET created_at = CURRENT_TIMESTAMP
29
+ WHERE created_at IS NULL;
30
+ `);
31
+ }
32
+
33
+ // Create index if it doesn't exist
34
+ const indexExists = db
35
+ .prepare(
36
+ `SELECT name FROM sqlite_master WHERE type='index' AND name='idx_messages_thread_created'`,
37
+ )
38
+ .get() as { name: string } | undefined;
39
+
40
+ if (!indexExists) {
41
+ db.exec(`
42
+ CREATE INDEX idx_messages_thread_created
43
+ ON messages(thread_id, created_at);
44
+ `);
45
+ }
46
+ },
47
+ };
48
+
@@ -0,0 +1,264 @@
1
+ import {
2
+ BaseLanguageModelCallOptions,
3
+ BaseLanguageModelInput,
4
+ } from '@langchain/core/language_models/base';
5
+ import {
6
+ BaseChatModel,
7
+ BaseChatModelCallOptions,
8
+ BindToolsInput,
9
+ } from '@langchain/core/language_models/chat_models';
10
+ import {
11
+ AIMessageChunk,
12
+ BaseMessage,
13
+ HumanMessage,
14
+ MessageStructure,
15
+ } from '@langchain/core/messages';
16
+ import { ChatGeneration, ChatResult } from '@langchain/core/outputs';
17
+ import Database from 'better-sqlite3';
18
+ import * as fs from 'fs';
19
+ import { createAgent } from 'langchain';
20
+ import { SqliteSaver } from '../index';
21
+
22
+ import { Runnable, RunnableLambda } from '@langchain/core/runnables';
23
+ import { MessagesZodMeta } from '@langchain/langgraph';
24
+ import { registry } from '@langchain/langgraph/zod';
25
+ import { z } from 'zod/v4-mini';
26
+
27
+ const State = z.object({
28
+ messages: z
29
+ .array(z.custom<BaseMessage>())
30
+ .register(registry, MessagesZodMeta),
31
+ extraField: z.number(),
32
+ });
33
+
34
+ // Mock Chat Model for testing without API calls
35
+ class MockChatModel extends BaseChatModel {
36
+ responses: string[];
37
+ callCount: number = 0;
38
+
39
+ constructor(responses: string[] = []) {
40
+ super({ cache: undefined });
41
+ this.responses = responses;
42
+ }
43
+
44
+ async _generate(
45
+ _messages: any[],
46
+ _options?: BaseLanguageModelCallOptions | undefined,
47
+ ): Promise<ChatResult> {
48
+ const response = this.responses[this.callCount % this.responses.length];
49
+ this.callCount++;
50
+
51
+ const message = new AIMessageChunk(response ?? '');
52
+ const generation: ChatGeneration = {
53
+ message,
54
+ text: response ?? '',
55
+ };
56
+
57
+ return {
58
+ generations: [generation],
59
+ };
60
+ }
61
+
62
+ _llmType(): string {
63
+ return 'mock';
64
+ }
65
+
66
+ override async invoke(_input: any, _config?: any): Promise<AIMessageChunk> {
67
+ const response = this.responses[this.callCount % this.responses.length];
68
+ this.callCount++;
69
+ return new AIMessageChunk(response ?? '');
70
+ }
71
+
72
+ override bindTools(
73
+ _tools: BindToolsInput[],
74
+ _kwargs?: Partial<BaseChatModelCallOptions> | undefined,
75
+ ): Runnable<
76
+ BaseLanguageModelInput,
77
+ AIMessageChunk<MessageStructure>,
78
+ BaseChatModelCallOptions
79
+ > {
80
+ return new RunnableLambda({
81
+ func: async (input: BaseLanguageModelInput) => {
82
+ return new AIMessageChunk(input as string);
83
+ },
84
+ }) as Runnable<
85
+ BaseLanguageModelInput,
86
+ AIMessageChunk<MessageStructure>,
87
+ BaseChatModelCallOptions
88
+ >;
89
+ }
90
+ }
91
+
92
+ describe('createAgent with SQLiteSaver Integration Tests', () => {
93
+ let dbPath: string;
94
+ let db: Database.Database;
95
+ let checkpointer: SqliteSaver;
96
+ let mockLlm: MockChatModel;
97
+
98
+ beforeEach(() => {
99
+ // Create temporary database
100
+ dbPath = `./test_checkpoint_${Date.now()}.db`;
101
+ db = new Database(dbPath);
102
+ checkpointer = new SqliteSaver(db);
103
+
104
+ // Mock LLM with predefined responses
105
+ mockLlm = new MockChatModel([
106
+ "I'll check the weather for you.",
107
+ 'The weather is sunny!',
108
+ 'Is there anything else?',
109
+ ]);
110
+ });
111
+
112
+ afterEach(() => {
113
+ // Cleanup
114
+ db.close();
115
+ if (fs.existsSync(dbPath)) {
116
+ fs.unlinkSync(dbPath);
117
+ }
118
+ });
119
+
120
+ it('should persist agent state across multiple invocations', async () => {
121
+ const agent = createAgent({
122
+ model: mockLlm,
123
+ tools: [],
124
+ checkpointer,
125
+ stateSchema: State,
126
+ });
127
+
128
+ const threadId = 'test_thread_1';
129
+ const config = { configurable: { thread_id: threadId } };
130
+
131
+ // First invocation
132
+ const result1 = await agent.invoke(
133
+ {
134
+ messages: [new HumanMessage("What's the weather in NYC?")],
135
+ extraField: 1,
136
+ },
137
+ config,
138
+ );
139
+
140
+ expect(result1).toBeDefined();
141
+ expect(result1.messages).toBeDefined();
142
+ expect(result1.messages.length).toBeGreaterThan(0);
143
+ expect(result1.extraField).toBe(1);
144
+
145
+ // Get initial state
146
+ const state1 = await agent.graph.getState(config);
147
+
148
+ expect(state1).toBeDefined();
149
+ expect(state1.values.messages).toBeDefined();
150
+ const initialMessageCount = state1.values.messages.length;
151
+
152
+ // Second invocation with same thread_id
153
+ const result2 = await agent.invoke(
154
+ { messages: [new HumanMessage('And in San Francisco?')], extraField: 2 },
155
+ config,
156
+ );
157
+
158
+ expect(result2).toBeDefined();
159
+
160
+ // Verify state was persisted (should have more messages)
161
+ const state2 = await agent.graph.getState(config);
162
+ if (!state2) {
163
+ throw new Error('State is undefined');
164
+ }
165
+ expect(state2.values.messages.length).toBeGreaterThan(initialMessageCount);
166
+ expect(state2.values.extraField).toBe(2);
167
+ });
168
+
169
+ it('should maintain separate state for different thread IDs', async () => {
170
+ const agent = createAgent({
171
+ model: mockLlm,
172
+ tools: [],
173
+ checkpointer,
174
+ stateSchema: State,
175
+ });
176
+
177
+ // First thread
178
+ const config1 = { configurable: { thread_id: 'thread_1' } };
179
+ await agent.invoke(
180
+ { messages: [new HumanMessage('NYC weather?')], extraField: 77 },
181
+ config1,
182
+ );
183
+ const state1 = await agent.graph.getState(config1);
184
+ if (!state1) {
185
+ throw new Error('State is undefined');
186
+ }
187
+
188
+ // Second thread
189
+ const config2 = { configurable: { thread_id: 'thread_2' } };
190
+ await agent.invoke(
191
+ { messages: [new HumanMessage('London weather?')], extraField: 88 },
192
+ config2,
193
+ );
194
+ const state2 = await agent.graph.getState(config2);
195
+ if (!state2) {
196
+ throw new Error('State is undefined');
197
+ }
198
+
199
+ // Both threads should exist independently
200
+ expect(state1).toBeDefined();
201
+ expect(state2).toBeDefined();
202
+ expect(state1.values.messages.length).toBeGreaterThanOrEqual(1);
203
+ expect(state2.values.messages.length).toBeGreaterThanOrEqual(1);
204
+ expect(state1.values.extraField).toBe(77);
205
+ expect(state2.values.extraField).toBe(88);
206
+ });
207
+
208
+ it('should persist data across agent instance recreation', async () => {
209
+ const threadId = 'persistence_test';
210
+ const config = { configurable: { thread_id: threadId } };
211
+
212
+ // First agent instance
213
+ let agent1 = createAgent({
214
+ model: mockLlm,
215
+ tools: [],
216
+ stateSchema: State,
217
+ checkpointer,
218
+ });
219
+
220
+ await agent1.invoke(
221
+ { messages: [new HumanMessage('First conversation')], extraField: 113 },
222
+ config,
223
+ );
224
+
225
+ const state1 = await agent1.graph.getState(config);
226
+ if (!state1) {
227
+ throw new Error('State is undefined');
228
+ }
229
+ const msgCount1 = state1.values.messages.length;
230
+
231
+ // Second agent instance (simulating restart)
232
+ const mockLlm2 = new MockChatModel([
233
+ "I'll help you.",
234
+ 'Sure thing!',
235
+ 'Done!',
236
+ ]);
237
+
238
+ agent1 = createAgent({
239
+ model: mockLlm2,
240
+ tools: [],
241
+ stateSchema: State,
242
+ checkpointer,
243
+ });
244
+
245
+ // New agent should recover previous state
246
+ const state2 = await agent1.graph.getState(config);
247
+ if (!state2) {
248
+ throw new Error('State is undefined');
249
+ }
250
+ expect(state2.values.messages.length).toBe(msgCount1);
251
+
252
+ // Continue conversation
253
+ await agent1.invoke(
254
+ { messages: [new HumanMessage('Second conversation')], extraField: 114 },
255
+ config,
256
+ );
257
+
258
+ const state3 = await agent1.graph.getState(config);
259
+ if (!state3) {
260
+ throw new Error('State is undefined');
261
+ }
262
+ expect(state3.values.messages.length).toBeGreaterThan(msgCount1);
263
+ });
264
+ });