@lobehub/lobehub 2.0.0-next.110 → 2.0.0-next.112

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 (40) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/development/database-schema.dbml +2 -1
  4. package/package.json +1 -1
  5. package/packages/context-engine/src/index.ts +1 -1
  6. package/packages/context-engine/src/providers/KnowledgeInjector.ts +78 -0
  7. package/packages/context-engine/src/providers/index.ts +2 -0
  8. package/packages/database/migrations/0047_add_slug_document.sql +1 -5
  9. package/packages/database/migrations/meta/0047_snapshot.json +30 -14
  10. package/packages/database/migrations/meta/_journal.json +1 -1
  11. package/packages/database/src/core/migrations.json +3 -3
  12. package/packages/database/src/models/__tests__/agent.test.ts +172 -3
  13. package/packages/database/src/models/__tests__/userMemories.test.ts +1382 -0
  14. package/packages/database/src/models/agent.ts +22 -1
  15. package/packages/database/src/models/userMemory.ts +993 -0
  16. package/packages/database/src/schemas/file.ts +5 -10
  17. package/packages/database/src/schemas/userMemories.ts +22 -5
  18. package/packages/model-bank/src/aiModels/qwen.ts +41 -3
  19. package/packages/model-runtime/src/providers/qwen/index.ts +9 -3
  20. package/packages/prompts/src/prompts/files/__snapshots__/knowledgeBase.test.ts.snap +103 -0
  21. package/packages/prompts/src/prompts/files/index.ts +3 -0
  22. package/packages/prompts/src/prompts/files/knowledgeBase.test.ts +167 -0
  23. package/packages/prompts/src/prompts/files/knowledgeBase.ts +85 -0
  24. package/packages/types/src/files/index.ts +1 -0
  25. package/packages/types/src/index.ts +1 -0
  26. package/packages/types/src/knowledgeBase/index.ts +1 -0
  27. package/packages/types/src/userMemory/index.ts +3 -0
  28. package/packages/types/src/userMemory/layers.ts +54 -0
  29. package/packages/types/src/userMemory/shared.ts +64 -0
  30. package/packages/types/src/userMemory/tools.ts +240 -0
  31. package/src/features/ChatList/Messages/index.tsx +16 -19
  32. package/src/features/ChatList/components/ContextMenu.tsx +23 -16
  33. package/src/helpers/toolEngineering/index.ts +5 -9
  34. package/src/hooks/useQueryParam.ts +24 -22
  35. package/src/server/routers/async/file.ts +2 -7
  36. package/src/server/routers/lambda/chunk.ts +6 -1
  37. package/src/services/chat/contextEngineering.ts +19 -0
  38. package/src/store/agent/slices/chat/selectors/agent.ts +4 -0
  39. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +5 -16
  40. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.112](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.111...v2.0.0-next.112)
6
+
7
+ <sup>Released on **2025-11-24**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Optimize files schema definition.
12
+
13
+ #### 💄 Styles
14
+
15
+ - **misc**: Add Kimi K2 Thinking to Qwen Provider.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### Code refactoring
23
+
24
+ - **misc**: Optimize files schema definition, closes [#10403](https://github.com/lobehub/lobe-chat/issues/10403) ([cf28c87](https://github.com/lobehub/lobe-chat/commit/cf28c87))
25
+
26
+ #### Styles
27
+
28
+ - **misc**: Add Kimi K2 Thinking to Qwen Provider, closes [#10287](https://github.com/lobehub/lobe-chat/issues/10287) ([bd2e838](https://github.com/lobehub/lobe-chat/commit/bd2e838))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 2.0.0-next.111](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.110...v2.0.0-next.111)
39
+
40
+ <sup>Released on **2025-11-24**</sup>
41
+
42
+ #### 🐛 Bug Fixes
43
+
44
+ - **misc**: Fix db migration snapshot not align with db schema, Separate agent file injection from knowledge base RAG search.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### What's fixed
52
+
53
+ - **misc**: Fix db migration snapshot not align with db schema, closes [#10399](https://github.com/lobehub/lobe-chat/issues/10399) ([760105a](https://github.com/lobehub/lobe-chat/commit/760105a))
54
+ - **misc**: Separate agent file injection from knowledge base RAG search, closes [#10398](https://github.com/lobehub/lobe-chat/issues/10398) ([e1c813a](https://github.com/lobehub/lobe-chat/commit/e1c813a))
55
+
56
+ </details>
57
+
58
+ <div align="right">
59
+
60
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
61
+
62
+ </div>
63
+
5
64
  ## [Version 2.0.0-next.110](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.109...v2.0.0-next.110)
6
65
 
7
66
  <sup>Released on **2025-11-24**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add Kimi K2 Thinking to Qwen Provider."
6
+ ]
7
+ },
8
+ "date": "2025-11-24",
9
+ "version": "2.0.0-next.112"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fix db migration snapshot not align with db schema, Separate agent file injection from knowledge base RAG search."
15
+ ]
16
+ },
17
+ "date": "2025-11-24",
18
+ "version": "2.0.0-next.111"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "improvements": [
@@ -187,6 +187,7 @@ table documents {
187
187
  user_id text [not null]
188
188
  client_id text
189
189
  editor_data jsonb
190
+ slug varchar(255)
190
191
  accessed_at "timestamp with time zone" [not null, default: `now()`]
191
192
  created_at "timestamp with time zone" [not null, default: `now()`]
192
193
  updated_at "timestamp with time zone" [not null, default: `now()`]
@@ -197,6 +198,7 @@ table documents {
197
198
  file_id [name: 'documents_file_id_idx']
198
199
  parent_id [name: 'documents_parent_id_idx']
199
200
  (client_id, user_id) [name: 'documents_client_id_user_id_unique', unique]
201
+ (slug, user_id) [name: 'documents_slug_user_id_unique', unique]
200
202
  }
201
203
  }
202
204
 
@@ -214,7 +216,6 @@ table files {
214
216
  metadata jsonb
215
217
  chunk_task_id uuid
216
218
  embedding_task_id uuid
217
- slug text [unique]
218
219
  accessed_at "timestamp with time zone" [not null, default: `now()`]
219
220
  created_at "timestamp with time zone" [not null, default: `now()`]
220
221
  updated_at "timestamp with time zone" [not null, default: `now()`]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.110",
3
+ "version": "2.0.0-next.112",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -10,7 +10,7 @@ export type { ContextEngineConfig } from './pipeline';
10
10
  export { ContextEngine } from './pipeline';
11
11
 
12
12
  // Context Providers
13
- export { HistorySummaryProvider, SystemRoleInjector, ToolSystemRoleProvider } from './providers';
13
+ export * from './providers';
14
14
 
15
15
  // Processors
16
16
  export {
@@ -0,0 +1,78 @@
1
+ import { promptAgentKnowledge } from '@lobechat/prompts';
2
+ import type { FileContent, KnowledgeBaseInfo } from '@lobechat/prompts';
3
+ import debug from 'debug';
4
+
5
+ import { BaseProvider } from '../base/BaseProvider';
6
+ import type { PipelineContext, ProcessorOptions } from '../types';
7
+
8
+ const log = debug('context-engine:provider:KnowledgeInjector');
9
+
10
+ export interface KnowledgeInjectorConfig {
11
+ /** File contents to inject */
12
+ fileContents?: FileContent[];
13
+ /** Knowledge bases to inject */
14
+ knowledgeBases?: KnowledgeBaseInfo[];
15
+ }
16
+
17
+ /**
18
+ * Knowledge Injector
19
+ * Responsible for injecting agent's knowledge (files and knowledge bases) into context
20
+ */
21
+ export class KnowledgeInjector extends BaseProvider {
22
+ readonly name = 'KnowledgeInjector';
23
+
24
+ constructor(
25
+ private config: KnowledgeInjectorConfig,
26
+ options: ProcessorOptions = {},
27
+ ) {
28
+ super(options);
29
+ }
30
+
31
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
32
+ const clonedContext = this.cloneContext(context);
33
+
34
+ const fileContents = this.config.fileContents || [];
35
+ const knowledgeBases = this.config.knowledgeBases || [];
36
+
37
+ // Generate unified knowledge prompt
38
+ const formattedContent = promptAgentKnowledge({ fileContents, knowledgeBases });
39
+
40
+ // Skip injection if no knowledge at all
41
+ if (!formattedContent) {
42
+ log('No knowledge to inject');
43
+ return this.markAsExecuted(clonedContext);
44
+ }
45
+
46
+ // Find the first user message index
47
+ const firstUserIndex = clonedContext.messages.findIndex((msg) => msg.role === 'user');
48
+
49
+ if (firstUserIndex === -1) {
50
+ log('No user messages found, skipping injection');
51
+ return this.markAsExecuted(clonedContext);
52
+ }
53
+
54
+ // Insert a new user message with knowledge before the first user message
55
+ // Mark it as application-level system injection
56
+ const knowledgeMessage = {
57
+ content: formattedContent,
58
+ createdAt: Date.now(),
59
+ id: `knowledge-${Date.now()}`,
60
+ meta: { injectType: 'knowledge', systemInjection: true },
61
+ role: 'user' as const,
62
+ updatedAt: Date.now(),
63
+ };
64
+
65
+ clonedContext.messages.splice(firstUserIndex, 0, knowledgeMessage);
66
+
67
+ // Update metadata
68
+ clonedContext.metadata.knowledgeInjected = true;
69
+ clonedContext.metadata.filesCount = fileContents.length;
70
+ clonedContext.metadata.knowledgeBasesCount = knowledgeBases.length;
71
+
72
+ log(
73
+ `Agent knowledge injected as new user message: ${fileContents.length} file(s), ${knowledgeBases.length} knowledge base(s)`,
74
+ );
75
+
76
+ return this.markAsExecuted(clonedContext);
77
+ }
78
+ }
@@ -1,9 +1,11 @@
1
1
  // Context Provider exports
2
2
  export { HistorySummaryProvider } from './HistorySummary';
3
+ export { KnowledgeInjector } from './KnowledgeInjector';
3
4
  export { SystemRoleInjector } from './SystemRoleInjector';
4
5
  export { ToolSystemRoleProvider } from './ToolSystemRole';
5
6
 
6
7
  // Re-export types
7
8
  export type { HistorySummaryConfig } from './HistorySummary';
9
+ export type { KnowledgeInjectorConfig } from './KnowledgeInjector';
8
10
  export type { SystemRoleInjectorConfig } from './SystemRoleInjector';
9
11
  export type { ToolSystemRoleConfig } from './ToolSystemRole';
@@ -1,6 +1,2 @@
1
1
  ALTER TABLE "documents" ADD COLUMN IF NOT EXISTS "slug" varchar(255);--> statement-breakpoint
2
- DO $$ BEGIN
3
- CREATE UNIQUE INDEX IF NOT EXISTS "documents_slug_user_id_unique" ON "documents" ("slug","user_id") WHERE "slug" IS NOT NULL;
4
- EXCEPTION
5
- WHEN duplicate_object THEN null;
6
- END $$;
2
+ CREATE UNIQUE INDEX IF NOT EXISTS "documents_slug_user_id_unique" ON "documents" USING btree ("slug","user_id") WHERE "documents"."slug" is not null;
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "dialect": "postgresql",
8
8
  "enums": {},
9
- "id": "8a423b68-c609-44e1-bd48-f2ed62fec5ef",
9
+ "id": "20a4c30c-04f0-4993-b493-0ae6a3144f9e",
10
10
  "policies": {},
11
11
  "prevId": "abe0cb47-a970-4ec0-a29f-1afa9bd2fe02",
12
12
  "roles": {},
@@ -1259,6 +1259,12 @@
1259
1259
  "primaryKey": false,
1260
1260
  "notNull": false
1261
1261
  },
1262
+ "slug": {
1263
+ "name": "slug",
1264
+ "type": "varchar(255)",
1265
+ "primaryKey": false,
1266
+ "notNull": false
1267
+ },
1262
1268
  "accessed_at": {
1263
1269
  "name": "accessed_at",
1264
1270
  "type": "timestamp with time zone",
@@ -1362,6 +1368,28 @@
1362
1368
  "concurrently": false,
1363
1369
  "method": "btree",
1364
1370
  "with": {}
1371
+ },
1372
+ "documents_slug_user_id_unique": {
1373
+ "name": "documents_slug_user_id_unique",
1374
+ "columns": [
1375
+ {
1376
+ "expression": "slug",
1377
+ "isExpression": false,
1378
+ "asc": true,
1379
+ "nulls": "last"
1380
+ },
1381
+ {
1382
+ "expression": "user_id",
1383
+ "isExpression": false,
1384
+ "asc": true,
1385
+ "nulls": "last"
1386
+ }
1387
+ ],
1388
+ "isUnique": true,
1389
+ "where": "\"documents\".\"slug\" is not null",
1390
+ "concurrently": false,
1391
+ "method": "btree",
1392
+ "with": {}
1365
1393
  }
1366
1394
  },
1367
1395
  "foreignKeys": {
@@ -1481,12 +1509,6 @@
1481
1509
  "primaryKey": false,
1482
1510
  "notNull": false
1483
1511
  },
1484
- "slug": {
1485
- "name": "slug",
1486
- "type": "text",
1487
- "primaryKey": false,
1488
- "notNull": false
1489
- },
1490
1512
  "accessed_at": {
1491
1513
  "name": "accessed_at",
1492
1514
  "type": "timestamp with time zone",
@@ -1610,13 +1632,7 @@
1610
1632
  }
1611
1633
  },
1612
1634
  "compositePrimaryKeys": {},
1613
- "uniqueConstraints": {
1614
- "files_slug_unique": {
1615
- "name": "files_slug_unique",
1616
- "nullsNotDistinct": false,
1617
- "columns": ["slug"]
1618
- }
1619
- },
1635
+ "uniqueConstraints": {},
1620
1636
  "policies": {},
1621
1637
  "checkConstraints": {},
1622
1638
  "isRLSEnabled": false
@@ -333,7 +333,7 @@
333
333
  {
334
334
  "idx": 47,
335
335
  "version": "7",
336
- "when": 1763535087148,
336
+ "when": 1763987922211,
337
337
  "tag": "0047_add_slug_document",
338
338
  "breakpoints": true
339
339
  }
@@ -792,10 +792,10 @@
792
792
  {
793
793
  "sql": [
794
794
  "ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"slug\" varchar(255);",
795
- "\nDO $$ BEGIN\n CREATE UNIQUE INDEX IF NOT EXISTS \"documents_slug_user_id_unique\" ON \"documents\" (\"slug\",\"user_id\") WHERE \"slug\" IS NOT NULL;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;"
795
+ "\nCREATE UNIQUE INDEX IF NOT EXISTS \"documents_slug_user_id_unique\" ON \"documents\" USING btree (\"slug\",\"user_id\") WHERE \"documents\".\"slug\" is not null;\n"
796
796
  ],
797
797
  "bps": true,
798
- "folderMillis": 1763535087148,
799
- "hash": "57a82ce14d96ffa9f140ff63f00af994e91a74703f4d2378286e36be259f117b"
798
+ "folderMillis": 1763987922211,
799
+ "hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
800
800
  }
801
801
  ]
@@ -2,17 +2,18 @@
2
2
  import { eq } from 'drizzle-orm';
3
3
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
4
 
5
- import { LobeChatDatabase } from '../../type';
6
5
  import {
7
6
  agents,
8
7
  agentsFiles,
9
8
  agentsKnowledgeBases,
10
9
  agentsToSessions,
10
+ documents,
11
11
  files,
12
12
  knowledgeBases,
13
13
  sessions,
14
14
  users,
15
15
  } from '../../schemas';
16
+ import { LobeChatDatabase } from '../../type';
16
17
  import { AgentModel } from '../agent';
17
18
  import { getTestDB } from './_util';
18
19
 
@@ -69,6 +70,76 @@ describe('AgentModel', () => {
69
70
  expect(result.knowledgeBases).toHaveLength(1);
70
71
  expect(result.files).toHaveLength(1);
71
72
  });
73
+
74
+ it('should fetch and include document content for enabled files', async () => {
75
+ const agentId = 'test-agent-with-docs';
76
+ await serverDB.insert(agents).values({ id: agentId, userId });
77
+ await serverDB.insert(agentsFiles).values({ agentId, fileId: '1', userId, enabled: true });
78
+ await serverDB.insert(documents).values({
79
+ id: 'doc1',
80
+ fileId: '1',
81
+ userId,
82
+ content: 'This is document content',
83
+ fileType: 'application/pdf',
84
+ totalCharCount: 100,
85
+ totalLineCount: 10,
86
+ sourceType: 'file',
87
+ source: 'document.pdf',
88
+ });
89
+
90
+ const result = await agentModel.getAgentConfigById(agentId);
91
+
92
+ expect(result).toBeDefined();
93
+ expect(result.files).toHaveLength(1);
94
+ expect(result.files[0].content).toBe('This is document content');
95
+ expect(result.files[0].enabled).toBe(true);
96
+ });
97
+
98
+ it('should not include content for disabled files', async () => {
99
+ const agentId = 'test-agent-disabled-file';
100
+ await serverDB.insert(agents).values({ id: agentId, userId });
101
+ await serverDB.insert(agentsFiles).values({ agentId, fileId: '1', userId, enabled: false });
102
+ await serverDB.insert(documents).values({
103
+ id: 'doc2',
104
+ fileId: '1',
105
+ userId,
106
+ content: 'This should not be included',
107
+ fileType: 'application/pdf',
108
+ totalCharCount: 100,
109
+ totalLineCount: 10,
110
+ sourceType: 'file',
111
+ source: 'document.pdf',
112
+ });
113
+
114
+ const result = await agentModel.getAgentConfigById(agentId);
115
+
116
+ expect(result).toBeDefined();
117
+ expect(result.files).toHaveLength(1);
118
+ expect(result.files[0].content).toBeUndefined();
119
+ expect(result.files[0].enabled).toBe(false);
120
+ });
121
+
122
+ it('should handle files without documents', async () => {
123
+ const agentId = 'test-agent-no-docs';
124
+ await serverDB.insert(agents).values({ id: agentId, userId });
125
+ await serverDB.insert(agentsFiles).values({ agentId, fileId: '2', userId, enabled: true });
126
+
127
+ const result = await agentModel.getAgentConfigById(agentId);
128
+
129
+ expect(result).toBeDefined();
130
+ expect(result.files).toHaveLength(1);
131
+ expect(result.files[0].content).toBeUndefined();
132
+ });
133
+
134
+ it('should handle agent with no files', async () => {
135
+ const agentId = 'test-agent-no-files';
136
+ await serverDB.insert(agents).values({ id: agentId, userId });
137
+
138
+ const result = await agentModel.getAgentConfigById(agentId);
139
+
140
+ expect(result).toBeDefined();
141
+ expect(result.files).toHaveLength(0);
142
+ });
72
143
  });
73
144
 
74
145
  describe('findBySessionId', () => {
@@ -84,10 +155,16 @@ describe('AgentModel', () => {
84
155
  expect(result).toBeDefined();
85
156
  expect(result?.id).toBe(agentId);
86
157
  });
158
+
159
+ it('should return undefined when session is not found', async () => {
160
+ const result = await agentModel.findBySessionId('non-existent-session');
161
+
162
+ expect(result).toBeUndefined();
163
+ });
87
164
  });
88
165
 
89
166
  describe('createAgentKnowledgeBase', () => {
90
- it('should create a new agent knowledge base association', async () => {
167
+ it('should create a new agent knowledge base association with enabled=true by default', async () => {
91
168
  const agent = await serverDB
92
169
  .insert(agents)
93
170
  .values({ userId })
@@ -107,6 +184,27 @@ describe('AgentModel', () => {
107
184
  enabled: true,
108
185
  });
109
186
  });
187
+
188
+ it('should create a new agent knowledge base association with enabled=false', async () => {
189
+ const agent = await serverDB
190
+ .insert(agents)
191
+ .values({ userId })
192
+ .returning()
193
+ .then((res) => res[0]);
194
+
195
+ await agentModel.createAgentKnowledgeBase(agent.id, knowledgeBase.id, false);
196
+
197
+ const result = await serverDB.query.agentsKnowledgeBases.findFirst({
198
+ where: eq(agentsKnowledgeBases.agentId, agent.id),
199
+ });
200
+
201
+ expect(result).toMatchObject({
202
+ agentId: agent.id,
203
+ knowledgeBaseId: knowledgeBase.id,
204
+ userId,
205
+ enabled: false,
206
+ });
207
+ });
110
208
  });
111
209
 
112
210
  describe('deleteAgentKnowledgeBase', () => {
@@ -153,7 +251,7 @@ describe('AgentModel', () => {
153
251
  });
154
252
 
155
253
  describe('createAgentFiles', () => {
156
- it('should create new agent file associations', async () => {
254
+ it('should create new agent file associations with enabled=true by default', async () => {
157
255
  const agent = await serverDB
158
256
  .insert(agents)
159
257
  .values({ userId })
@@ -174,6 +272,77 @@ describe('AgentModel', () => {
174
272
  ]),
175
273
  );
176
274
  });
275
+
276
+ it('should create new agent file associations with enabled=false', async () => {
277
+ const agent = await serverDB
278
+ .insert(agents)
279
+ .values({ userId })
280
+ .returning()
281
+ .then((res) => res[0]);
282
+
283
+ await agentModel.createAgentFiles(agent.id, ['1'], false);
284
+
285
+ const results = await serverDB.query.agentsFiles.findMany({
286
+ where: eq(agentsFiles.agentId, agent.id),
287
+ });
288
+
289
+ expect(results).toHaveLength(1);
290
+ expect(results[0]).toMatchObject({
291
+ agentId: agent.id,
292
+ fileId: '1',
293
+ userId,
294
+ enabled: false,
295
+ });
296
+ });
297
+
298
+ it('should skip files that already exist', async () => {
299
+ const agent = await serverDB
300
+ .insert(agents)
301
+ .values({ userId })
302
+ .returning()
303
+ .then((res) => res[0]);
304
+
305
+ // First insert
306
+ await serverDB.insert(agentsFiles).values({ agentId: agent.id, fileId: '1', userId });
307
+
308
+ // Try to insert the same file again
309
+ await agentModel.createAgentFiles(agent.id, ['1', '2']);
310
+
311
+ const results = await serverDB.query.agentsFiles.findMany({
312
+ where: eq(agentsFiles.agentId, agent.id),
313
+ });
314
+
315
+ // Should only have 2 files (1 existing + 1 new), not 3
316
+ expect(results).toHaveLength(2);
317
+ expect(results.map((r) => r.fileId).sort()).toEqual(['1', '2']);
318
+ });
319
+
320
+ it('should return early when all files already exist', async () => {
321
+ const agent = await serverDB
322
+ .insert(agents)
323
+ .values({ userId })
324
+ .returning()
325
+ .then((res) => res[0]);
326
+
327
+ // First insert
328
+ await serverDB.insert(agentsFiles).values([
329
+ { agentId: agent.id, fileId: '1', userId },
330
+ { agentId: agent.id, fileId: '2', userId },
331
+ ]);
332
+
333
+ // Try to insert the same files again
334
+ const result = await agentModel.createAgentFiles(agent.id, ['1', '2']);
335
+
336
+ // Should return undefined (early return)
337
+ expect(result).toBeUndefined();
338
+
339
+ const results = await serverDB.query.agentsFiles.findMany({
340
+ where: eq(agentsFiles.agentId, agent.id),
341
+ });
342
+
343
+ // Should still only have 2 files
344
+ expect(results).toHaveLength(2);
345
+ });
177
346
  });
178
347
 
179
348
  describe('deleteAgentFile', () => {