🐛 fix: fix group broadcast trigger tool use (#11646)

* fix broadcast issue

* fix broadcast

* fix broadcast

* fix group slug
This commit is contained in:
Arvin Xu
2026-01-22 17:37:09 +08:00
committed by GitHub
parent ad32a61704
commit 831a9b34f9
8 changed files with 415 additions and 68 deletions

View File

@@ -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,
},

View File

@@ -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',
},

View File

@@ -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

View File

@@ -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);
});
});
});

View File

@@ -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,
};
};

View File

@@ -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

View File

@@ -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());

View File

@@ -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,