@lobehub/lobehub 2.0.0-next.305 → 2.0.0-next.307
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/.vscode/settings.json +18 -3
- package/CHANGELOG.md +53 -0
- package/changelog/v1.json +18 -0
- package/e2e/src/steps/community/detail-pages.steps.ts +3 -1
- package/e2e/src/steps/community/interactions.steps.ts +4 -4
- package/package.json +2 -2
- package/packages/builtin-agents/src/agents/group-supervisor/index.ts +1 -7
- package/packages/builtin-tool-group-agent-builder/src/ExecutionRuntime/index.ts +29 -0
- package/packages/builtin-tool-group-agent-builder/src/executor.ts +18 -0
- package/packages/builtin-tool-group-agent-builder/src/manifest.ts +17 -0
- package/packages/builtin-tool-group-agent-builder/src/types.ts +10 -0
- package/packages/builtin-tool-group-management/src/executor.test.ts +0 -12
- package/packages/builtin-tool-group-management/src/executor.ts +8 -47
- package/packages/builtin-tool-group-management/src/manifest.ts +0 -17
- package/packages/builtin-tool-group-management/src/systemRole.ts +1 -8
- package/packages/builtin-tool-group-management/src/types.ts +0 -10
- package/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts +70 -31
- package/packages/builtin-tool-local-system/src/executor/index.ts +94 -60
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +9 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +103 -0
- package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -31
- package/packages/context-engine/src/providers/__tests__/GroupAgentBuilderContextInjector.test.ts +307 -0
- package/packages/database/src/repositories/agentGroup/index.ts +23 -0
- package/packages/prompts/src/prompts/fileSystem/formatCommandOutput.test.ts +61 -0
- package/packages/prompts/src/prompts/fileSystem/formatCommandOutput.ts +21 -0
- package/packages/prompts/src/prompts/fileSystem/formatCommandResult.test.ts +87 -0
- package/packages/prompts/src/prompts/fileSystem/formatCommandResult.ts +35 -0
- package/packages/prompts/src/prompts/fileSystem/formatEditResult.test.ts +57 -0
- package/packages/prompts/src/prompts/fileSystem/formatEditResult.ts +17 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileContent.test.ts +59 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileContent.ts +14 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileList.test.ts +62 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileList.ts +13 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileSearchResults.test.ts +34 -0
- package/packages/prompts/src/prompts/fileSystem/formatFileSearchResults.ts +12 -0
- package/packages/prompts/src/prompts/fileSystem/formatGlobResults.test.ts +64 -0
- package/packages/prompts/src/prompts/fileSystem/formatGlobResults.ts +23 -0
- package/packages/prompts/src/prompts/fileSystem/formatGrepResults.test.ts +85 -0
- package/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts +24 -0
- package/packages/prompts/src/prompts/fileSystem/formatKillResult.test.ts +30 -0
- package/packages/prompts/src/prompts/fileSystem/formatKillResult.ts +9 -0
- package/packages/prompts/src/prompts/fileSystem/formatMoveResults.test.ts +37 -0
- package/packages/prompts/src/prompts/fileSystem/formatMoveResults.ts +20 -0
- package/packages/prompts/src/prompts/fileSystem/formatMultipleFiles.test.ts +54 -0
- package/packages/prompts/src/prompts/fileSystem/formatMultipleFiles.ts +9 -0
- package/packages/prompts/src/prompts/fileSystem/formatRenameResult.test.ts +35 -0
- package/packages/prompts/src/prompts/fileSystem/formatRenameResult.ts +17 -0
- package/packages/prompts/src/prompts/fileSystem/formatWriteResult.test.ts +30 -0
- package/packages/prompts/src/prompts/fileSystem/formatWriteResult.ts +11 -0
- package/packages/prompts/src/prompts/fileSystem/index.ts +13 -0
- package/packages/prompts/src/prompts/index.ts +1 -0
- package/packages/prompts/src/prompts/userMemory/__snapshots__/index.test.ts.snap +14 -38
- package/packages/prompts/src/prompts/userMemory/index.ts +5 -24
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/Actions.tsx +4 -3
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/useDropdownMenu.tsx +12 -2
- package/src/app/[variants]/(main)/community/(detail)/assistant/index.tsx +1 -1
- package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/AddGroupAgent.tsx +69 -17
- package/src/app/[variants]/(main)/community/(detail)/mcp/index.tsx +1 -1
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/Actions.tsx +4 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/useDropdownMenu.tsx +12 -2
- package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -2
- package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +13 -3
- package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +26 -3
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +8 -2
- package/src/features/ResourceManager/components/Header/AddButton.tsx +20 -3
- package/src/server/routers/lambda/__tests__/agentGroup.test.ts +1 -0
- package/src/server/routers/lambda/agentGroup.ts +22 -0
- package/src/services/chat/index.ts +1 -0
- package/src/services/chat/mecha/agentConfigResolver.test.ts +62 -45
- package/src/services/chat/mecha/agentConfigResolver.ts +77 -10
- package/src/services/chat/mecha/modelParamsResolver.test.ts +211 -0
- package/src/services/chatGroup/index.ts +14 -0
- package/src/store/agentGroup/action.ts +30 -0
- package/src/store/agentGroup/slices/lifecycle.test.ts +77 -18
- package/src/store/agentGroup/slices/lifecycle.ts +7 -9
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -2
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +124 -0
- package/src/store/chat/slices/operation/selectors.ts +22 -0
|
@@ -800,4 +800,215 @@ describe('resolveModelExtendParams', () => {
|
|
|
800
800
|
expect(modelExtendParamsSpy).toHaveBeenCalledWith('test-model', 'test-provider');
|
|
801
801
|
});
|
|
802
802
|
});
|
|
803
|
+
|
|
804
|
+
describe('parameter precedence and conflicts', () => {
|
|
805
|
+
beforeEach(() => {
|
|
806
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'isModelHasExtendParams').mockReturnValue(
|
|
807
|
+
() => true,
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe('reasoning effort variants precedence', () => {
|
|
812
|
+
it('should give precedence to later reasoning effort variants when multiple are configured', () => {
|
|
813
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
814
|
+
'reasoningEffort',
|
|
815
|
+
'gpt5ReasoningEffort',
|
|
816
|
+
'gpt5_1ReasoningEffort',
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
const result = resolveModelExtendParams({
|
|
820
|
+
chatConfig: {
|
|
821
|
+
gpt5_1ReasoningEffort: 'high',
|
|
822
|
+
gpt5ReasoningEffort: 'medium',
|
|
823
|
+
reasoningEffort: 'low',
|
|
824
|
+
} as any,
|
|
825
|
+
model: 'gpt-5.1',
|
|
826
|
+
provider: 'openai',
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// gpt5_1ReasoningEffort should win as it's processed last
|
|
830
|
+
expect(result.reasoning_effort).toBe('high');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should handle mixed reasoning effort variants with only some configured', () => {
|
|
834
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
835
|
+
'reasoningEffort',
|
|
836
|
+
'gpt5ReasoningEffort',
|
|
837
|
+
'gpt5_2ReasoningEffort',
|
|
838
|
+
'gpt5_2ProReasoningEffort',
|
|
839
|
+
]);
|
|
840
|
+
|
|
841
|
+
const result = resolveModelExtendParams({
|
|
842
|
+
chatConfig: {
|
|
843
|
+
gpt5_2ProReasoningEffort: undefined,
|
|
844
|
+
gpt5_2ReasoningEffort: 'medium',
|
|
845
|
+
gpt5ReasoningEffort: undefined,
|
|
846
|
+
reasoningEffort: 'low',
|
|
847
|
+
} as any,
|
|
848
|
+
model: 'gpt-5.2',
|
|
849
|
+
provider: 'openai',
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// gpt5_2ReasoningEffort should be set, others are undefined
|
|
853
|
+
expect(result.reasoning_effort).toBe('medium');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should use the last supported variant in processing order', () => {
|
|
857
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
858
|
+
'reasoningEffort',
|
|
859
|
+
'gpt5_2ProReasoningEffort',
|
|
860
|
+
]);
|
|
861
|
+
|
|
862
|
+
const result = resolveModelExtendParams({
|
|
863
|
+
chatConfig: {
|
|
864
|
+
gpt5_2ProReasoningEffort: 'high',
|
|
865
|
+
reasoningEffort: 'low',
|
|
866
|
+
} as any,
|
|
867
|
+
model: 'gpt-5.2-pro',
|
|
868
|
+
provider: 'openai',
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// gpt5_2ProReasoningEffort is processed after reasoningEffort
|
|
872
|
+
expect(result.reasoning_effort).toBe('high');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
describe('thinking configuration conflicts', () => {
|
|
877
|
+
it('should allow thinking type param to overwrite enableReasoning thinking config', () => {
|
|
878
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
879
|
+
'enableReasoning',
|
|
880
|
+
'thinking',
|
|
881
|
+
]);
|
|
882
|
+
|
|
883
|
+
const result = resolveModelExtendParams({
|
|
884
|
+
chatConfig: {
|
|
885
|
+
enableReasoning: true,
|
|
886
|
+
reasoningBudgetToken: 2048,
|
|
887
|
+
thinking: 'extended',
|
|
888
|
+
} as any,
|
|
889
|
+
model: 'model',
|
|
890
|
+
provider: 'provider',
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// thinking param overwrites enableReasoning's thinking config
|
|
894
|
+
expect(result.thinking).toEqual({
|
|
895
|
+
type: 'extended',
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('should handle reasoningBudgetToken with thinking type param', () => {
|
|
900
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
901
|
+
'reasoningBudgetToken',
|
|
902
|
+
'thinking',
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
const result = resolveModelExtendParams({
|
|
906
|
+
chatConfig: {
|
|
907
|
+
reasoningBudgetToken: 4096,
|
|
908
|
+
thinking: 'basic',
|
|
909
|
+
} as any,
|
|
910
|
+
model: 'model',
|
|
911
|
+
provider: 'provider',
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// thinking param should overwrite the entire thinking config
|
|
915
|
+
expect(result.thinking).toEqual({
|
|
916
|
+
type: 'basic',
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('should combine independent thinking params without conflict', () => {
|
|
921
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
922
|
+
'thinking',
|
|
923
|
+
'thinkingBudget',
|
|
924
|
+
'thinkingLevel',
|
|
925
|
+
]);
|
|
926
|
+
|
|
927
|
+
const result = resolveModelExtendParams({
|
|
928
|
+
chatConfig: {
|
|
929
|
+
thinking: 'enabled',
|
|
930
|
+
thinkingBudget: 5000,
|
|
931
|
+
thinkingLevel: 'advanced',
|
|
932
|
+
} as any,
|
|
933
|
+
model: 'model',
|
|
934
|
+
provider: 'provider',
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// These are independent params and should all be set
|
|
938
|
+
expect(result.thinking).toEqual({ type: 'enabled' });
|
|
939
|
+
expect(result.thinkingBudget).toBe(5000);
|
|
940
|
+
expect(result.thinkingLevel).toBe('advanced');
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe('complex multi-parameter scenarios', () => {
|
|
945
|
+
it('should handle all reasoning variants with context caching and verbosity', () => {
|
|
946
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
947
|
+
'enableReasoning',
|
|
948
|
+
'reasoningEffort',
|
|
949
|
+
'gpt5ReasoningEffort',
|
|
950
|
+
'disableContextCaching',
|
|
951
|
+
'textVerbosity',
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
const result = resolveModelExtendParams({
|
|
955
|
+
chatConfig: {
|
|
956
|
+
disableContextCaching: true,
|
|
957
|
+
enableReasoning: true,
|
|
958
|
+
gpt5ReasoningEffort: 'high',
|
|
959
|
+
reasoningBudgetToken: 3000,
|
|
960
|
+
reasoningEffort: 'medium',
|
|
961
|
+
textVerbosity: 'verbose',
|
|
962
|
+
} as any,
|
|
963
|
+
model: 'gpt-5',
|
|
964
|
+
provider: 'openai',
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
expect(result).toEqual({
|
|
968
|
+
enabledContextCaching: false,
|
|
969
|
+
reasoning_effort: 'high',
|
|
970
|
+
thinking: {
|
|
971
|
+
budget_tokens: 3000,
|
|
972
|
+
type: 'enabled',
|
|
973
|
+
},
|
|
974
|
+
verbosity: 'verbose',
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should handle all params when none are configured', () => {
|
|
979
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
980
|
+
'enableReasoning',
|
|
981
|
+
'reasoningEffort',
|
|
982
|
+
'textVerbosity',
|
|
983
|
+
'thinking',
|
|
984
|
+
'thinkingBudget',
|
|
985
|
+
'thinkingLevel',
|
|
986
|
+
'urlContext',
|
|
987
|
+
'imageAspectRatio',
|
|
988
|
+
'imageResolution',
|
|
989
|
+
'disableContextCaching',
|
|
990
|
+
]);
|
|
991
|
+
|
|
992
|
+
const result = resolveModelExtendParams({
|
|
993
|
+
chatConfig: {} as any,
|
|
994
|
+
model: 'model',
|
|
995
|
+
provider: 'provider',
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// Only enableReasoning should set thinking to disabled, others should be undefined
|
|
999
|
+
expect(result.thinking).toEqual({
|
|
1000
|
+
budget_tokens: 0,
|
|
1001
|
+
type: 'disabled',
|
|
1002
|
+
});
|
|
1003
|
+
expect(result.reasoning_effort).toBeUndefined();
|
|
1004
|
+
expect(result.verbosity).toBeUndefined();
|
|
1005
|
+
expect(result.thinkingBudget).toBeUndefined();
|
|
1006
|
+
expect(result.thinkingLevel).toBeUndefined();
|
|
1007
|
+
expect(result.urlContext).toBeUndefined();
|
|
1008
|
+
expect(result.imageAspectRatio).toBeUndefined();
|
|
1009
|
+
expect(result.imageResolution).toBeUndefined();
|
|
1010
|
+
expect(result.enabledContextCaching).toBeUndefined();
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
803
1014
|
});
|
|
@@ -20,6 +20,18 @@ export interface GroupMemberConfig {
|
|
|
20
20
|
title?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface SupervisorConfig {
|
|
24
|
+
avatar?: string;
|
|
25
|
+
backgroundColor?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
params?: any;
|
|
29
|
+
provider?: string;
|
|
30
|
+
systemRole?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
title?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
class ChatGroupService {
|
|
24
36
|
/**
|
|
25
37
|
* Create a group with a supervisor agent.
|
|
@@ -42,6 +54,7 @@ class ChatGroupService {
|
|
|
42
54
|
createGroupWithMembers = (
|
|
43
55
|
groupConfig: Omit<NewChatGroup, 'userId'>,
|
|
44
56
|
members: GroupMemberConfig[],
|
|
57
|
+
supervisorConfig?: SupervisorConfig,
|
|
45
58
|
): Promise<{ agentIds: string[]; groupId: string; supervisorAgentId: string }> => {
|
|
46
59
|
return lambdaClient.group.createGroupWithMembers.mutate({
|
|
47
60
|
groupConfig: {
|
|
@@ -49,6 +62,7 @@ class ChatGroupService {
|
|
|
49
62
|
config: groupConfig.config as any,
|
|
50
63
|
},
|
|
51
64
|
members: members as Partial<AgentItem>[],
|
|
65
|
+
supervisorConfig,
|
|
52
66
|
});
|
|
53
67
|
};
|
|
54
68
|
|
|
@@ -48,6 +48,12 @@ export interface ChatGroupInternalAction {
|
|
|
48
48
|
type: string;
|
|
49
49
|
},
|
|
50
50
|
) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch group detail directly and update store.
|
|
53
|
+
* Unlike refreshGroupDetail which uses SWR mutate, this method fetches immediately
|
|
54
|
+
* and is useful when SWR hook is not yet mounted (e.g., after createGroup).
|
|
55
|
+
*/
|
|
56
|
+
internal_fetchGroupDetail: (groupId: string) => Promise<void>;
|
|
51
57
|
internal_updateGroupMaps: (groups: ChatGroupItem[]) => void;
|
|
52
58
|
loadGroups: () => Promise<void>;
|
|
53
59
|
refreshGroupDetail: (groupId: string) => Promise<void>;
|
|
@@ -91,6 +97,30 @@ const chatGroupInternalSlice: StateCreator<
|
|
|
91
97
|
return {
|
|
92
98
|
internal_dispatchChatGroup: dispatch,
|
|
93
99
|
|
|
100
|
+
internal_fetchGroupDetail: async (groupId: string) => {
|
|
101
|
+
const groupDetail = await chatGroupService.getGroupDetail(groupId);
|
|
102
|
+
if (!groupDetail) return;
|
|
103
|
+
|
|
104
|
+
// Update groupMap with full group detail including supervisorAgentId and agents
|
|
105
|
+
dispatch({ payload: { id: groupDetail.id, value: groupDetail }, type: 'updateGroup' });
|
|
106
|
+
|
|
107
|
+
// Sync group agents to agentStore for builtin agent resolution
|
|
108
|
+
const agentStore = getAgentStoreState();
|
|
109
|
+
for (const agent of groupDetail.agents) {
|
|
110
|
+
agentStore.internal_dispatchAgentMap(agent.id, agent as any);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Set activeAgentId to supervisor for correct model resolution
|
|
114
|
+
if (groupDetail.supervisorAgentId) {
|
|
115
|
+
agentStore.setActiveAgentId(groupDetail.supervisorAgentId);
|
|
116
|
+
useChatStore.setState(
|
|
117
|
+
{ activeAgentId: groupDetail.supervisorAgentId },
|
|
118
|
+
false,
|
|
119
|
+
'syncActiveAgentIdFromAgentGroup',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
94
124
|
internal_updateGroupMaps: (groups) => {
|
|
95
125
|
// Build a candidate map from incoming groups
|
|
96
126
|
const incomingMap = groups.reduce(
|
|
@@ -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/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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.
|
|
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
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
56
|
-
await
|
|
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
|
-
|
|
57
|
+
switchToGroup(group.id);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
return group.id;
|
|
@@ -160,14 +160,16 @@ export const streamingExecutor: StateCreator<
|
|
|
160
160
|
// - agentId is used for session ID (message storage location)
|
|
161
161
|
const effectiveAgentId = paramSubAgentId || agentId;
|
|
162
162
|
|
|
163
|
-
// Get scope from operation context if available
|
|
163
|
+
// Get scope and groupId from operation context if available
|
|
164
164
|
const operation = operationId ? get().operations[operationId] : undefined;
|
|
165
165
|
const scope = operation?.context.scope;
|
|
166
|
+
const groupId = operation?.context.groupId;
|
|
166
167
|
|
|
167
168
|
// Resolve agent config with builtin agent runtime config merged
|
|
168
169
|
// This ensures runtime plugins (e.g., 'lobe-agent-builder' for Agent Builder) are included
|
|
169
170
|
const { agentConfig: agentConfigData, plugins: pluginIds } = resolveAgentConfig({
|
|
170
171
|
agentId: effectiveAgentId || '',
|
|
172
|
+
groupId, // Pass groupId for supervisor detection
|
|
171
173
|
scope, // Pass scope from operation context
|
|
172
174
|
});
|
|
173
175
|
|
|
@@ -341,6 +343,7 @@ export const streamingExecutor: StateCreator<
|
|
|
341
343
|
// - max_tokens/reasoning_effort based on chatConfig settings
|
|
342
344
|
const resolved = resolveAgentConfig({
|
|
343
345
|
agentId: effectiveAgentId,
|
|
346
|
+
groupId, // Pass groupId for supervisor detection
|
|
344
347
|
scope, // scope is already available from line 329
|
|
345
348
|
});
|
|
346
349
|
const finalAgentConfig = agentConfig || resolved.agentConfig;
|
|
@@ -594,7 +597,8 @@ export const streamingExecutor: StateCreator<
|
|
|
594
597
|
// - max_tokens/reasoning_effort based on chatConfig settings
|
|
595
598
|
const { agentConfig: agentConfigData } = resolveAgentConfig({
|
|
596
599
|
agentId: effectiveAgentId || '',
|
|
597
|
-
|
|
600
|
+
groupId, // Pass groupId for supervisor detection
|
|
601
|
+
scope: context.scope, // Pass scope from context parameter
|
|
598
602
|
});
|
|
599
603
|
|
|
600
604
|
// Use agent config from agentId
|
|
@@ -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());
|