mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: fix group broadcast trigger tool use (#11646)
* fix broadcast issue * fix broadcast * fix broadcast * fix group slug
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -235,13 +235,14 @@ export const createGroupOrchestrationExecutors = (
|
||||
parallel_call_agents: async (instruction, state): Promise<GroupOrchestrationExecutorOutput> => {
|
||||
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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<string, unknown> = {};
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user