From 831a9b34f9497d26c3cebdd38aed2b6b637d236b Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 22 Jan 2026 17:37:09 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20group=20broadcast?= =?UTF-8?q?=20trigger=20tool=20use=20(#11646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix broadcast issue * fix broadcast * fix broadcast * fix group slug --- .../GroupOrchestrationSupervisor.ts | 2 + .../GroupOrchestrationSupervisor.test.ts | 4 +- .../src/groupOrchestration/types.ts | 5 + .../chat/mecha/agentConfigResolver.test.ts | 197 ++++++++++++++++++ .../chat/mecha/agentConfigResolver.ts | 57 +++-- .../createGroupOrchestrationExecutors.ts | 77 +++---- .../__tests__/streamingExecutor.test.ts | 78 +++++++ .../aiChat/actions/streamingExecutor.ts | 63 ++++-- 8 files changed, 415 insertions(+), 68 deletions(-) diff --git a/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts b/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts index 0fbb05bfab..da9a50339c 100644 --- a/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts +++ b/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts @@ -76,6 +76,8 @@ export class GroupOrchestrationSupervisor implements IGroupOrchestrationSupervis return { payload: { agentIds: params.agentIds as string[], + // Broadcast agents should not call tools by default + disableTools: true, instruction: params.instruction as string | undefined, toolMessageId: params.toolMessageId as string, }, diff --git a/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts b/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts index cf6afd0404..c7d65593cc 100644 --- a/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts +++ b/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts @@ -96,7 +96,7 @@ describe('GroupOrchestrationSupervisor', () => { }); }); - it('should return parallel_call_agents instruction for broadcast decision', async () => { + it('should return parallel_call_agents instruction for broadcast decision with disableTools: true', async () => { const supervisor = new GroupOrchestrationSupervisor(defaultConfig); const state = createMockState(); @@ -119,6 +119,8 @@ describe('GroupOrchestrationSupervisor', () => { type: 'parallel_call_agents', payload: { agentIds: ['agent-1', 'agent-2'], + // Broadcast agents should have tools disabled by default + disableTools: true, instruction: 'Discuss', toolMessageId: 'tool-msg-1', }, diff --git a/packages/agent-runtime/src/groupOrchestration/types.ts b/packages/agent-runtime/src/groupOrchestration/types.ts index 153d45ffa8..9081c18a57 100644 --- a/packages/agent-runtime/src/groupOrchestration/types.ts +++ b/packages/agent-runtime/src/groupOrchestration/types.ts @@ -32,6 +32,11 @@ export interface SupervisorInstructionCallAgent { export interface SupervisorInstructionParallelCallAgents { payload: { agentIds: string[]; + /** + * Whether to disable tools for broadcast agents + * When true, agents will respond without calling any tools + */ + disableTools?: boolean; instruction?: string; /** * The tool message ID that triggered the broadcast diff --git a/src/services/chat/mecha/agentConfigResolver.test.ts b/src/services/chat/mecha/agentConfigResolver.test.ts index 4d13b2d82a..a3859e6f48 100644 --- a/src/services/chat/mecha/agentConfigResolver.test.ts +++ b/src/services/chat/mecha/agentConfigResolver.test.ts @@ -651,6 +651,96 @@ describe('resolveAgentConfig', () => { ); }); + describe('supervisor with own slug (priority check)', () => { + // This tests the fix for LOBE-4127: When supervisor agent has its own slug, + // it should still use 'group-supervisor' slug when in group scope + + it('should use group-supervisor slug even when agent has its own slug in group scope', () => { + // Supervisor agent has its own slug (e.g., from being a builtin agent) + vi.spyOn(agentSelectors.agentSelectors, 'getAgentSlugById').mockReturnValue( + () => 'some-agent-slug', + ); + + // Mock: groupById returns the group + vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue( + () => mockGroupWithSupervisor as any, + ); + + vi.spyOn(agentGroupSelectors.agentGroupSelectors, 'getGroupMembers').mockReturnValue( + () => + [ + { id: 'member-agent-1', title: 'Agent 1' }, + { id: 'member-agent-2', title: 'Agent 2' }, + ] as any, + ); + + vi.spyOn(builtinAgents, 'getAgentRuntimeConfig').mockReturnValue({ + chatConfig: { enableHistoryCount: false }, + plugins: [GroupManagementIdentifier, GTDIdentifier], + systemRole: 'You are a group supervisor...', + }); + + const result = resolveAgentConfig({ + agentId: 'supervisor-agent-id', + groupId: 'group-123', + scope: 'group', // Key: must be 'group' scope + }); + + // Should use group-supervisor, NOT the agent's own slug + expect(result.isBuiltinAgent).toBe(true); + expect(result.slug).toBe('group-supervisor'); + expect(result.plugins).toContain(GroupManagementIdentifier); + }); + + it('should use agent own slug when scope is not group', () => { + // Supervisor agent has its own slug + vi.spyOn(agentSelectors.agentSelectors, 'getAgentSlugById').mockReturnValue( + () => 'some-agent-slug', + ); + + // Mock: groupById returns the group + vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue( + () => mockGroupWithSupervisor as any, + ); + + vi.spyOn(builtinAgents, 'getAgentRuntimeConfig').mockReturnValue({ + plugins: ['agent-specific-plugin'], + systemRole: 'Agent specific system role', + }); + + const result = resolveAgentConfig({ + agentId: 'supervisor-agent-id', + groupId: 'group-123', + scope: 'main', // Not 'group' scope + }); + + // Should use agent's own slug since scope is not 'group' + expect(result.isBuiltinAgent).toBe(true); + expect(result.slug).toBe('some-agent-slug'); + }); + + it('should use agent own slug when groupId is not provided', () => { + // Supervisor agent has its own slug + vi.spyOn(agentSelectors.agentSelectors, 'getAgentSlugById').mockReturnValue( + () => 'some-agent-slug', + ); + + vi.spyOn(builtinAgents, 'getAgentRuntimeConfig').mockReturnValue({ + plugins: ['agent-specific-plugin'], + systemRole: 'Agent specific system role', + }); + + const result = resolveAgentConfig({ + agentId: 'supervisor-agent-id', + scope: 'group', // Even with group scope, no groupId + }); + + // Should use agent's own slug since no groupId + expect(result.isBuiltinAgent).toBe(true); + expect(result.slug).toBe('some-agent-slug'); + }); + }); + it('should detect supervisor agent using groupId for direct lookup', () => { // Mock: groupById returns the group vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue( @@ -676,6 +766,7 @@ describe('resolveAgentConfig', () => { const result = resolveAgentConfig({ agentId: 'supervisor-agent-id', groupId: 'group-123', + scope: 'group', // Required: supervisor detection only works in group scope }); expect(result.isBuiltinAgent).toBe(true); @@ -710,6 +801,7 @@ describe('resolveAgentConfig', () => { resolveAgentConfig({ agentId: 'supervisor-agent-id', groupId: 'group-123', + scope: 'group', // Required: supervisor detection only works in group scope }); expect(getAgentRuntimeConfigSpy).toHaveBeenCalledWith( @@ -789,6 +881,7 @@ describe('resolveAgentConfig', () => { const result = resolveAgentConfig({ agentId: 'supervisor-agent-id', groupId: 'group-123', + scope: 'group', // Required: supervisor detection only works in group scope }); // Should correctly identify as builtin supervisor agent @@ -913,4 +1006,108 @@ describe('resolveAgentConfig', () => { expect(result.plugins).toContain('lobe-gtd'); }); }); + + describe('disableTools (broadcast scenario)', () => { + beforeEach(() => { + vi.spyOn(agentSelectors.agentSelectors, 'getAgentSlugById').mockReturnValue(() => undefined); + }); + + it('should return empty plugins when disableTools is true for regular agent', () => { + vi.spyOn(agentSelectors.agentSelectors, 'getAgentConfigById').mockReturnValue( + () => + ({ + ...mockAgentConfig, + plugins: ['plugin-a', 'plugin-b', 'lobe-gtd'], + }) as any, + ); + + const result = resolveAgentConfig({ + agentId: 'test-agent', + disableTools: true, + }); + + expect(result.plugins).toEqual([]); + }); + + it('should keep plugins when disableTools is false', () => { + const result = resolveAgentConfig({ + agentId: 'test-agent', + disableTools: false, + }); + + expect(result.plugins).toEqual(['plugin-a', 'plugin-b']); + }); + + it('should keep plugins when disableTools is undefined', () => { + const result = resolveAgentConfig({ agentId: 'test-agent' }); + + expect(result.plugins).toEqual(['plugin-a', 'plugin-b']); + }); + + it('should return empty plugins for builtin agent when disableTools is true', () => { + vi.spyOn(agentSelectors.agentSelectors, 'getAgentSlugById').mockReturnValue( + () => 'some-builtin-slug', + ); + vi.spyOn(builtinAgents, 'getAgentRuntimeConfig').mockReturnValue({ + plugins: ['runtime-plugin-1', 'runtime-plugin-2'], + systemRole: 'Runtime system role', + }); + + const result = resolveAgentConfig({ + agentId: 'builtin-agent', + disableTools: true, + }); + + expect(result.plugins).toEqual([]); + expect(result.isBuiltinAgent).toBe(true); + }); + + it('should return empty plugins in page scope when disableTools is true', () => { + vi.spyOn(builtinAgents, 'getAgentRuntimeConfig').mockReturnValue({ + plugins: [PageAgentIdentifier], + systemRole: 'Page agent system role', + }); + + const result = resolveAgentConfig({ + agentId: 'test-agent', + disableTools: true, + scope: 'page', + }); + + // disableTools should override page scope injection + expect(result.plugins).toEqual([]); + }); + + it('should take precedence over isSubTask filtering', () => { + vi.spyOn(agentSelectors.agentSelectors, 'getAgentConfigById').mockReturnValue( + () => + ({ + ...mockAgentConfig, + plugins: ['lobe-gtd', 'plugin-a'], + }) as any, + ); + + const result = resolveAgentConfig({ + agentId: 'test-agent', + disableTools: true, + isSubTask: true, + }); + + // disableTools should result in empty plugins regardless of isSubTask + expect(result.plugins).toEqual([]); + }); + + it('should preserve agentConfig and chatConfig when disableTools is true', () => { + const result = resolveAgentConfig({ + agentId: 'test-agent', + disableTools: true, + }); + + // Only plugins should be empty, other config should be preserved + expect(result.plugins).toEqual([]); + expect(result.agentConfig).toEqual(mockAgentConfig); + expect(result.chatConfig).toEqual(mockChatConfig); + expect(result.isBuiltinAgent).toBe(false); + }); + }); }); diff --git a/src/services/chat/mecha/agentConfigResolver.ts b/src/services/chat/mecha/agentConfigResolver.ts index c2293a6d88..7cd9acf6fe 100644 --- a/src/services/chat/mecha/agentConfigResolver.ts +++ b/src/services/chat/mecha/agentConfigResolver.ts @@ -51,6 +51,12 @@ export interface AgentConfigResolverContext { /** Agent ID to resolve config for */ agentId: string; + /** + * Whether to disable all tools for this agent execution. + * When true, returns empty plugins array (used for broadcast scenarios). + */ + disableTools?: boolean; + // Builtin agent specific context /** Document content for page-agent */ documentContent?: string; @@ -112,13 +118,27 @@ export interface ResolvedAgentConfig { * For regular agents, this simply returns the config from the store. */ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAgentConfig => { - const { agentId, model, documentContent, plugins, targetAgentConfig, isSubTask } = ctx; + const { agentId, model, documentContent, plugins, targetAgentConfig, isSubTask, disableTools } = + ctx; - log('resolveAgentConfig called with agentId: %s, scope: %s, isSubTask: %s', agentId, ctx.scope, isSubTask); + log( + 'resolveAgentConfig called with agentId: %s, scope: %s, isSubTask: %s, disableTools: %s', + agentId, + ctx.scope, + isSubTask, + disableTools, + ); - // Helper to filter out lobe-gtd in sub-task context to prevent nested sub-task creation - const applySubTaskFilter = (pluginIds: string[]) => - isSubTask ? pluginIds.filter((id) => id !== 'lobe-gtd') : pluginIds; + // Helper to apply plugin filters: + // 1. If disableTools is true, return empty array (for broadcast scenarios) + // 2. If isSubTask is true, filter out lobe-gtd to prevent nested sub-task creation + const applyPluginFilters = (pluginIds: string[]) => { + if (disableTools) { + log('disableTools is true, returning empty plugins'); + return []; + } + return isSubTask ? pluginIds.filter((id) => id !== 'lobe-gtd') : pluginIds; + }; const agentStoreState = getAgentStoreState(); @@ -130,18 +150,18 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge const basePlugins = agentConfig.plugins ?? []; // Check if this is a builtin agent - // First check agent store, then check if this is a supervisor agent via groupId - let slug = agentSelectors.getAgentSlugById(agentId)(agentStoreState); - log('slug from agentStore: %s (agentId: %s)', slug, agentId); + // Priority: supervisor check (when in group scope) > agent store slug + let slug: string | undefined; - // If not found in agent store, check if this is a supervisor agent using groupId - // This is more reliable than iterating all groups to find a match - if (!slug && ctx.groupId) { + // IMPORTANT: When in group scope with groupId, check if this agent is the group's supervisor FIRST + // This takes priority because supervisor needs special group-supervisor behavior, + // even if the agent has its own slug + if (ctx.groupId && ctx.scope === 'group') { const groupStoreState = getChatGroupStoreState(); const group = agentGroupByIdSelectors.groupById(ctx.groupId)(groupStoreState); log( - 'checking supervisor via groupId %s: group=%o', + 'checking supervisor FIRST (scope=group): groupId=%s, group=%O, agentId=%s', ctx.groupId, group ? { @@ -150,6 +170,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge title: group.title, } : null, + agentId, ); // Check if this agent is the supervisor of the specified group @@ -164,6 +185,12 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge } } + // If not identified as supervisor, check agent store for slug + if (!slug) { + slug = agentSelectors.getAgentSlugById(agentId)(agentStoreState) ?? undefined; + log('slug from agentStore: %s (agentId: %s)', slug, agentId); + } + if (!slug) { log('agentId %s is not a builtin agent (no slug found)', agentId); // Regular agent - use provided plugins if available, fallback to agent's plugins @@ -209,7 +236,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge agentConfig: finalAgentConfig, chatConfig: finalChatConfig, isBuiltinAgent: false, - plugins: applySubTaskFilter(pageAgentPlugins), + plugins: applyPluginFilters(pageAgentPlugins), }; } @@ -218,7 +245,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge agentConfig: finalAgentConfig, chatConfig: finalChatConfig, isBuiltinAgent: false, - plugins: applySubTaskFilter(finalPlugins), + plugins: applyPluginFilters(finalPlugins), }; } @@ -339,7 +366,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge agentConfig: finalAgentConfig, chatConfig: resolvedChatConfig, isBuiltinAgent: true, - plugins: applySubTaskFilter(finalPlugins), + plugins: applyPluginFilters(finalPlugins), slug, }; }; diff --git a/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts b/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts index edaf42be69..17d4441295 100644 --- a/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +++ b/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts @@ -235,13 +235,14 @@ export const createGroupOrchestrationExecutors = ( parallel_call_agents: async (instruction, state): Promise => { const { agentIds, + disableTools, instruction: agentInstruction, toolMessageId, } = (instruction as SupervisorInstructionParallelCallAgents).payload; const sessionLogId = `${state.operationId}:parallel_call_agents`; log( - `[${sessionLogId}] Broadcasting to agents: ${agentIds.join(', ')}, instruction: ${agentInstruction}, toolMessageId: ${toolMessageId}`, + `[${sessionLogId}] Broadcasting to agents: ${agentIds.join(', ')}, instruction: ${agentInstruction}, toolMessageId: ${toolMessageId}, disableTools: ${disableTools}`, ); const messages = getMessages(); @@ -279,10 +280,12 @@ export const createGroupOrchestrationExecutors = ( // - messageContext keeps the group's main conversation context (for message storage) // - subAgentId specifies which agent's config to use for each agent // - toolMessageId is used as parentMessageId so agent responses are children of the tool message + // - disableTools prevents broadcast agents from calling tools (expected behavior for broadcast) await Promise.all( agentIds.map(async (agentId) => { await get().internal_execAgentRuntime({ context: { ...messageContext, subAgentId: agentId }, + disableTools, messages: messagesWithInstruction, parentMessageId: toolMessageId, parentMessageType: 'tool', @@ -776,48 +779,48 @@ export const createGroupOrchestrationExecutors = ( } switch (status.status) { - case 'completed': { - tracker.status = 'completed'; - tracker.result = status.result; - log(`[${taskLogId}] Task completed successfully`); - if (status.result) { + case 'completed': { + tracker.status = 'completed'; + tracker.result = status.result; + log(`[${taskLogId}] Task completed successfully`); + if (status.result) { + await get().optimisticUpdateMessageContent( + tracker.taskMessageId, + status.result, + undefined, + { operationId: state.operationId }, + ); + } + + break; + } + case 'failed': { + tracker.status = 'failed'; + tracker.error = status.error; + console.error(`[${taskLogId}] Task failed: ${status.error}`); await get().optimisticUpdateMessageContent( tracker.taskMessageId, - status.result, + `Task failed: ${status.error}`, undefined, { operationId: state.operationId }, ); + + break; } - - break; - } - case 'failed': { - tracker.status = 'failed'; - tracker.error = status.error; - console.error(`[${taskLogId}] Task failed: ${status.error}`); - await get().optimisticUpdateMessageContent( - tracker.taskMessageId, - `Task failed: ${status.error}`, - undefined, - { operationId: state.operationId }, - ); - - break; - } - case 'cancel': { - tracker.status = 'failed'; - tracker.error = 'Task was cancelled'; - log(`[${taskLogId}] Task was cancelled`); - await get().optimisticUpdateMessageContent( - tracker.taskMessageId, - 'Task was cancelled', - undefined, - { operationId: state.operationId }, - ); - - break; - } - // No default + case 'cancel': { + tracker.status = 'failed'; + tracker.error = 'Task was cancelled'; + log(`[${taskLogId}] Task was cancelled`); + await get().optimisticUpdateMessageContent( + tracker.taskMessageId, + 'Task was cancelled', + undefined, + { operationId: state.operationId }, + ); + + break; + } + // No default } // Check individual task timeout diff --git a/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts b/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts index 05a12528c4..49a9783eb9 100644 --- a/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +++ b/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts @@ -1451,6 +1451,84 @@ describe('StreamingExecutor actions', () => { }); }); + describe('internal_createAgentState with disableTools', () => { + it('should return empty toolManifestMap when disableTools is true', async () => { + act(() => { + useChatStore.setState({ internal_execAgentRuntime: realExecAgentRuntime }); + }); + + const { result } = renderHook(() => useChatStore()); + const userMessage = { + id: TEST_IDS.USER_MESSAGE_ID, + role: 'user', + content: TEST_CONTENT.USER_MESSAGE, + sessionId: TEST_IDS.SESSION_ID, + topicId: TEST_IDS.TOPIC_ID, + } as UIChatMessage; + + // Get actual internal_createAgentState result with disableTools: true + const { state } = result.current.internal_createAgentState({ + messages: [userMessage], + parentMessageId: userMessage.id, + agentId: TEST_IDS.SESSION_ID, + topicId: TEST_IDS.TOPIC_ID, + disableTools: true, + }); + + // toolManifestMap should be empty when disableTools is true + expect(state.toolManifestMap).toEqual({}); + }); + + it('should include tools in toolManifestMap when disableTools is false or undefined', async () => { + act(() => { + useChatStore.setState({ internal_execAgentRuntime: realExecAgentRuntime }); + }); + + const { result } = renderHook(() => useChatStore()); + const userMessage = { + id: TEST_IDS.USER_MESSAGE_ID, + role: 'user', + content: TEST_CONTENT.USER_MESSAGE, + sessionId: TEST_IDS.SESSION_ID, + topicId: TEST_IDS.TOPIC_ID, + } as UIChatMessage; + + // Mock resolveAgentConfig to return plugins + vi.spyOn(agentConfigResolver, 'resolveAgentConfig').mockReturnValue({ + agentConfig: { + ...createMockAgentConfig(), + plugins: ['test-plugin'], + }, + chatConfig: createMockChatConfig(), + isBuiltinAgent: false, + plugins: ['test-plugin'], + }); + + // Get actual internal_createAgentState result without disableTools + const { state: stateWithoutDisable } = result.current.internal_createAgentState({ + messages: [userMessage], + parentMessageId: userMessage.id, + agentId: TEST_IDS.SESSION_ID, + topicId: TEST_IDS.TOPIC_ID, + // disableTools not set (undefined) + }); + + // Get actual internal_createAgentState result with disableTools: false + const { state: stateWithDisableFalse } = result.current.internal_createAgentState({ + messages: [userMessage], + parentMessageId: userMessage.id, + agentId: TEST_IDS.SESSION_ID, + topicId: TEST_IDS.TOPIC_ID, + disableTools: false, + }); + + // Both should have the same toolManifestMap (tools enabled) + // Note: The actual content depends on what plugins are resolved, + // but the key point is they should not be empty (unless no plugins are configured) + expect(stateWithoutDisable.toolManifestMap).toEqual(stateWithDisableFalse.toolManifestMap); + }); + }); + describe('operation status handling', () => { it('should complete operation when state is waiting_for_human', async () => { const { result } = renderHook(() => useChatStore()); diff --git a/src/store/chat/slices/aiChat/actions/streamingExecutor.ts b/src/store/chat/slices/aiChat/actions/streamingExecutor.ts index 45ef9bb34d..ce74a2208d 100644 --- a/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +++ b/src/store/chat/slices/aiChat/actions/streamingExecutor.ts @@ -59,6 +59,11 @@ export interface StreamingExecutorAction { * Explicit agentId for this execution (avoids using global activeAgentId) */ agentId?: string; + /** + * Whether to disable tools for this agent execution + * When true, agent will respond without calling any tools + */ + disableTools?: boolean; /** * Explicit topicId for this execution (avoids using global activeTopicId) */ @@ -117,6 +122,11 @@ export interface StreamingExecutorAction { * Contains agentId, topicId, threadId, groupId, scope, etc. */ context: ConversationContext; + /** + * Whether to disable tools for this agent execution + * When true, agent will respond without calling any tools + */ + disableTools?: boolean; /** * Initial agent runtime context (for resuming execution from a specific phase) */ @@ -156,6 +166,7 @@ export const streamingExecutor: StateCreator< messages, parentMessageId, agentId: paramAgentId, + disableTools, topicId: paramTopicId, threadId, initialState, @@ -181,29 +192,48 @@ export const streamingExecutor: StateCreator< // Resolve agent config with builtin agent runtime config merged // This ensures runtime plugins (e.g., 'lobe-agent-builder' for Agent Builder) are included - // isSubTask is passed to filter out lobe-gtd tools to prevent nested sub-task creation + // - isSubTask: filters out lobe-gtd tools to prevent nested sub-task creation + // - disableTools: clears all plugins for broadcast scenarios const agentConfig = resolveAgentConfig({ agentId: effectiveAgentId || '', + disableTools, // Clear plugins for broadcast scenarios groupId, // Pass groupId for supervisor detection isSubTask, // Filter out lobe-gtd in sub-task context scope, // Pass scope from operation context }); const { agentConfig: agentConfigData, plugins: pluginIds } = agentConfig; - log('[internal_createAgentState] resolved plugins=%o, isSubTask=%s', pluginIds, isSubTask); + log( + '[internal_createAgentState] resolved plugins=%o, isSubTask=%s, disableTools=%s', + pluginIds, + isSubTask, + disableTools, + ); - // Get tools manifest map - const toolsEngine = createAgentToolsEngine({ - model: agentConfigData.model, - provider: agentConfigData.provider!, - }); - const { enabledToolIds } = toolsEngine.generateToolsDetailed({ - model: agentConfigData.model, - provider: agentConfigData.provider!, - toolIds: pluginIds, - }); - const toolManifestMap = Object.fromEntries( - toolsEngine.getEnabledPluginManifests(enabledToolIds).entries(), + // Get tools manifest map (skip if disableTools is true / no plugins) + let toolManifestMap: Record = {}; + let enabledToolIds: string[] = []; + + if (pluginIds.length > 0) { + const toolsEngine = createAgentToolsEngine({ + model: agentConfigData.model, + provider: agentConfigData.provider!, + }); + const toolsDetailed = toolsEngine.generateToolsDetailed({ + model: agentConfigData.model, + provider: agentConfigData.provider!, + toolIds: pluginIds, + }); + enabledToolIds = toolsDetailed.enabledToolIds; + toolManifestMap = Object.fromEntries( + toolsEngine.getEnabledPluginManifests(enabledToolIds).entries(), + ); + } + + log( + '[internal_createAgentState] toolManifestMap keys=%o, count=%d', + Object.keys(toolManifestMap), + Object.keys(toolManifestMap).length, ); // Get user intervention config @@ -549,6 +579,7 @@ export const streamingExecutor: StateCreator< internal_execAgentRuntime: async (params) => { const { + disableTools, messages: originalMessages, parentMessageId, parentMessageType, @@ -588,7 +619,7 @@ export const streamingExecutor: StateCreator< } log( - '[internal_execAgentRuntime] start, operationId: %s, agentId: %s, subAgentId: %s, effectiveAgentId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d', + '[internal_execAgentRuntime] start, operationId: %s, agentId: %s, subAgentId: %s, effectiveAgentId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d, disableTools: %s', operationId, agentId, subAgentId, @@ -598,6 +629,7 @@ export const streamingExecutor: StateCreator< parentMessageId, parentMessageType, originalMessages.length, + disableTools, ); // Create a new array to avoid modifying the original messages @@ -615,6 +647,7 @@ export const streamingExecutor: StateCreator< messages, parentMessageId: params.parentMessageId, agentId, + disableTools, topicId, threadId: threadId ?? undefined, initialState: params.initialState,