@lobehub/lobehub 2.0.0-next.35 → 2.0.0-next.37

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 (156) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
  29. package/packages/database/src/models/message.ts +18 -19
  30. package/packages/types/src/aiChat.ts +2 -0
  31. package/packages/types/src/importer.ts +2 -2
  32. package/packages/types/src/message/ui/chat.ts +17 -1
  33. package/packages/types/src/message/ui/extra.ts +2 -2
  34. package/packages/types/src/message/ui/params.ts +2 -2
  35. package/packages/types/src/user/preference.ts +0 -4
  36. package/packages/utils/src/tokenizer/index.ts +3 -11
  37. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  38. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
  40. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
  41. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
  42. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  43. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  44. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  45. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  46. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  47. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  48. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  49. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  50. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  51. package/src/features/Conversation/Error/index.tsx +0 -5
  52. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  53. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  54. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  55. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  56. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  59. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  60. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  61. package/src/features/Conversation/Messages/Default.tsx +1 -0
  62. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  63. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  64. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  65. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  66. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  67. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  68. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  69. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  71. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  72. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  73. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  74. package/src/features/Conversation/Messages/index.tsx +3 -3
  75. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  76. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  77. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  78. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  80. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  81. package/src/hooks/useHotkeys/chatScope.ts +15 -7
  82. package/src/libs/trpc/client/lambda.ts +4 -3
  83. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  84. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  85. package/src/server/routers/lambda/aiChat.ts +3 -2
  86. package/src/server/routers/lambda/message.ts +8 -16
  87. package/src/server/services/message/__tests__/index.test.ts +29 -39
  88. package/src/server/services/message/index.ts +41 -36
  89. package/src/services/electron/desktopNotification.ts +6 -6
  90. package/src/services/electron/file.ts +6 -6
  91. package/src/services/file/ClientS3/index.ts +8 -8
  92. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  93. package/src/services/message/index.ts +21 -15
  94. package/src/services/upload.ts +11 -11
  95. package/src/services/utils/abortableRequest.test.ts +161 -0
  96. package/src/services/utils/abortableRequest.ts +67 -0
  97. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  98. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  99. package/src/store/chat/helpers.test.ts +0 -99
  100. package/src/store/chat/helpers.ts +0 -11
  101. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  102. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  103. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  104. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  105. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  106. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  107. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  108. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  109. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  110. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  111. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  112. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  113. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  114. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  115. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  116. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  117. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  118. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  119. package/src/store/chat/slices/message/action.test.ts +79 -68
  120. package/src/store/chat/slices/message/actions/index.ts +39 -0
  121. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  122. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  123. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  124. package/src/store/chat/slices/message/actions/query.ts +120 -0
  125. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  126. package/src/store/chat/slices/message/initialState.ts +13 -0
  127. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  128. package/src/store/chat/slices/message/reducer.ts +17 -81
  129. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  130. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  131. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  132. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  133. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  134. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  135. package/src/store/chat/slices/plugin/action.ts +34 -28
  136. package/src/store/chat/slices/thread/action.test.ts +28 -31
  137. package/src/store/chat/slices/thread/action.ts +13 -10
  138. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  139. package/src/store/chat/slices/topic/reducer.ts +11 -3
  140. package/src/store/chat/store.ts +1 -1
  141. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  142. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  143. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  144. package/packages/database/src/utils/groupMessages.ts +0 -361
  145. package/packages/utils/src/tokenizer/client.ts +0 -35
  146. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  147. package/packages/utils/src/tokenizer/server.ts +0 -11
  148. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  149. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  150. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  151. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  152. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  153. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  154. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  155. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  156. package/src/store/chat/slices/message/action.ts +0 -629
@@ -68,13 +68,13 @@ class UploadService {
68
68
  const state = getElectronStoreState();
69
69
  const isSyncActive = electronSyncSelectors.isSyncActive(state);
70
70
 
71
- // 桌面端上传逻辑(并且没开启 sync 同步)
71
+ // Desktop upload logic (when sync is not enabled)
72
72
  if (isDesktop && !isSyncActive) {
73
73
  const data = await this.uploadToDesktopS3(file, { directory, pathname });
74
74
  return { data, success: true };
75
75
  }
76
76
 
77
- // 服务端上传逻辑
77
+ // Server-side upload logic
78
78
  if (isServerMode) {
79
79
  // if is server mode, upload to server s3,
80
80
 
@@ -83,7 +83,7 @@ class UploadService {
83
83
  }
84
84
 
85
85
  // upload to client s3
86
- // 客户端上传逻辑
86
+ // Client-side upload logic
87
87
  if (!skipCheckFileType && !file.type.startsWith('image') && !file.type.startsWith('video')) {
88
88
  onNotSupported?.();
89
89
  return { data: undefined as unknown as FileMetadata, success: false };
@@ -103,18 +103,18 @@ class UploadService {
103
103
  base64Data: string,
104
104
  options: UploadFileToS3Options = {},
105
105
  ): Promise<UploadBase64ToS3Result> => {
106
- // 解析 base64 数据
106
+ // Parse base64 data
107
107
  const { base64, mimeType, type } = parseDataUri(base64Data);
108
108
 
109
109
  if (!base64 || !mimeType || type !== 'base64') {
110
110
  throw new Error('Invalid base64 data for image');
111
111
  }
112
112
 
113
- // base64 转换为 Blob
113
+ // Convert base64 to Blob
114
114
  const byteCharacters = atob(base64);
115
115
  const byteArrays = [];
116
116
 
117
- // 分块处理以避免内存问题
117
+ // Process in chunks to avoid memory issues
118
118
  for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
119
119
  const slice = byteCharacters.slice(offset, offset + 1024);
120
120
 
@@ -129,14 +129,14 @@ class UploadService {
129
129
 
130
130
  const blob = new Blob(byteArrays, { type: mimeType });
131
131
 
132
- // 确定文件扩展名
132
+ // Determine file extension
133
133
  const fileExtension = mimeType.split('/')[1] || 'png';
134
134
  const fileName = `${options.filename || `image_${dayjs().format('YYYY-MM-DD-hh-mm-ss')}`}.${fileExtension}`;
135
135
 
136
- // 创建文件对象
136
+ // Create file object
137
137
  const file = new File([blob], fileName, { type: mimeType });
138
138
 
139
- // 使用统一的上传方法
139
+ // Use unified upload method
140
140
  const { data: metadata } = await this.uploadFileToS3(file, options);
141
141
  const hash = sha256(await file.arrayBuffer());
142
142
 
@@ -221,7 +221,7 @@ class UploadService {
221
221
  const fileArrayBuffer = await file.arrayBuffer();
222
222
  const hash = sha256(fileArrayBuffer);
223
223
 
224
- // 生成文件路径元数据
224
+ // Generate file path metadata
225
225
  const { pathname } = generateFilePathMetadata(file.name, options);
226
226
 
227
227
  const { desktopFileAPI } = await import('@/services/electron/file');
@@ -261,7 +261,7 @@ class UploadService {
261
261
  preSignUrl: string;
262
262
  }
263
263
  > => {
264
- // 生成文件路径元数据
264
+ // Generate file path metadata
265
265
  const { date, dirname, filename, pathname } = generateFilePathMetadata(file.name, options);
266
266
 
267
267
  const preSignUrl = await lambdaClient.upload.createS3PreSignedUrl.mutate({ pathname });
@@ -0,0 +1,161 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { abortableRequest } from './abortableRequest';
4
+
5
+ describe('AbortableRequestManager', () => {
6
+ beforeEach(() => {
7
+ abortableRequest.cancelAll();
8
+ });
9
+
10
+ describe('execute', () => {
11
+ it('should execute request successfully', async () => {
12
+ const mockFetcher = vi.fn(async (signal: AbortSignal) => {
13
+ return 'result';
14
+ });
15
+
16
+ const result = await abortableRequest.execute('test-key', mockFetcher);
17
+
18
+ expect(result).toBe('result');
19
+ expect(mockFetcher).toHaveBeenCalledTimes(1);
20
+ expect(mockFetcher).toHaveBeenCalledWith(expect.any(AbortSignal));
21
+ });
22
+
23
+ it('should cancel previous request when new request with same key is triggered', async () => {
24
+ let firstRequestAborted = false;
25
+ let secondRequestAborted = false;
26
+
27
+ const firstFetcher = vi.fn(
28
+ async (signal: AbortSignal) =>
29
+ new Promise((resolve, reject) => {
30
+ signal.addEventListener('abort', () => {
31
+ firstRequestAborted = true;
32
+ reject(new Error('Aborted'));
33
+ });
34
+ setTimeout(() => resolve('first'), 100);
35
+ }),
36
+ );
37
+
38
+ const secondFetcher = vi.fn(
39
+ async (signal: AbortSignal) =>
40
+ new Promise((resolve, reject) => {
41
+ signal.addEventListener('abort', () => {
42
+ secondRequestAborted = true;
43
+ reject(new Error('Aborted'));
44
+ });
45
+ setTimeout(() => resolve('second'), 100);
46
+ }),
47
+ );
48
+
49
+ // Start first request
50
+ const firstPromise = abortableRequest.execute('same-key', firstFetcher);
51
+
52
+ // Start second request with same key (should cancel first)
53
+ await new Promise((resolve) => setTimeout(resolve, 10));
54
+ const secondPromise = abortableRequest.execute('same-key', secondFetcher);
55
+
56
+ // First should be aborted
57
+ await expect(firstPromise).rejects.toThrow('Aborted');
58
+ expect(firstRequestAborted).toBe(true);
59
+
60
+ // Second should succeed
61
+ const result = await secondPromise;
62
+ expect(result).toBe('second');
63
+ expect(secondRequestAborted).toBe(false);
64
+ });
65
+
66
+ it('should allow concurrent requests with different keys', async () => {
67
+ const fetcher1 = vi.fn(async () => 'result1');
68
+ const fetcher2 = vi.fn(async () => 'result2');
69
+
70
+ const [result1, result2] = await Promise.all([
71
+ abortableRequest.execute('key1', fetcher1),
72
+ abortableRequest.execute('key2', fetcher2),
73
+ ]);
74
+
75
+ expect(result1).toBe('result1');
76
+ expect(result2).toBe('result2');
77
+ expect(fetcher1).toHaveBeenCalledTimes(1);
78
+ expect(fetcher2).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ it('should clean up controller after request completes', async () => {
82
+ const fetcher = vi.fn(async () => 'result');
83
+
84
+ await abortableRequest.execute('cleanup-test', fetcher);
85
+
86
+ // Manually check that controller is cleaned up by starting a new request
87
+ // and verifying it doesn't abort anything (since map should be empty)
88
+ let aborted = false;
89
+ const fetcher2 = vi.fn(
90
+ async (signal: AbortSignal) =>
91
+ new Promise((resolve) => {
92
+ signal.addEventListener('abort', () => {
93
+ aborted = true;
94
+ });
95
+ setTimeout(() => resolve('result2'), 50);
96
+ }),
97
+ );
98
+
99
+ await abortableRequest.execute('cleanup-test', fetcher2);
100
+ expect(aborted).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('cancel', () => {
105
+ it('should cancel specific request by key', async () => {
106
+ let aborted = false;
107
+ const fetcher = vi.fn(
108
+ async (signal: AbortSignal) =>
109
+ new Promise((resolve, reject) => {
110
+ signal.addEventListener('abort', () => {
111
+ aborted = true;
112
+ reject(new Error('Cancelled'));
113
+ });
114
+ setTimeout(() => resolve('result'), 100);
115
+ }),
116
+ );
117
+
118
+ const promise = abortableRequest.execute('cancel-key', fetcher);
119
+
120
+ await new Promise((resolve) => setTimeout(resolve, 10));
121
+ abortableRequest.cancel('cancel-key');
122
+
123
+ await expect(promise).rejects.toThrow('Cancelled');
124
+ expect(aborted).toBe(true);
125
+ });
126
+
127
+ it('should do nothing when canceling non-existent key', () => {
128
+ expect(() => abortableRequest.cancel('non-existent')).not.toThrow();
129
+ });
130
+ });
131
+
132
+ describe('cancelAll', () => {
133
+ it('should cancel all pending requests', async () => {
134
+ const results = { req1: false, req2: false, req3: false };
135
+
136
+ const createFetcher = (key: keyof typeof results) =>
137
+ vi.fn(
138
+ async (signal: AbortSignal) =>
139
+ new Promise((resolve, reject) => {
140
+ signal.addEventListener('abort', () => {
141
+ results[key] = true;
142
+ reject(new Error('Cancelled'));
143
+ });
144
+ setTimeout(() => resolve(`result-${key}`), 100);
145
+ }),
146
+ );
147
+
148
+ const promise1 = abortableRequest.execute('key1', createFetcher('req1'));
149
+ const promise2 = abortableRequest.execute('key2', createFetcher('req2'));
150
+ const promise3 = abortableRequest.execute('key3', createFetcher('req3'));
151
+
152
+ await new Promise((resolve) => setTimeout(resolve, 10));
153
+ abortableRequest.cancelAll();
154
+
155
+ await expect(Promise.all([promise1, promise2, promise3])).rejects.toThrow();
156
+ expect(results.req1).toBe(true);
157
+ expect(results.req2).toBe(true);
158
+ expect(results.req3).toBe(true);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Abortable Request Manager
3
+ *
4
+ * Provides race condition control for async requests by canceling previous
5
+ * requests when a new one with the same key is triggered.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const result = await abortableRequest.execute(
10
+ * 'update-user-profile',
11
+ * (signal) => api.updateProfile(data, { signal })
12
+ * );
13
+ * ```
14
+ */
15
+ class AbortableRequestManager {
16
+ private controllers = new Map<string, AbortController>();
17
+
18
+ /**
19
+ * Execute a request with race condition control
20
+ * @param key - Unique key to identify the request group
21
+ * @param fetcher - Request function that accepts AbortSignal
22
+ * @returns Promise with the request result
23
+ */
24
+ async execute<T>(key: string, fetcher: (signal: AbortSignal) => Promise<T>): Promise<T> {
25
+ // Cancel previous request with same key
26
+ const existingController = this.controllers.get(key);
27
+ if (existingController) {
28
+ existingController.abort('New request triggered');
29
+ }
30
+
31
+ const controller = new AbortController();
32
+ this.controllers.set(key, controller);
33
+
34
+ try {
35
+ return await fetcher(controller.signal);
36
+ } finally {
37
+ // Clean up controller if it's still the active one
38
+ if (this.controllers.get(key) === controller) {
39
+ this.controllers.delete(key);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Manually cancel a request by key
46
+ * @param key - Request key to cancel
47
+ */
48
+ cancel(key: string): void {
49
+ const controller = this.controllers.get(key);
50
+ if (controller) {
51
+ controller.abort('Manually cancelled');
52
+ this.controllers.delete(key);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Cancel all pending requests
58
+ */
59
+ cancelAll(): void {
60
+ for (const controller of this.controllers.values()) {
61
+ controller.abort('All requests cancelled');
62
+ }
63
+ this.controllers.clear();
64
+ }
65
+ }
66
+
67
+ export const abortableRequest = new AbortableRequestManager();
@@ -0,0 +1,137 @@
1
+ import {
2
+ Agent,
3
+ AgentInstruction,
4
+ AgentRuntimeContext,
5
+ AgentState,
6
+ GeneralAgentCallLLMInstructionPayload,
7
+ GeneralAgentCallLLMResultPayload,
8
+ GeneralAgentCallToolResultPayload,
9
+ GeneralAgentCallToolsBatchInstructionPayload,
10
+ GeneralAgentCallingToolInstructionPayload,
11
+ GeneralAgentConfig,
12
+ } from '@lobechat/agent-runtime';
13
+
14
+ /**
15
+ * ChatAgent - The "Brain" of the chat agent
16
+ *
17
+ * This agent implements a simple but powerful decision loop:
18
+ * 1. user_input → call_llm (with optional RAG/Search preprocessing)
19
+ * 2. llm_result → check for tool_calls
20
+ * - If has tool_calls → call_tools_batch (parallel execution)
21
+ * - If no tool_calls → finish
22
+ * 3. tools_batch_result → call_llm (process tool results)
23
+ *
24
+ * Note: RAG and Search workflow preprocessing are handled externally
25
+ * before creating the agent runtime, keeping the agent logic simple.
26
+ */
27
+ export class GeneralChatAgent implements Agent {
28
+ private config: GeneralAgentConfig;
29
+
30
+ constructor(config: GeneralAgentConfig) {
31
+ this.config = config;
32
+ }
33
+
34
+ async runner(
35
+ context: AgentRuntimeContext,
36
+ state: AgentState,
37
+ ): Promise<AgentInstruction | AgentInstruction[]> {
38
+ switch (context.phase) {
39
+ case 'init':
40
+ case 'user_input': {
41
+ // User input received, call LLM to generate response
42
+ // At this point, messages may have been preprocessed with RAG/Search
43
+ return {
44
+ payload: {
45
+ ...(context.payload as any),
46
+ messages: state.messages,
47
+ } as GeneralAgentCallLLMInstructionPayload,
48
+ type: 'call_llm',
49
+ };
50
+ }
51
+
52
+ case 'llm_result': {
53
+ // LLM response received, check if it contains tool calls
54
+ const { hasToolsCalling, toolsCalling, parentMessageId } =
55
+ context.payload as GeneralAgentCallLLMResultPayload;
56
+
57
+ if (hasToolsCalling && toolsCalling && toolsCalling.length > 0) {
58
+ // No intervention needed, proceed with tool execution
59
+ // Use batch execution for multiple tool calls to improve performance
60
+ if (toolsCalling.length > 1) {
61
+ return {
62
+ payload: {
63
+ parentMessageId,
64
+ toolsCalling,
65
+ } as GeneralAgentCallToolsBatchInstructionPayload,
66
+ type: 'call_tools_batch',
67
+ };
68
+ } else if (toolsCalling.length === 1) {
69
+ // Single tool executes directly
70
+ return {
71
+ payload: {
72
+ parentMessageId,
73
+ toolCalling: toolsCalling[0],
74
+ } as GeneralAgentCallingToolInstructionPayload,
75
+ type: 'call_tool',
76
+ };
77
+ }
78
+ }
79
+
80
+ // No tool calls, conversation is complete
81
+ return {
82
+ reason: 'completed',
83
+ reasonDetail: 'LLM response completed without tool calls',
84
+ type: 'finish',
85
+ };
86
+ }
87
+
88
+ case 'tool_result': {
89
+ const { parentMessageId } = context.payload as GeneralAgentCallToolResultPayload;
90
+
91
+ return {
92
+ payload: {
93
+ messages: state.messages,
94
+ model: this.config.modelRuntimeConfig?.model,
95
+ parentMessageId,
96
+ provider: this.config.modelRuntimeConfig?.provider,
97
+ tools: state.tools,
98
+ } as GeneralAgentCallLLMInstructionPayload,
99
+ type: 'call_llm',
100
+ };
101
+ }
102
+
103
+ case 'tools_batch_result': {
104
+ const { parentMessageId } = context.payload as GeneralAgentCallToolResultPayload;
105
+ return {
106
+ payload: {
107
+ messages: state.messages,
108
+ model: this.config.modelRuntimeConfig?.model,
109
+ parentMessageId,
110
+ provider: this.config.modelRuntimeConfig?.provider,
111
+ tools: state.tools,
112
+ } as GeneralAgentCallLLMInstructionPayload,
113
+ type: 'call_llm',
114
+ };
115
+ }
116
+
117
+ case 'error': {
118
+ // Error occurred, finish execution
119
+ const { error } = context.payload as { error: any };
120
+ return {
121
+ reason: 'error_recovery',
122
+ reasonDetail: error?.message || 'Unknown error occurred',
123
+ type: 'finish',
124
+ };
125
+ }
126
+
127
+ default: {
128
+ // Unknown phase, finish execution
129
+ return {
130
+ reason: 'agent_decision',
131
+ reasonDetail: `Unknown phase: ${context.phase}`,
132
+ type: 'finish',
133
+ };
134
+ }
135
+ }
136
+ }
137
+ }