@lobehub/lobehub 2.0.0-next.305 → 2.0.0-next.306

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 (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/e2e/src/steps/community/detail-pages.steps.ts +3 -1
  4. package/e2e/src/steps/community/interactions.steps.ts +4 -4
  5. package/package.json +1 -1
  6. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +9 -6
  7. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +103 -0
  8. package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -31
  9. package/packages/context-engine/src/providers/__tests__/GroupAgentBuilderContextInjector.test.ts +307 -0
  10. package/packages/prompts/src/prompts/userMemory/__snapshots__/index.test.ts.snap +14 -38
  11. package/packages/prompts/src/prompts/userMemory/index.ts +5 -24
  12. package/src/app/[variants]/(main)/community/(detail)/assistant/index.tsx +1 -1
  13. package/src/app/[variants]/(main)/community/(detail)/mcp/index.tsx +1 -1
  14. package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/index.tsx +2 -2
  15. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -2
  16. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -1
  17. package/src/features/Conversation/Messages/components/ContentLoading.tsx +8 -2
  18. package/src/services/chat/mecha/agentConfigResolver.ts +65 -0
  19. package/src/services/chat/mecha/modelParamsResolver.test.ts +211 -0
  20. package/src/store/agentGroup/action.ts +30 -0
  21. package/src/store/agentGroup/slices/lifecycle.test.ts +77 -18
  22. package/src/store/agentGroup/slices/lifecycle.ts +7 -9
  23. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +124 -0
  24. package/src/store/chat/slices/operation/selectors.ts +22 -0
@@ -10,19 +10,31 @@ vi.mock('@/services/chatGroup', () => ({
10
10
  chatGroupService: {
11
11
  addAgentsToGroup: vi.fn(),
12
12
  createGroup: vi.fn(),
13
+ getGroupDetail: vi.fn(),
13
14
  getGroups: vi.fn(),
14
15
  },
15
16
  }));
16
17
 
17
- vi.mock('@/store/session', () => ({
18
- getSessionStoreState: vi.fn(() => ({
19
- activeId: 'some-session-id',
20
- refreshSessions: vi.fn().mockResolvedValue(undefined),
21
- sessions: [],
22
- switchSession: vi.fn(),
18
+ vi.mock('@/store/home', () => ({
19
+ getHomeStoreState: vi.fn(() => ({
20
+ refreshAgentList: vi.fn(),
21
+ switchToGroup: vi.fn(),
23
22
  })),
24
23
  }));
25
24
 
25
+ vi.mock('@/store/agent', () => ({
26
+ getAgentStoreState: vi.fn(() => ({
27
+ internal_dispatchAgentMap: vi.fn(),
28
+ setActiveAgentId: vi.fn(),
29
+ })),
30
+ }));
31
+
32
+ vi.mock('@/store/chat', () => ({
33
+ useChatStore: {
34
+ setState: vi.fn(),
35
+ },
36
+ }));
37
+
26
38
  describe('ChatGroupLifecycleSlice', () => {
27
39
  beforeEach(() => {
28
40
  vi.clearAllMocks();
@@ -47,12 +59,17 @@ describe('ChatGroupLifecycleSlice', () => {
47
59
  title: 'Test Group',
48
60
  userId: 'user-1',
49
61
  };
62
+ const mockGroupDetail = {
63
+ ...mockGroup,
64
+ agents: [],
65
+ supervisorAgentId: 'supervisor-1',
66
+ };
50
67
 
51
68
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
52
69
  group: mockGroup as any,
53
70
  supervisorAgentId: 'supervisor-1',
54
71
  });
55
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
72
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
56
73
 
57
74
  const { result } = renderHook(() => useAgentGroupStore());
58
75
 
@@ -71,13 +88,18 @@ describe('ChatGroupLifecycleSlice', () => {
71
88
  title: 'Test Group',
72
89
  userId: 'user-1',
73
90
  };
91
+ const mockGroupDetail = {
92
+ ...mockGroup,
93
+ agents: [],
94
+ supervisorAgentId: 'supervisor-1',
95
+ };
74
96
 
75
97
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
76
98
  group: mockGroup as any,
77
99
  supervisorAgentId: 'supervisor-1',
78
100
  });
79
101
  vi.mocked(chatGroupService.addAgentsToGroup).mockResolvedValue({ added: [], existing: [] });
80
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
102
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
81
103
 
82
104
  const { result } = renderHook(() => useAgentGroupStore());
83
105
 
@@ -91,14 +113,46 @@ describe('ChatGroupLifecycleSlice', () => {
91
113
  ]);
92
114
  });
93
115
 
94
- it('should not switch session when silent is true', async () => {
95
- const mockSwitchSession = vi.fn();
96
- const { getSessionStoreState } = await import('@/store/session');
97
- vi.mocked(getSessionStoreState).mockReturnValue({
98
- activeId: 'some-session-id',
99
- refreshSessions: vi.fn().mockResolvedValue(undefined),
100
- sessions: [],
101
- switchSession: mockSwitchSession,
116
+ it('should fetch group detail and store supervisorAgentId for tools injection', async () => {
117
+ const mockGroup = {
118
+ id: 'new-group-id',
119
+ title: 'Test Group',
120
+ userId: 'user-1',
121
+ };
122
+ const mockSupervisorAgentId = 'supervisor-agent-123';
123
+ const mockGroupDetail = {
124
+ ...mockGroup,
125
+ agents: [],
126
+ supervisorAgentId: mockSupervisorAgentId,
127
+ };
128
+
129
+ vi.mocked(chatGroupService.createGroup).mockResolvedValue({
130
+ group: mockGroup as any,
131
+ supervisorAgentId: mockSupervisorAgentId,
132
+ });
133
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
134
+
135
+ const { result } = renderHook(() => useAgentGroupStore());
136
+
137
+ await act(async () => {
138
+ await result.current.createGroup({ title: 'Test Group' });
139
+ });
140
+
141
+ // Verify getGroupDetail was called to fetch full group info
142
+ expect(chatGroupService.getGroupDetail).toHaveBeenCalledWith('new-group-id');
143
+
144
+ // Verify supervisorAgentId is stored in groupMap for tools injection
145
+ const groupDetail = result.current.groupMap['new-group-id'];
146
+ expect(groupDetail).toBeDefined();
147
+ expect(groupDetail.supervisorAgentId).toBe(mockSupervisorAgentId);
148
+ });
149
+
150
+ it('should not switch to group when silent is true', async () => {
151
+ const mockSwitchToGroup = vi.fn();
152
+ const { getHomeStoreState } = await import('@/store/home');
153
+ vi.mocked(getHomeStoreState).mockReturnValue({
154
+ refreshAgentList: vi.fn(),
155
+ switchToGroup: mockSwitchToGroup,
102
156
  } as any);
103
157
 
104
158
  const mockGroup = {
@@ -106,12 +160,17 @@ describe('ChatGroupLifecycleSlice', () => {
106
160
  title: 'Test Group',
107
161
  userId: 'user-1',
108
162
  };
163
+ const mockGroupDetail = {
164
+ ...mockGroup,
165
+ agents: [],
166
+ supervisorAgentId: 'supervisor-1',
167
+ };
109
168
 
110
169
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
111
170
  group: mockGroup as any,
112
171
  supervisorAgentId: 'supervisor-1',
113
172
  });
114
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
173
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
115
174
 
116
175
  const { result } = renderHook(() => useAgentGroupStore());
117
176
 
@@ -119,7 +178,7 @@ describe('ChatGroupLifecycleSlice', () => {
119
178
  await result.current.createGroup({ title: 'Test Group' }, [], true);
120
179
  });
121
180
 
122
- expect(mockSwitchSession).not.toHaveBeenCalled();
181
+ expect(mockSwitchToGroup).not.toHaveBeenCalled();
123
182
  });
124
183
  });
125
184
  });
@@ -5,7 +5,7 @@ import { type StateCreator } from 'zustand/vanilla';
5
5
  import { chatGroupService } from '@/services/chatGroup';
6
6
  import { type ChatGroupStore } from '@/store/agentGroup/store';
7
7
  import { useChatStore } from '@/store/chat';
8
- import { getSessionStoreState } from '@/store/session';
8
+ import { getHomeStoreState } from '@/store/home';
9
9
 
10
10
  export interface ChatGroupLifecycleAction {
11
11
  createGroup: (
@@ -14,7 +14,6 @@ export interface ChatGroupLifecycleAction {
14
14
  silent?: boolean,
15
15
  ) => Promise<string>;
16
16
  /**
17
- * @deprecated Use switchTopic(undefined) instead
18
17
  * Switch to a new topic in the group
19
18
  * Clears activeTopicId and navigates to group root
20
19
  */
@@ -32,11 +31,8 @@ export const chatGroupLifecycleSlice: StateCreator<
32
31
  [],
33
32
  ChatGroupLifecycleAction
34
33
  > = (_, get) => ({
35
- /**
36
- * @param silent - if true, do not switch to the new group session
37
- */
38
34
  createGroup: async (newGroup, agentIds, silent = false) => {
39
- const { switchSession } = getSessionStoreState();
35
+ const { switchToGroup, refreshAgentList } = getHomeStoreState();
40
36
 
41
37
  const { group } = await chatGroupService.createGroup(newGroup);
42
38
 
@@ -52,11 +48,13 @@ export const chatGroupLifecycleSlice: StateCreator<
52
48
 
53
49
  get().internal_dispatchChatGroup({ payload: group, type: 'addGroup' });
54
50
 
55
- await get().loadGroups();
56
- await getSessionStoreState().refreshSessions();
51
+ // Fetch full group detail to get supervisorAgentId and agents for tools injection
52
+ await get().internal_fetchGroupDetail(group.id);
53
+
54
+ refreshAgentList();
57
55
 
58
56
  if (!silent) {
59
- switchSession(group.id);
57
+ switchToGroup(group.id);
60
58
  }
61
59
 
62
60
  return group.id;
@@ -509,6 +509,130 @@ describe('Operation Selectors', () => {
509
509
  });
510
510
  });
511
511
 
512
+ describe('isAgentRunning', () => {
513
+ it('should return false when no operations exist', () => {
514
+ const { result } = renderHook(() => useChatStore());
515
+
516
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
517
+ });
518
+
519
+ it('should return true only for the agent with running operations', () => {
520
+ const { result } = renderHook(() => useChatStore());
521
+
522
+ act(() => {
523
+ result.current.startOperation({
524
+ type: 'execAgentRuntime',
525
+ context: { agentId: 'agent1' },
526
+ });
527
+ });
528
+
529
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
530
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
531
+ });
532
+
533
+ it('should return false when operation completes', () => {
534
+ const { result } = renderHook(() => useChatStore());
535
+
536
+ let opId: string;
537
+
538
+ act(() => {
539
+ opId = result.current.startOperation({
540
+ type: 'execAgentRuntime',
541
+ context: { agentId: 'agent1' },
542
+ }).operationId;
543
+ });
544
+
545
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
546
+
547
+ act(() => {
548
+ result.current.completeOperation(opId!);
549
+ });
550
+
551
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
552
+ });
553
+
554
+ it('should exclude aborting operations', () => {
555
+ const { result } = renderHook(() => useChatStore());
556
+
557
+ let opId: string;
558
+
559
+ act(() => {
560
+ opId = result.current.startOperation({
561
+ type: 'execAgentRuntime',
562
+ context: { agentId: 'agent1' },
563
+ }).operationId;
564
+ });
565
+
566
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
567
+
568
+ act(() => {
569
+ result.current.updateOperationMetadata(opId!, { isAborting: true });
570
+ });
571
+
572
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
573
+ });
574
+
575
+ it('should detect any topic with running operations for the agent', () => {
576
+ const { result } = renderHook(() => useChatStore());
577
+
578
+ act(() => {
579
+ // Agent 1, topic 1
580
+ result.current.startOperation({
581
+ type: 'execAgentRuntime',
582
+ context: { agentId: 'agent1', topicId: 'topic1' },
583
+ });
584
+
585
+ // Agent 1, topic 2
586
+ result.current.startOperation({
587
+ type: 'execAgentRuntime',
588
+ context: { agentId: 'agent1', topicId: 'topic2' },
589
+ });
590
+
591
+ // Agent 2, topic 3
592
+ result.current.startOperation({
593
+ type: 'execAgentRuntime',
594
+ context: { agentId: 'agent2', topicId: 'topic3' },
595
+ });
596
+ });
597
+
598
+ // Agent 1 should be running (has 2 topics with operations)
599
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
600
+ // Agent 2 should also be running
601
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(true);
602
+ // Agent 3 should not be running
603
+ expect(operationSelectors.isAgentRunning('agent3')(result.current)).toBe(false);
604
+ });
605
+
606
+ it('should detect server agent runtime operations', () => {
607
+ const { result } = renderHook(() => useChatStore());
608
+
609
+ act(() => {
610
+ result.current.startOperation({
611
+ type: 'execServerAgentRuntime',
612
+ context: { agentId: 'agent1', groupId: 'group1' },
613
+ });
614
+ });
615
+
616
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
617
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
618
+ });
619
+
620
+ it('should not detect non-AI-runtime operations', () => {
621
+ const { result } = renderHook(() => useChatStore());
622
+
623
+ act(() => {
624
+ // sendMessage is not an AI runtime operation type
625
+ result.current.startOperation({
626
+ type: 'sendMessage',
627
+ context: { agentId: 'agent1' },
628
+ });
629
+ });
630
+
631
+ // sendMessage is not in AI_RUNTIME_OPERATION_TYPES, so should return false
632
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
633
+ });
634
+ });
635
+
512
636
  describe('backward compatibility selectors', () => {
513
637
  it('isAgentRuntimeRunning should work', () => {
514
638
  const { result } = renderHook(() => useChatStore());
@@ -234,6 +234,27 @@ const isAgentRuntimeRunningByContext =
234
234
  };
235
235
 
236
236
  // === Backward Compatibility ===
237
+
238
+ /**
239
+ * Check if a specific agent has running AI runtime operations
240
+ * Used for agent list item loading states where we need per-agent granularity
241
+ */
242
+ const isAgentRunning =
243
+ (agentId: string) =>
244
+ (s: ChatStoreState): boolean => {
245
+ for (const type of AI_RUNTIME_OPERATION_TYPES) {
246
+ const operationIds = s.operationsByType[type] || [];
247
+ const hasRunning = operationIds.some((id) => {
248
+ const op = s.operations[id];
249
+ return (
250
+ op && op.status === 'running' && !op.metadata.isAborting && op.context.agentId === agentId
251
+ );
252
+ });
253
+ if (hasRunning) return true;
254
+ }
255
+ return false;
256
+ };
257
+
237
258
  /**
238
259
  * Check if agent runtime is running (including both main window and thread)
239
260
  * Checks both client-side (execAgentRuntime) and server-side (execServerAgentRuntime) operations
@@ -477,6 +498,7 @@ export const operationSelectors = {
477
498
 
478
499
  isAborting,
479
500
 
501
+ isAgentRunning,
480
502
  isAgentRuntimeRunning,
481
503
  isAgentRuntimeRunningByContext,
482
504
  isAnyMessageLoading,