mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: fix group subagent task issue (#11589)
* improve WriteFile feeling * refactor exector * improve task title * fix flick * improve i18n * fix tests
This commit is contained in:
@@ -70,7 +70,8 @@
|
||||
"builtins.lobe-group-management.apiName.summarize": "Summarize conversation",
|
||||
"builtins.lobe-group-management.apiName.vote": "Start vote",
|
||||
"builtins.lobe-group-management.inspector.broadcast.title": "Following Agents speak:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.title": "Assigning task to:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.assignTo": "Assign",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.task": "task:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTasks.title": "Assigning tasks to:",
|
||||
"builtins.lobe-group-management.inspector.speak.title": "Designated Agent speaks:",
|
||||
"builtins.lobe-group-management.title": "Group Coordinator",
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
"builtins.lobe-cloud-sandbox.apiName.writeLocalFile": "写入文件",
|
||||
"builtins.lobe-cloud-sandbox.title": "云端沙盒",
|
||||
"builtins.lobe-group-agent-builder.apiName.batchCreateAgents": "批量创建 Agent",
|
||||
"builtins.lobe-group-agent-builder.apiName.createAgent": "创建代理",
|
||||
"builtins.lobe-group-agent-builder.apiName.createAgent": "创建助理",
|
||||
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "获取可用模型",
|
||||
"builtins.lobe-group-agent-builder.apiName.installPlugin": "安装技能",
|
||||
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "邀请成员",
|
||||
"builtins.lobe-group-agent-builder.apiName.removeAgent": "移除成员",
|
||||
"builtins.lobe-group-agent-builder.apiName.searchAgent": "搜索代理",
|
||||
"builtins.lobe-group-agent-builder.apiName.searchAgent": "搜索助理",
|
||||
"builtins.lobe-group-agent-builder.apiName.searchMarketTools": "搜索技能市场",
|
||||
"builtins.lobe-group-agent-builder.apiName.updateAgentConfig": "更新代理配置",
|
||||
"builtins.lobe-group-agent-builder.apiName.updateAgentConfig": "更新助理配置",
|
||||
"builtins.lobe-group-agent-builder.apiName.updateAgentPrompt": "更新助理提示词",
|
||||
"builtins.lobe-group-agent-builder.apiName.updateGroup": "更新群组",
|
||||
"builtins.lobe-group-agent-builder.apiName.updateGroupPrompt": "更新群组提示词",
|
||||
@@ -70,7 +70,8 @@
|
||||
"builtins.lobe-group-management.apiName.summarize": "总结对话",
|
||||
"builtins.lobe-group-management.apiName.vote": "发起投票",
|
||||
"builtins.lobe-group-management.inspector.broadcast.title": "以下 Agent 发言:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.title": "分配任务给:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.assignTo": "分配给",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTask.task": "任务:",
|
||||
"builtins.lobe-group-management.inspector.executeAgentTasks.title": "分配任务给:",
|
||||
"builtins.lobe-group-management.inspector.speak.title": "指定 Agent 发言:",
|
||||
"builtins.lobe-group-management.title": "群组协调",
|
||||
|
||||
@@ -32,6 +32,7 @@ export const ExecuteAgentTaskInspector = memo<BuiltinInspectorProps<ExecuteTaskP
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const agentId = args?.agentId || partialArgs?.agentId;
|
||||
const taskTitle = args?.title || partialArgs?.title;
|
||||
|
||||
// Get active group ID and agent from store
|
||||
const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
|
||||
@@ -42,12 +43,47 @@ export const ExecuteAgentTaskInspector = memo<BuiltinInspectorProps<ExecuteTaskP
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
if (isArgumentsStreaming && !agent) {
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-group-management.apiName.executeAgentTask')}</span>
|
||||
</div>
|
||||
);
|
||||
if (isArgumentsStreaming) {
|
||||
if (!agent && !taskTitle)
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-group-management.apiName.executeAgentTask')}</span>
|
||||
</div>
|
||||
);
|
||||
if (agent) {
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
|
||||
gap={8}
|
||||
horizontal
|
||||
>
|
||||
<span className={styles.title}>
|
||||
{t('builtins.lobe-group-management.inspector.executeAgentTask.assignTo')}
|
||||
</span>
|
||||
{agent && (
|
||||
<>
|
||||
<Avatar
|
||||
avatar={agent.avatar || DEFAULT_AVATAR}
|
||||
background={agent.backgroundColor || theme.colorBgContainer}
|
||||
shape={'square'}
|
||||
size={24}
|
||||
title={agent.title || undefined}
|
||||
/>
|
||||
<span>{agent?.title}</span>
|
||||
</>
|
||||
)}
|
||||
{taskTitle && (
|
||||
<>
|
||||
<span className={styles.title}>
|
||||
{t('builtins.lobe-group-management.inspector.executeAgentTask.task')}
|
||||
</span>
|
||||
<span className={highlightTextStyles.primary}>{taskTitle}</span>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const agentName = agent?.title || agentId;
|
||||
@@ -60,7 +96,7 @@ export const ExecuteAgentTaskInspector = memo<BuiltinInspectorProps<ExecuteTaskP
|
||||
horizontal
|
||||
>
|
||||
<span className={styles.title}>
|
||||
{t('builtins.lobe-group-management.inspector.executeAgentTask.title')}
|
||||
{t('builtins.lobe-group-management.inspector.executeAgentTask.assignTo')}
|
||||
</span>
|
||||
{agent && (
|
||||
<Avatar
|
||||
@@ -71,7 +107,15 @@ export const ExecuteAgentTaskInspector = memo<BuiltinInspectorProps<ExecuteTaskP
|
||||
title={agent.title || undefined}
|
||||
/>
|
||||
)}
|
||||
{agentName && <span className={highlightTextStyles.primary}>{agentName}</span>}
|
||||
{agentName && <span>{agentName}</span>}
|
||||
{taskTitle && (
|
||||
<>
|
||||
<span className={styles.title}>
|
||||
{t('builtins.lobe-group-management.inspector.executeAgentTask.task')}
|
||||
</span>
|
||||
<span className={highlightTextStyles.primary}>{taskTitle}</span>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { BuiltinRenderProps } from '@lobechat/types';
|
||||
import { Avatar, Flexbox, Text } from '@lobehub/ui';
|
||||
import { Flexbox, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
import type { ExecuteTaskParams, ExecuteTaskState } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
@@ -40,14 +37,6 @@ const ExecuteTaskRender = memo<BuiltinRenderProps<ExecuteTaskParams, ExecuteTask
|
||||
({ args }) => {
|
||||
const { t } = useTranslation('tool');
|
||||
|
||||
// Get agent info from store
|
||||
const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
|
||||
const agent = useAgentGroupStore((s) =>
|
||||
args?.agentId && activeGroupId
|
||||
? agentGroupSelectors.getAgentByIdFromGroup(activeGroupId, args.agentId)(s)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const timeoutMinutes = args?.timeout ? Math.round(args.timeout / 60_000) : 30;
|
||||
|
||||
return (
|
||||
@@ -55,15 +44,7 @@ const ExecuteTaskRender = memo<BuiltinRenderProps<ExecuteTaskParams, ExecuteTask
|
||||
{/* Header: Agent info + Timeout */}
|
||||
<Flexbox align={'center'} gap={12} horizontal justify={'space-between'}>
|
||||
<Flexbox align={'center'} flex={1} gap={12} horizontal style={{ minWidth: 0 }}>
|
||||
<Avatar
|
||||
avatar={agent?.avatar || '🤖'}
|
||||
background={agent?.backgroundColor || undefined}
|
||||
size={24}
|
||||
style={{ borderRadius: 8, flexShrink: 0 }}
|
||||
/>
|
||||
<span className={styles.agentTitle}>
|
||||
{agent?.title || t('agentGroupManagement.executeTask.intervention.unknownAgent')}
|
||||
</span>
|
||||
<span className={styles.agentTitle}>{args?.title}</span>
|
||||
</Flexbox>
|
||||
<Flexbox align="center" className={styles.timeout} gap={4} horizontal>
|
||||
<Clock size={14} />
|
||||
|
||||
@@ -280,7 +280,7 @@ describe('GroupManagementExecutor', () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
const result = await groupManagementExecutor.executeAgentTask(
|
||||
{ agentId: 'agent-1', task: 'Do something' },
|
||||
{ agentId: 'agent-1', task: 'Do something', title: 'Test Task' },
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -291,6 +291,7 @@ describe('GroupManagementExecutor', () => {
|
||||
agentId: 'agent-1',
|
||||
task: 'Do something',
|
||||
timeout: undefined,
|
||||
title: 'Test Task',
|
||||
type: 'executeAgentTask',
|
||||
});
|
||||
});
|
||||
@@ -314,7 +315,7 @@ describe('GroupManagementExecutor', () => {
|
||||
);
|
||||
|
||||
await groupManagementExecutor.executeAgentTask(
|
||||
{ agentId: 'agent-1', task: 'Do something', timeout: 30000 },
|
||||
{ agentId: 'agent-1', task: 'Do something', timeout: 30000, title: 'Test Task' },
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -339,7 +340,7 @@ describe('GroupManagementExecutor', () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
const result = await groupManagementExecutor.executeAgentTask(
|
||||
{ agentId: 'agent-1', task: 'Do something' },
|
||||
{ agentId: 'agent-1', task: 'Do something', title: 'Test Task' },
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -351,7 +352,7 @@ describe('GroupManagementExecutor', () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
const result = await groupManagementExecutor.executeAgentTask(
|
||||
{ agentId: 'agent-1', task: 'Do something', timeout: 60000 },
|
||||
{ agentId: 'agent-1', task: 'Do something', timeout: 60000, title: 'Test Task' },
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -360,6 +361,7 @@ describe('GroupManagementExecutor', () => {
|
||||
agentId: 'agent-1',
|
||||
task: 'Do something',
|
||||
timeout: 60000,
|
||||
title: 'Test Task',
|
||||
type: 'executeAgentTask',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,10 @@ export const GroupManagementManifest: BuiltinToolManifest = {
|
||||
description: 'The ID of the agent to execute the task.',
|
||||
type: 'string',
|
||||
},
|
||||
title: {
|
||||
description: 'Brief title describing what this task does (shown in UI).',
|
||||
type: 'string',
|
||||
},
|
||||
task: {
|
||||
description:
|
||||
'Clear description of the task to perform. Be specific about expected deliverables.',
|
||||
@@ -113,7 +117,7 @@ export const GroupManagementManifest: BuiltinToolManifest = {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['agentId', 'task'],
|
||||
required: ['agentId', 'title', 'task'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -80,6 +80,8 @@ export interface ExecuteTaskParams {
|
||||
skipCallSupervisor?: boolean;
|
||||
task: string;
|
||||
timeout?: number;
|
||||
/** Brief title describing what this task does (shown in UI) */
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
|
||||
@@ -1,22 +1,65 @@
|
||||
import { type WriteLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinRenderProps } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Skeleton } from '@lobehub/ui';
|
||||
import { Flexbox, Highlighter, Icon, Markdown, Skeleton } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import path from 'path-browserify-esm';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LocalFile, LocalFolder } from '@/features/LocalFile';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
padding: 8px;
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
background: ${cssVar.colorFillQuaternary};
|
||||
`,
|
||||
previewBox: css`
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
const WriteFile = memo<BuiltinRenderProps<WriteLocalFileParams>>(({ args }) => {
|
||||
if (!args) return <Skeleton active />;
|
||||
|
||||
const { base, dir } = path.parse(args.path);
|
||||
const ext = path.extname(args.path).slice(1).toLowerCase();
|
||||
|
||||
const renderContent = () => {
|
||||
if (!args.content) return null;
|
||||
|
||||
if (ext === 'md' || ext === 'mdx') {
|
||||
return (
|
||||
<Markdown style={{ maxHeight: 240, overflow: 'auto', padding: '0 8px' }}>
|
||||
{args.content}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Highlighter
|
||||
language={ext || 'text'}
|
||||
showLanguage={false}
|
||||
style={{ maxHeight: 240, overflow: 'auto' }}
|
||||
variant={'borderless'}
|
||||
wrap
|
||||
>
|
||||
{args.content}
|
||||
</Highlighter>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox horizontal>
|
||||
<LocalFolder path={dir} />
|
||||
<Icon icon={ChevronRight} />
|
||||
<LocalFile name={base} path={args.path} />
|
||||
<Flexbox className={styles.container} gap={12}>
|
||||
<Flexbox align={'center'} horizontal>
|
||||
<LocalFolder path={dir} />
|
||||
<Icon icon={ChevronRight} />
|
||||
<LocalFile name={base} path={args.path} />
|
||||
</Flexbox>
|
||||
|
||||
{args.content && <Flexbox className={styles.previewBox}>{renderContent()}</Flexbox>}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { type WriteLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Highlighter, Markdown } from '@lobehub/ui';
|
||||
import path from 'path-browserify-esm';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const WriteFileStreaming = memo<BuiltinStreamingProps<WriteLocalFileParams>>(({ args }) => {
|
||||
const { content, path: filePath } = args || {};
|
||||
|
||||
// Don't render if no content yet
|
||||
if (!content) return null;
|
||||
|
||||
const ext = path
|
||||
.extname(filePath || '')
|
||||
.slice(1)
|
||||
.toLowerCase();
|
||||
|
||||
// Use Markdown for .md files, Highlighter for others
|
||||
if (ext === 'md' || ext === 'mdx') {
|
||||
return <Markdown style={{ overflow: 'auto' }}>{content}</Markdown>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Highlighter
|
||||
animated
|
||||
language={ext || 'text'}
|
||||
showLanguage={false}
|
||||
style={{ padding: '4px 8px' }}
|
||||
variant={'outlined'}
|
||||
wrap
|
||||
>
|
||||
{content}
|
||||
</Highlighter>
|
||||
);
|
||||
});
|
||||
|
||||
WriteFileStreaming.displayName = 'WriteFileStreaming';
|
||||
@@ -1,9 +1,11 @@
|
||||
import { LocalSystemApiName } from '../..';
|
||||
import { RunCommandStreaming } from './RunCommand';
|
||||
import { WriteFileStreaming } from './WriteFile';
|
||||
|
||||
/**
|
||||
* Local System Streaming Components Registry
|
||||
*/
|
||||
export const LocalSystemStreamings = {
|
||||
[LocalSystemApiName.runCommand]: RunCommandStreaming,
|
||||
[LocalSystemApiName.writeLocalFile]: WriteFileStreaming,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Suspense, memo } from 'react';
|
||||
|
||||
import NavHeader from '@/features/NavHeader';
|
||||
import WideScreenButton from '@/features/WideScreenContainer/WideScreenButton';
|
||||
@@ -15,7 +15,9 @@ const Header = memo(() => {
|
||||
right={
|
||||
<Flexbox horizontal style={{ backgroundColor: cssVar.colorBgContainer }}>
|
||||
<WideScreenButton />
|
||||
<ShareButton />
|
||||
<Suspense>
|
||||
<ShareButton />
|
||||
</Suspense>
|
||||
</Flexbox>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -70,7 +70,8 @@ export default {
|
||||
'builtins.lobe-group-management.apiName.summarize': 'Summarize conversation',
|
||||
'builtins.lobe-group-management.apiName.vote': 'Start vote',
|
||||
'builtins.lobe-group-management.inspector.broadcast.title': 'Following Agents speak:',
|
||||
'builtins.lobe-group-management.inspector.executeAgentTask.title': 'Assigning task to:',
|
||||
'builtins.lobe-group-management.inspector.executeAgentTask.assignTo': 'Assign',
|
||||
'builtins.lobe-group-management.inspector.executeAgentTask.task': 'task:',
|
||||
'builtins.lobe-group-management.inspector.executeAgentTasks.title': 'Assigning tasks to:',
|
||||
'builtins.lobe-group-management.inspector.speak.title': 'Designated Agent speaks:',
|
||||
'builtins.lobe-group-management.title': 'Group Coordinator',
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import type { AgentState } from '@lobechat/agent-runtime';
|
||||
import type { UIChatMessage } from '@lobechat/types';
|
||||
import { nanoid } from '@lobechat/utils';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ChatStore } from '@/store/chat/store';
|
||||
|
||||
import { createGroupOrchestrationExecutors } from '../createGroupOrchestrationExecutors';
|
||||
|
||||
const TEST_IDS = {
|
||||
GROUP_ID: 'test-group-id',
|
||||
OPERATION_ID: 'test-operation-id',
|
||||
ORCHESTRATION_OPERATION_ID: 'test-orchestration-operation-id',
|
||||
SUPERVISOR_AGENT_ID: 'test-supervisor-agent-id',
|
||||
TOPIC_ID: 'test-topic-id',
|
||||
USER_MESSAGE_ID: 'test-user-message-id',
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a minimal mock store for group orchestration executor tests
|
||||
*/
|
||||
const createMockStore = (overrides: Partial<ChatStore> = {}): ChatStore => {
|
||||
const operations: Record<string, any> = {};
|
||||
|
||||
return {
|
||||
dbMessagesMap: {},
|
||||
internal_execAgentRuntime: vi.fn().mockResolvedValue(undefined),
|
||||
messagesMap: {},
|
||||
operations,
|
||||
startOperation: vi.fn().mockImplementation((config) => {
|
||||
const operationId = `op_${nanoid()}`;
|
||||
const abortController = new AbortController();
|
||||
operations[operationId] = {
|
||||
abortController,
|
||||
context: config.context || {},
|
||||
id: operationId,
|
||||
status: 'running',
|
||||
type: config.type,
|
||||
};
|
||||
return { abortController, operationId };
|
||||
}),
|
||||
...overrides,
|
||||
} as unknown as ChatStore;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create initial agent state for testing
|
||||
*/
|
||||
const createInitialState = (overrides: Partial<AgentState> = {}): AgentState => {
|
||||
return {
|
||||
cost: {
|
||||
calculatedAt: new Date().toISOString(),
|
||||
currency: 'USD',
|
||||
llm: { byModel: [], currency: 'USD', total: 0 },
|
||||
tools: { byTool: [], currency: 'USD', total: 0 },
|
||||
total: 0,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
lastModified: new Date().toISOString(),
|
||||
maxSteps: 10,
|
||||
messages: [],
|
||||
operationId: TEST_IDS.OPERATION_ID,
|
||||
status: 'running',
|
||||
stepCount: 0,
|
||||
toolManifestMap: {},
|
||||
usage: {
|
||||
humanInteraction: {
|
||||
approvalRequests: 0,
|
||||
promptRequests: 0,
|
||||
selectRequests: 0,
|
||||
totalWaitingTimeMs: 0,
|
||||
},
|
||||
llm: { apiCalls: 0, processingTimeMs: 0, tokens: { input: 0, output: 0, total: 0 } },
|
||||
tools: { byTool: [], totalCalls: 0, totalTimeMs: 0 },
|
||||
},
|
||||
userInterventionConfig: { allowList: [], approvalMode: 'auto' },
|
||||
...overrides,
|
||||
} as AgentState;
|
||||
};
|
||||
|
||||
describe('createGroupOrchestrationExecutors', () => {
|
||||
describe('call_supervisor executor', () => {
|
||||
it('should NOT pass operationId to internal_execAgentRuntime (creates new child operation)', async () => {
|
||||
const mockStore = createMockStore({
|
||||
dbMessagesMap: {
|
||||
[`group_${TEST_IDS.GROUP_ID}_${TEST_IDS.TOPIC_ID}`]: [
|
||||
{
|
||||
content: 'Hello',
|
||||
createdAt: Date.now(),
|
||||
id: TEST_IDS.USER_MESSAGE_ID,
|
||||
role: 'user',
|
||||
updatedAt: Date.now(),
|
||||
} as UIChatMessage,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const executors = createGroupOrchestrationExecutors({
|
||||
get: () => mockStore,
|
||||
messageContext: {
|
||||
agentId: TEST_IDS.GROUP_ID,
|
||||
scope: 'group',
|
||||
topicId: TEST_IDS.TOPIC_ID,
|
||||
},
|
||||
orchestrationOperationId: TEST_IDS.ORCHESTRATION_OPERATION_ID,
|
||||
supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID,
|
||||
});
|
||||
|
||||
const callSupervisorExecutor = executors.call_supervisor!;
|
||||
|
||||
await callSupervisorExecutor(
|
||||
{
|
||||
payload: { round: 1, supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID },
|
||||
type: 'call_supervisor',
|
||||
},
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
// Verify internal_execAgentRuntime was called
|
||||
expect(mockStore.internal_execAgentRuntime).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify operationId is NOT passed (should be undefined)
|
||||
// This ensures a new child operation is created
|
||||
const callArgs = (mockStore.internal_execAgentRuntime as any).mock.calls[0][0];
|
||||
expect(callArgs.operationId).toBeUndefined();
|
||||
|
||||
// Verify parentOperationId is passed correctly
|
||||
expect(callArgs.parentOperationId).toBe(TEST_IDS.ORCHESTRATION_OPERATION_ID);
|
||||
|
||||
// Verify isSupervisor is passed in context
|
||||
expect(callArgs.context.isSupervisor).toBe(true);
|
||||
|
||||
// Verify agentId is the supervisor agent id
|
||||
expect(callArgs.context.agentId).toBe(TEST_IDS.SUPERVISOR_AGENT_ID);
|
||||
});
|
||||
|
||||
it('should pass isSupervisor: true in context for supervisor messages metadata', async () => {
|
||||
const mockStore = createMockStore({
|
||||
dbMessagesMap: {
|
||||
[`group_${TEST_IDS.GROUP_ID}_${TEST_IDS.TOPIC_ID}`]: [
|
||||
{
|
||||
content: 'Hello',
|
||||
createdAt: Date.now(),
|
||||
id: TEST_IDS.USER_MESSAGE_ID,
|
||||
role: 'user',
|
||||
updatedAt: Date.now(),
|
||||
} as UIChatMessage,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const executors = createGroupOrchestrationExecutors({
|
||||
get: () => mockStore,
|
||||
messageContext: {
|
||||
agentId: TEST_IDS.GROUP_ID,
|
||||
scope: 'group',
|
||||
topicId: TEST_IDS.TOPIC_ID,
|
||||
},
|
||||
orchestrationOperationId: TEST_IDS.ORCHESTRATION_OPERATION_ID,
|
||||
supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID,
|
||||
});
|
||||
|
||||
const callSupervisorExecutor = executors.call_supervisor!;
|
||||
|
||||
await callSupervisorExecutor(
|
||||
{
|
||||
payload: { round: 1, supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID },
|
||||
type: 'call_supervisor',
|
||||
},
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
const callArgs = (mockStore.internal_execAgentRuntime as any).mock.calls[0][0];
|
||||
|
||||
// The key assertion: isSupervisor must be true
|
||||
// This is used by createAgentExecutors to set metadata.isSupervisor on assistant messages
|
||||
expect(callArgs.context).toMatchObject({
|
||||
agentId: TEST_IDS.SUPERVISOR_AGENT_ID,
|
||||
isSupervisor: true,
|
||||
scope: 'group',
|
||||
topicId: TEST_IDS.TOPIC_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('call_agent executor', () => {
|
||||
it('should NOT pass operationId to internal_execAgentRuntime (creates new child operation)', async () => {
|
||||
const mockStore = createMockStore({
|
||||
dbMessagesMap: {
|
||||
[`group_${TEST_IDS.GROUP_ID}_${TEST_IDS.TOPIC_ID}`]: [
|
||||
{
|
||||
content: 'Hello',
|
||||
createdAt: Date.now(),
|
||||
id: TEST_IDS.USER_MESSAGE_ID,
|
||||
role: 'user',
|
||||
updatedAt: Date.now(),
|
||||
} as UIChatMessage,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const executors = createGroupOrchestrationExecutors({
|
||||
get: () => mockStore,
|
||||
messageContext: {
|
||||
agentId: TEST_IDS.GROUP_ID,
|
||||
scope: 'group',
|
||||
topicId: TEST_IDS.TOPIC_ID,
|
||||
},
|
||||
orchestrationOperationId: TEST_IDS.ORCHESTRATION_OPERATION_ID,
|
||||
supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID,
|
||||
});
|
||||
|
||||
const callAgentExecutor = executors.call_agent!;
|
||||
const targetAgentId = 'target-agent-id';
|
||||
|
||||
await callAgentExecutor(
|
||||
{
|
||||
payload: { agentId: targetAgentId, instruction: 'Please respond' },
|
||||
type: 'call_agent',
|
||||
},
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
// Verify internal_execAgentRuntime was called
|
||||
expect(mockStore.internal_execAgentRuntime).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify operationId is NOT passed (should be undefined)
|
||||
const callArgs = (mockStore.internal_execAgentRuntime as any).mock.calls[0][0];
|
||||
expect(callArgs.operationId).toBeUndefined();
|
||||
|
||||
// Verify parentOperationId is passed correctly
|
||||
expect(callArgs.parentOperationId).toBe(TEST_IDS.ORCHESTRATION_OPERATION_ID);
|
||||
|
||||
// Verify subAgentId is passed (NOT isSupervisor)
|
||||
expect(callArgs.context.subAgentId).toBe(targetAgentId);
|
||||
expect(callArgs.context.isSupervisor).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('operation structure comparison', () => {
|
||||
it('call_supervisor and call_agent should both create independent child operations', async () => {
|
||||
const mockStore = createMockStore({
|
||||
dbMessagesMap: {
|
||||
[`group_${TEST_IDS.GROUP_ID}_${TEST_IDS.TOPIC_ID}`]: [
|
||||
{
|
||||
content: 'Hello',
|
||||
createdAt: Date.now(),
|
||||
id: TEST_IDS.USER_MESSAGE_ID,
|
||||
role: 'user',
|
||||
updatedAt: Date.now(),
|
||||
} as UIChatMessage,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const executors = createGroupOrchestrationExecutors({
|
||||
get: () => mockStore,
|
||||
messageContext: {
|
||||
agentId: TEST_IDS.GROUP_ID,
|
||||
scope: 'group',
|
||||
topicId: TEST_IDS.TOPIC_ID,
|
||||
},
|
||||
orchestrationOperationId: TEST_IDS.ORCHESTRATION_OPERATION_ID,
|
||||
supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID,
|
||||
});
|
||||
|
||||
// Execute call_supervisor
|
||||
await executors.call_supervisor!(
|
||||
{
|
||||
payload: { round: 1, supervisorAgentId: TEST_IDS.SUPERVISOR_AGENT_ID },
|
||||
type: 'call_supervisor',
|
||||
},
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
// Execute call_agent
|
||||
await executors.call_agent!(
|
||||
{
|
||||
payload: { agentId: 'agent-1', instruction: 'test' },
|
||||
type: 'call_agent',
|
||||
},
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
// Verify both were called
|
||||
expect(mockStore.internal_execAgentRuntime).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Get both call arguments
|
||||
const supervisorCallArgs = (mockStore.internal_execAgentRuntime as any).mock.calls[0][0];
|
||||
const agentCallArgs = (mockStore.internal_execAgentRuntime as any).mock.calls[1][0];
|
||||
|
||||
// Both should NOT have operationId (create new child operations)
|
||||
expect(supervisorCallArgs.operationId).toBeUndefined();
|
||||
expect(agentCallArgs.operationId).toBeUndefined();
|
||||
|
||||
// Both should have same parentOperationId (orchestration operation)
|
||||
expect(supervisorCallArgs.parentOperationId).toBe(TEST_IDS.ORCHESTRATION_OPERATION_ID);
|
||||
expect(agentCallArgs.parentOperationId).toBe(TEST_IDS.ORCHESTRATION_OPERATION_ID);
|
||||
|
||||
// Supervisor should have isSupervisor: true
|
||||
expect(supervisorCallArgs.context.isSupervisor).toBe(true);
|
||||
expect(agentCallArgs.context.isSupervisor).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,10 +126,11 @@ export const createGroupOrchestrationExecutors = (
|
||||
|
||||
// Execute Supervisor agent with the supervisor's agentId in context
|
||||
// Mark isSupervisor=true so assistant messages get metadata.isSupervisor for UI rendering
|
||||
// Note: Don't pass operationId - let it create a new child operation (same as call_agent)
|
||||
// This ensures each call has its own immutable context with isSupervisor properly set
|
||||
await get().internal_execAgentRuntime({
|
||||
context: { ...messageContext, agentId: supervisorAgentId, isSupervisor: true },
|
||||
messages,
|
||||
operationId: state.operationId,
|
||||
parentMessageId: lastMessage.id,
|
||||
parentMessageType: lastMessage.role as 'user' | 'assistant' | 'tool',
|
||||
parentOperationId: orchestrationOperationId,
|
||||
|
||||
92
src/store/chat/slices/plugin/actions/exector.ts
Normal file
92
src/store/chat/slices/plugin/actions/exector.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { type MCPToolCallResult } from '@/libs/mcp';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { type ChatToolPayload } from '@/types/message';
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
|
||||
/**
|
||||
* Executor function type for remote tool invocation
|
||||
* @param payload - Tool call payload
|
||||
* @returns Promise with MCPToolCallResult data
|
||||
*/
|
||||
export type RemoteToolExecutor = (payload: ChatToolPayload) => Promise<MCPToolCallResult>;
|
||||
|
||||
/**
|
||||
* Create a failed MCPToolCallResult
|
||||
*/
|
||||
const createFailedResult = (
|
||||
errorMessage: string,
|
||||
): { content: string; error: any; state: any; success: false } => ({
|
||||
content: errorMessage,
|
||||
error: { message: errorMessage },
|
||||
state: {},
|
||||
success: false,
|
||||
});
|
||||
|
||||
export const klavisExecutor: RemoteToolExecutor = async (p) => {
|
||||
// payload.identifier 现在是存储用的 identifier(如 'google-calendar')
|
||||
const identifier = p.identifier;
|
||||
const klavisServers = useToolStore.getState().servers || [];
|
||||
const server = klavisServers.find((s) => s.identifier === identifier);
|
||||
|
||||
if (!server) {
|
||||
return createFailedResult(`Klavis server not found: ${identifier}`);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(p.arguments) || {};
|
||||
|
||||
// Call Klavis tool via store action
|
||||
const result = await useToolStore.getState().callKlavisTool({
|
||||
serverUrl: server.serverUrl,
|
||||
toolArgs: args,
|
||||
toolName: p.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return createFailedResult(result.error || 'Klavis tool execution failed');
|
||||
}
|
||||
|
||||
// result.data is MCPToolCallProcessedResult from server
|
||||
// Convert to MCPToolCallResult format
|
||||
const toolResult = result.data;
|
||||
if (toolResult) {
|
||||
return {
|
||||
content: toolResult.content,
|
||||
error: toolResult.state?.isError ? toolResult.state : undefined,
|
||||
state: toolResult.state,
|
||||
success: toolResult.success,
|
||||
};
|
||||
}
|
||||
|
||||
return createFailedResult('Klavis tool returned empty result');
|
||||
};
|
||||
|
||||
export const lobehubSkillExecutor: RemoteToolExecutor = async (p) => {
|
||||
// payload.identifier is the provider id (e.g., 'linear', 'microsoft')
|
||||
const provider = p.identifier;
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(p.arguments) || {};
|
||||
|
||||
// Call LobeHub Skill tool via store action
|
||||
const result = await useToolStore.getState().callLobehubSkillTool({
|
||||
args,
|
||||
provider,
|
||||
toolName: p.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return createFailedResult(
|
||||
result.error || `LobeHub Skill tool ${provider} ${p.apiName} execution failed`,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to MCPToolCallResult format
|
||||
const content = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
|
||||
return {
|
||||
content,
|
||||
error: undefined,
|
||||
state: { content: [{ text: content, type: 'text' }] },
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
|
||||
import { dbMessageSelectors } from '../../message/selectors';
|
||||
import { RemoteToolExecutor, klavisExecutor, lobehubSkillExecutor } from './exector';
|
||||
|
||||
const log = debug('lobe-store:plugin-types');
|
||||
|
||||
@@ -86,6 +87,16 @@ export interface PluginTypesAction {
|
||||
* Internal method to call plugin API
|
||||
*/
|
||||
internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Internal unified method to invoke remote tool plugins (Klavis, LobeHub Skill, etc.)
|
||||
*/
|
||||
internal_invokeRemoteToolPlugin: (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
executor: RemoteToolExecutor,
|
||||
logPrefix: string,
|
||||
) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export const pluginTypes: StateCreator<
|
||||
@@ -360,191 +371,21 @@ export const pluginTypes: StateCreator<
|
||||
},
|
||||
|
||||
invokeKlavisTypePlugin: async (id, payload) => {
|
||||
let data: MCPToolCallResult | undefined;
|
||||
|
||||
// Get message to extract sessionId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const operation = operationId ? get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[invokeKlavisTypePlugin] messageId=%s, tool=%s, operationId=%s, aborted=%s',
|
||||
return get().internal_invokeRemoteToolPlugin(
|
||||
id,
|
||||
payload.apiName,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
payload,
|
||||
klavisExecutor,
|
||||
'invokeKlavisTypePlugin',
|
||||
);
|
||||
|
||||
try {
|
||||
// payload.identifier 现在是存储用的 identifier(如 'google-calendar')
|
||||
const identifier = payload.identifier;
|
||||
const klavisServers = useToolStore.getState().servers || [];
|
||||
const server = klavisServers.find((s) => s.identifier === identifier);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Klavis server not found: ${identifier}`);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(payload.arguments) || {};
|
||||
|
||||
// Call Klavis tool via store action
|
||||
const result = await useToolStore.getState().callKlavisTool({
|
||||
serverUrl: server.serverUrl,
|
||||
toolArgs: args,
|
||||
toolName: payload.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Klavis tool execution failed');
|
||||
}
|
||||
|
||||
// result.data is MCPToolCallProcessedResult from server
|
||||
// Convert to MCPToolCallResult format
|
||||
const toolResult = result.data;
|
||||
if (toolResult) {
|
||||
data = {
|
||||
content: toolResult.content,
|
||||
error: toolResult.state?.isError ? toolResult.state : undefined,
|
||||
state: toolResult.state,
|
||||
success: toolResult.success,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[invokeKlavisTypePlugin] Error:', error);
|
||||
|
||||
// ignore the aborted request error
|
||||
const err = error as Error;
|
||||
if (err.message.includes('aborted')) {
|
||||
log('[invokeKlavisTypePlugin] Request aborted: messageId=%s, tool=%s', id, payload.apiName);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
get().replaceMessages(result.messages, {
|
||||
context: {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果报错则结束了
|
||||
if (!data) return;
|
||||
|
||||
// operationId already declared above, reuse it
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
||||
await get().optimisticUpdateToolMessage(
|
||||
id,
|
||||
{
|
||||
content: data.content,
|
||||
pluginError: data.success ? undefined : data.error,
|
||||
pluginState: data.success ? data.state : undefined,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
return data.content;
|
||||
},
|
||||
|
||||
invokeLobehubSkillTypePlugin: async (id, payload) => {
|
||||
let data: MCPToolCallResult | undefined;
|
||||
|
||||
// Get message to extract sessionId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const operation = operationId ? get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[invokeLobehubSkillTypePlugin] messageId=%s, tool=%s, operationId=%s, aborted=%s',
|
||||
return get().internal_invokeRemoteToolPlugin(
|
||||
id,
|
||||
payload.apiName,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
payload,
|
||||
lobehubSkillExecutor,
|
||||
'invokeLobehubSkillTypePlugin',
|
||||
);
|
||||
|
||||
try {
|
||||
// payload.identifier is the provider id (e.g., 'linear', 'microsoft')
|
||||
const provider = payload.identifier;
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(payload.arguments) || {};
|
||||
|
||||
// Call LobeHub Skill tool via store action
|
||||
const result = await useToolStore.getState().callLobehubSkillTool({
|
||||
args,
|
||||
provider,
|
||||
toolName: payload.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'LobeHub Skill tool execution failed');
|
||||
}
|
||||
|
||||
// Convert to MCPToolCallResult format
|
||||
const content = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
|
||||
data = {
|
||||
content,
|
||||
error: undefined,
|
||||
state: { content: [{ text: content, type: 'text' }] },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[invokeLobehubSkillTypePlugin] Error:', error);
|
||||
|
||||
// ignore the aborted request error
|
||||
const err = error as Error;
|
||||
if (err.message.includes('aborted')) {
|
||||
log(
|
||||
'[invokeLobehubSkillTypePlugin] Request aborted: messageId=%s, tool=%s',
|
||||
id,
|
||||
payload.apiName,
|
||||
);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
get().replaceMessages(result.messages, {
|
||||
context: {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If error occurred, exit
|
||||
if (!data) return;
|
||||
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
||||
await get().optimisticUpdateToolMessage(
|
||||
id,
|
||||
{
|
||||
content: data.content,
|
||||
pluginError: data.success ? undefined : data.error,
|
||||
pluginState: data.success ? data.state : undefined,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
return data.content;
|
||||
},
|
||||
|
||||
invokeMarkdownTypePlugin: async (id, payload) => {
|
||||
@@ -653,6 +494,70 @@ export const pluginTypes: StateCreator<
|
||||
return data.content;
|
||||
},
|
||||
|
||||
internal_invokeRemoteToolPlugin: async (id, payload, executor, logPrefix) => {
|
||||
let data: MCPToolCallResult | undefined;
|
||||
|
||||
// Get message to extract sessionId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const operation = operationId ? get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[%s] messageId=%s, tool=%s, operationId=%s, aborted=%s',
|
||||
logPrefix,
|
||||
id,
|
||||
payload.apiName,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
);
|
||||
|
||||
try {
|
||||
data = await executor(payload);
|
||||
} catch (error) {
|
||||
console.error(`[${logPrefix}] Error:`, error);
|
||||
|
||||
// ignore the aborted request error
|
||||
const err = error as Error;
|
||||
if (err.message.includes('aborted')) {
|
||||
log('[%s] Request aborted: messageId=%s, tool=%s', logPrefix, id, payload.apiName);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
get().replaceMessages(result.messages, {
|
||||
context: {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If error occurred, exit
|
||||
if (!data) return;
|
||||
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
||||
await get().optimisticUpdateToolMessage(
|
||||
id,
|
||||
{
|
||||
content: data.content,
|
||||
pluginError: data.success ? undefined : data.error,
|
||||
pluginState: data.success ? data.state : undefined,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
return data.content;
|
||||
},
|
||||
|
||||
internal_callPluginApi: async (id, payload) => {
|
||||
const { optimisticUpdateMessageContent } = get();
|
||||
let data: string;
|
||||
|
||||
Reference in New Issue
Block a user