@lobehub/lobehub 2.0.0-next.343 → 2.0.0-next.345

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 (169) hide show
  1. package/.cursor/rules/i18n.mdc +1 -1
  2. package/.cursor/rules/modal-imperative.mdc +162 -0
  3. package/.cursor/rules/rules-index.mdc +1 -0
  4. package/.env.example +0 -14
  5. package/.eslintrc.js +8 -1
  6. package/CHANGELOG.md +66 -0
  7. package/Dockerfile +3 -13
  8. package/README.md +3 -5
  9. package/README.zh-CN.md +3 -5
  10. package/changelog/v1.json +24 -0
  11. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +11 -42
  12. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +10 -41
  13. package/e2e/src/support/webServer.ts +2 -0
  14. package/locales/ar/error.json +0 -4
  15. package/locales/bg-BG/error.json +0 -4
  16. package/locales/de-DE/error.json +0 -4
  17. package/locales/en-US/error.json +0 -4
  18. package/locales/es-ES/error.json +0 -4
  19. package/locales/fa-IR/error.json +0 -4
  20. package/locales/fr-FR/error.json +0 -4
  21. package/locales/it-IT/error.json +0 -4
  22. package/locales/ja-JP/error.json +0 -4
  23. package/locales/ko-KR/error.json +0 -4
  24. package/locales/nl-NL/error.json +0 -4
  25. package/locales/pl-PL/error.json +0 -4
  26. package/locales/pt-BR/error.json +0 -4
  27. package/locales/ru-RU/error.json +0 -4
  28. package/locales/tr-TR/error.json +0 -4
  29. package/locales/vi-VN/error.json +0 -4
  30. package/locales/zh-CN/error.json +0 -4
  31. package/locales/zh-TW/error.json +0 -4
  32. package/package.json +7 -9
  33. package/packages/builtin-agents/package.json +2 -0
  34. package/packages/builtin-agents/src/agents/agent-builder/index.ts +4 -2
  35. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +4 -2
  36. package/packages/builtin-agents/src/agents/page-agent/index.ts +5 -2
  37. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +161 -12
  38. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +9 -9
  39. package/packages/context-engine/src/providers/GroupContextInjector.ts +19 -33
  40. package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +79 -43
  41. package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +5 -15
  42. package/packages/database/src/repositories/userMemory/__tests__/UserMemoryTopicRepository.test.ts +24 -3
  43. package/packages/model-bank/src/modelProviders/comfyui.ts +0 -1
  44. package/packages/model-bank/src/modelProviders/fal.ts +0 -1
  45. package/packages/types/src/fetch.ts +1 -2
  46. package/packages/utils/src/server/__tests__/auth.test.ts +0 -47
  47. package/packages/utils/src/server/auth.ts +1 -9
  48. package/scripts/_shared/checkDeprecatedClerkEnv.js +42 -0
  49. package/scripts/changelogWorkflow/buildStaticChangelog.ts +2 -1
  50. package/scripts/clerk-to-betterauth/_internal/types.ts +53 -20
  51. package/scripts/clerk-to-betterauth/export-clerk-users-with-api.ts +43 -36
  52. package/scripts/countEnWord.ts +1 -1
  53. package/scripts/electronWorkflow/modifiers/appCode.mts +2 -131
  54. package/scripts/i18nWorkflow/protectedPatterns.ts +1 -2
  55. package/scripts/prebuild.mts +10 -8
  56. package/scripts/serverLauncher/startServer.js +23 -5
  57. package/src/app/(backend)/middleware/auth/index.test.ts +8 -4
  58. package/src/app/(backend)/middleware/auth/index.ts +0 -15
  59. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -28
  60. package/src/app/(backend)/middleware/auth/utils.ts +2 -17
  61. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +3 -51
  62. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -4
  63. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +7 -6
  64. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -16
  65. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/index.tsx +1 -1
  66. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +13 -13
  67. package/src/app/[variants]/(main)/home/features/RecentPage/Item.tsx +2 -2
  68. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +3 -21
  69. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -2
  70. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -22
  71. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +12 -14
  72. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +8 -14
  73. package/src/app/[variants]/(main)/settings/skill/index.tsx +7 -5
  74. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +2 -35
  75. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -20
  76. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -2
  77. package/src/app/[variants]/(mobile)/me/profile/features/Category.tsx +3 -13
  78. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +2 -3
  79. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +1 -1
  80. package/src/app/[variants]/share/t/[id]/index.tsx +1 -1
  81. package/src/app/robots.tsx +1 -1
  82. package/src/envs/auth.ts +2 -27
  83. package/src/envs/llm.ts +2 -2
  84. package/src/features/AgentSetting/AgentPlugin/index.tsx +9 -12
  85. package/src/features/ChatInput/ActionBar/Tools/index.tsx +7 -5
  86. package/src/features/ChatMiniMap/utils.ts +1 -1
  87. package/src/features/CommandMenu/SearchResults.tsx +1 -1
  88. package/src/features/Conversation/ChatList/components/AutoScroll/DebugInspector.tsx +166 -0
  89. package/src/features/Conversation/ChatList/components/AutoScroll/index.tsx +86 -0
  90. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +11 -17
  91. package/src/features/Conversation/Messages/AgentCouncil/components/AutoScrollShadow.tsx +25 -14
  92. package/src/features/Conversation/Messages/AgentCouncil/components/CouncilMember.tsx +1 -1
  93. package/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx +305 -0
  94. package/src/features/IntegrationDetailModal/index.tsx +21 -283
  95. package/src/features/MCPPluginDetail/Deployment/index.tsx +1 -1
  96. package/src/features/MCPPluginDetail/Schema/Prompts.tsx +1 -1
  97. package/src/features/MCPPluginDetail/Schema/Tools.tsx +1 -1
  98. package/src/features/ProfileEditor/AgentTool.tsx +14 -20
  99. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/NoteFileItem.tsx +1 -1
  100. package/src/features/SkillStore/LobeHubList/index.tsx +50 -87
  101. package/src/features/SkillStore/Search/index.tsx +1 -1
  102. package/src/features/SkillStore/{Content.tsx → SkillStoreContent.tsx} +3 -8
  103. package/src/features/SkillStore/index.tsx +15 -33
  104. package/src/features/User/UserPanel/PanelContent.tsx +0 -8
  105. package/src/features/User/__tests__/PanelContent.test.tsx +1 -35
  106. package/src/features/User/__tests__/UserAvatar.test.tsx +30 -57
  107. package/src/features/User/__tests__/useMenu.test.tsx +2 -43
  108. package/src/layout/AuthProvider/index.tsx +0 -5
  109. package/src/libs/next/config/define-config.ts +6 -0
  110. package/src/libs/next/proxy/createRouteMatcher.test.ts +121 -0
  111. package/src/libs/next/proxy/createRouteMatcher.ts +18 -0
  112. package/src/libs/next/proxy/define-config.ts +4 -53
  113. package/src/libs/next-auth/adapter/index.ts +1 -2
  114. package/src/libs/oidc-provider/provider.test.ts +5 -316
  115. package/src/libs/trpc/lambda/context.test.ts +0 -13
  116. package/src/libs/trpc/lambda/context.ts +3 -22
  117. package/src/libs/trpc/middleware/userAuth.ts +2 -4
  118. package/src/libs/trusted-client/getSessionUser.ts +2 -17
  119. package/src/locales/default/error.ts +0 -6
  120. package/src/locales/default/index.ts +0 -2
  121. package/src/proxy.ts +0 -1
  122. package/src/server/routers/lambda/__tests__/user.test.ts +0 -71
  123. package/src/server/routers/lambda/user.ts +6 -63
  124. package/src/server/services/changelog/index.test.ts +3 -2
  125. package/src/server/services/changelog/index.ts +1 -1
  126. package/src/server/services/user/index.ts +0 -83
  127. package/src/services/chat/index.ts +1 -2
  128. package/src/services/chat/mecha/agentConfigResolver.test.ts +43 -0
  129. package/src/services/chat/mecha/agentConfigResolver.ts +3 -1
  130. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +58 -14
  131. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +10 -2
  132. package/src/store/user/slices/auth/action.test.ts +1 -81
  133. package/src/store/user/slices/auth/action.ts +3 -28
  134. package/src/store/user/slices/auth/initialState.ts +1 -18
  135. package/src/store/user/slices/auth/selectors.test.ts +2 -127
  136. package/src/store/user/slices/auth/selectors.ts +1 -21
  137. package/src/utils/errorResponse.ts +1 -4
  138. package/src/utils/markdownToTxt.ts +20 -0
  139. package/locales/ar/clerk.json +0 -545
  140. package/locales/bg-BG/clerk.json +0 -545
  141. package/locales/de-DE/clerk.json +0 -545
  142. package/locales/en-US/clerk.json +0 -545
  143. package/locales/es-ES/clerk.json +0 -545
  144. package/locales/fa-IR/clerk.json +0 -545
  145. package/locales/fr-FR/clerk.json +0 -545
  146. package/locales/it-IT/clerk.json +0 -545
  147. package/locales/ja-JP/clerk.json +0 -545
  148. package/locales/ko-KR/clerk.json +0 -545
  149. package/locales/nl-NL/clerk.json +0 -545
  150. package/locales/pl-PL/clerk.json +0 -545
  151. package/locales/pt-BR/clerk.json +0 -545
  152. package/locales/ru-RU/clerk.json +0 -545
  153. package/locales/tr-TR/clerk.json +0 -545
  154. package/locales/vi-VN/clerk.json +0 -545
  155. package/locales/zh-CN/clerk.json +0 -545
  156. package/locales/zh-TW/clerk.json +0 -545
  157. package/src/app/(backend)/api/webhooks/clerk/__tests__/fixtures/createUser.json +0 -73
  158. package/src/app/(backend)/api/webhooks/clerk/route.ts +0 -95
  159. package/src/app/(backend)/api/webhooks/clerk/validateRequest.ts +0 -22
  160. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +0 -27
  161. package/src/app/[variants]/(main)/settings/security/features/ClerkProfile.tsx +0 -67
  162. package/src/features/Conversation/ChatList/components/AutoScroll.tsx +0 -25
  163. package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +0 -40
  164. package/src/layout/AuthProvider/Clerk/index.tsx +0 -54
  165. package/src/layout/AuthProvider/Clerk/useAppearance.ts +0 -133
  166. package/src/libs/clerk-auth/index.test.ts +0 -216
  167. package/src/libs/clerk-auth/index.ts +0 -80
  168. package/src/locales/default/clerk.ts +0 -677
  169. package/src/server/services/user/index.test.ts +0 -220
@@ -65,6 +65,14 @@ export class CloudSandboxExecutionRuntime {
65
65
  try {
66
66
  const result = await this.callTool('listLocalFiles', args);
67
67
 
68
+ if (!result.success) {
69
+ return {
70
+ content: result.error?.message || JSON.stringify(result.error),
71
+ state: { files: [] },
72
+ success: true,
73
+ };
74
+ }
75
+
68
76
  const files = result.result?.files || [];
69
77
  const state: ListLocalFilesState = { files };
70
78
 
@@ -90,6 +98,19 @@ export class CloudSandboxExecutionRuntime {
90
98
  try {
91
99
  const result = await this.callTool('readLocalFile', args);
92
100
 
101
+ if (!result.success) {
102
+ return {
103
+ content: result.error?.message || JSON.stringify(result.error),
104
+ state: {
105
+ content: '',
106
+ endLine: args.endLine,
107
+ path: args.path,
108
+ startLine: args.startLine,
109
+ },
110
+ success: true,
111
+ };
112
+ }
113
+
93
114
  const state: ReadLocalFileState = {
94
115
  content: result.result?.content || '',
95
116
  endLine: args.endLine,
@@ -123,6 +144,17 @@ export class CloudSandboxExecutionRuntime {
123
144
  try {
124
145
  const result = await this.callTool('writeLocalFile', args);
125
146
 
147
+ if (!result.success) {
148
+ return {
149
+ content: result.error?.message || JSON.stringify(result.error),
150
+ state: {
151
+ path: args.path,
152
+ success: false,
153
+ },
154
+ success: true,
155
+ };
156
+ }
157
+
126
158
  const state: WriteLocalFileState = {
127
159
  bytesWritten: result.result?.bytesWritten,
128
160
  path: args.path,
@@ -148,6 +180,17 @@ export class CloudSandboxExecutionRuntime {
148
180
  try {
149
181
  const result = await this.callTool('editLocalFile', args);
150
182
 
183
+ if (!result.success) {
184
+ return {
185
+ content: result.error?.message || JSON.stringify(result.error),
186
+ state: {
187
+ path: args.path,
188
+ replacements: 0,
189
+ },
190
+ success: true,
191
+ };
192
+ }
193
+
151
194
  const state: EditLocalFileState = {
152
195
  diffText: result.result?.diffText,
153
196
  linesAdded: result.result?.linesAdded,
@@ -177,6 +220,17 @@ export class CloudSandboxExecutionRuntime {
177
220
  try {
178
221
  const result = await this.callTool('searchLocalFiles', args);
179
222
 
223
+ if (!result.success) {
224
+ return {
225
+ content: result.error?.message || JSON.stringify(result.error),
226
+ state: {
227
+ results: [],
228
+ totalCount: 0,
229
+ },
230
+ success: true,
231
+ };
232
+ }
233
+
180
234
  const results = result.result?.results || [];
181
235
  const state: SearchLocalFilesState = {
182
236
  results,
@@ -201,6 +255,18 @@ export class CloudSandboxExecutionRuntime {
201
255
  try {
202
256
  const result = await this.callTool('moveLocalFiles', args);
203
257
 
258
+ if (!result.success) {
259
+ return {
260
+ content: result.error?.message || JSON.stringify(result.error),
261
+ state: {
262
+ results: [],
263
+ successCount: 0,
264
+ totalCount: args.operations.length,
265
+ },
266
+ success: true,
267
+ };
268
+ }
269
+
204
270
  const results = result.result?.results || [];
205
271
  const state: MoveLocalFilesState = {
206
272
  results,
@@ -224,6 +290,19 @@ export class CloudSandboxExecutionRuntime {
224
290
  try {
225
291
  const result = await this.callTool('renameLocalFile', args);
226
292
 
293
+ if (!result.success) {
294
+ return {
295
+ content: result.error?.message || JSON.stringify(result.error),
296
+ state: {
297
+ error: result.error?.message,
298
+ newPath: '',
299
+ oldPath: args.oldPath,
300
+ success: false,
301
+ },
302
+ success: true,
303
+ };
304
+ }
305
+
227
306
  const state: RenameLocalFileState = {
228
307
  error: result.result?.error,
229
308
  newPath: result.result?.newPath || '',
@@ -241,7 +320,7 @@ export class CloudSandboxExecutionRuntime {
241
320
  return {
242
321
  content,
243
322
  state,
244
- success: result.success,
323
+ success: true,
245
324
  };
246
325
  } catch (error) {
247
326
  return this.handleError(error);
@@ -264,15 +343,24 @@ export class CloudSandboxExecutionRuntime {
264
343
  language,
265
344
  output: result.result?.output,
266
345
  stderr: result.result?.stderr,
267
- success: result.success,
346
+ success: result.success || false,
268
347
  };
269
348
 
349
+ if (!result.success) {
350
+ return {
351
+ content: result.error?.message || JSON.stringify(result.error),
352
+ state,
353
+ success: true,
354
+ };
355
+ }
356
+
270
357
  return {
271
358
  content: JSON.stringify(result.result),
272
359
  state,
273
- success: result.success,
360
+ success: true,
274
361
  };
275
362
  } catch (error) {
363
+ console.log('executeCode error', error);
276
364
  return this.handleError(error);
277
365
  }
278
366
  }
@@ -283,6 +371,18 @@ export class CloudSandboxExecutionRuntime {
283
371
  try {
284
372
  const result = await this.callTool('runCommand', args);
285
373
 
374
+ if (!result.success) {
375
+ return {
376
+ content: result.error?.message || JSON.stringify(result.error),
377
+ state: {
378
+ error: result.error?.message,
379
+ isBackground: args.background || false,
380
+ success: false,
381
+ },
382
+ success: true,
383
+ };
384
+ }
385
+
286
386
  const state: RunCommandState = {
287
387
  commandId: result.result?.commandId,
288
388
  error: result.result?.error,
@@ -296,7 +396,7 @@ export class CloudSandboxExecutionRuntime {
296
396
  return {
297
397
  content: JSON.stringify(result.result),
298
398
  state,
299
- success: result.success,
399
+ success: true,
300
400
  };
301
401
  } catch (error) {
302
402
  return this.handleError(error);
@@ -307,6 +407,18 @@ export class CloudSandboxExecutionRuntime {
307
407
  try {
308
408
  const result = await this.callTool('getCommandOutput', args);
309
409
 
410
+ if (!result.success) {
411
+ return {
412
+ content: result.error?.message || JSON.stringify(result.error),
413
+ state: {
414
+ error: result.error?.message,
415
+ running: false,
416
+ success: false,
417
+ },
418
+ success: true,
419
+ };
420
+ }
421
+
310
422
  const state: GetCommandOutputState = {
311
423
  error: result.result?.error,
312
424
  newOutput: result.result?.newOutput,
@@ -317,7 +429,7 @@ export class CloudSandboxExecutionRuntime {
317
429
  return {
318
430
  content: JSON.stringify(result.result),
319
431
  state,
320
- success: result.success,
432
+ success: true,
321
433
  };
322
434
  } catch (error) {
323
435
  return this.handleError(error);
@@ -328,6 +440,18 @@ export class CloudSandboxExecutionRuntime {
328
440
  try {
329
441
  const result = await this.callTool('killCommand', args);
330
442
 
443
+ if (!result.success) {
444
+ return {
445
+ content: result.error?.message || JSON.stringify(result.error),
446
+ state: {
447
+ commandId: args.commandId,
448
+ error: result.error?.message,
449
+ success: false,
450
+ },
451
+ success: true,
452
+ };
453
+ }
454
+
331
455
  const state: KillCommandState = {
332
456
  commandId: args.commandId,
333
457
  error: result.result?.error,
@@ -340,7 +464,7 @@ export class CloudSandboxExecutionRuntime {
340
464
  success: true,
341
465
  }),
342
466
  state,
343
- success: result.success,
467
+ success: true,
344
468
  };
345
469
  } catch (error) {
346
470
  return this.handleError(error);
@@ -353,6 +477,18 @@ export class CloudSandboxExecutionRuntime {
353
477
  try {
354
478
  const result = await this.callTool('grepContent', args);
355
479
 
480
+ if (!result.success) {
481
+ return {
482
+ content: result.error?.message || JSON.stringify(result.error),
483
+ state: {
484
+ matches: [],
485
+ pattern: args.pattern,
486
+ totalMatches: 0,
487
+ },
488
+ success: true,
489
+ };
490
+ }
491
+
356
492
  const state: GrepContentState = {
357
493
  matches: result.result?.matches || [],
358
494
  pattern: args.pattern,
@@ -373,6 +509,18 @@ export class CloudSandboxExecutionRuntime {
373
509
  try {
374
510
  const result = await this.callTool('globLocalFiles', args);
375
511
 
512
+ if (!result.success) {
513
+ return {
514
+ content: result.error?.message || JSON.stringify(result.error),
515
+ state: {
516
+ files: [],
517
+ pattern: args.pattern,
518
+ totalCount: 0,
519
+ },
520
+ success: true,
521
+ };
522
+ }
523
+
376
524
  const files = result.result?.files || [];
377
525
  const totalCount = result.result?.totalCount || 0;
378
526
 
@@ -433,7 +581,7 @@ export class CloudSandboxExecutionRuntime {
433
581
  success: false,
434
582
  }),
435
583
  state,
436
- success: false,
584
+ success: true,
437
585
  };
438
586
  }
439
587
 
@@ -455,13 +603,14 @@ export class CloudSandboxExecutionRuntime {
455
603
  private async callTool(
456
604
  toolName: string,
457
605
  params: Record<string, any>,
458
- ): Promise<{ result: any; sessionExpiredAndRecreated?: boolean; success: boolean }> {
606
+ ): Promise<{
607
+ error?: { message: string; name?: string };
608
+ result: any;
609
+ sessionExpiredAndRecreated?: boolean;
610
+ success: boolean;
611
+ }> {
459
612
  const result = await this.sandboxService.callTool(toolName, params);
460
613
 
461
- if (!result.success) {
462
- throw new Error(result.error?.message || `Cloud Sandbox tool ${toolName} failed`);
463
- }
464
-
465
614
  return result;
466
615
  }
467
616
 
@@ -162,6 +162,15 @@ export class MessagesEngine {
162
162
  // 2. System role injection (agent's system role)
163
163
  new SystemRoleInjector({ systemRole }),
164
164
 
165
+ // =============================================
166
+ // Phase 2.5: First User Message Context Injection
167
+ // These providers inject content before the first user message
168
+ // Order matters: first executed = first in content
169
+ // =============================================
170
+
171
+ // 4. User memory injection (conditionally added, injected first)
172
+ ...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
173
+
165
174
  // 3. Group context injection (agent identity and group info for multi-agent chat)
166
175
  new GroupContextInjector({
167
176
  currentAgentId: agentGroup?.currentAgentId,
@@ -173,15 +182,6 @@ export class MessagesEngine {
173
182
  systemPrompt: agentGroup?.systemPrompt,
174
183
  }),
175
184
 
176
- // =============================================
177
- // Phase 2.5: First User Message Context Injection
178
- // These providers inject content before the first user message
179
- // Order matters: first executed = first in content
180
- // =============================================
181
-
182
- // 4. User memory injection (conditionally added, injected first)
183
- ...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
184
-
185
185
  // 4.5. GTD Plan injection (conditionally added, after user memory, before knowledge)
186
186
  ...(isGTDPlanEnabled ? [new GTDPlanInjector({ enabled: true, plan: gtd.plan })] : []),
187
187
 
@@ -5,7 +5,7 @@ import {
5
5
  } from '@lobechat/prompts';
6
6
  import debug from 'debug';
7
7
 
8
- import { BaseProvider } from '../base/BaseProvider';
8
+ import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
9
9
  import type { PipelineContext, ProcessorOptions } from '../types';
10
10
 
11
11
  const log = debug('context-engine:provider:GroupContextInjector');
@@ -59,13 +59,13 @@ export interface GroupContextInjectorConfig {
59
59
  /**
60
60
  * Group Context Injector
61
61
  *
62
- * Responsible for injecting group context information into the system role
62
+ * Responsible for injecting group context information before the first user message
63
63
  * for multi-agent group chat scenarios. This helps the model understand:
64
64
  * - Its own identity within the group
65
65
  * - The group composition and other members
66
66
  * - Rules for handling system metadata
67
67
  *
68
- * The injector appends a GROUP CONTEXT block at the end of the system message,
68
+ * The injector creates a system injection message before the first user message,
69
69
  * containing:
70
70
  * - Agent's identity (name, role, ID)
71
71
  * - Group info (name, member list)
@@ -87,7 +87,7 @@ export interface GroupContextInjectorConfig {
87
87
  * });
88
88
  * ```
89
89
  */
90
- export class GroupContextInjector extends BaseProvider {
90
+ export class GroupContextInjector extends BaseFirstUserContentProvider {
91
91
  readonly name = 'GroupContextInjector';
92
92
 
93
93
  constructor(
@@ -97,44 +97,32 @@ export class GroupContextInjector extends BaseProvider {
97
97
  super(options);
98
98
  }
99
99
 
100
- protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
101
- const clonedContext = this.cloneContext(context);
102
-
103
- // Skip if not enabled or missing required config
100
+ protected buildContent(): string | null {
101
+ // Skip if not enabled
104
102
  if (!this.config.enabled) {
105
103
  log('Group context injection disabled, skipping');
106
- return this.markAsExecuted(clonedContext);
107
- }
108
-
109
- // Find the system message to append to
110
- const systemMessageIndex = clonedContext.messages.findIndex((msg) => msg.role === 'system');
111
-
112
- if (systemMessageIndex === -1) {
113
- log('No system message found, skipping group context injection');
114
- return this.markAsExecuted(clonedContext);
104
+ return null;
115
105
  }
116
106
 
117
- const systemMessage = clonedContext.messages[systemMessageIndex];
118
- const groupContext = this.buildGroupContextBlock();
107
+ const content = this.buildGroupContextBlock();
108
+ log('Group context prepared for injection');
119
109
 
120
- // Append group context to system message content
121
- if (typeof systemMessage.content === 'string') {
122
- clonedContext.messages[systemMessageIndex] = {
123
- ...systemMessage,
124
- content: systemMessage.content + groupContext,
125
- };
110
+ return content;
111
+ }
126
112
 
127
- log('Group context injected into system message');
128
- }
113
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
114
+ const result = await super.doProcess(context);
129
115
 
130
116
  // Update metadata
131
- clonedContext.metadata.groupContextInjected = true;
117
+ if (this.config.enabled) {
118
+ result.metadata.groupContextInjected = true;
119
+ }
132
120
 
133
- return this.markAsExecuted(clonedContext);
121
+ return result;
134
122
  }
135
123
 
136
124
  /**
137
- * Build the group context block to append to system message
125
+ * Build the group context block
138
126
  * Uses template from @lobechat/prompts with direct variable replacement
139
127
  */
140
128
  private buildGroupContextBlock(): string {
@@ -159,9 +147,7 @@ export class GroupContextInjector extends BaseProvider {
159
147
  .replace('{{SYSTEM_PROMPT}}', systemPrompt || '')
160
148
  .replace('{{GROUP_MEMBERS}}', membersText);
161
149
 
162
- return `
163
-
164
- <group_context>
150
+ return `<group_context>
165
151
  ${groupContextContent}
166
152
  </group_context>`;
167
153
  }
@@ -12,7 +12,7 @@ describe('GroupContextInjector', () => {
12
12
  });
13
13
 
14
14
  describe('Basic Scenarios', () => {
15
- it('should inject group context into system message', async () => {
15
+ it('should inject group context before first user message', async () => {
16
16
  const injector = new GroupContextInjector({
17
17
  currentAgentId: 'agt_editor',
18
18
  currentAgentName: 'Editor',
@@ -35,31 +35,39 @@ describe('GroupContextInjector', () => {
35
35
  const context = createContext(input);
36
36
  const result = await injector.process(context);
37
37
 
38
- const systemContent = result.messages[0].content;
38
+ // System message should be unchanged
39
+ expect(result.messages[0].content).toBe('You are a helpful editor.');
40
+
41
+ // Should have 3 messages now (system, injected, user)
42
+ expect(result.messages).toHaveLength(3);
39
43
 
40
- // Original content should be preserved
41
- expect(systemContent).toContain('You are a helpful editor.');
44
+ // Check injected message (second message)
45
+ const injectedContent = result.messages[1].content;
46
+ expect(result.messages[1].role).toBe('user');
42
47
 
43
48
  // Agent identity (plain text, no wrapper)
44
- expect(systemContent).toContain('You are "Editor"');
45
- expect(systemContent).toContain('acting as a participant');
46
- expect(systemContent).toContain('"Writing Team"');
47
- expect(systemContent).toContain('agt_editor');
48
- expect(systemContent).not.toContain('<agent_identity>');
49
+ expect(injectedContent).toContain('You are "Editor"');
50
+ expect(injectedContent).toContain('acting as a participant');
51
+ expect(injectedContent).toContain('"Writing Team"');
52
+ expect(injectedContent).toContain('agt_editor');
53
+ expect(injectedContent).not.toContain('<agent_identity>');
49
54
 
50
55
  // Group context section with system prompt
51
- expect(systemContent).toContain('<group_context>');
52
- expect(systemContent).toContain('A team for collaborative writing');
56
+ expect(injectedContent).toContain('<group_context>');
57
+ expect(injectedContent).toContain('A team for collaborative writing');
53
58
 
54
59
  // Participants section with XML format
55
- expect(systemContent).toContain('<group_participants>');
56
- expect(systemContent).toContain('<member name="Supervisor" id="agt_supervisor" />');
57
- expect(systemContent).toContain('<member name="Writer" id="agt_writer" />');
58
- expect(systemContent).toContain('<member name="Editor" id="agt_editor" you="true" />');
60
+ expect(injectedContent).toContain('<group_participants>');
61
+ expect(injectedContent).toContain('<member name="Supervisor" id="agt_supervisor" />');
62
+ expect(injectedContent).toContain('<member name="Writer" id="agt_writer" />');
63
+ expect(injectedContent).toContain('<member name="Editor" id="agt_editor" you="true" />');
59
64
 
60
65
  // Identity rules
61
- expect(systemContent).toContain('<identity_rules>');
62
- expect(systemContent).toContain('NEVER expose or display agent IDs');
66
+ expect(injectedContent).toContain('<identity_rules>');
67
+ expect(injectedContent).toContain('NEVER expose or display agent IDs');
68
+
69
+ // Original user message should be third
70
+ expect(result.messages[2].content).toBe('Please review this.');
63
71
 
64
72
  // Metadata should be updated
65
73
  expect(result.metadata.groupContextInjected).toBe(true);
@@ -72,35 +80,37 @@ describe('GroupContextInjector', () => {
72
80
  enabled: false, // Disabled
73
81
  });
74
82
 
75
- const input: any[] = [{ role: 'system', content: 'You are a helpful editor.' }];
83
+ const input: any[] = [
84
+ { role: 'system', content: 'You are a helpful editor.' },
85
+ { role: 'user', content: 'Hello' },
86
+ ];
76
87
 
77
88
  const context = createContext(input);
78
89
  const result = await injector.process(context);
79
90
 
80
- // Should be unchanged
91
+ // Should be unchanged - no injection
92
+ expect(result.messages).toHaveLength(2);
81
93
  expect(result.messages[0].content).toBe('You are a helpful editor.');
94
+ expect(result.messages[1].content).toBe('Hello');
82
95
  expect(result.metadata.groupContextInjected).toBeUndefined();
83
96
  });
84
97
 
85
- it('should skip injection when no system message exists', async () => {
98
+ it('should skip injection when no user message exists', async () => {
86
99
  const injector = new GroupContextInjector({
87
100
  currentAgentId: 'agt_editor',
88
101
  currentAgentName: 'Editor',
89
102
  enabled: true,
90
103
  });
91
104
 
92
- const input: any[] = [
93
- { role: 'user', content: 'Hello' },
94
- { role: 'assistant', content: 'Hi there!' },
95
- ];
105
+ const input: any[] = [{ role: 'system', content: 'You are a helpful editor.' }];
96
106
 
97
107
  const context = createContext(input);
98
108
  const result = await injector.process(context);
99
109
 
100
- // Messages should be unchanged
101
- expect(result.messages[0].content).toBe('Hello');
102
- expect(result.messages[1].content).toBe('Hi there!');
103
- expect(result.metadata.groupContextInjected).toBeUndefined();
110
+ // Messages should be unchanged - no user message to inject before
111
+ expect(result.messages).toHaveLength(1);
112
+ expect(result.messages[0].content).toBe('You are a helpful editor.');
113
+ expect(result.metadata.groupContextInjected).toBe(true);
104
114
  });
105
115
  });
106
116
 
@@ -113,12 +123,16 @@ describe('GroupContextInjector', () => {
113
123
  enabled: true,
114
124
  });
115
125
 
116
- const input: any[] = [{ content: 'You are an editor.', role: 'system' }];
126
+ const input: any[] = [
127
+ { content: 'You are an editor.', role: 'system' },
128
+ { content: 'Hello', role: 'user' },
129
+ ];
117
130
 
118
131
  const context = createContext(input);
119
132
  const result = await injector.process(context);
120
133
 
121
- expect(result.messages[0].content).toMatchSnapshot();
134
+ // Check injected message content
135
+ expect(result.messages[1].content).toMatchSnapshot();
122
136
  });
123
137
 
124
138
  it('should handle config with only group info', async () => {
@@ -129,12 +143,16 @@ describe('GroupContextInjector', () => {
129
143
  systemPrompt: 'Test group description',
130
144
  });
131
145
 
132
- const input: any[] = [{ content: 'System prompt.', role: 'system' }];
146
+ const input: any[] = [
147
+ { content: 'System prompt.', role: 'system' },
148
+ { content: 'Hello', role: 'user' },
149
+ ];
133
150
 
134
151
  const context = createContext(input);
135
152
  const result = await injector.process(context);
136
153
 
137
- expect(result.messages[0].content).toMatchSnapshot();
154
+ // Check injected message content
155
+ expect(result.messages[1].content).toMatchSnapshot();
138
156
  });
139
157
 
140
158
  it('should handle empty config', async () => {
@@ -142,12 +160,16 @@ describe('GroupContextInjector', () => {
142
160
  enabled: true,
143
161
  });
144
162
 
145
- const input: any[] = [{ content: 'Base prompt.', role: 'system' }];
163
+ const input: any[] = [
164
+ { content: 'Base prompt.', role: 'system' },
165
+ { content: 'Hello', role: 'user' },
166
+ ];
146
167
 
147
168
  const context = createContext(input);
148
169
  const result = await injector.process(context);
149
170
 
150
- expect(result.messages[0].content).toMatchSnapshot();
171
+ // Check injected message content
172
+ expect(result.messages[1].content).toMatchSnapshot();
151
173
  });
152
174
  });
153
175
 
@@ -158,15 +180,19 @@ describe('GroupContextInjector', () => {
158
180
  // Minimal config
159
181
  });
160
182
 
161
- const input: any[] = [{ role: 'system', content: 'Base prompt.' }];
183
+ const input: any[] = [
184
+ { role: 'system', content: 'Base prompt.' },
185
+ { role: 'user', content: 'Hello' },
186
+ ];
162
187
 
163
188
  const context = createContext(input);
164
189
  const result = await injector.process(context);
165
190
 
166
- const systemContent = result.messages[0].content;
191
+ // Check injected message content
192
+ const injectedContent = result.messages[1].content;
167
193
 
168
194
  // Even with minimal config, identity rules should be present
169
- expect(systemContent).toMatchSnapshot();
195
+ expect(injectedContent).toMatchSnapshot();
170
196
  });
171
197
  });
172
198
 
@@ -179,12 +205,16 @@ describe('GroupContextInjector', () => {
179
205
  systemPrompt: 'Empty group description',
180
206
  });
181
207
 
182
- const input: any[] = [{ content: 'Prompt.', role: 'system' }];
208
+ const input: any[] = [
209
+ { content: 'Prompt.', role: 'system' },
210
+ { content: 'Hello', role: 'user' },
211
+ ];
183
212
 
184
213
  const context = createContext(input);
185
214
  const result = await injector.process(context);
186
215
 
187
- expect(result.messages[0].content).toMatchSnapshot();
216
+ // Check injected message content
217
+ expect(result.messages[1].content).toMatchSnapshot();
188
218
  });
189
219
 
190
220
  it('should preserve other messages unchanged', async () => {
@@ -204,10 +234,16 @@ describe('GroupContextInjector', () => {
204
234
  const context = createContext(input);
205
235
  const result = await injector.process(context);
206
236
 
207
- // Only system message should be modified
208
- expect(result.messages[0].content).toContain('<group_context>');
209
- expect(result.messages[1].content).toBe('User message.');
210
- expect(result.messages[2].content).toBe('Assistant response.');
237
+ // System message should be unchanged
238
+ expect(result.messages[0].content).toBe('System prompt.');
239
+
240
+ // Injected message should be second
241
+ expect(result.messages[1].role).toBe('user');
242
+ expect(result.messages[1].content).toContain('<group_context>');
243
+
244
+ // Original messages should be preserved
245
+ expect(result.messages[2].content).toBe('User message.');
246
+ expect(result.messages[3].content).toBe('Assistant response.');
211
247
  });
212
248
  });
213
249
  });