@lobehub/lobehub 2.0.0-next.326 → 2.0.0-next.328
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/locales/en-US/chat.json +6 -1
- package/locales/zh-CN/chat.json +5 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
- package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
- package/packages/agent-runtime/src/types/instruction.ts +46 -2
- package/packages/builtin-tool-gtd/src/const.ts +1 -0
- package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
- package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
- package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
- package/packages/builtin-tool-gtd/src/types.ts +55 -33
- package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
- package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
- package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
- package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
- package/packages/database/src/models/message.ts +8 -1
- package/packages/database/src/models/thread.ts +1 -1
- package/packages/types/src/message/ui/chat.ts +2 -0
- package/packages/types/src/topic/thread.ts +20 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx +6 -6
- package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
- package/src/features/Conversation/Messages/Task/index.tsx +11 -6
- package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
- package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
- package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
- package/src/features/Conversation/components/Thinking/index.tsx +9 -30
- package/src/features/Conversation/store/slices/data/action.ts +2 -3
- package/src/features/NavPanel/components/BackButton.tsx +10 -13
- package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
- package/src/hooks/useAutoScroll.ts +117 -0
- package/src/locales/default/chat.ts +6 -1
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/services/message/__tests__/index.test.ts +37 -0
- package/src/server/services/message/index.ts +6 -1
- package/src/services/aiAgent.ts +51 -0
- package/src/store/chat/agents/createAgentExecutors.ts +714 -12
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
- package/src/store/chat/slices/message/actions/query.ts +33 -1
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
- package/src/store/chat/slices/operation/types.ts +4 -0
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { ActionIcon, type ActionIconProps } from '@lobehub/ui';
|
|
2
2
|
import { ChevronLeftIcon } from 'lucide-react';
|
|
3
3
|
import { memo } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Link } from 'react-router-dom';
|
|
5
5
|
|
|
6
6
|
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
7
7
|
|
|
8
8
|
export const BACK_BUTTON_ID = 'lobe-back-button';
|
|
9
9
|
|
|
10
10
|
const BackButton = memo<ActionIconProps & { to?: string }>(({ to = '/', onClick, ...rest }) => {
|
|
11
|
-
const navigate = useNavigate();
|
|
12
|
-
|
|
13
11
|
return (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/>
|
|
12
|
+
// @ts-expect-error
|
|
13
|
+
<Link onClick={onClick} to={to}>
|
|
14
|
+
<ActionIcon
|
|
15
|
+
icon={ChevronLeftIcon}
|
|
16
|
+
id={BACK_BUTTON_ID}
|
|
17
|
+
size={DESKTOP_HEADER_ICON_SIZE}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
20
|
+
</Link>
|
|
24
21
|
);
|
|
25
22
|
});
|
|
26
23
|
|
|
@@ -13,6 +13,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|
|
13
13
|
import { isMacOS } from '@/utils/platform';
|
|
14
14
|
|
|
15
15
|
import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
|
|
16
|
+
import { BACK_BUTTON_ID } from './BackButton';
|
|
16
17
|
|
|
17
18
|
const motionVariants = {
|
|
18
19
|
animate: { opacity: 1, x: 0 },
|
|
@@ -76,6 +77,9 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
76
77
|
opacity,
|
|
77
78
|
width 0.2s ${cssVar.motionEaseOut};
|
|
78
79
|
}
|
|
80
|
+
#${BACK_BUTTON_ID} {
|
|
81
|
+
width: 24px !important;
|
|
82
|
+
}
|
|
79
83
|
|
|
80
84
|
&:hover {
|
|
81
85
|
#${TOGGLE_BUTTON_ID} {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseAutoScrollOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Dependencies that trigger auto-scroll when changed
|
|
6
|
+
*/
|
|
7
|
+
deps?: unknown[];
|
|
8
|
+
/**
|
|
9
|
+
* Whether auto-scroll is enabled (e.g., only when streaming/executing)
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Distance threshold from bottom to consider "near bottom" (in pixels)
|
|
15
|
+
* @default 20
|
|
16
|
+
*/
|
|
17
|
+
threshold?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseAutoScrollReturn<T extends HTMLElement> {
|
|
21
|
+
/**
|
|
22
|
+
* Callback to handle scroll events, attach to onScroll
|
|
23
|
+
*/
|
|
24
|
+
handleScroll: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Ref to attach to the scrollable container
|
|
27
|
+
*/
|
|
28
|
+
ref: RefObject<T | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Reset the scroll lock state (e.g., when new content starts)
|
|
31
|
+
*/
|
|
32
|
+
resetScrollLock: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Whether user has scrolled away from bottom (scroll lock active)
|
|
35
|
+
*/
|
|
36
|
+
userHasScrolled: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook for auto-scrolling content with user scroll detection
|
|
41
|
+
*
|
|
42
|
+
* Features:
|
|
43
|
+
* - Auto-scrolls to bottom when dependencies change
|
|
44
|
+
* - Detects when user scrolls away from bottom and stops auto-scrolling
|
|
45
|
+
* - Provides reset function for when new content starts
|
|
46
|
+
* - Ignores scroll events triggered by auto-scroll itself
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
|
|
51
|
+
* deps: [content],
|
|
52
|
+
* enabled: isStreaming,
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* return (
|
|
56
|
+
* <ScrollShadow ref={ref} onScroll={handleScroll}>
|
|
57
|
+
* {content}
|
|
58
|
+
* </ScrollShadow>
|
|
59
|
+
* );
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useAutoScroll<T extends HTMLElement = HTMLDivElement>(
|
|
63
|
+
options: UseAutoScrollOptions = {},
|
|
64
|
+
): UseAutoScrollReturn<T> {
|
|
65
|
+
const { deps = [], enabled = true, threshold = 20 } = options;
|
|
66
|
+
|
|
67
|
+
const ref = useRef<T | null>(null);
|
|
68
|
+
const [userHasScrolled, setUserHasScrolled] = useState(false);
|
|
69
|
+
const isAutoScrollingRef = useRef(false);
|
|
70
|
+
|
|
71
|
+
// Handle user scroll detection
|
|
72
|
+
const handleScroll = useCallback(() => {
|
|
73
|
+
// Ignore scroll events triggered by auto-scroll
|
|
74
|
+
if (isAutoScrollingRef.current) return;
|
|
75
|
+
|
|
76
|
+
const container = ref.current;
|
|
77
|
+
if (!container) return;
|
|
78
|
+
|
|
79
|
+
// Check if user scrolled away from bottom
|
|
80
|
+
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
81
|
+
const isAtBottom = distanceToBottom < threshold;
|
|
82
|
+
|
|
83
|
+
// If user scrolled up, stop auto-scrolling
|
|
84
|
+
if (!isAtBottom) {
|
|
85
|
+
setUserHasScrolled(true);
|
|
86
|
+
}
|
|
87
|
+
}, [threshold]);
|
|
88
|
+
|
|
89
|
+
// Reset scroll lock state
|
|
90
|
+
const resetScrollLock = useCallback(() => {
|
|
91
|
+
setUserHasScrolled(false);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
// Auto scroll to bottom when deps change (unless user has scrolled or disabled)
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!enabled || userHasScrolled) return;
|
|
97
|
+
|
|
98
|
+
const container = ref.current;
|
|
99
|
+
if (!container) return;
|
|
100
|
+
|
|
101
|
+
isAutoScrollingRef.current = true;
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
container.scrollTop = container.scrollHeight;
|
|
104
|
+
// Reset the flag after scroll completes
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
isAutoScrollingRef.current = false;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}, [enabled, userHasScrolled, ...deps]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
handleScroll,
|
|
113
|
+
ref,
|
|
114
|
+
resetScrollLock,
|
|
115
|
+
userHasScrolled,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -230,6 +230,7 @@ export default {
|
|
|
230
230
|
'noSelectedAgents': 'No members selected yet',
|
|
231
231
|
'openInNewWindow': 'Open in New Window',
|
|
232
232
|
'operation.execAgentRuntime': 'Preparing response',
|
|
233
|
+
'operation.execClientTask': 'Executing task',
|
|
233
234
|
'operation.sendMessage': 'Sending message',
|
|
234
235
|
'owner': 'Group owner',
|
|
235
236
|
'pageCopilot.title': 'Page Agent',
|
|
@@ -354,13 +355,17 @@ export default {
|
|
|
354
355
|
'tab.profile': 'Agent Profile',
|
|
355
356
|
'tab.search': 'Search',
|
|
356
357
|
'task.activity.calling': 'Calling Skill...',
|
|
358
|
+
'task.activity.clientExecuting': 'Executing locally...',
|
|
357
359
|
'task.activity.generating': 'Generating response...',
|
|
358
360
|
'task.activity.gotResult': 'Tool result received',
|
|
359
361
|
'task.activity.toolCalling': 'Calling {{toolName}}...',
|
|
360
362
|
'task.activity.toolResult': '{{toolName}} result received',
|
|
361
363
|
'task.batchTasks': '{{count}} Batch Subtasks',
|
|
364
|
+
'task.instruction': 'Task Instruction',
|
|
365
|
+
'task.intermediateSteps': '{{count}} intermediate steps',
|
|
366
|
+
'task.metrics.duration': '(took {{duration}})',
|
|
362
367
|
'task.metrics.stepsShort': 'steps',
|
|
363
|
-
'task.metrics.toolCallsShort': '
|
|
368
|
+
'task.metrics.toolCallsShort': 'skill uses',
|
|
364
369
|
'task.status.cancelled': 'Task Cancelled',
|
|
365
370
|
'task.status.failed': 'Task Failed',
|
|
366
371
|
'task.status.initializing': 'Initializing task...',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type AgentRuntimeContext } from '@lobechat/agent-runtime';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type TaskCurrentActivity,
|
|
4
|
+
type TaskStatusResult,
|
|
5
|
+
ThreadStatus,
|
|
6
|
+
ThreadType,
|
|
7
|
+
} from '@lobechat/types';
|
|
3
8
|
import { TRPCError } from '@trpc/server';
|
|
4
9
|
import debug from 'debug';
|
|
5
10
|
import pMap from 'p-map';
|
|
@@ -154,6 +159,49 @@ const ExecSubAgentTaskSchema = z.object({
|
|
|
154
159
|
topicId: z.string(),
|
|
155
160
|
});
|
|
156
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Schema for createClientTaskThread - create Thread for client-side task execution
|
|
164
|
+
* This is used when runInClient=true on desktop client
|
|
165
|
+
*/
|
|
166
|
+
const CreateClientTaskThreadSchema = z.object({
|
|
167
|
+
/** The Agent ID to execute the task */
|
|
168
|
+
agentId: z.string(),
|
|
169
|
+
/** The Group ID (optional, only for Group mode) */
|
|
170
|
+
groupId: z.string().optional(),
|
|
171
|
+
/** Initial user message content (task instruction) */
|
|
172
|
+
instruction: z.string(),
|
|
173
|
+
/** The parent message ID (task message) */
|
|
174
|
+
parentMessageId: z.string(),
|
|
175
|
+
/** Task title (shown in UI, used as thread title) */
|
|
176
|
+
title: z.string().optional(),
|
|
177
|
+
/** The Topic ID */
|
|
178
|
+
topicId: z.string(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Schema for updateClientTaskThreadStatus - update Thread status after client-side execution
|
|
183
|
+
*/
|
|
184
|
+
const UpdateClientTaskThreadStatusSchema = z.object({
|
|
185
|
+
/** Completion reason */
|
|
186
|
+
completionReason: z.enum(['done', 'error', 'interrupted']),
|
|
187
|
+
/** Error message if failed */
|
|
188
|
+
error: z.string().optional(),
|
|
189
|
+
/** Thread metadata to update */
|
|
190
|
+
metadata: z
|
|
191
|
+
.object({
|
|
192
|
+
totalCost: z.number().optional(),
|
|
193
|
+
totalMessages: z.number().optional(),
|
|
194
|
+
totalSteps: z.number().optional(),
|
|
195
|
+
totalTokens: z.number().optional(),
|
|
196
|
+
totalToolCalls: z.number().optional(),
|
|
197
|
+
})
|
|
198
|
+
.optional(),
|
|
199
|
+
/** Result content (last assistant message) */
|
|
200
|
+
resultContent: z.string().optional(),
|
|
201
|
+
/** The Thread ID */
|
|
202
|
+
threadId: z.string(),
|
|
203
|
+
});
|
|
204
|
+
|
|
157
205
|
/**
|
|
158
206
|
* Schema for interruptTask - interrupt a running task
|
|
159
207
|
*/
|
|
@@ -184,6 +232,100 @@ const aiAgentProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
184
232
|
});
|
|
185
233
|
|
|
186
234
|
export const aiAgentRouter = router({
|
|
235
|
+
/**
|
|
236
|
+
* Create Thread for client-side task execution
|
|
237
|
+
*
|
|
238
|
+
* This endpoint is called by desktop client when runInClient=true.
|
|
239
|
+
* It creates the Thread but does NOT execute the task - execution happens on client side.
|
|
240
|
+
*/
|
|
241
|
+
createClientTaskThread: aiAgentProcedure
|
|
242
|
+
.input(CreateClientTaskThreadSchema)
|
|
243
|
+
.mutation(async ({ input, ctx }) => {
|
|
244
|
+
const { agentId, groupId, instruction, parentMessageId, title, topicId } = input;
|
|
245
|
+
|
|
246
|
+
log('createClientTaskThread: agentId=%s, groupId=%s', agentId, groupId);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// 1. Create Thread for isolated task execution
|
|
250
|
+
const startedAt = new Date().toISOString();
|
|
251
|
+
const thread = await ctx.threadModel.create({
|
|
252
|
+
agentId,
|
|
253
|
+
groupId,
|
|
254
|
+
metadata: { clientMode: true, startedAt },
|
|
255
|
+
sourceMessageId: parentMessageId,
|
|
256
|
+
status: ThreadStatus.Processing,
|
|
257
|
+
title,
|
|
258
|
+
topicId,
|
|
259
|
+
type: ThreadType.Isolation,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!thread) {
|
|
263
|
+
throw new TRPCError({
|
|
264
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
265
|
+
message: 'Failed to create thread for task execution',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
log('createClientTaskThread: created thread %s', thread.id);
|
|
270
|
+
|
|
271
|
+
// 2. Create initial user message (persisted to database)
|
|
272
|
+
const userMessage = await ctx.messageModel.create({
|
|
273
|
+
agentId,
|
|
274
|
+
content: instruction,
|
|
275
|
+
parentId: parentMessageId,
|
|
276
|
+
role: 'user',
|
|
277
|
+
threadId: thread.id,
|
|
278
|
+
topicId,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
log('createClientTaskThread: created user message %s', userMessage.id);
|
|
282
|
+
|
|
283
|
+
// 3. Query thread messages and main chat messages in parallel
|
|
284
|
+
const [threadMessages, messages] = await Promise.all([
|
|
285
|
+
// Thread messages (messages within this thread)
|
|
286
|
+
ctx.messageModel.query({
|
|
287
|
+
agentId,
|
|
288
|
+
threadId: thread.id,
|
|
289
|
+
topicId,
|
|
290
|
+
}),
|
|
291
|
+
// Main chat messages (messages without threadId, includes updated taskDetail)
|
|
292
|
+
ctx.messageModel.query({
|
|
293
|
+
agentId,
|
|
294
|
+
topicId,
|
|
295
|
+
// No threadId - matchThread will filter for threadId IS NULL (main chat)
|
|
296
|
+
}),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
log(
|
|
300
|
+
'createClientTaskThread: queried %d thread messages, %d main messages',
|
|
301
|
+
threadMessages.length,
|
|
302
|
+
messages.length,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// 4. Return Thread, userMessageId, threadMessages and messages
|
|
306
|
+
return {
|
|
307
|
+
messages,
|
|
308
|
+
startedAt,
|
|
309
|
+
success: true,
|
|
310
|
+
threadId: thread.id,
|
|
311
|
+
threadMessages,
|
|
312
|
+
userMessageId: userMessage.id,
|
|
313
|
+
};
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
log('createClientTaskThread failed: %O', error);
|
|
316
|
+
|
|
317
|
+
if (error instanceof TRPCError) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw new TRPCError({
|
|
322
|
+
cause: error,
|
|
323
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
324
|
+
message: `Failed to create client task thread: ${error.message}`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}),
|
|
328
|
+
|
|
187
329
|
createOperation: aiAgentProcedure
|
|
188
330
|
.input(CreateAgentOperationSchema)
|
|
189
331
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -846,4 +988,100 @@ export const aiAgentRouter = router({
|
|
|
846
988
|
timestamp: new Date().toISOString(),
|
|
847
989
|
};
|
|
848
990
|
}),
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Update Thread status after client-side task execution completes
|
|
994
|
+
*
|
|
995
|
+
* This endpoint is called by desktop client after task execution finishes.
|
|
996
|
+
* It updates the Thread status and metadata similar to server-side completion.
|
|
997
|
+
*/
|
|
998
|
+
updateClientTaskThreadStatus: aiAgentProcedure
|
|
999
|
+
.input(UpdateClientTaskThreadStatusSchema)
|
|
1000
|
+
.mutation(async ({ input, ctx }) => {
|
|
1001
|
+
const { threadId, completionReason, error, resultContent, metadata } = input;
|
|
1002
|
+
|
|
1003
|
+
log('updateClientTaskThreadStatus: threadId=%s, reason=%s', threadId, completionReason);
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
// Find thread
|
|
1007
|
+
const thread = await ctx.threadModel.findById(threadId);
|
|
1008
|
+
if (!thread) {
|
|
1009
|
+
throw new TRPCError({
|
|
1010
|
+
code: 'NOT_FOUND',
|
|
1011
|
+
message: 'Thread not found',
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const completedAt = new Date().toISOString();
|
|
1016
|
+
const startedAt = thread.metadata?.startedAt;
|
|
1017
|
+
const duration = startedAt ? Date.now() - new Date(startedAt).getTime() : undefined;
|
|
1018
|
+
|
|
1019
|
+
// Determine thread status based on completion reason
|
|
1020
|
+
let status: ThreadStatus;
|
|
1021
|
+
switch (completionReason) {
|
|
1022
|
+
case 'done': {
|
|
1023
|
+
status = ThreadStatus.Completed;
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
case 'error': {
|
|
1027
|
+
status = ThreadStatus.Failed;
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case 'interrupted': {
|
|
1031
|
+
status = ThreadStatus.Cancel;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
default: {
|
|
1035
|
+
status = ThreadStatus.Completed;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Update Thread metadata and status
|
|
1040
|
+
await ctx.threadModel.update(threadId, {
|
|
1041
|
+
metadata: {
|
|
1042
|
+
...thread.metadata,
|
|
1043
|
+
completedAt,
|
|
1044
|
+
duration,
|
|
1045
|
+
error: error || undefined,
|
|
1046
|
+
totalCost: metadata?.totalCost,
|
|
1047
|
+
totalMessages: metadata?.totalMessages,
|
|
1048
|
+
totalSteps: metadata?.totalSteps,
|
|
1049
|
+
totalTokens: metadata?.totalTokens,
|
|
1050
|
+
totalToolCalls: metadata?.totalToolCalls,
|
|
1051
|
+
},
|
|
1052
|
+
status,
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// Update task message (sourceMessageId) with result content if provided
|
|
1056
|
+
if (resultContent && thread.sourceMessageId) {
|
|
1057
|
+
await ctx.messageModel.update(thread.sourceMessageId, {
|
|
1058
|
+
content: resultContent,
|
|
1059
|
+
});
|
|
1060
|
+
log(
|
|
1061
|
+
'updateClientTaskThreadStatus: updated task message %s with result',
|
|
1062
|
+
thread.sourceMessageId,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
log('updateClientTaskThreadStatus: thread %s completed with status %s', threadId, status);
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
status,
|
|
1070
|
+
success: true,
|
|
1071
|
+
threadId,
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error: any) {
|
|
1074
|
+
log('updateClientTaskThreadStatus failed: %O', error);
|
|
1075
|
+
|
|
1076
|
+
if (error instanceof TRPCError) {
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
throw new TRPCError({
|
|
1081
|
+
cause: error,
|
|
1082
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1083
|
+
message: `Failed to update client task thread status: ${error.message}`,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}),
|
|
849
1087
|
});
|
|
@@ -21,6 +21,7 @@ const threadProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
21
21
|
export const threadRouter = router({
|
|
22
22
|
createThread: threadProcedure.input(createThreadSchema).mutation(async ({ input, ctx }) => {
|
|
23
23
|
const thread = await ctx.threadModel.create({
|
|
24
|
+
metadata: input.metadata,
|
|
24
25
|
parentThreadId: input.parentThreadId,
|
|
25
26
|
sourceMessageId: input.sourceMessageId,
|
|
26
27
|
title: input.title,
|
|
@@ -38,6 +39,7 @@ export const threadRouter = router({
|
|
|
38
39
|
)
|
|
39
40
|
.mutation(async ({ input, ctx }) => {
|
|
40
41
|
const thread = await ctx.threadModel.create({
|
|
42
|
+
metadata: input.metadata,
|
|
41
43
|
parentThreadId: input.parentThreadId,
|
|
42
44
|
sourceMessageId: input.sourceMessageId,
|
|
43
45
|
title: input.message.content.slice(0, 20),
|
|
@@ -293,6 +293,7 @@ describe('MessageService', () => {
|
|
|
293
293
|
current: 0,
|
|
294
294
|
groupId: undefined,
|
|
295
295
|
pageSize: 9999,
|
|
296
|
+
threadId: undefined,
|
|
296
297
|
topicId: undefined,
|
|
297
298
|
},
|
|
298
299
|
expect.objectContaining({
|
|
@@ -327,6 +328,42 @@ describe('MessageService', () => {
|
|
|
327
328
|
current: 0,
|
|
328
329
|
groupId: 'group-1',
|
|
329
330
|
pageSize: 9999,
|
|
331
|
+
threadId: undefined,
|
|
332
|
+
topicId: 'topic-1',
|
|
333
|
+
},
|
|
334
|
+
expect.objectContaining({
|
|
335
|
+
postProcessUrl: expect.any(Function),
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
expect(result.id).toBe('msg-1');
|
|
339
|
+
expect(result.messages).toEqual(mockMessages);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should create message with threadId and query thread messages', async () => {
|
|
343
|
+
const params = {
|
|
344
|
+
agentId: 'agent-1',
|
|
345
|
+
content: 'Hello in thread',
|
|
346
|
+
groupId: 'group-1',
|
|
347
|
+
role: 'user' as const,
|
|
348
|
+
threadId: 'thread-1',
|
|
349
|
+
topicId: 'topic-1',
|
|
350
|
+
};
|
|
351
|
+
const createdMessage = { id: 'msg-1', ...params };
|
|
352
|
+
const mockMessages = [createdMessage];
|
|
353
|
+
|
|
354
|
+
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
355
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
356
|
+
|
|
357
|
+
const result = await messageService.createMessage(params as any);
|
|
358
|
+
|
|
359
|
+
expect(mockMessageModel.create).toHaveBeenCalledWith(params);
|
|
360
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
361
|
+
{
|
|
362
|
+
agentId: 'agent-1',
|
|
363
|
+
current: 0,
|
|
364
|
+
groupId: 'group-1',
|
|
365
|
+
pageSize: 9999,
|
|
366
|
+
threadId: 'thread-1',
|
|
330
367
|
topicId: 'topic-1',
|
|
331
368
|
},
|
|
332
369
|
expect.objectContaining({
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type LobeChatDatabase } from '@lobechat/database';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CreateMessageParams,
|
|
4
|
+
type UIChatMessage,
|
|
5
|
+
type UpdateMessageParams,
|
|
6
|
+
} from '@lobechat/types';
|
|
3
7
|
|
|
4
8
|
import { MessageModel } from '@/database/models/message';
|
|
5
9
|
|
|
@@ -95,6 +99,7 @@ export class MessageService {
|
|
|
95
99
|
current: 0,
|
|
96
100
|
groupId: params.groupId,
|
|
97
101
|
pageSize: 9999,
|
|
102
|
+
threadId: params.threadId,
|
|
98
103
|
topicId: params.topicId,
|
|
99
104
|
},
|
|
100
105
|
{
|
package/src/services/aiAgent.ts
CHANGED
|
@@ -40,6 +40,38 @@ export interface InterruptTaskParams {
|
|
|
40
40
|
threadId?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Parameters for createClientTaskThread
|
|
45
|
+
* Creates a Thread for client-side task execution (desktop only)
|
|
46
|
+
*/
|
|
47
|
+
export interface CreateClientTaskThreadParams {
|
|
48
|
+
agentId: string;
|
|
49
|
+
groupId?: string;
|
|
50
|
+
/** Initial user message content (task instruction) */
|
|
51
|
+
instruction: string;
|
|
52
|
+
parentMessageId: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
topicId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parameters for updateClientTaskThreadStatus
|
|
59
|
+
* Updates Thread status after client-side execution completes
|
|
60
|
+
*/
|
|
61
|
+
export interface UpdateClientTaskThreadStatusParams {
|
|
62
|
+
completionReason: 'done' | 'error' | 'interrupted';
|
|
63
|
+
error?: string;
|
|
64
|
+
metadata?: {
|
|
65
|
+
totalCost?: number;
|
|
66
|
+
totalMessages?: number;
|
|
67
|
+
totalSteps?: number;
|
|
68
|
+
totalTokens?: number;
|
|
69
|
+
totalToolCalls?: number;
|
|
70
|
+
};
|
|
71
|
+
resultContent?: string;
|
|
72
|
+
threadId: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
class AiAgentService {
|
|
44
76
|
/**
|
|
45
77
|
* Execute a single Agent task
|
|
@@ -72,6 +104,25 @@ class AiAgentService {
|
|
|
72
104
|
async interruptTask(params: InterruptTaskParams) {
|
|
73
105
|
return await lambdaClient.aiAgent.interruptTask.mutate(params);
|
|
74
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create Thread for client-side task execution (desktop only)
|
|
110
|
+
*
|
|
111
|
+
* This method is called when runInClient=true on desktop client.
|
|
112
|
+
* It creates the Thread but does NOT execute the task - execution happens locally.
|
|
113
|
+
*/
|
|
114
|
+
async createClientTaskThread(params: CreateClientTaskThreadParams) {
|
|
115
|
+
return await lambdaClient.aiAgent.createClientTaskThread.mutate(params);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Update Thread status after client-side task execution completes
|
|
120
|
+
*
|
|
121
|
+
* This method is called by desktop client after task execution finishes.
|
|
122
|
+
*/
|
|
123
|
+
async updateClientTaskThreadStatus(params: UpdateClientTaskThreadStatusParams) {
|
|
124
|
+
return await lambdaClient.aiAgent.updateClientTaskThreadStatus.mutate(params);
|
|
125
|
+
}
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
export const aiAgentService = new AiAgentService();
|