@lobehub/lobehub 2.0.9 → 2.0.11

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 (108) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/Dockerfile +44 -52
  3. package/changelog/v2.json +18 -0
  4. package/locales/ar/chat.json +4 -0
  5. package/locales/ar/models.json +65 -0
  6. package/locales/bg-BG/chat.json +4 -0
  7. package/locales/bg-BG/models.json +10 -0
  8. package/locales/de-DE/chat.json +4 -0
  9. package/locales/de-DE/models.json +41 -0
  10. package/locales/en-US/chat.json +4 -0
  11. package/locales/es-ES/chat.json +4 -0
  12. package/locales/es-ES/models.json +50 -0
  13. package/locales/fa-IR/chat.json +4 -0
  14. package/locales/fa-IR/models.json +39 -0
  15. package/locales/fr-FR/chat.json +4 -0
  16. package/locales/fr-FR/models.json +9 -0
  17. package/locales/it-IT/chat.json +4 -0
  18. package/locales/it-IT/models.json +62 -0
  19. package/locales/ja-JP/chat.json +4 -0
  20. package/locales/ja-JP/models.json +40 -0
  21. package/locales/ko-KR/chat.json +4 -0
  22. package/locales/ko-KR/models.json +31 -0
  23. package/locales/nl-NL/chat.json +4 -0
  24. package/locales/nl-NL/models.json +52 -0
  25. package/locales/pl-PL/chat.json +4 -0
  26. package/locales/pl-PL/models.json +43 -0
  27. package/locales/pt-BR/chat.json +4 -0
  28. package/locales/pt-BR/models.json +92 -0
  29. package/locales/ru-RU/chat.json +4 -0
  30. package/locales/ru-RU/models.json +34 -0
  31. package/locales/tr-TR/chat.json +4 -0
  32. package/locales/tr-TR/models.json +55 -0
  33. package/locales/vi-VN/chat.json +4 -0
  34. package/locales/vi-VN/models.json +31 -0
  35. package/locales/zh-CN/chat.json +4 -0
  36. package/locales/zh-TW/chat.json +4 -0
  37. package/package.json +1 -1
  38. package/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts +18 -1
  39. package/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts +76 -5
  40. package/packages/agent-runtime/src/groupOrchestration/types.ts +3 -3
  41. package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTask.tsx +11 -11
  42. package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTasks.tsx +78 -79
  43. package/packages/builtin-tool-group-management/src/client/Render/ExecuteTask/index.tsx +3 -3
  44. package/packages/builtin-tool-group-management/src/client/Render/ExecuteTasks/index.tsx +61 -63
  45. package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTask/index.tsx +3 -3
  46. package/packages/builtin-tool-group-management/src/executor.test.ts +7 -9
  47. package/packages/builtin-tool-group-management/src/executor.ts +3 -3
  48. package/packages/builtin-tool-group-management/src/manifest.ts +49 -50
  49. package/packages/builtin-tool-group-management/src/systemRole.ts +153 -5
  50. package/packages/builtin-tool-group-management/src/types.ts +3 -2
  51. package/packages/builtin-tool-gtd/src/systemRole.ts +4 -4
  52. package/packages/context-engine/src/processors/TasksFlatten.ts +7 -5
  53. package/packages/context-engine/src/processors/__tests__/TasksFlatten.test.ts +164 -0
  54. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/index.ts +4 -0
  55. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/supervisor-after-multi-tasks.json +91 -0
  56. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/supervisor-content-only.json +74 -0
  57. package/packages/conversation-flow/src/__tests__/parse.test.ts +37 -0
  58. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +70 -4
  59. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +147 -0
  60. package/packages/model-bank/src/aiModels/cerebras.ts +2 -22
  61. package/packages/model-bank/src/aiModels/google.ts +1 -44
  62. package/packages/model-bank/src/aiModels/nvidia.ts +12 -16
  63. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  64. package/packages/model-bank/src/aiModels/volcengine.ts +69 -0
  65. package/packages/model-bank/src/aiModels/wenxin.ts +41 -38
  66. package/packages/model-bank/src/aiModels/zhipu.ts +58 -28
  67. package/packages/model-bank/src/types/aiModel.ts +29 -0
  68. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +2 -2
  69. package/packages/model-runtime/src/providers/google/createImage.test.ts +12 -12
  70. package/packages/model-runtime/src/providers/openrouter/index.test.ts +102 -0
  71. package/packages/model-runtime/src/providers/openrouter/index.ts +19 -7
  72. package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +47 -0
  73. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +7 -1
  74. package/packages/types/src/message/ui/chat.ts +2 -0
  75. package/packages/types/src/tool/builtin.ts +5 -5
  76. package/src/features/Conversation/ChatItem/components/Title.tsx +1 -1
  77. package/src/features/Conversation/ChatList/index.tsx +0 -1
  78. package/src/features/Conversation/Messages/GroupTasks/TaskItem/ClientTaskItem.tsx +183 -0
  79. package/src/features/Conversation/Messages/GroupTasks/TaskItem/ServerTaskItem.tsx +94 -0
  80. package/src/features/Conversation/Messages/GroupTasks/TaskItem/TaskTitle.tsx +177 -0
  81. package/src/features/Conversation/Messages/GroupTasks/TaskItem/index.tsx +26 -0
  82. package/src/features/Conversation/Messages/GroupTasks/TaskItem/useClientTaskStats.ts +93 -0
  83. package/src/features/Conversation/Messages/GroupTasks/index.tsx +151 -0
  84. package/src/features/Conversation/Messages/Supervisor/index.tsx +7 -1
  85. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +72 -91
  86. package/src/features/Conversation/Messages/Task/TaskDetailPanel/StatusContent.tsx +46 -17
  87. package/src/features/Conversation/Messages/Tasks/TaskItem/ClientTaskItem.tsx +9 -24
  88. package/src/features/Conversation/Messages/Tasks/TaskItem/ServerTaskItem.tsx +18 -38
  89. package/src/features/Conversation/Messages/Tasks/shared/ErrorState.tsx +45 -2
  90. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +16 -1
  91. package/src/features/Conversation/Messages/Tasks/shared/TaskContent.tsx +68 -0
  92. package/src/features/Conversation/Messages/Tasks/shared/TaskMessages.tsx +383 -0
  93. package/src/features/Conversation/Messages/Tasks/shared/index.ts +4 -0
  94. package/src/features/Conversation/Messages/Tasks/shared/useTaskPolling.ts +48 -0
  95. package/src/features/Conversation/Messages/index.tsx +5 -0
  96. package/src/locales/default/chat.ts +4 -0
  97. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +4 -0
  98. package/src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts +106 -1
  99. package/src/server/services/aiAgent/__tests__/execAgent.threadId.test.ts +2 -2
  100. package/src/server/utils/truncateToolResult.ts +1 -4
  101. package/src/store/chat/agents/GroupOrchestration/__tests__/batch-exec-async-tasks.test.ts +15 -15
  102. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +22 -15
  103. package/src/store/chat/agents/__tests__/createAgentExecutors/exec-tasks.test.ts +21 -10
  104. package/src/store/chat/agents/createAgentExecutors.ts +2 -0
  105. package/src/store/chat/slices/aiAgent/actions/groupOrchestration.ts +10 -7
  106. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +0 -108
  107. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +0 -63
  108. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +0 -123
@@ -0,0 +1,383 @@
1
+ 'use client';
2
+
3
+ import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
4
+ import {
5
+ Accordion,
6
+ AccordionItem,
7
+ Block,
8
+ Flexbox,
9
+ Icon,
10
+ Markdown,
11
+ ScrollShadow,
12
+ Text,
13
+ } from '@lobehub/ui';
14
+ import { createStaticStyles, cssVar } from 'antd-style';
15
+ import { ScrollText, Workflow } from 'lucide-react';
16
+ import { type RefObject, memo, useEffect, useMemo, useState } from 'react';
17
+ import { useTranslation } from 'react-i18next';
18
+
19
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
20
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
21
+
22
+ import ContentBlock from '../../AssistantGroup/components/ContentBlock';
23
+ import Usage from '../../components/Extras/Usage';
24
+ import AnimatedNumber from '../../components/Extras/Usage/UsageDetail/AnimatedNumber';
25
+ import { accumulateUsage, formatDuration, formatElapsedTime } from './utils';
26
+
27
+ const styles = createStaticStyles(({ css }) => ({
28
+ contentScroll: css`
29
+ max-height: min(50vh, 300px);
30
+ `,
31
+ instructionContent: css`
32
+ overflow: auto;
33
+ max-height: 300px;
34
+ `,
35
+ }));
36
+
37
+ /**
38
+ * InstructionAccordion - Shows the task instruction in a collapsible accordion
39
+ */
40
+ const InstructionAccordion = memo<{ childrenCount: number; instruction: string }>(
41
+ ({ instruction, childrenCount }) => {
42
+ const { t } = useTranslation('chat');
43
+
44
+ // Auto-collapse instruction when children count exceeds threshold
45
+ const [expandedKeys, setExpandedKeys] = useState<string[]>(['instruction']);
46
+
47
+ useEffect(() => {
48
+ if (childrenCount > 1) {
49
+ setExpandedKeys([]);
50
+ }
51
+ }, [childrenCount > 1]);
52
+
53
+ return (
54
+ <Accordion
55
+ expandedKeys={expandedKeys}
56
+ gap={8}
57
+ onExpandedChange={(keys) => setExpandedKeys(keys as string[])}
58
+ >
59
+ <AccordionItem
60
+ itemKey="instruction"
61
+ paddingBlock={4}
62
+ paddingInline={4}
63
+ title={
64
+ <Flexbox align="center" gap={8} horizontal>
65
+ <Block
66
+ align="center"
67
+ flex="none"
68
+ gap={4}
69
+ height={24}
70
+ horizontal
71
+ justify="center"
72
+ style={{ fontSize: 12 }}
73
+ variant="outlined"
74
+ width={24}
75
+ >
76
+ <Icon color={cssVar.colorTextSecondary} icon={ScrollText} />
77
+ </Block>
78
+ <Text as="span" type="secondary">
79
+ {t('task.instruction')}
80
+ </Text>
81
+ </Flexbox>
82
+ }
83
+ >
84
+ <Block
85
+ className={styles.instructionContent}
86
+ padding={12}
87
+ style={{ marginBlock: 8 }}
88
+ variant={'outlined'}
89
+ >
90
+ <Markdown variant={'chat'}>{instruction}</Markdown>
91
+ </Block>
92
+ </AccordionItem>
93
+ </Accordion>
94
+ );
95
+ },
96
+ );
97
+
98
+ InstructionAccordion.displayName = 'InstructionAccordion';
99
+
100
+ interface TaskMessagesProps {
101
+ /**
102
+ * Task duration in ms (for completed state)
103
+ */
104
+ duration?: number;
105
+ /**
106
+ * Whether the task is currently processing
107
+ */
108
+ isProcessing?: boolean;
109
+ /**
110
+ * Messages from task execution (parsed by conversation-flow)
111
+ * Will extract assistantGroup.children as blocks for rendering
112
+ */
113
+ messages: UIChatMessage[];
114
+ /**
115
+ * Model name for usage display
116
+ */
117
+ model?: string;
118
+ /**
119
+ * Provider name for usage display
120
+ */
121
+ provider?: string;
122
+ /**
123
+ * Task start time for elapsed time calculation
124
+ */
125
+ startTime?: number;
126
+ /**
127
+ * Total cost (for completed state)
128
+ */
129
+ totalCost?: number;
130
+ }
131
+
132
+ /**
133
+ * Processing state - shows all blocks with loading indicator
134
+ */
135
+ const ProcessingView = memo<{
136
+ accumulatedUsage: { cost?: number; totalTokens?: number };
137
+ assistantId: string;
138
+ blocks: AssistantContentBlock[];
139
+ model?: string;
140
+ provider?: string;
141
+ startTime?: number;
142
+ totalToolCalls: number;
143
+ }>(({ blocks, assistantId, startTime, model, provider, totalToolCalls, accumulatedUsage }) => {
144
+ const { t } = useTranslation('chat');
145
+ const [elapsedTime, setElapsedTime] = useState(0);
146
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
147
+ deps: [blocks],
148
+ enabled: true,
149
+ });
150
+
151
+ // Calculate initial elapsed time
152
+ useEffect(() => {
153
+ if (startTime) {
154
+ setElapsedTime(Math.max(0, Date.now() - startTime));
155
+ }
156
+ }, [startTime]);
157
+
158
+ // Timer for updating elapsed time every second
159
+ useEffect(() => {
160
+ if (!startTime) return;
161
+
162
+ const timer = setInterval(() => {
163
+ setElapsedTime(Math.max(0, Date.now() - startTime));
164
+ }, 1000);
165
+
166
+ return () => clearInterval(timer);
167
+ }, [startTime]);
168
+
169
+ return (
170
+ <Flexbox gap={8}>
171
+ <Flexbox align="center" gap={8} horizontal paddingInline={4}>
172
+ <Block
173
+ align="center"
174
+ flex="none"
175
+ gap={4}
176
+ height={24}
177
+ horizontal
178
+ justify="center"
179
+ style={{ fontSize: 12 }}
180
+ variant="outlined"
181
+ width={24}
182
+ >
183
+ <NeuralNetworkLoading size={16} />
184
+ </Block>
185
+ <Flexbox align="center" gap={4} horizontal>
186
+ <Text as="span" type="secondary" weight={500}>
187
+ <AnimatedNumber
188
+ duration={500}
189
+ formatter={(v) => Math.round(v).toString()}
190
+ value={totalToolCalls}
191
+ />
192
+ </Text>
193
+ <Text as="span" type="secondary">
194
+ {t('task.metrics.toolCallsShort')}
195
+ </Text>
196
+ {startTime && (
197
+ <Text as="span" type="secondary">
198
+ ({formatElapsedTime(elapsedTime)})
199
+ </Text>
200
+ )}
201
+ </Flexbox>
202
+ </Flexbox>
203
+ <ScrollShadow
204
+ className={styles.contentScroll}
205
+ offset={12}
206
+ onScroll={handleScroll}
207
+ ref={ref as RefObject<HTMLDivElement>}
208
+ size={8}
209
+ >
210
+ <Flexbox gap={8}>
211
+ {blocks.map((block) => (
212
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
213
+ ))}
214
+ </Flexbox>
215
+ </ScrollShadow>
216
+
217
+ {/* Usage display */}
218
+ {model && provider && <Usage model={model} provider={provider} usage={accumulatedUsage} />}
219
+ </Flexbox>
220
+ );
221
+ });
222
+
223
+ ProcessingView.displayName = 'ProcessingView';
224
+
225
+ /**
226
+ * Completed state - shows intermediate steps in accordion, final result visible
227
+ */
228
+ const CompletedView = memo<{
229
+ assistantId: string;
230
+ blocks: AssistantContentBlock[];
231
+ duration?: number;
232
+ model?: string;
233
+ provider?: string;
234
+ totalCost?: number;
235
+ totalTokens?: number;
236
+ totalToolCalls: number;
237
+ }>(({ blocks, assistantId, duration, totalToolCalls, model, provider, totalTokens, totalCost }) => {
238
+ const { t } = useTranslation('chat');
239
+
240
+ // Split blocks: intermediate steps (all but last) and final result (last)
241
+ const { intermediateBlocks, finalBlock } = useMemo(() => {
242
+ if (blocks.length === 0) return { finalBlock: null, intermediateBlocks: [] };
243
+ if (blocks.length === 1) return { finalBlock: blocks[0], intermediateBlocks: [] };
244
+
245
+ return {
246
+ finalBlock: blocks.at(-1)!,
247
+ intermediateBlocks: blocks.slice(0, -1),
248
+ };
249
+ }, [blocks]);
250
+
251
+ if (!finalBlock) return null;
252
+
253
+ const title = (
254
+ <Flexbox align="center" gap={8} horizontal>
255
+ <Block
256
+ align="center"
257
+ flex="none"
258
+ gap={4}
259
+ height={24}
260
+ horizontal
261
+ justify="center"
262
+ style={{ fontSize: 12 }}
263
+ variant="outlined"
264
+ width={24}
265
+ >
266
+ <Icon color={cssVar.colorTextSecondary} icon={Workflow} />
267
+ </Block>
268
+ <Flexbox align="center" gap={4} horizontal>
269
+ <Text as="span" type="secondary" weight={500}>
270
+ {totalToolCalls}
271
+ </Text>
272
+ <Text as="span" type="secondary">
273
+ {t('task.metrics.toolCallsShort')}
274
+ </Text>
275
+ {/* Duration display */}
276
+ {duration && (
277
+ <Text as="span" type="secondary">
278
+ {t('task.metrics.duration', { duration: formatDuration(duration) })}
279
+ </Text>
280
+ )}
281
+ </Flexbox>
282
+ </Flexbox>
283
+ );
284
+
285
+ return (
286
+ <Flexbox gap={8}>
287
+ {/* Intermediate steps - collapsed by default */}
288
+ {intermediateBlocks.length > 0 && (
289
+ <Accordion defaultExpandedKeys={[]} gap={8}>
290
+ <AccordionItem itemKey="intermediate" paddingBlock={4} paddingInline={4} title={title}>
291
+ <Flexbox gap={8} paddingInline={4} style={{ marginTop: 8 }}>
292
+ {intermediateBlocks.map((block) => (
293
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
294
+ ))}
295
+ </Flexbox>
296
+ </AccordionItem>
297
+ </Accordion>
298
+ )}
299
+
300
+ {/* Final result - always visible */}
301
+ <ContentBlock {...finalBlock} assistantId={assistantId} disableEditing />
302
+
303
+ {/* Usage display */}
304
+ {model && provider && (
305
+ <Usage model={model} provider={provider} usage={{ cost: totalCost, totalTokens }} />
306
+ )}
307
+ </Flexbox>
308
+ );
309
+ });
310
+
311
+ CompletedView.displayName = 'CompletedView';
312
+
313
+ /**
314
+ * TaskMessages - Renders task execution messages (blocks) for both processing and completed states
315
+ *
316
+ * Extracts assistantGroup.children (blocks) from messages and renders them:
317
+ * - Processing: Shows all blocks with loading indicator and real-time updates
318
+ * - Completed: Shows intermediate steps in accordion, final result always visible
319
+ */
320
+ const TaskMessages = memo<TaskMessagesProps>(
321
+ ({ messages, isProcessing = false, startTime, duration, model, provider, totalCost }) => {
322
+ // Extract blocks and instruction from messages
323
+ const { blocks, assistantId, instruction } = useMemo(() => {
324
+ if (!messages || messages.length === 0)
325
+ return { assistantId: '', blocks: [], instruction: undefined };
326
+
327
+ const assistantGroupMessage = messages.find((item) => item.role === 'assistantGroup');
328
+ const userMessage = messages.find((item) => item.role === 'user');
329
+
330
+ return {
331
+ assistantId: assistantGroupMessage?.id ?? '',
332
+ blocks: assistantGroupMessage?.children ?? [],
333
+ instruction: userMessage?.content,
334
+ };
335
+ }, [messages]);
336
+
337
+ // Calculate total tool calls
338
+ const totalToolCalls = useMemo(
339
+ () => blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0),
340
+ [blocks],
341
+ );
342
+
343
+ // Accumulate usage from all blocks
344
+ const accumulatedUsage = useMemo(() => accumulateUsage(blocks), [blocks]);
345
+
346
+ return (
347
+ <Flexbox gap={4}>
348
+ {/* Instruction accordion */}
349
+ {instruction && (
350
+ <InstructionAccordion childrenCount={blocks.length} instruction={instruction} />
351
+ )}
352
+
353
+ {/* Processing or Completed view */}
354
+ {isProcessing ? (
355
+ <ProcessingView
356
+ accumulatedUsage={accumulatedUsage}
357
+ assistantId={assistantId}
358
+ blocks={blocks}
359
+ model={model}
360
+ provider={provider}
361
+ startTime={startTime}
362
+ totalToolCalls={totalToolCalls}
363
+ />
364
+ ) : (
365
+ <CompletedView
366
+ assistantId={assistantId}
367
+ blocks={blocks}
368
+ duration={duration}
369
+ model={model}
370
+ provider={provider}
371
+ totalCost={totalCost}
372
+ totalTokens={accumulatedUsage.totalTokens}
373
+ totalToolCalls={totalToolCalls}
374
+ />
375
+ )}
376
+ </Flexbox>
377
+ );
378
+ },
379
+ );
380
+
381
+ TaskMessages.displayName = 'TaskMessages';
382
+
383
+ export default TaskMessages;
@@ -5,4 +5,8 @@ export { default as ErrorState } from './ErrorState';
5
5
  export { default as InitializingState } from './InitializingState';
6
6
  export type { ProcessingStateVariant } from './ProcessingState';
7
7
  export { default as ProcessingState } from './ProcessingState';
8
+ export type { TaskContentProps } from './TaskContent';
9
+ export { default as TaskContent } from './TaskContent';
10
+ export { default as TaskMessages } from './TaskMessages';
11
+ export * from './useTaskPolling';
8
12
  export * from './utils';
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { ThreadStatus } from '@lobechat/types';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ import { isProcessingStatus } from './utils';
9
+
10
+ interface UseTaskPollingParams {
11
+ messageId: string;
12
+ status: ThreadStatus | undefined;
13
+ threadId: string | undefined;
14
+ }
15
+
16
+ export const useTaskPolling = ({ messageId, threadId, status }: UseTaskPollingParams) => {
17
+ const isProcessing = isProcessingStatus(status);
18
+ const [hasFetched, setHasFetched] = useState(false);
19
+
20
+ const [useEnablePollingTaskStatus, operations] = useChatStore((s) => [
21
+ s.useEnablePollingTaskStatus,
22
+ s.operations,
23
+ ]);
24
+
25
+ // Check if exec_async_task is already polling for this message
26
+ const hasActiveOperationPolling = Object.values(operations).some(
27
+ (op) =>
28
+ op.status === 'running' &&
29
+ op.type === 'execAgentRuntime' &&
30
+ op.context?.messageId === messageId,
31
+ );
32
+
33
+ // Enable polling when:
34
+ // 1. Has threadId
35
+ // 2. Not already being polled by an active operation
36
+ // 3. Either hasn't fetched yet (initial fetch) or is still processing (continuous polling)
37
+ const shouldPoll = !!threadId && !hasActiveOperationPolling && (!hasFetched || isProcessing);
38
+ const { data } = useEnablePollingTaskStatus(threadId, messageId, shouldPoll);
39
+
40
+ // Mark as fetched when we get data
41
+ useEffect(() => {
42
+ if (data?.taskDetail && !hasFetched) {
43
+ setHasFetched(true);
44
+ }
45
+ }, [data?.taskDetail, hasFetched]);
46
+
47
+ return { isProcessing };
48
+ };
@@ -15,6 +15,7 @@ import AgentCouncilMessage from './AgentCouncil';
15
15
  import AssistantMessage from './Assistant';
16
16
  import AssistantGroupMessage from './AssistantGroup';
17
17
  import CompressedGroupMessage from './CompressedGroup';
18
+ import GroupTasksMessage from './GroupTasks';
18
19
  import SupervisorMessage from './Supervisor';
19
20
  import TaskMessage from './Task';
20
21
  import TasksMessage from './Tasks';
@@ -159,6 +160,10 @@ const MessageItem = memo<MessageItemProps>(
159
160
  return <TasksMessage id={id} index={index} />;
160
161
  }
161
162
 
163
+ case 'groupTasks': {
164
+ return <GroupTasksMessage id={id} index={index} />;
165
+ }
166
+
162
167
  case 'agentCouncil': {
163
168
  return <AgentCouncilMessage id={id} index={index} />;
164
169
  }
@@ -369,6 +369,9 @@ export default {
369
369
  'task.activity.toolCalling': 'Calling {{toolName}}...',
370
370
  'task.activity.toolResult': '{{toolName}} result received',
371
371
  'task.batchTasks': '{{count}} Batch Subtasks',
372
+ 'task.groupTasks': '{{count}} Parallel Tasks',
373
+ 'task.groupTasksTitle': '{{agents}} and {{count}} agents tasks',
374
+ 'task.groupTasksTitleSimple': '{{agents}} {{count}} tasks',
372
375
  'task.instruction': 'Task Instruction',
373
376
  'task.intermediateSteps': '{{count}} intermediate steps',
374
377
  'task.metrics.duration': '(took {{duration}})',
@@ -376,6 +379,7 @@ export default {
376
379
  'task.metrics.toolCallsShort': 'skill uses',
377
380
  'task.status.cancelled': 'Task Cancelled',
378
381
  'task.status.failed': 'Task Failed',
382
+ 'task.status.fetchingDetails': 'Fetching details...',
379
383
  'task.status.initializing': 'Initializing task...',
380
384
  'task.subtask': 'Subtask',
381
385
  'thread.divider': 'Subtopic',
@@ -753,7 +753,11 @@ export const createRuntimeExecutors = (
753
753
  const newState = structuredClone(state);
754
754
 
755
755
  // Query latest messages from database
756
+ // Must pass agentId to ensure correct query scope, otherwise when topicId is undefined,
757
+ // the query will use isNull(topicId) condition which won't find messages with actual topicId
756
758
  const latestMessages = await ctx.messageModel.query({
759
+ agentId: state.metadata?.agentId,
760
+ threadId: state.metadata?.threadId,
757
761
  topicId: state.metadata?.topicId,
758
762
  });
759
763
 
@@ -655,8 +655,10 @@ describe('RuntimeExecutors', () => {
655
655
 
656
656
  const result = await executors.call_tools_batch!(instruction, state);
657
657
 
658
- // Should query messages from database
658
+ // Should query messages from database with agentId, threadId, and topicId
659
659
  expect(mockMessageModel.query).toHaveBeenCalledWith({
660
+ agentId: 'agent-123',
661
+ threadId: 'thread-123',
660
662
  topicId: 'topic-123',
661
663
  });
662
664
 
@@ -952,6 +954,109 @@ describe('RuntimeExecutors', () => {
952
954
  }),
953
955
  );
954
956
  });
957
+
958
+ it('should query messages with correct metadata fields when state.metadata is defined', async () => {
959
+ const executors = createRuntimeExecutors(ctx);
960
+ const state = createMockState({
961
+ metadata: {
962
+ agentId: 'agent-abc',
963
+ threadId: 'thread-xyz',
964
+ topicId: 'topic-abc-123',
965
+ },
966
+ });
967
+
968
+ const instruction = {
969
+ payload: {
970
+ parentMessageId: 'assistant-msg-123',
971
+ toolsCalling: [
972
+ {
973
+ apiName: 'search',
974
+ arguments: '{}',
975
+ id: 'tool-call-1',
976
+ identifier: 'web-search',
977
+ type: 'default' as const,
978
+ },
979
+ ],
980
+ },
981
+ type: 'call_tools_batch' as const,
982
+ };
983
+
984
+ await executors.call_tools_batch!(instruction, state);
985
+
986
+ // Should query messages with agentId, threadId, and topicId from state.metadata
987
+ expect(mockMessageModel.query).toHaveBeenCalledWith({
988
+ agentId: 'agent-abc',
989
+ threadId: 'thread-xyz',
990
+ topicId: 'topic-abc-123',
991
+ });
992
+ });
993
+
994
+ it('should preserve messages in newState even when state.metadata.topicId is undefined', async () => {
995
+ // Regression test: When state.metadata.topicId is undefined, previously the query
996
+ // only passed topicId, which caused isNull(topicId) condition and returned 0 messages.
997
+ // This led to "messages: at least one message is required" error in the next call_llm step.
998
+ //
999
+ // Fix: Now we also pass agentId and threadId, so even when topicId is undefined,
1000
+ // the query can still find messages by agentId scope.
1001
+
1002
+ // Mock: query returns messages when agentId is provided (regardless of topicId)
1003
+ mockMessageModel.query = vi
1004
+ .fn()
1005
+ .mockImplementation((params: { agentId?: string; topicId?: string }) => {
1006
+ // With the fix, agentId is always passed, so we can find messages
1007
+ if (params.agentId) {
1008
+ return Promise.resolve([
1009
+ { id: 'msg-1', content: 'Hello', role: 'user' },
1010
+ { id: 'msg-2', content: 'Response', role: 'assistant', tool_calls: [] },
1011
+ ]);
1012
+ }
1013
+ // Without agentId (old buggy behavior), return empty
1014
+ return Promise.resolve([]);
1015
+ });
1016
+
1017
+ const executors = createRuntimeExecutors(ctx);
1018
+ // State with undefined topicId but has agentId
1019
+ const state = createMockState({
1020
+ messages: [
1021
+ { content: 'Hello', role: 'user' },
1022
+ { content: 'Response', role: 'assistant', tool_calls: [] },
1023
+ ],
1024
+ metadata: {
1025
+ agentId: 'agent-123',
1026
+ threadId: 'thread-123',
1027
+ topicId: undefined, // topicId is undefined
1028
+ },
1029
+ });
1030
+
1031
+ const instruction = {
1032
+ payload: {
1033
+ parentMessageId: 'assistant-msg-123',
1034
+ toolsCalling: [
1035
+ {
1036
+ apiName: 'search',
1037
+ arguments: '{}',
1038
+ id: 'tool-call-1',
1039
+ identifier: 'web-search',
1040
+ type: 'default' as const,
1041
+ },
1042
+ ],
1043
+ },
1044
+ type: 'call_tools_batch' as const,
1045
+ };
1046
+
1047
+ const result = await executors.call_tools_batch!(instruction, state);
1048
+
1049
+ // Verify agentId is passed in the query
1050
+ expect(mockMessageModel.query).toHaveBeenCalledWith({
1051
+ agentId: 'agent-123',
1052
+ threadId: 'thread-123',
1053
+ topicId: undefined,
1054
+ });
1055
+
1056
+ // Expected: newState.messages should NOT be empty
1057
+ // The next call_llm step needs messages to work properly
1058
+ expect(result.newState.messages.length).toBeGreaterThan(0);
1059
+ });
955
1060
  });
956
1061
 
957
1062
  describe('resolve_aborted_tools executor', () => {
@@ -205,7 +205,7 @@ describe('AiAgentService.execAgent - threadId handling', () => {
205
205
 
206
206
  expect(assistantMessageCall).toBeDefined();
207
207
  expect(assistantMessageCall![0].threadId).toBeUndefined();
208
- });
208
+ }, 10_000);
209
209
  });
210
210
 
211
211
  describe('when appContext is undefined', () => {
@@ -247,6 +247,6 @@ describe('AiAgentService.execAgent - threadId handling', () => {
247
247
 
248
248
  // Verify groupId is passed to AgentRuntimeService (checked in appContext)
249
249
  // This is handled by the createOperation call
250
- });
250
+ }, 10_000);
251
251
  });
252
252
  });
@@ -6,9 +6,8 @@
6
6
  /**
7
7
  * Default maximum length for tool execution result content (in characters)
8
8
  * This prevents context overflow when sending results back to LLM
9
- * @default 6000 characters (~1,500 tokens for English, ~3,000 tokens for Chinese)
10
9
  */
11
- export const DEFAULT_TOOL_RESULT_MAX_LENGTH = 6000;
10
+ export const DEFAULT_TOOL_RESULT_MAX_LENGTH = 25_000;
12
11
 
13
12
  /**
14
13
  * Truncate tool result content if it exceeds the maximum length
@@ -21,8 +20,6 @@ export const DEFAULT_TOOL_RESULT_MAX_LENGTH = 6000;
21
20
  export function truncateToolResult(content: string, maxLength?: number): string {
22
21
  const limit = maxLength ?? DEFAULT_TOOL_RESULT_MAX_LENGTH;
23
22
 
24
- console.log('content-limit', content, limit);
25
-
26
23
  if (!content || content.length <= limit) {
27
24
  return content;
28
25
  }