💄 style: suppot agent management (#12061)

* feat: improve the inject model context plugins decriptions

fix: change the conversation-flow to change the subAgent message show place

fix: eslint fixed

fix: slove the inject not work problem

feat: add the lost agent management inject open

feat: add the AgentManagementInjector

fix: add the exec task mode & improve the Pre-load agents

fix: improve the executor import way & update the getEffectiveAgentId function

fix: slove the test problem

🐛 fix: support agnet manager ments (#12171)

feat: add the sub agents in context scope to support call subagent

refactor agent management implement

update

add builtin agent management

* fix types

* fix import

* fix test

* fix tests

* fix tests
This commit is contained in:
Arvin Xu 2026-02-28 13:52:35 +08:00 committed by GitHub
parent 4f3055e0c5
commit eef04c499f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 4407 additions and 1584 deletions

View file

@ -324,11 +324,6 @@
"count": 1
}
},
"src/server/routers/lambda/aiAgent.ts": {
"no-console": {
"count": 1
}
},
"src/server/routers/lambda/aiChat.ts": {
"prefer-const": {
"count": 1

View file

@ -188,6 +188,7 @@
"@lobechat/builtin-agents": "workspace:*",
"@lobechat/builtin-skills": "workspace:*",
"@lobechat/builtin-tool-agent-builder": "workspace:*",
"@lobechat/builtin-tool-agent-management": "workspace:*",
"@lobechat/builtin-tool-calculator": "workspace:*",
"@lobechat/builtin-tool-cloud-sandbox": "workspace:*",
"@lobechat/builtin-tool-group-agent-builder": "workspace:*",

View file

@ -0,0 +1,14 @@
{
"name": "@lobechat/agent-manager-runtime",
"version": "1.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/const": "workspace:*",
"@lobechat/prompts": "workspace:*",
"@lobechat/types": "workspace:*"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,440 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentManagerRuntime } from '../AgentManagerRuntime';
import type { IAgentService, IDiscoverService } from '../types';
// Create mock services
const mockAgentService: IAgentService = {
createAgent: vi.fn(),
queryAgents: vi.fn(),
removeAgent: vi.fn(),
};
const mockDiscoverService: IDiscoverService = {
getAssistantList: vi.fn(),
getMcpList: vi.fn(),
};
// Mock stores
const mockAgentConfig = {
plugins: ['plugin-1'],
systemRole: 'Previous prompt',
};
const mockAgentMeta = {
avatar: '🤖',
title: 'Test Agent',
};
vi.mock('@/store/agent', () => ({
getAgentStoreState: vi.fn(() => ({
appendStreamingSystemRole: vi.fn(),
finishStreamingSystemRole: vi.fn(),
optimisticUpdateAgentConfig: vi.fn(),
optimisticUpdateAgentMeta: vi.fn(),
startStreamingSystemRole: vi.fn(),
})),
}));
vi.mock('@/store/agent/selectors/selectors', () => ({
agentSelectors: {
getAgentConfigById: vi.fn(() => () => mockAgentConfig),
getAgentMetaById: vi.fn(() => () => mockAgentMeta),
},
}));
vi.mock('@/store/aiInfra', () => ({
getAiInfraStoreState: vi.fn(() => ({
enabledChatModelList: [
{
id: 'openai',
name: 'OpenAI',
children: [
{ id: 'gpt-4o', displayName: 'GPT-4o', abilities: { functionCall: true, vision: true } },
{ id: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo' },
],
},
{
id: 'anthropic',
name: 'Anthropic',
children: [
{
id: 'claude-3-5-sonnet',
displayName: 'Claude 3.5 Sonnet',
abilities: { reasoning: true },
},
],
},
],
})),
}));
vi.mock('@/store/tool', () => ({
getToolStoreState: vi.fn(() => ({
installMCPPlugin: vi.fn().mockResolvedValue(true),
refreshPlugins: vi.fn(),
})),
}));
vi.mock('@/store/tool/selectors', () => ({
builtinToolSelectors: {
metaList: vi.fn(() => [{ identifier: 'lobe-web-browsing', meta: { title: 'Web Browsing' } }]),
},
klavisStoreSelectors: {
getServers: vi.fn(() => []),
},
lobehubSkillStoreSelectors: {
getServers: vi.fn(() => []),
},
pluginSelectors: {
getInstalledPluginById: vi.fn(() => () => null),
isPluginInstalled: vi.fn(() => () => false),
},
}));
vi.mock('@/store/user', () => ({
getUserStoreState: vi.fn(() => ({})),
}));
vi.mock('@/store/user/selectors', () => ({
userProfileSelectors: {
userId: vi.fn(() => 'test-user-id'),
},
}));
describe('AgentManagerRuntime', () => {
let runtime: AgentManagerRuntime;
beforeEach(() => {
vi.clearAllMocks();
runtime = new AgentManagerRuntime({
agentService: mockAgentService,
discoverService: mockDiscoverService,
});
});
describe('createAgent', () => {
it('should create an agent successfully', async () => {
vi.mocked(mockAgentService.createAgent).mockResolvedValue({
agentId: 'new-agent-id',
sessionId: 'new-session-id',
});
const result = await runtime.createAgent({
title: 'My New Agent',
description: 'A test agent',
systemRole: 'You are a helpful assistant',
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully created agent');
expect(result.content).toContain('My New Agent');
expect(result.state).toMatchObject({
agentId: 'new-agent-id',
sessionId: 'new-session-id',
success: true,
});
});
it('should handle creation failure', async () => {
vi.mocked(mockAgentService.createAgent).mockRejectedValue(new Error('Creation failed'));
const result = await runtime.createAgent({
title: 'My Agent',
});
expect(result.success).toBe(false);
expect(result.content).toContain('Failed to create agent');
expect(result.error).toMatchObject({
message: 'Creation failed',
type: 'RuntimeError',
});
});
});
describe('updateAgentConfig', () => {
it('should update agent config successfully', async () => {
const result = await runtime.updateAgentConfig('agent-id', {
config: { model: 'gpt-4o' },
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully updated agent');
expect(result.state).toMatchObject({
success: true,
config: {
updatedFields: ['model'],
},
});
});
it('should update agent meta successfully', async () => {
const result = await runtime.updateAgentConfig('agent-id', {
meta: { title: 'New Title', avatar: '🎉' },
});
expect(result.success).toBe(true);
expect(result.content).toContain('meta fields: title, avatar');
});
it('should handle togglePlugin', async () => {
const result = await runtime.updateAgentConfig('agent-id', {
togglePlugin: { pluginId: 'new-plugin', enabled: true },
});
expect(result.success).toBe(true);
expect(result.content).toContain('plugin new-plugin enabled');
expect(result.state).toMatchObject({
success: true,
togglePlugin: {
enabled: true,
pluginId: 'new-plugin',
},
});
});
it('should return no fields message when nothing to update', async () => {
const result = await runtime.updateAgentConfig('agent-id', {});
expect(result.success).toBe(true);
expect(result.content).toBe('No fields to update.');
});
});
describe('deleteAgent', () => {
it('should delete agent successfully', async () => {
vi.mocked(mockAgentService.removeAgent).mockResolvedValue({} as any);
const result = await runtime.deleteAgent('agent-to-delete');
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully deleted agent');
expect(result.state).toMatchObject({
agentId: 'agent-to-delete',
success: true,
});
});
it('should handle deletion failure', async () => {
vi.mocked(mockAgentService.removeAgent).mockRejectedValue(new Error('Deletion failed'));
const result = await runtime.deleteAgent('agent-id');
expect(result.success).toBe(false);
expect(result.content).toContain('Failed to delete agent');
});
});
describe('searchAgents', () => {
it('should search user agents', async () => {
vi.mocked(mockAgentService.queryAgents).mockResolvedValue([
{
id: 'agent-1',
title: 'Agent One',
description: 'First agent',
avatar: null,
backgroundColor: null,
},
{
id: 'agent-2',
title: 'Agent Two',
description: 'Second agent',
avatar: null,
backgroundColor: null,
},
] as any);
const result = await runtime.searchAgents({
keyword: 'test',
source: 'user',
});
expect(result.success).toBe(true);
expect(result.content).toContain('Found 2 agents');
expect(result.state).toMatchObject({
agents: expect.arrayContaining([
expect.objectContaining({ id: 'agent-1', isMarket: false }),
expect.objectContaining({ id: 'agent-2', isMarket: false }),
]),
source: 'user',
totalCount: 2,
});
});
it('should search marketplace agents', async () => {
vi.mocked(mockDiscoverService.getAssistantList).mockResolvedValue({
items: [
{
identifier: 'market-agent-1',
title: 'Market Agent',
description: 'From market',
} as any,
],
totalCount: 1,
} as any);
const result = await runtime.searchAgents({
keyword: 'market',
source: 'market',
});
expect(result.success).toBe(true);
expect(result.state).toMatchObject({
agents: expect.arrayContaining([
expect.objectContaining({ id: 'market-agent-1', isMarket: true }),
]),
source: 'market',
});
});
it('should search all sources by default', async () => {
vi.mocked(mockAgentService.queryAgents).mockResolvedValue([
{
id: 'user-agent',
title: 'User Agent',
avatar: null,
backgroundColor: null,
description: null,
},
] as any);
vi.mocked(mockDiscoverService.getAssistantList).mockResolvedValue({
items: [{ identifier: 'market-agent', title: 'Market Agent' } as any],
totalCount: 1,
} as any);
const result = await runtime.searchAgents({ keyword: 'test' });
expect(result.success).toBe(true);
expect(result.state?.source).toBe('all');
expect(result.state?.agents).toHaveLength(2);
});
it('should return no agents found message', async () => {
vi.mocked(mockAgentService.queryAgents).mockResolvedValue([]);
const result = await runtime.searchAgents({ keyword: 'nonexistent', source: 'user' });
expect(result.success).toBe(true);
expect(result.content).toContain('No agents found');
});
});
describe('getAvailableModels', () => {
it('should return all available models', async () => {
const result = await runtime.getAvailableModels({});
expect(result.success).toBe(true);
expect(result.content).toContain('Found 2 provider(s)');
expect(result.content).toContain('3 model(s)');
expect(result.state).toMatchObject({
providers: expect.arrayContaining([
expect.objectContaining({ id: 'openai' }),
expect.objectContaining({ id: 'anthropic' }),
]),
});
});
it('should filter by providerId', async () => {
const result = await runtime.getAvailableModels({ providerId: 'openai' });
expect(result.success).toBe(true);
expect(result.state?.providers).toHaveLength(1);
expect(result.state?.providers[0].id).toBe('openai');
});
});
describe('updatePrompt', () => {
it('should update prompt without streaming', async () => {
const result = await runtime.updatePrompt('agent-id', {
prompt: 'New system prompt',
streaming: false,
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully updated system prompt');
expect(result.content).toContain('17 characters');
expect(result.state).toMatchObject({
newPrompt: 'New system prompt',
previousPrompt: 'Previous prompt',
success: true,
});
});
it('should clear prompt when empty', async () => {
const result = await runtime.updatePrompt('agent-id', {
prompt: '',
streaming: false,
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully cleared system prompt');
});
});
describe('searchMarketTools', () => {
it('should search market tools', async () => {
vi.mocked(mockDiscoverService.getMcpList).mockResolvedValue({
items: [
{
identifier: 'tool-1',
name: 'Tool One',
description: 'First tool',
author: 'Author',
} as any,
{ identifier: 'tool-2', name: 'Tool Two', description: 'Second tool' } as any,
],
totalCount: 2,
} as any);
const result = await runtime.searchMarketTools({ query: 'test' });
expect(result.success).toBe(true);
expect(result.content).toContain('Found 2 tool(s)');
expect(result.state).toMatchObject({
query: 'test',
tools: expect.arrayContaining([
expect.objectContaining({ identifier: 'tool-1' }),
expect.objectContaining({ identifier: 'tool-2' }),
]),
totalCount: 2,
});
});
});
describe('installPlugin', () => {
it('should install builtin tool', async () => {
const result = await runtime.installPlugin('agent-id', {
identifier: 'lobe-web-browsing',
source: 'official',
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully enabled builtin tool');
expect(result.state).toMatchObject({
installed: true,
pluginId: 'lobe-web-browsing',
success: true,
});
});
it('should return error for unknown official tool', async () => {
const result = await runtime.installPlugin('agent-id', {
identifier: 'unknown-tool',
source: 'official',
});
expect(result.success).toBe(false);
expect(result.content).toContain('not found');
});
it('should install market plugin', async () => {
const result = await runtime.installPlugin('agent-id', {
identifier: 'market-plugin',
source: 'market',
});
expect(result.success).toBe(true);
expect(result.content).toContain('Successfully installed and enabled MCP plugin');
});
});
});

View file

@ -0,0 +1,2 @@
export { AgentManagerRuntime } from './AgentManagerRuntime';
export * from './types';

View file

@ -0,0 +1,237 @@
import type { LobeAgentConfig, MetaData } from '@lobechat/types';
import type { PartialDeep } from 'type-fest';
// ==================== Service Interfaces ====================
/**
* Interface for agent service operations
* Can be implemented by client-side or server-side services
*/
export interface IAgentService {
createAgent: (params: { config: Record<string, unknown> }) => Promise<{
agentId?: string;
sessionId?: string;
}>;
queryAgents: (params: { keyword?: string; limit?: number }) => Promise<
Array<{
avatar?: string | null;
backgroundColor?: string | null;
description?: string | null;
id: string;
title?: string | null;
}>
>;
removeAgent: (agentId: string) => Promise<unknown>;
}
/**
* Interface for discover/marketplace service operations
*/
export interface IDiscoverService {
getAssistantList: (params: { category?: string; pageSize?: number; q?: string }) => Promise<{
items: Array<{
avatar?: string;
backgroundColor?: string;
description?: string;
identifier: string;
title?: string;
}>;
totalCount: number;
}>;
getMcpList: (params: { category?: string; pageSize?: number; q?: string }) => Promise<{
items: Array<{
author?: string;
cloudEndPoint?: string;
description?: string;
haveCloudEndpoint?: boolean;
icon?: string;
identifier: string;
name: string;
tags?: string[];
}>;
totalCount: number;
}>;
}
/**
* Required services for AgentManagerRuntime
* Services must be injected for runtime-agnostic usage
*/
export interface AgentManagerRuntimeServices {
/**
* Agent service for CRUD operations
*/
agentService: IAgentService;
/**
* Discover service for marketplace operations
*/
discoverService: IDiscoverService;
}
// ==================== Agent CRUD Types ====================
export interface CreateAgentParams {
avatar?: string;
backgroundColor?: string;
description?: string;
model?: string;
openingMessage?: string;
openingQuestions?: string[];
plugins?: string[];
provider?: string;
systemRole?: string;
tags?: string[];
title: string;
}
export interface CreateAgentState {
agentId?: string;
error?: string;
sessionId?: string;
success: boolean;
}
export interface UpdateAgentConfigParams {
config?: PartialDeep<LobeAgentConfig>;
meta?: Partial<MetaData>;
togglePlugin?: {
enabled?: boolean;
pluginId: string;
};
}
export interface UpdateAgentConfigState {
config?: {
newValues: Record<string, unknown>;
previousValues: Record<string, unknown>;
updatedFields: string[];
};
meta?: {
newValues: Partial<MetaData>;
previousValues: Partial<MetaData>;
updatedFields: string[];
};
success: boolean;
togglePlugin?: {
enabled: boolean;
pluginId: string;
};
}
export interface DeleteAgentState {
agentId: string;
success: boolean;
}
// ==================== Search Types ====================
export type SearchAgentSource = 'user' | 'market' | 'all';
export interface SearchAgentParams {
category?: string;
keyword?: string;
limit?: number;
source?: SearchAgentSource;
}
export interface AgentSearchItem {
avatar?: string;
backgroundColor?: string;
description?: string;
id: string;
isMarket?: boolean;
title?: string;
}
export interface SearchAgentState {
agents: AgentSearchItem[];
keyword?: string;
source: SearchAgentSource;
totalCount: number;
}
// ==================== Models Types ====================
export interface GetAvailableModelsParams {
providerId?: string;
}
export interface AvailableModel {
abilities?: {
files?: boolean;
functionCall?: boolean;
reasoning?: boolean;
vision?: boolean;
};
description?: string;
id: string;
name: string;
}
export interface AvailableProvider {
id: string;
models: AvailableModel[];
name: string;
}
export interface GetAvailableModelsState {
providers: AvailableProvider[];
}
// ==================== Prompt Types ====================
export interface UpdatePromptParams {
prompt: string;
streaming?: boolean;
}
export interface UpdatePromptState {
newPrompt: string;
previousPrompt?: string;
success: boolean;
}
// ==================== Plugin/Tools Types ====================
export interface SearchMarketToolsParams {
category?: string;
pageSize?: number;
query?: string;
}
export interface MarketToolItem {
author?: string;
cloudEndPoint?: string;
description?: string;
haveCloudEndpoint?: boolean;
icon?: string;
identifier: string;
installed?: boolean;
name: string;
tags?: string[];
}
export interface SearchMarketToolsState {
query?: string;
tools: MarketToolItem[];
totalCount: number;
}
export interface InstallPluginParams {
identifier: string;
source: 'market' | 'official';
}
export interface InstallPluginState {
awaitingApproval?: boolean;
error?: string;
installed: boolean;
isKlavis?: boolean;
isLobehubSkill?: boolean;
oauthUrl?: string;
pluginId: string;
pluginName?: string;
serverName?: string;
serverStatus?: 'connected' | 'pending_auth' | 'error' | 'not_connected';
success: boolean;
}

View file

@ -5,11 +5,11 @@
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/executor.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
"./executor": "./src/executor.ts"
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/agent-manager-runtime": "workspace:*",
"@lobechat/const": "workspace:*",
"@lobechat/prompts": "workspace:*",
"type-fest": "^4.18.3"

View file

@ -2,11 +2,15 @@
* Agent Builder Executor
*
* Handles all agent builder tool calls for configuring and customizing agents.
* Delegates to AgentManagerRuntime for actual implementation.
*/
import { AgentManagerRuntime } from '@lobechat/agent-manager-runtime';
import type { BuiltinToolContext, BuiltinToolResult } from '@lobechat/types';
import { BaseExecutor } from '@lobechat/types';
import { AgentBuilderExecutionRuntime } from './ExecutionRuntime';
import { agentService } from '@/services/agent';
import { discoverService } from '@/services/discover';
import type {
GetAvailableModelsParams,
InstallPluginParams,
@ -16,7 +20,10 @@ import type {
} from './types';
import { AgentBuilderApiName, AgentBuilderIdentifier } from './types';
const runtime = new AgentBuilderExecutionRuntime();
const runtime = new AgentManagerRuntime({
agentService,
discoverService,
});
class AgentBuilderExecutor extends BaseExecutor<typeof AgentBuilderApiName> {
readonly identifier = AgentBuilderIdentifier;
@ -25,27 +32,11 @@ class AgentBuilderExecutor extends BaseExecutor<typeof AgentBuilderApiName> {
// ==================== Read Operations ====================
getAvailableModels = async (params: GetAvailableModelsParams): Promise<BuiltinToolResult> => {
const result = await runtime.getAvailableModels(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return runtime.getAvailableModels(params);
};
searchMarketTools = async (params: SearchMarketToolsParams): Promise<BuiltinToolResult> => {
const result = await runtime.searchMarketTools(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return runtime.searchMarketTools(params);
};
// ==================== Write Operations ====================
@ -64,15 +55,7 @@ class AgentBuilderExecutor extends BaseExecutor<typeof AgentBuilderApiName> {
};
}
const result = await runtime.updateAgentConfig(agentId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return runtime.updateAgentConfig(agentId, params);
};
updatePrompt = async (
@ -89,18 +72,10 @@ class AgentBuilderExecutor extends BaseExecutor<typeof AgentBuilderApiName> {
};
}
const result = await runtime.updatePrompt(agentId, {
return runtime.updatePrompt(agentId, {
streaming: true,
...params,
});
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
};
installPlugin = async (
@ -117,15 +92,7 @@ class AgentBuilderExecutor extends BaseExecutor<typeof AgentBuilderApiName> {
};
}
const result = await runtime.installPlugin(agentId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return runtime.installPlugin(agentId, params);
};
}

View file

@ -0,0 +1,17 @@
{
"name": "@lobechat/builtin-tool-agent-management",
"version": "1.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/executor.ts"
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/agent-manager-runtime": "workspace:*"
},
"devDependencies": {
"@lobechat/types": "workspace:*"
}
}

View file

@ -0,0 +1,82 @@
'use client';
import { DEFAULT_AVATAR } from '@lobechat/const';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx, useTheme } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import type { CallAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
export const CallAgentInspector = memo<BuiltinInspectorProps<CallAgentParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const theme = useTheme();
const agentId = args?.agentId || partialArgs?.agentId;
const runAsTask = args?.runAsTask || partialArgs?.runAsTask;
// Get agent meta from store
const agentMeta = useAgentStore((s) =>
agentId ? agentSelectors.getAgentMetaById(agentId)(s) : undefined,
);
if (isArgumentsStreaming && !agentId) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-agent-management.apiName.callAgent')}</span>
</div>
);
}
const titleKey = runAsTask
? 'builtins.lobe-agent-management.inspector.callAgent.task'
: 'builtins.lobe-agent-management.inspector.callAgent.sync';
const agentName = agentMeta?.title || agentId;
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>{t(titleKey)}</span>
{agentMeta && (
<Avatar
avatar={agentMeta.avatar || DEFAULT_AVATAR}
background={agentMeta.backgroundColor || theme.colorBgContainer}
shape={'square'}
size={24}
title={agentMeta.title || undefined}
/>
)}
{agentName && <span className={highlightTextStyles.primary}>{agentName}</span>}
</Flexbox>
);
},
);
CallAgentInspector.displayName = 'CallAgentInspector';
export default CallAgentInspector;

View file

@ -0,0 +1,70 @@
'use client';
import { DEFAULT_AVATAR } from '@lobechat/const';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx, useTheme } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import type { CreateAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
export const CreateAgentInspector = memo<BuiltinInspectorProps<CreateAgentParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const theme = useTheme();
const title = args?.title || partialArgs?.title;
const avatar = args?.avatar || partialArgs?.avatar;
const backgroundColor = args?.backgroundColor || partialArgs?.backgroundColor;
if (isArgumentsStreaming && !title) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-agent-management.apiName.createAgent')}</span>
</div>
);
}
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>
{t('builtins.lobe-agent-management.inspector.createAgent.title')}
</span>
<Avatar
avatar={avatar || DEFAULT_AVATAR}
background={backgroundColor || theme.colorBgContainer}
shape={'square'}
size={24}
title={title || undefined}
/>
{title && <span className={highlightTextStyles.primary}>{title}</span>}
</Flexbox>
);
},
);
CreateAgentInspector.displayName = 'CreateAgentInspector';
export default CreateAgentInspector;

View file

@ -0,0 +1,74 @@
'use client';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import type { SearchAgentParams, SearchAgentSource } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
const getSourceTitleKey = (source: SearchAgentSource = 'all') => {
switch (source) {
case 'user': {
return 'builtins.lobe-agent-management.inspector.searchAgent.user';
}
case 'market': {
return 'builtins.lobe-agent-management.inspector.searchAgent.market';
}
default: {
return 'builtins.lobe-agent-management.inspector.searchAgent.all';
}
}
};
export const SearchAgentInspector = memo<BuiltinInspectorProps<SearchAgentParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const keyword = args?.keyword || partialArgs?.keyword;
const source = args?.source || partialArgs?.source || 'all';
const titleKey = useMemo(() => getSourceTitleKey(source), [source]);
if (isArgumentsStreaming && !keyword) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-agent-management.apiName.searchAgent')}</span>
</div>
);
}
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>{t(titleKey)}</span>
{keyword && <span className={highlightTextStyles.primary}>{keyword}</span>}
</Flexbox>
);
},
);
SearchAgentInspector.displayName = 'SearchAgentInspector';
export default SearchAgentInspector;

View file

@ -0,0 +1,59 @@
'use client';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, shinyTextStyles } from '@/styles';
import type { UpdateAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
export const UpdateAgentInspector = memo<BuiltinInspectorProps<UpdateAgentParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const agentId = args?.agentId || partialArgs?.agentId;
if (isArgumentsStreaming && !agentId) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-agent-management.apiName.updateAgent')}</span>
</div>
);
}
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>
{t('builtins.lobe-agent-management.inspector.updateAgent.title')}
</span>
{agentId && <span className={highlightTextStyles.primary}>{agentId}</span>}
</Flexbox>
);
},
);
UpdateAgentInspector.displayName = 'UpdateAgentInspector';
export default UpdateAgentInspector;

View file

@ -0,0 +1,20 @@
import { type BuiltinInspector } from '@lobechat/types';
import { AgentManagementApiName } from '../../types';
import { CallAgentInspector } from './CallAgent';
import { CreateAgentInspector } from './CreateAgent';
import { SearchAgentInspector } from './SearchAgent';
import { UpdateAgentInspector } from './UpdateAgent';
/**
* Agent Management Inspector Components Registry
*
* Inspector components customize the title/header area
* of tool calls in the conversation UI.
*/
export const AgentManagementInspectors: Record<string, BuiltinInspector> = {
[AgentManagementApiName.callAgent]: CallAgentInspector as BuiltinInspector,
[AgentManagementApiName.createAgent]: CreateAgentInspector as BuiltinInspector,
[AgentManagementApiName.searchAgent]: SearchAgentInspector as BuiltinInspector,
[AgentManagementApiName.updateAgent]: UpdateAgentInspector as BuiltinInspector,
};

View file

@ -0,0 +1,38 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { CallAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
instruction: css`
font-size: 13px;
color: ${cssVar.colorTextSecondary};
`,
}));
export const CallAgentRender = memo<BuiltinRenderProps<CallAgentParams>>(({ args }) => {
const { instruction } = args || {};
if (!instruction) return null;
return (
<div className={styles.container}>
<div className={styles.instruction}>
<Markdown variant={'chat'}>{instruction}</Markdown>
</div>
</div>
);
});
CallAgentRender.displayName = 'CallAgentRender';
export default CallAgentRender;

View file

@ -0,0 +1,88 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Block, Markdown, Tag , Flexbox } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { CreateAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
field: css`
margin-block-end: 8px;
&:last-child {
margin-block-end: 0;
}
`,
label: css`
margin-block-end: 4px;
font-size: 12px;
font-weight: 500;
color: ${cssVar.colorTextSecondary};
`,
value: css`
font-size: 13px;
`,
}));
export const CreateAgentRender = memo<BuiltinRenderProps<CreateAgentParams>>(({ args }) => {
const { title, description, systemRole, plugins, model, provider } = args || {};
if (!title && !description && !systemRole && !plugins?.length) return null;
return (
<div className={styles.container}>
{title && (
<div className={styles.field}>
<div className={styles.label}>Title</div>
<div className={styles.value}>{title}</div>
</div>
)}
{description && (
<div className={styles.field}>
<div className={styles.label}>Description</div>
<div className={styles.value}>{description}</div>
</div>
)}
{(model || provider) && (
<div className={styles.field}>
<div className={styles.label}>Model</div>
<div className={styles.value}>
{provider && `${provider}/`}
{model}
</div>
</div>
)}
{plugins && plugins.length > 0 && (
<div className={styles.field}>
<div className={styles.label}>Plugins</div>
<Flexbox gap={4} horizontal wrap={'wrap'}>
{plugins.map((plugin) => (
<Tag key={plugin}>{plugin}</Tag>
))}
</Flexbox>
</div>
)}
{systemRole && (
<div className={styles.field}>
<div className={styles.label}>System Prompt</div>
<Block paddingBlock={8} paddingInline={12} variant={'outlined'} width="100%">
<Markdown fontSize={13} variant={'chat'}>
{systemRole}
</Markdown>
</Block>
</div>
)}
</div>
);
});
CreateAgentRender.displayName = 'CreateAgentRender';
export default CreateAgentRender;

View file

@ -0,0 +1,100 @@
'use client';
import { DEFAULT_AVATAR } from '@lobechat/const';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, useTheme } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import type { AgentSearchItem, SearchAgentParams, SearchAgentState } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
agentItem: css`
padding-block: 8px;
padding-inline: 12px;
border-radius: 6px;
background: ${cssVar.colorFillQuaternary};
`,
agentTitle: css`
font-size: 13px;
font-weight: 500;
`,
container: css`
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
description: css`
overflow: hidden;
font-size: 12px;
color: ${cssVar.colorTextSecondary};
text-overflow: ellipsis;
white-space: nowrap;
`,
marketBadge: css`
padding-block: 2px;
padding-inline: 6px;
border-radius: 4px;
font-size: 10px;
color: ${cssVar.colorPrimary};
background: ${cssVar.colorPrimaryBg};
`,
noResults: css`
padding: 12px;
font-size: 13px;
color: ${cssVar.colorTextSecondary};
text-align: center;
`,
}));
export const SearchAgentRender = memo<BuiltinRenderProps<SearchAgentParams, SearchAgentState>>(
({ pluginState }) => {
const { t } = useTranslation('plugin');
const theme = useTheme();
const agents = pluginState?.agents || [];
if (agents.length === 0) {
return (
<div className={styles.noResults}>
{t('builtins.lobe-agent-builder.inspector.noResults')}
</div>
);
}
return (
<div className={styles.container}>
{agents.map((agent: AgentSearchItem) => (
<Flexbox align={'center'} className={styles.agentItem} gap={12} horizontal key={agent.id}>
<Avatar
avatar={agent.avatar || DEFAULT_AVATAR}
background={agent.backgroundColor || theme.colorBgContainer}
shape={'square'}
size={32}
title={agent.title || undefined}
/>
<Flexbox flex={1} gap={2}>
<Flexbox align={'center'} gap={8} horizontal>
<span className={styles.agentTitle}>{agent.title || agent.id}</span>
{agent.isMarket && <span className={styles.marketBadge}>Market</span>}
</Flexbox>
{agent.description && <span className={styles.description}>{agent.description}</span>}
</Flexbox>
</Flexbox>
))}
</div>
);
},
);
SearchAgentRender.displayName = 'SearchAgentRender';
export default SearchAgentRender;

View file

@ -0,0 +1,78 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Block, Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { UpdateAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
field: css`
margin-block-end: 8px;
&:last-child {
margin-block-end: 0;
}
`,
label: css`
margin-block-end: 4px;
font-size: 12px;
font-weight: 500;
color: ${cssVar.colorTextSecondary};
`,
value: css`
font-size: 13px;
`,
}));
export const UpdateAgentRender = memo<BuiltinRenderProps<UpdateAgentParams>>(({ args }) => {
const { config, meta } = args || {};
const hasConfig = config && Object.keys(config).length > 0;
const hasMeta = meta && Object.keys(meta).length > 0;
if (!hasConfig && !hasMeta) return null;
return (
<div className={styles.container}>
{meta?.title && (
<div className={styles.field}>
<div className={styles.label}>Title</div>
<div className={styles.value}>{meta.title}</div>
</div>
)}
{meta?.description && (
<div className={styles.field}>
<div className={styles.label}>Description</div>
<div className={styles.value}>{meta.description}</div>
</div>
)}
{config?.systemRole && (
<div className={styles.field}>
<div className={styles.label}>System Prompt</div>
<Block paddingBlock={8} paddingInline={12} variant={'outlined'} width="100%">
<Markdown fontSize={13} variant={'chat'}>
{config.systemRole as string}
</Markdown>
</Block>
</div>
)}
{config?.model && (
<div className={styles.field}>
<div className={styles.label}>Model</div>
<div className={styles.value}>{config.model as string}</div>
</div>
)}
</div>
);
});
UpdateAgentRender.displayName = 'UpdateAgentRender';
export default UpdateAgentRender;

View file

@ -0,0 +1,20 @@
import { AgentManagementApiName } from '../../types';
import CallAgentRender from './CallAgent';
import CreateAgentRender from './CreateAgent';
import SearchAgentRender from './SearchAgent';
import UpdateAgentRender from './UpdateAgent';
/**
* Agent Management Tool Render Components Registry
*/
export const AgentManagementRenders = {
[AgentManagementApiName.callAgent]: CallAgentRender,
[AgentManagementApiName.createAgent]: CreateAgentRender,
[AgentManagementApiName.searchAgent]: SearchAgentRender,
[AgentManagementApiName.updateAgent]: UpdateAgentRender,
};
export { default as CallAgentRender } from './CallAgent';
export { default as CreateAgentRender } from './CreateAgent';
export { default as SearchAgentRender } from './SearchAgent';
export { default as UpdateAgentRender } from './UpdateAgent';

View file

@ -0,0 +1,85 @@
'use client';
import type { BuiltinStreamingProps } from '@lobechat/types';
import { Block, Flexbox, Markdown, Tag } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { CreateAgentParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
display: flex;
flex-direction: column;
gap: 12px;
`,
field: css`
display: flex;
flex-direction: column;
gap: 4px;
`,
label: css`
font-size: 12px;
font-weight: 500;
color: ${cssVar.colorTextSecondary};
`,
value: css`
font-size: 13px;
`,
}));
export const CreateAgentStreaming = memo<BuiltinStreamingProps<CreateAgentParams>>(({ args }) => {
const { title, description, systemRole, plugins, model, provider } = args || {};
if (!title && !description && !systemRole && !plugins?.length) return null;
return (
<div className={styles.container}>
{title && (
<div className={styles.field}>
<div className={styles.label}>Title</div>
<div className={styles.value}>{title}</div>
</div>
)}
{description && (
<div className={styles.field}>
<div className={styles.label}>Description</div>
<div className={styles.value}>{description}</div>
</div>
)}
{(model || provider) && (
<div className={styles.field}>
<div className={styles.label}>Model</div>
<div className={styles.value}>
{provider && `${provider}/`}
{model}
</div>
</div>
)}
{plugins && plugins.length > 0 && (
<div className={styles.field}>
<div className={styles.label}>Plugins</div>
<Flexbox gap={4} horizontal wrap={'wrap'}>
{plugins.map((plugin) => (
<Tag key={plugin}>{plugin}</Tag>
))}
</Flexbox>
</div>
)}
{systemRole && (
<div className={styles.field}>
<div className={styles.label}>System Prompt</div>
<Block paddingBlock={8} paddingInline={12} variant={'outlined'} width="100%">
<Markdown animated variant={'chat'}>
{systemRole}
</Markdown>
</Block>
</div>
)}
</div>
);
});
CreateAgentStreaming.displayName = 'CreateAgentStreaming';
export default CreateAgentStreaming;

View file

@ -0,0 +1,16 @@
import type { BuiltinStreaming } from '@lobechat/types';
import { AgentManagementApiName } from '../../types';
import { CreateAgentStreaming } from './CreateAgent';
/**
* Agent Management Streaming Components Registry
*
* Streaming components render tool calls while they are
* still executing, allowing real-time feedback to users.
*/
export const AgentManagementStreamings: Record<string, BuiltinStreaming> = {
[AgentManagementApiName.createAgent]: CreateAgentStreaming as BuiltinStreaming,
};
export { CreateAgentStreaming } from './CreateAgent';

View file

@ -0,0 +1,12 @@
// Inspector components (title/header area)
export { AgentManagementInspectors } from './Inspector';
// Streaming components (real-time feedback)
export { AgentManagementStreamings } from './Streaming';
// Render components (read-only snapshots)
export { AgentManagementRenders } from './Render';
// Re-export types and manifest for convenience
export { AgentManagementManifest } from '../manifest';
export * from '../types';

View file

@ -0,0 +1,236 @@
/**
* Agent Management Executor
*
* Handles all agent management tool calls for creating, updating,
* deleting, searching, and calling AI agents.
* Delegates to AgentManagerRuntime for actual implementation.
*/
import { AgentManagerRuntime } from '@lobechat/agent-manager-runtime';
import {
BaseExecutor,
type BuiltinToolContext,
type BuiltinToolResult,
type ConversationContext,
} from '@lobechat/types';
import { agentService } from '@/services/agent';
import { discoverService } from '@/services/discover';
import { useAgentStore } from '@/store/agent';
import { useChatStore } from '@/store/chat';
import { dbMessageSelectors } from '@/store/chat/slices/message/selectors';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
import {
AgentManagementApiName,
AgentManagementIdentifier,
type CallAgentParams,
type CallAgentState,
type CreateAgentParams,
type DeleteAgentParams,
type SearchAgentParams,
type UpdateAgentParams,
} from './types';
const runtime = new AgentManagerRuntime({
agentService,
discoverService,
});
class AgentManagementExecutor extends BaseExecutor<typeof AgentManagementApiName> {
readonly identifier = AgentManagementIdentifier;
protected readonly apiEnum = AgentManagementApiName;
// ==================== Agent CRUD ====================
createAgent = async (params: CreateAgentParams): Promise<BuiltinToolResult> => {
return runtime.createAgent(params);
};
updateAgent = async (params: UpdateAgentParams): Promise<BuiltinToolResult> => {
const { agentId, config, meta } = params;
return runtime.updateAgentConfig(agentId, { config, meta });
};
deleteAgent = async (params: DeleteAgentParams): Promise<BuiltinToolResult> => {
return runtime.deleteAgent(params.agentId);
};
// ==================== Search ====================
searchAgent = async (params: SearchAgentParams): Promise<BuiltinToolResult> => {
return runtime.searchAgents(params);
};
// ==================== Execution ====================
callAgent = async (
params: CallAgentParams,
ctx: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
const { agentId, instruction, runAsTask, taskTitle, timeout, skipCallSupervisor = false } =
params;
if (runAsTask) {
// Execute as async task using GTD exec_task pattern
// Pre-load target agent config to ensure it exists
const targetAgentExists = useAgentStore.getState().agentMap[agentId];
if (!targetAgentExists) {
try {
const config = await agentService.getAgentConfigById(agentId);
if (!config) {
return {
content: `Agent "${agentId}" not found in your workspace. Please check the agent ID and try again.`,
success: false,
};
}
useAgentStore.getState().internal_dispatchAgentMap(agentId, config);
} catch (error) {
console.error('[callAgent] Failed to load agent config:', error);
return {
content: `Failed to load agent "${agentId}": ${(error as Error).message}`,
success: false,
};
}
}
// Return special state that will be recognized by AgentRuntime's exec_task executor
// Following GTD execTask pattern: stop: true + state.type = 'execTask'
return {
content: `🚀 Triggered async task to call agent "${agentId}"${taskTitle ? `: ${taskTitle}` : ''}`,
state: {
parentMessageId: ctx.messageId,
task: {
description: taskTitle || `Call agent ${agentId}`,
instruction,
targetAgentId: agentId, // Special field for callAgent - indicates target agent
timeout: timeout || 1_800_000,
},
type: 'execTask', // Use same type as GTD to reuse existing executor
},
stop: true,
success: true,
};
}
// Execute as synchronous speak
// Two modes: Group vs Agents
// Mode 1: Group environment - use group orchestration
if (ctx.groupId && ctx.groupOrchestration && ctx.agentId && ctx.registerAfterCompletion) {
// Register afterCompletion callback to trigger group orchestration
ctx.registerAfterCompletion(() =>
ctx.groupOrchestration!.triggerSpeak({
agentId,
instruction,
skipCallSupervisor,
supervisorAgentId: ctx.agentId!,
}),
);
return {
content: `Triggered agent "${agentId}" to respond.`,
state: {
agentId,
instruction,
mode: 'speak',
skipCallSupervisor,
} as CallAgentState,
stop: true,
success: true,
};
}
// Mode 2: Agents mode (non-group) - execute directly with subAgentId
if (ctx.registerAfterCompletion) {
// Pre-load target agent config if not already loaded (before registerAfterCompletion)
// This ensures we fail fast with a clear error message if agent doesn't exist
const targetAgentExists = useAgentStore.getState().agentMap[agentId];
if (!targetAgentExists) {
try {
const config = await agentService.getAgentConfigById(agentId);
if (!config) {
return {
content: `Agent "${agentId}" not found in your workspace. Please check the agent ID and try again.`,
success: false,
};
}
useAgentStore.getState().internal_dispatchAgentMap(agentId, config);
} catch (error) {
console.error('[callAgent] Failed to load agent config:', error);
return {
content: `Failed to load agent "${agentId}": ${(error as Error).message}`,
success: false,
};
}
}
// Register afterCompletion to execute the agent
ctx.registerAfterCompletion(async () => {
const get = useChatStore.getState;
// Build conversation context - use current agent's context
const conversationContext: ConversationContext = {
agentId: ctx.agentId || '',
topicId: ctx.topicId || null,
// subAgentId will be set when calling internal_execAgentRuntime
};
// Get current messages
const chatKey = messageMapKey(conversationContext);
const messages = dbMessageSelectors.getDbMessagesByKey(chatKey)(get());
if (messages.length === 0) {
console.error('[callAgent] No messages found in current conversation');
return;
}
try {
// Execute with subAgentId + scope: 'sub_agent'
// - context.agentId = current agent (for message storage and message.agentId)
// - context.topicId = current topic
// - context.subAgentId = target agent (for agent config - model, prompt, etc.)
// - context.scope = 'sub_agent' (indicates this is agent-to-agent call, not group)
// This will create messages in current agent's conversation but use target agent's config
// The message.agentId will still be current agent, but metadata stores subAgentId + scope
await get().internal_execAgentRuntime({
context: { ...conversationContext, subAgentId: agentId, scope: 'sub_agent' },
messages: messages,
parentMessageId: ctx.messageId,
parentMessageType: 'tool',
});
} catch (error) {
console.error('[callAgent] internal_execAgentRuntime failed:', error);
throw error;
}
});
return {
content: `Called agent "${agentId}" to respond.`,
state: {
agentId,
instruction,
mode: 'speak',
skipCallSupervisor,
} as CallAgentState,
stop: true,
success: true,
};
}
// Fallback if registerAfterCompletion not available
console.warn('[callAgent] registerAfterCompletion not available in context');
return {
content: `Called agent "${agentId}" but execution may not complete properly.`,
state: {
agentId,
instruction,
mode: 'speak',
skipCallSupervisor,
} as CallAgentState,
stop: true,
success: false,
};
};
}
export const agentManagementExecutor = new AgentManagementExecutor();

View file

@ -0,0 +1,3 @@
export * from './manifest';
export * from './systemRole';
export * from './types';

View file

@ -0,0 +1,248 @@
import type { BuiltinToolManifest } from '@lobechat/types';
import { systemPrompt } from './systemRole';
import { AgentManagementApiName, AgentManagementIdentifier } from './types';
export const AgentManagementManifest: BuiltinToolManifest = {
/* eslint-disable sort-keys-fix/sort-keys-fix */
api: [
// ==================== Agent CRUD ====================
{
description:
'Create a new AI agent with custom configuration. The agent will be added to your workspace and can be used for conversations or tasks.',
name: AgentManagementApiName.createAgent,
parameters: {
properties: {
title: {
description: 'The display name for the agent (required)',
type: 'string',
},
description: {
description: 'A brief description of what the agent does',
type: 'string',
},
systemRole: {
description:
"The system prompt that defines the agent's personality, expertise, and behavior. This is the core instruction for the agent.",
type: 'string',
},
avatar: {
description: 'Agent avatar (emoji like "🤖" or image URL)',
type: 'string',
},
backgroundColor: {
description: 'Background color for the agent card (hex color code)',
type: 'string',
},
model: {
description:
'The AI model to use (e.g., "gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-20241022")',
type: 'string',
},
provider: {
description: 'The AI provider (e.g., "openai", "anthropic", "google")',
type: 'string',
},
plugins: {
description: 'Array of plugin identifiers to enable for this agent',
items: { type: 'string' },
type: 'array',
},
openingMessage: {
description: 'Welcome message displayed when starting a new conversation',
type: 'string',
},
openingQuestions: {
description: 'Suggested questions to help users start the conversation',
items: { type: 'string' },
type: 'array',
},
tags: {
description: 'Tags for categorizing the agent',
items: { type: 'string' },
type: 'array',
},
},
required: ['title'],
type: 'object',
},
},
{
description:
'Update an existing agent configuration. Only include fields you want to change.',
name: AgentManagementApiName.updateAgent,
parameters: {
properties: {
agentId: {
description: 'The ID of the agent to update',
type: 'string',
},
config: {
description: 'Partial agent configuration to update',
properties: {
model: {
description: 'The AI model to use',
type: 'string',
},
provider: {
description: 'The AI provider',
type: 'string',
},
systemRole: {
description: 'The system prompt',
type: 'string',
},
plugins: {
description: 'Array of enabled plugin identifiers',
items: { type: 'string' },
type: 'array',
},
openingMessage: {
description: 'Opening message for new conversations',
type: 'string',
},
openingQuestions: {
description: 'Suggested opening questions',
items: { type: 'string' },
type: 'array',
},
},
type: 'object',
},
meta: {
description: 'Partial metadata to update',
properties: {
title: {
description: 'Agent display name',
type: 'string',
},
description: {
description: 'Agent description',
type: 'string',
},
avatar: {
description: 'Agent avatar',
type: 'string',
},
backgroundColor: {
description: 'Background color',
type: 'string',
},
tags: {
description: 'Tags for categorization',
items: { type: 'string' },
type: 'array',
},
},
type: 'object',
},
},
required: ['agentId'],
type: 'object',
},
},
{
description:
'Delete an agent from your workspace. This action cannot be undone. The agent and its associated session will be removed.',
humanIntervention: 'required',
name: AgentManagementApiName.deleteAgent,
parameters: {
properties: {
agentId: {
description: 'The ID of the agent to delete',
type: 'string',
},
},
required: ['agentId'],
type: 'object',
},
},
// ==================== Search ====================
{
description:
"Search for agents in your workspace or the marketplace. Use 'user' source to find your own agents, 'market' for marketplace agents, or 'all' for both.",
name: AgentManagementApiName.searchAgent,
parameters: {
properties: {
keyword: {
description: 'Search keywords to find agents by name or description',
type: 'string',
},
source: {
description:
"Where to search: 'user' (your agents), 'market' (marketplace), 'all' (both). Default: 'all'",
enum: ['user', 'market', 'all'],
type: 'string',
},
category: {
description:
'Category filter for marketplace search (e.g., "programming", "writing", "translation")',
type: 'string',
},
limit: {
default: 10,
description: 'Maximum number of results to return (default: 10, max: 20)',
type: 'number',
},
},
required: [],
type: 'object',
},
},
// ==================== Execution ====================
{
description:
'Call an agent to handle a specific task or respond to an instruction. Can run synchronously (immediate response) or as a background task for longer operations.',
name: AgentManagementApiName.callAgent,
parameters: {
properties: {
agentId: {
description: 'The ID of the agent to call',
type: 'string',
},
instruction: {
description:
'The instruction or task for the agent to execute. Be specific about expected deliverables.',
type: 'string',
},
runAsTask: {
default: false,
description:
'If true, run as a background task for longer operations. The agent will work asynchronously and return results upon completion.',
type: 'boolean',
},
taskTitle: {
description: 'Brief title for the task (shown in UI). Required when runAsTask is true.',
type: 'string',
},
timeout: {
default: 1_800_000,
description:
'Maximum time in milliseconds to wait for task completion (default: 1800000 = 30 minutes). Only applies when runAsTask is true.',
type: 'number',
},
skipCallSupervisor: {
default: false,
description:
'If true (and in a group context), the orchestration will end after this agent responds, without calling the supervisor again. Only relevant when used within agent groups.',
type: 'boolean',
},
},
required: ['agentId', 'instruction'],
type: 'object',
},
},
],
identifier: AgentManagementIdentifier,
meta: {
avatar: '🤖',
description: 'Create, manage, and orchestrate AI agents',
title: 'Agent Management',
},
systemRole: systemPrompt,
type: 'builtin',
};
export { AgentManagementApiName, AgentManagementIdentifier } from './types';

View file

@ -0,0 +1,193 @@
/**
* System role for Agent Management tool
*
* This provides guidance on how to effectively use the agent management tools
* to create, configure, search, and orchestrate AI agents.
*/
export const systemPrompt = `You have Agent Management tools to create, configure, and orchestrate AI agents. Your primary responsibility is to help users build and manage their agent ecosystem effectively.
<core_capabilities>
## Tool Overview
**Agent CRUD:**
- **createAgent**: Create a new agent with custom configuration (title, description, systemRole, model, provider, plugins, avatar, etc.)
- **updateAgent**: Modify an existing agent's settings
- **deleteAgent**: Remove an agent from the workspace
**Discovery:**
- **searchAgent**: Find agents in user's workspace or marketplace
**Execution:**
- **callAgent**: Invoke an agent to handle a task (synchronously or as async background task)
</core_capabilities>
<context_injection>
## Available Resources
When this tool is enabled, you will receive contextual information about:
- **Available Models**: List of AI models and providers you can use when creating/updating agents
- **Available Plugins**: List of plugins (builtin tools, Klavis integrations, LobehubSkill providers) you can enable for agents
This information is automatically injected into the conversation context. Use the exact IDs from the context when specifying model/provider/plugins parameters.
</context_injection>
<agent_creation_guide>
## Creating Effective Agents
When creating an agent using createAgent, you can specify:
### 1. Basic Information (Required)
- **title** (required): Clear, concise name that reflects the agent's purpose
- **description** (optional): Brief summary of capabilities and use cases
### 2. System Prompt (systemRole)
The system prompt is the most important element. A good system prompt should:
- Define the agent's role and expertise
- Specify the communication style and tone
- Include constraints and guidelines
- Provide examples when helpful
**Example structure:**
\`\`\`
You are a [role] specialized in [domain].
## Core Responsibilities
- [Responsibility 1]
- [Responsibility 2]
## Guidelines
- [Guideline 1]
- [Guideline 2]
## Response Format
[How to structure responses]
\`\`\`
### 3. Model & Provider Selection
**CRITICAL: You MUST select from the available models and providers listed in the injected context above. Do NOT use models that are not explicitly listed.**
When selecting a model, follow this priority order:
1. **First Priority - LobeHub Provider Models**:
- If available, prioritize models from the "lobehub" provider
- These are optimized for the LobeHub ecosystem
2. **Second Priority - Premium Frontier Models**:
- **Anthropic**: Claude Sonnet 4.5, Claude Opus 4.5, or newer Opus/Sonnet series
- **OpenAI**: GPT-5 or higher (exclude mini variants)
- **Google**: Gemini 2.5 Pro or newer versions
3. **Third Priority - Standard Models**:
- If none of the above are available, choose from other enabled models based on task requirements
- Consider model capabilities (reasoning, vision, function calling) from the injected context
**Task-Based Recommendations**:
- **Complex reasoning, analysis**: Choose models with strong reasoning capabilities
- **Fast, simple tasks**: Choose lighter models for cost-effectiveness
- **Multimodal tasks**: Ensure the model supports vision/video if needed
- **Tool use**: Verify function calling support for agents using plugins
**IMPORTANT:** Always specify both \`model\` and \`provider\` parameters together using the exact IDs from the injected context.
### 4. Plugins (Optional)
You can specify plugins during agent creation using the \`plugins\` parameter:
- **plugins**: Array of plugin identifiers (e.g., ["lobe-image-designer", "search-engine"])
**Plugin types available:**
- **Builtin tools**: Core system tools (e.g., web search, image generation)
- **Klavis integrations**: Third-party service integrations requiring OAuth
- **LobehubSkill providers**: Advanced skill providers
Refer to the injected context for available plugin IDs and descriptions.
### 5. Visual Customization (Optional)
- **avatar**: Emoji or image URL (e.g., "🤖")
- **backgroundColor**: Hex color code (e.g., "#3B82F6")
- **tags**: Array of tags for categorization (e.g., ["coding", "assistant"])
### 6. User Experience (Optional)
- **openingMessage**: Welcome message displayed when starting a new conversation
- **openingQuestions**: Array of suggested questions to help users start (e.g., ["What can you help me with?"])
</agent_creation_guide>
<search_guide>
## Finding the Right Agent
Use searchAgent to discover agents:
**User Agents** (source: 'user'):
- Your personally created agents
- Previously used marketplace agents
**Marketplace Agents** (source: 'market'):
- Community-created agents
- Professional templates
- Specialized tools
**Search Tips:**
- Use specific keywords related to the task
- Filter by category when browsing marketplace
- Check agent descriptions for capability details
</search_guide>
<execution_guide>
## Calling Agents
### Synchronous Call (default)
For quick responses in the conversation context:
\`\`\`
callAgent(agentId, instruction)
\`\`\`
The agent will respond directly in the current conversation.
### Asynchronous Task
For longer operations that benefit from focused execution:
\`\`\`
callAgent(agentId, instruction, runAsTask: true, taskTitle: "Brief description")
\`\`\`
The agent will work in the background and return results upon completion.
**When to use runAsTask:**
- Complex multi-step operations
- Tasks requiring extended processing time
- Work that shouldn't block the conversation flow
- Operations that benefit from isolated execution context
</execution_guide>
<workflow_patterns>
## Common Workflows
### Pattern 1: Create with Full Configuration
1. Review available models and plugins from injected context
2. Create agent with complete configuration (title, systemRole, model, provider, plugins)
3. Test the agent with sample tasks
### Pattern 2: Create and Refine
1. Create agent with basic configuration (title, systemRole, model, provider)
2. Test with sample tasks
3. Update configuration based on results (add plugins, adjust settings)
### Pattern 3: Find and Use
1. Search for existing agents (workspace or marketplace)
2. Select the best match for the task
3. Call agent with specific instruction
### Pattern 4: Create, Call, and Iterate
1. Create a specialized agent for a specific task
2. Immediately call the agent to execute the task
3. Refine agent configuration based on results
</workflow_patterns>
<best_practices>
## Best Practices
1. **Use Context Information**: Always refer to the injected context for accurate model IDs, provider IDs, and plugin IDs
2. **Specify Model AND Provider**: When setting a model, always specify both \`model\` and \`provider\` together
3. **Start with Essential Config**: Begin with title, systemRole, model, and provider. Add plugins and other settings as needed
4. **Clear Instructions**: When calling agents, be specific about expected outcomes and deliverables
5. **Right Tool for the Job**: Match agent capabilities (model, plugins) to task requirements
6. **Meaningful Metadata**: Use descriptive titles, tags, and descriptions for easy discovery
7. **Test and Iterate**: Test agents with sample tasks and refine configuration based on actual usage
8. **Plugin Selection**: Only enable plugins that are relevant to the agent's purpose to avoid unnecessary overhead
</best_practices>`;

View file

@ -0,0 +1,304 @@
import type { LobeAgentConfig, MetaData } from '@lobechat/types';
import type { PartialDeep } from 'type-fest';
/**
* Agent Management Tool Identifier
*/
export const AgentManagementIdentifier = 'lobe-agent-management';
/**
* Agent Management API Names
*/
export const AgentManagementApiName = {
// ==================== Execution ====================
/** Call an agent to handle a task */
callAgent: 'callAgent',
// ==================== Agent CRUD ====================
/** Create a new agent */
createAgent: 'createAgent',
/** Delete an agent */
deleteAgent: 'deleteAgent',
// ==================== Search ====================
/** Search agents (user's own and marketplace) */
searchAgent: 'searchAgent',
/** Update an existing agent */
updateAgent: 'updateAgent',
} as const;
export type AgentManagementApiNameType =
(typeof AgentManagementApiName)[keyof typeof AgentManagementApiName];
// ==================== Create Agent ====================
export interface CreateAgentParams {
/**
* Agent avatar (emoji or image URL)
*/
avatar?: string;
/**
* Background color for the agent card
*/
backgroundColor?: string;
/**
* Agent description
*/
description?: string;
/**
* AI model to use (e.g., "gpt-4o", "claude-3-5-sonnet")
*/
model?: string;
/**
* Opening message for new conversations
*/
openingMessage?: string;
/**
* Suggested opening questions
*/
openingQuestions?: string[];
/**
* Enabled plugins
*/
plugins?: string[];
/**
* AI provider (e.g., "openai", "anthropic")
*/
provider?: string;
/**
* System prompt that defines the agent's behavior
*/
systemRole?: string;
/**
* Tags for categorization
*/
tags?: string[];
/**
* Agent display name/title
*/
title: string;
}
export interface CreateAgentState {
/**
* The created agent's ID
*/
agentId?: string;
/**
* Error message if creation failed
*/
error?: string;
/**
* The associated session ID
*/
sessionId?: string;
/**
* Whether the creation was successful
*/
success: boolean;
}
// ==================== Update Agent ====================
export interface UpdateAgentParams {
/**
* The agent ID to update
*/
agentId: string;
/**
* Partial agent configuration to update
*/
config?: PartialDeep<LobeAgentConfig>;
/**
* Partial metadata to update
*/
meta?: Partial<MetaData>;
}
export interface UpdateAgentState {
/**
* The agent ID that was updated
*/
agentId: string;
/**
* Updated configuration fields
*/
config?: {
newValues: Record<string, unknown>;
previousValues: Record<string, unknown>;
updatedFields: string[];
};
/**
* Updated metadata fields
*/
meta?: {
newValues: Partial<MetaData>;
previousValues: Partial<MetaData>;
updatedFields: string[];
};
/**
* Whether the update was successful
*/
success: boolean;
}
// ==================== Delete Agent ====================
export interface DeleteAgentParams {
/**
* The agent ID to delete
*/
agentId: string;
}
export interface DeleteAgentState {
/**
* The deleted agent ID
*/
agentId: string;
/**
* Whether the deletion was successful
*/
success: boolean;
}
// ==================== Search Agent ====================
export type SearchAgentSource = 'user' | 'market' | 'all';
export interface SearchAgentParams {
/**
* Category filter for marketplace search
*/
category?: string;
/**
* Search keywords
*/
keyword?: string;
/**
* Maximum number of results (default: 10)
*/
limit?: number;
/**
* Search source: 'user' (own agents), 'market' (marketplace), 'all' (both)
*/
source?: SearchAgentSource;
}
export interface AgentSearchItem {
/**
* Agent avatar
*/
avatar?: string;
/**
* Background color
*/
backgroundColor?: string;
/**
* Agent description
*/
description?: string;
/**
* Agent ID (for user agents) or identifier (for market agents)
*/
id: string;
/**
* Whether this is a marketplace agent
*/
isMarket?: boolean;
/**
* Agent title
*/
title?: string;
}
export interface SearchAgentState {
/**
* List of matching agents
*/
agents: AgentSearchItem[];
/**
* The search keyword used
*/
keyword?: string;
/**
* The search source used
*/
source: SearchAgentSource;
/**
* Total count of matching agents
*/
totalCount: number;
}
// ==================== Call Agent ====================
export interface CallAgentParams {
/**
* The agent ID to call
*/
agentId: string;
/**
* Instruction or task for the agent to execute
*/
instruction: string;
/**
* If true, execute as an async background task
*/
runAsTask?: boolean;
/**
* Task title (required when runAsTask is true)
*/
taskTitle?: string;
/**
* Timeout in milliseconds for task execution (default: 1800000 = 30 minutes)
*/
timeout?: number;
/**
* If true (and in a group context), skip calling supervisor after agent responds.
* Only relevant when used within agent groups. Default: false
*/
skipCallSupervisor?: boolean;
}
export interface CallAgentState {
/**
* The agent ID being called
*/
agentId: string;
/**
* The instruction given
*/
instruction: string;
/**
* Execution mode
*/
mode: 'speak' | 'task';
/**
* Task ID if running as background task
*/
taskId?: string;
/**
* Whether to skip calling supervisor after agent responds (only relevant in group context)
*/
skipCallSupervisor?: boolean;
}

View file

@ -10,6 +10,7 @@
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/agent-manager-runtime": "workspace:*",
"@lobechat/builtin-tool-agent-builder": "workspace:*"
},
"devDependencies": {

View file

@ -1,5 +1,5 @@
import { formatAgentProfile } from '@lobechat/prompts';
import type { BuiltinServerRuntimeOutput } from '@lobechat/types';
import type { BuiltinToolResult } from '@lobechat/types';
import { agentService } from '@/services/agent';
import type { GroupMemberConfig } from '@/services/chatGroup';
@ -42,10 +42,11 @@ export class GroupAgentBuilderExecutionRuntime {
async getAgentInfo(
groupId: string | undefined,
args: GetAgentInfoParams,
): Promise<BuiltinServerRuntimeOutput> {
): Promise<BuiltinToolResult> {
if (!groupId) {
return {
content: 'No group context available',
error: { message: 'No group context available', type: 'NoGroupContext' },
success: false,
};
}
@ -54,7 +55,11 @@ export class GroupAgentBuilderExecutionRuntime {
const agent = agentGroupSelectors.getAgentByIdFromGroup(groupId, args.agentId)(state);
if (!agent) {
return { content: `Agent "${args.agentId}" not found in this group`, success: false };
return {
content: `Agent "${args.agentId}" not found in this group`,
error: { message: `Agent "${args.agentId}" not found`, type: 'AgentNotFound' },
success: false,
};
}
// Return formatted agent profile for the supervisor
@ -66,7 +71,7 @@ export class GroupAgentBuilderExecutionRuntime {
/**
* Search for agents that can be invited to the group
*/
async searchAgent(args: SearchAgentParams): Promise<BuiltinServerRuntimeOutput> {
async searchAgent(args: SearchAgentParams): Promise<BuiltinToolResult> {
const { query, limit = 10 } = args;
try {
@ -107,19 +112,14 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to search agents: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to search agents');
}
}
/**
* Create a new agent and add it to the group
*/
async createAgent(groupId: string, args: CreateAgentParams): Promise<BuiltinServerRuntimeOutput> {
async createAgent(groupId: string, args: CreateAgentParams): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.getGroupById(groupId)(state);
@ -127,7 +127,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'Group not found',
error: 'Group not found',
error: { message: 'Group not found', type: 'GroupNotFound' },
success: false,
};
}
@ -149,6 +149,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!result.agentId) {
return {
content: 'Failed to create agent: No agent ID returned',
error: { message: 'No agent ID returned', type: 'CreateError' },
success: false,
};
}
@ -166,12 +167,7 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to create agent: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to create agent');
}
}
@ -182,7 +178,7 @@ export class GroupAgentBuilderExecutionRuntime {
async batchCreateAgents(
groupId: string,
args: BatchCreateAgentsParams,
): Promise<BuiltinServerRuntimeOutput> {
): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.getGroupById(groupId)(state);
@ -190,7 +186,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'Group not found',
error: 'Group not found',
error: { message: 'Group not found', type: 'GroupNotFound' },
success: false,
};
}
@ -231,19 +227,14 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to create agents: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to create agents');
}
}
/**
* Invite an agent to the group
*/
async inviteAgent(groupId: string, args: InviteAgentParams): Promise<BuiltinServerRuntimeOutput> {
async inviteAgent(groupId: string, args: InviteAgentParams): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.getGroupById(groupId)(state);
@ -251,7 +242,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'Group not found',
error: 'Group not found',
error: { message: 'Group not found', type: 'GroupNotFound' },
success: false,
};
}
@ -302,19 +293,14 @@ export class GroupAgentBuilderExecutionRuntime {
success: wasAdded,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to invite agent: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to invite agent');
}
}
/**
* Remove an agent from the group
*/
async removeAgent(groupId: string, args: RemoveAgentParams): Promise<BuiltinServerRuntimeOutput> {
async removeAgent(groupId: string, args: RemoveAgentParams): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.getGroupById(groupId)(state);
@ -322,7 +308,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'Group not found',
error: 'Group not found',
error: { message: 'Group not found', type: 'GroupNotFound' },
success: false,
};
}
@ -379,12 +365,7 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to remove agent: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to remove agent');
}
}
@ -396,7 +377,7 @@ export class GroupAgentBuilderExecutionRuntime {
async updateAgentPrompt(
groupId: string,
args: UpdateAgentPromptParams,
): Promise<BuiltinServerRuntimeOutput> {
): Promise<BuiltinToolResult> {
try {
const { agentId, prompt } = args;
@ -432,19 +413,14 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to update agent prompt: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to update agent prompt');
}
}
/**
* Update group configuration and metadata (unified method)
*/
async updateGroup(args: UpdateGroupParams): Promise<BuiltinServerRuntimeOutput> {
async updateGroup(args: UpdateGroupParams): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.currentGroup(state);
@ -452,7 +428,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'No active group found',
error: 'No active group found',
error: { message: 'No active group found', type: 'NoGroupContext' },
success: false,
};
}
@ -462,7 +438,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!config && !meta) {
return {
content: 'No configuration or metadata provided',
error: 'No configuration or metadata provided',
error: { message: 'No configuration or metadata provided', type: 'NoDataProvided' },
success: false,
};
}
@ -532,19 +508,14 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to update group: ${err.message}`,
error,
success: false,
};
return this.handleError(error, 'Failed to update group');
}
}
/**
* Update group shared prompt/content
*/
async updateGroupPrompt(args: UpdateGroupPromptParams): Promise<BuiltinServerRuntimeOutput> {
async updateGroupPrompt(args: UpdateGroupPromptParams): Promise<BuiltinToolResult> {
try {
const state = getChatGroupStoreState();
const group = agentGroupSelectors.currentGroup(state);
@ -552,7 +523,7 @@ export class GroupAgentBuilderExecutionRuntime {
if (!group) {
return {
content: 'No active group found',
error: 'No active group found',
error: { message: 'No active group found', type: 'NoGroupContext' },
success: false,
};
}
@ -589,16 +560,10 @@ export class GroupAgentBuilderExecutionRuntime {
success: true,
};
} catch (error) {
const err = error as Error;
return {
content: `Failed to update group prompt: ${err.message}`,
error,
state: {
newPrompt: args.prompt,
success: false,
} as UpdateGroupPromptState,
return this.handleErrorWithState(error, 'Failed to update group prompt', {
newPrompt: args.prompt,
success: false,
};
} as UpdateGroupPromptState);
}
}
@ -613,4 +578,37 @@ export class GroupAgentBuilderExecutionRuntime {
await state.updateGroup(group.id, { content: prompt });
}
// ==================== Error Handling ====================
private handleError(error: unknown, context: string): BuiltinToolResult {
const err = error as Error;
return {
content: `${context}: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'RuntimeError',
},
success: false,
};
}
private handleErrorWithState<T extends object>(
error: unknown,
context: string,
state: T,
): BuiltinToolResult {
const err = error as Error;
return {
content: `${context}: ${err.message}`,
error: {
body: error,
message: err.message,
type: 'RuntimeError',
},
state,
success: false,
};
}
}

View file

@ -4,15 +4,18 @@
* Handles all group agent builder tool calls for configuring groups and their agents.
* Extends AgentBuilder functionality with group-specific operations.
*/
import { AgentManagerRuntime } from '@lobechat/agent-manager-runtime';
import type {
GetAvailableModelsParams,
InstallPluginParams,
SearchMarketToolsParams,
} from '@lobechat/builtin-tool-agent-builder';
import { AgentBuilderExecutionRuntime } from '@lobechat/builtin-tool-agent-builder/executionRuntime';
import type { BuiltinToolContext, BuiltinToolResult } from '@lobechat/types';
import { BaseExecutor } from '@lobechat/types';
import { agentService } from '@/services/agent';
import { discoverService } from '@/services/discover';
import { GroupAgentBuilderExecutionRuntime } from './ExecutionRuntime';
import type {
BatchCreateAgentsParams,
@ -28,7 +31,10 @@ import type {
} from './types';
import { GroupAgentBuilderApiName, GroupAgentBuilderIdentifier } from './types';
const agentBuilderRuntime = new AgentBuilderExecutionRuntime();
const agentManagerRuntime = new AgentManagerRuntime({
agentService,
discoverService,
});
const groupAgentBuilderRuntime = new GroupAgentBuilderExecutionRuntime();
class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApiName> {
@ -41,29 +47,13 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
params: GetAgentInfoParams,
ctx: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
const result = await groupAgentBuilderRuntime.getAgentInfo(ctx.groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.getAgentInfo(ctx.groupId, params);
};
// ==================== Group Member Management ====================
searchAgent = async (params: SearchAgentParams): Promise<BuiltinToolResult> => {
const result = await groupAgentBuilderRuntime.searchAgent(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.searchAgent(params);
};
createAgent = async (
@ -80,15 +70,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await groupAgentBuilderRuntime.createAgent(groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.createAgent(groupId, params);
};
batchCreateAgents = async (
@ -105,15 +87,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await groupAgentBuilderRuntime.batchCreateAgents(groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.batchCreateAgents(groupId, params);
};
inviteAgent = async (
@ -130,15 +104,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await groupAgentBuilderRuntime.inviteAgent(groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.inviteAgent(groupId, params);
};
removeAgent = async (
@ -155,15 +121,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await groupAgentBuilderRuntime.removeAgent(groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.removeAgent(groupId, params);
};
// ==================== Group Configuration ====================
@ -182,68 +140,28 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await groupAgentBuilderRuntime.updateAgentPrompt(groupId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.updateAgentPrompt(groupId, params);
};
updateGroup = async (params: UpdateGroupParams): Promise<BuiltinToolResult> => {
const result = await groupAgentBuilderRuntime.updateGroup(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return groupAgentBuilderRuntime.updateGroup(params);
};
updateGroupPrompt = async (params: UpdateGroupPromptParams): Promise<BuiltinToolResult> => {
const result = await groupAgentBuilderRuntime.updateGroupPrompt({
return groupAgentBuilderRuntime.updateGroupPrompt({
streaming: true,
...params,
});
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
};
// ==================== Inherited Operations (for supervisor agent) ====================
getAvailableModels = async (params: GetAvailableModelsParams): Promise<BuiltinToolResult> => {
const result = await agentBuilderRuntime.getAvailableModels(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return agentManagerRuntime.getAvailableModels(params);
};
searchMarketTools = async (params: SearchMarketToolsParams): Promise<BuiltinToolResult> => {
const result = await agentBuilderRuntime.searchMarketTools(params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return agentManagerRuntime.searchMarketTools(params);
};
updateConfig = async (
@ -263,15 +181,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await agentBuilderRuntime.updateAgentConfig(agentId, restParams);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return agentManagerRuntime.updateAgentConfig(agentId, restParams);
};
installPlugin = async (
@ -288,15 +198,7 @@ class GroupAgentBuilderExecutor extends BaseExecutor<typeof GroupAgentBuilderApi
};
}
const result = await agentBuilderRuntime.installPlugin(agentId, params);
return {
content: result.content,
error: result.error
? { body: result.error, message: String(result.error), type: 'RuntimeError' }
: undefined,
state: result.state,
success: result.success,
};
return agentManagerRuntime.installPlugin(agentId, params);
};
}

View file

@ -1,4 +1,5 @@
import { AgentBuilderManifest } from '@lobechat/builtin-tool-agent-builder';
import { AgentManagementManifest } from '@lobechat/builtin-tool-agent-management';
import { CalculatorManifest } from '@lobechat/builtin-tool-calculator';
import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
import { GroupAgentBuilderManifest } from '@lobechat/builtin-tool-group-agent-builder';
@ -15,6 +16,7 @@ import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
export const builtinToolIdentifiers: string[] = [
AgentBuilderManifest.identifier,
AgentManagementManifest.identifier,
CalculatorManifest.identifier,
LocalSystemManifest.identifier,
WebBrowsingManifest.identifier,

View file

@ -1,5 +1,6 @@
import { AgentBuilderManifest } from '@lobechat/builtin-tool-agent-builder';
import { CalculatorManifest } from '@lobechat/builtin-tool-calculator';
import { AgentManagementManifest } from '@lobechat/builtin-tool-agent-management';
import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
import { GroupAgentBuilderManifest } from '@lobechat/builtin-tool-group-agent-builder';
import { GroupManagementManifest } from '@lobechat/builtin-tool-group-management';
@ -107,6 +108,12 @@ export const builtinTools: LobeBuiltinTool[] = [
manifest: GroupManagementManifest,
type: 'builtin',
},
{
hidden: true,
identifier: AgentManagementManifest.identifier,
manifest: AgentManagementManifest,
type: 'builtin',
},
{
identifier: GTDManifest.identifier,
manifest: GTDManifest,

View file

@ -2,6 +2,10 @@ import {
AgentBuilderInspectors,
AgentBuilderManifest,
} from '@lobechat/builtin-tool-agent-builder/client';
import {
AgentManagementInspectors,
AgentManagementManifest,
} from '@lobechat/builtin-tool-agent-management/client';
import {
CloudSandboxIdentifier,
CloudSandboxInspectors,
@ -47,6 +51,10 @@ import { type BuiltinInspector } from '@lobechat/types';
*/
const BuiltinToolInspectors: Record<string, Record<string, BuiltinInspector>> = {
[AgentBuilderManifest.identifier]: AgentBuilderInspectors as Record<string, BuiltinInspector>,
[AgentManagementManifest.identifier]: AgentManagementInspectors as Record<
string,
BuiltinInspector
>,
[CloudSandboxIdentifier]: CloudSandboxInspectors as Record<string, BuiltinInspector>,
[GroupAgentBuilderManifest.identifier]: GroupAgentBuilderInspectors as Record<
string,

View file

@ -1,5 +1,7 @@
import { AgentBuilderManifest } from '@lobechat/builtin-tool-agent-builder';
import { AgentBuilderRenders } from '@lobechat/builtin-tool-agent-builder/client';
import { AgentManagementManifest } from '@lobechat/builtin-tool-agent-management';
import { AgentManagementRenders } from '@lobechat/builtin-tool-agent-management/client';
import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
import { CloudSandboxRenders } from '@lobechat/builtin-tool-cloud-sandbox/client';
import { GroupAgentBuilderManifest } from '@lobechat/builtin-tool-group-agent-builder';
@ -31,6 +33,7 @@ import { type BuiltinRender } from '@lobechat/types';
*/
const BuiltinToolsRenders: Record<string, Record<string, BuiltinRender>> = {
[AgentBuilderManifest.identifier]: AgentBuilderRenders as Record<string, BuiltinRender>,
[AgentManagementManifest.identifier]: AgentManagementRenders as Record<string, BuiltinRender>,
[CloudSandboxManifest.identifier]: CloudSandboxRenders as Record<string, BuiltinRender>,
[GroupAgentBuilderManifest.identifier]: GroupAgentBuilderRenders as Record<string, BuiltinRender>,
[GroupManagementManifest.identifier]: GroupManagementRenders as Record<string, BuiltinRender>,

View file

@ -2,6 +2,10 @@ import {
AgentBuilderManifest,
AgentBuilderStreamings,
} from '@lobechat/builtin-tool-agent-builder/client';
import {
AgentManagementManifest,
AgentManagementStreamings,
} from '@lobechat/builtin-tool-agent-management/client';
import {
CloudSandboxManifest,
CloudSandboxStreamings,
@ -33,6 +37,10 @@ import { type BuiltinStreaming } from '@lobechat/types';
*/
const BuiltinToolStreamings: Record<string, Record<string, BuiltinStreaming>> = {
[AgentBuilderManifest.identifier]: AgentBuilderStreamings as Record<string, BuiltinStreaming>,
[AgentManagementManifest.identifier]: AgentManagementStreamings as Record<
string,
BuiltinStreaming
>,
[CloudSandboxManifest.identifier]: CloudSandboxStreamings as Record<string, BuiltinStreaming>,
[GroupAgentBuilderManifest.identifier]: GroupAgentBuilderStreamings as Record<
string,

View file

@ -24,10 +24,11 @@ import {
AgentBuilderContextInjector,
EvalContextSystemInjector,
ForceFinishSummaryInjector,
GroupAgentBuilderContextInjector,
GroupContextInjector,
AgentManagementContextInjector,
GTDPlanInjector,
GTDTodoInjector,
GroupAgentBuilderContextInjector,
GroupContextInjector,
HistorySummaryProvider,
KnowledgeInjector,
PageEditorContextInjector,
@ -131,6 +132,7 @@ export class MessagesEngine {
fileContext,
agentBuilderContext,
evalContext,
agentManagementContext,
groupAgentBuilderContext,
agentGroup,
gtd,
@ -142,6 +144,8 @@ export class MessagesEngine {
} = this.params;
const isAgentBuilderEnabled = !!agentBuilderContext;
const isAgentManagementEnabled = !!agentManagementContext;
const isGroupAgentBuilderEnabled = !!groupAgentBuilderContext;
const isAgentGroupEnabled = agentGroup?.agentMap && Object.keys(agentGroup.agentMap).length > 0;
const isGroupContextEnabled =
@ -218,7 +222,13 @@ export class MessagesEngine {
agentContext: agentBuilderContext,
}),
// 10. Group Agent Builder context injection (current group config/members for editing)
// 7. Agent Management context injection (available models and plugins for agent creation)
new AgentManagementContextInjector({
enabled: isAgentManagementEnabled,
context: agentManagementContext,
}),
// 8. Group Agent Builder context injection (current group config/members for editing)
new GroupAgentBuilderContextInjector({
enabled: isGroupAgentBuilderEnabled,
groupContext: groupAgentBuilderContext,
@ -236,13 +246,13 @@ export class MessagesEngine {
// 12. Tool system role injection (conditionally added)
...(toolsConfig?.manifests && toolsConfig.manifests.length > 0
? [
new ToolSystemRoleProvider({
isCanUseFC: capabilities?.isCanUseFC || (() => true),
manifests: toolsConfig.manifests,
model,
provider,
}),
]
new ToolSystemRoleProvider({
isCanUseFC: capabilities?.isCanUseFC || (() => true),
manifests: toolsConfig.manifests,
model,
provider,
}),
]
: []),
// 13. History summary injection
@ -262,15 +272,15 @@ export class MessagesEngine {
? pageContentContext
: initialContext?.pageEditor
? {
markdown: initialContext.pageEditor.markdown,
metadata: {
charCount: initialContext.pageEditor.metadata.charCount,
lineCount: initialContext.pageEditor.metadata.lineCount,
title: initialContext.pageEditor.metadata.title,
},
// Use latest XML from stepContext if available, otherwise fallback to initial XML
xml: stepContext?.stepPageEditor?.xml || initialContext.pageEditor.xml,
}
markdown: initialContext.pageEditor.markdown,
metadata: {
charCount: initialContext.pageEditor.metadata.charCount,
lineCount: initialContext.pageEditor.metadata.lineCount,
title: initialContext.pageEditor.metadata.title,
},
// Use latest XML from stepContext if available, otherwise fallback to initial XML
xml: stepContext?.stepPageEditor?.xml || initialContext.pageEditor.xml,
}
: undefined,
}),
@ -311,26 +321,26 @@ export class MessagesEngine {
// This must be BEFORE GroupRoleTransformProcessor so we filter based on original agentId/tools
...(isAgentGroupEnabled && agentGroup.agentMap && agentGroup.currentAgentId
? [
new GroupOrchestrationFilterProcessor({
agentMap: Object.fromEntries(
Object.entries(agentGroup.agentMap).map(([id, info]) => [id, { role: info.role }]),
),
currentAgentId: agentGroup.currentAgentId,
// Only enabled when current agent is NOT supervisor (supervisor needs to see orchestration history)
enabled: agentGroup.currentAgentRole !== 'supervisor',
}),
]
new GroupOrchestrationFilterProcessor({
agentMap: Object.fromEntries(
Object.entries(agentGroup.agentMap).map(([id, info]) => [id, { role: info.role }]),
),
currentAgentId: agentGroup.currentAgentId,
// Only enabled when current agent is NOT supervisor (supervisor needs to see orchestration history)
enabled: agentGroup.currentAgentRole !== 'supervisor',
}),
]
: []),
// 26. Group role transform (convert other agents' messages to user role with speaker tags)
// This must be BEFORE ToolCallProcessor so other agents' tool messages are converted first
...(isAgentGroupEnabled && agentGroup.currentAgentId
? [
new GroupRoleTransformProcessor({
agentMap: agentGroup.agentMap!,
currentAgentId: agentGroup.currentAgentId,
}),
]
new GroupRoleTransformProcessor({
agentMap: agentGroup.agentMap!,
currentAgentId: agentGroup.currentAgentId,
}),
]
: []),
// =============================================

View file

@ -9,6 +9,7 @@ import type { AgentBuilderContext } from '../../providers/AgentBuilderContextInj
import type { EvalContext } from '../../providers/EvalContextSystemInjector';
import type { GroupAgentBuilderContext } from '../../providers/GroupAgentBuilderContextInjector';
import type { GroupMemberInfo } from '../../providers/GroupContextInjector';
import type { AgentManagementContext } from '../../providers/AgentManagementContextInjector';
import type { GTDPlan } from '../../providers/GTDPlanInjector';
import type { GTDTodoList } from '../../providers/GTDTodoInjector';
import type { SkillMeta } from '../../providers/SkillContextProvider';
@ -245,6 +246,8 @@ export interface MessagesEngineParams {
agentBuilderContext?: AgentBuilderContext;
/** Eval context for injecting environment prompts into system message */
evalContext?: EvalContext;
/** Agent Management context */
agentManagementContext?: AgentManagementContext;
/** Agent group configuration for multi-agent scenarios */
agentGroup?: AgentGroupConfig;
/** Group Agent Builder context */
@ -300,6 +303,7 @@ export interface MessagesEngineResult {
export { type AgentInfo } from '../../processors/GroupRoleTransform';
export { type AgentBuilderContext } from '../../providers/AgentBuilderContextInjector';
export { type EvalContext } from '../../providers/EvalContextSystemInjector';
export { type AgentManagementContext } from '../../providers/AgentManagementContextInjector';
export { type GroupAgentBuilderContext } from '../../providers/GroupAgentBuilderContextInjector';
export { type GTDPlan } from '../../providers/GTDPlanInjector';
export { type GTDTodoItem, type GTDTodoList } from '../../providers/GTDTodoInjector';

View file

@ -0,0 +1,235 @@
import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
const log = debug('context-engine:provider:AgentManagementContextInjector');
/**
* Escape XML special characters
*/
const escapeXml = (str: string): string => {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
};
/**
* Available model info for Agent Management context
*/
export interface AvailableModelInfo {
/** Model abilities */
abilities?: {
files?: boolean;
functionCall?: boolean;
reasoning?: boolean;
vision?: boolean;
};
/** Model description */
description?: string;
/** Model ID */
id: string;
/** Model display name */
name: string;
}
/**
* Available provider info for Agent Management context
*/
export interface AvailableProviderInfo {
/** Provider ID */
id: string;
/** Available models under this provider */
models: AvailableModelInfo[];
/** Provider display name */
name: string;
}
/**
* Available plugin info for Agent Management context
*/
export interface AvailablePluginInfo {
/** Plugin description */
description?: string;
/** Plugin identifier */
identifier: string;
/** Plugin display name */
name: string;
/** Plugin type: 'builtin' for built-in tools, 'klavis' for Klavis servers, 'lobehub-skill' for LobehubSkill providers */
type: 'builtin' | 'klavis' | 'lobehub-skill';
}
/**
* Agent Management context
*/
export interface AgentManagementContext {
/** Available plugins (all types) */
availablePlugins?: AvailablePluginInfo[];
/** Available providers and models */
availableProviders?: AvailableProviderInfo[];
}
export interface AgentManagementContextInjectorConfig {
/** Agent Management context to inject */
context?: AgentManagementContext;
/** Whether Agent Management tool is enabled */
enabled?: boolean;
/** Function to format Agent Management context */
formatContext?: (context: AgentManagementContext) => string;
}
/**
* Format Agent Management context as XML for injection
*/
const defaultFormatContext = (context: AgentManagementContext): string => {
const parts: string[] = [];
// Add available models section
if (context.availableProviders && context.availableProviders.length > 0) {
const providersXml = context.availableProviders
.map((provider) => {
const modelsXml = provider.models
.map((model) => {
const attrs: string[] = [`id="${model.id}"`];
if (model.abilities) {
if (model.abilities.functionCall) attrs.push('functionCall="true"');
if (model.abilities.vision) attrs.push('vision="true"');
if (model.abilities.files) attrs.push('files="true"');
if (model.abilities.reasoning) attrs.push('reasoning="true"');
}
const desc = model.description ? ` - ${escapeXml(model.description)}` : '';
return ` <model ${attrs.join(' ')}>${escapeXml(model.name)}${desc}</model>`;
})
.join('\n');
return ` <provider id="${provider.id}" name="${escapeXml(provider.name)}">\n${modelsXml}\n </provider>`;
})
.join('\n');
parts.push(`<available_models>\n${providersXml}\n</available_models>`);
}
// Add available plugins section
if (context.availablePlugins && context.availablePlugins.length > 0) {
const builtinPlugins = context.availablePlugins.filter((p) => p.type === 'builtin');
const klavisPlugins = context.availablePlugins.filter((p) => p.type === 'klavis');
const lobehubSkillPlugins = context.availablePlugins.filter((p) => p.type === 'lobehub-skill');
const pluginsSections: string[] = [];
if (builtinPlugins.length > 0) {
const builtinItems = builtinPlugins
.map((p) => {
const desc = p.description ? ` - ${escapeXml(p.description)}` : '';
return ` <plugin id="${p.identifier}">${escapeXml(p.name)}${desc}</plugin>`;
})
.join('\n');
pluginsSections.push(` <builtin_plugins>\n${builtinItems}\n </builtin_plugins>`);
}
if (klavisPlugins.length > 0) {
const klavisItems = klavisPlugins
.map((p) => {
const desc = p.description ? ` - ${escapeXml(p.description)}` : '';
return ` <plugin id="${p.identifier}">${escapeXml(p.name)}${desc}</plugin>`;
})
.join('\n');
pluginsSections.push(` <klavis_plugins>\n${klavisItems}\n </klavis_plugins>`);
}
if (lobehubSkillPlugins.length > 0) {
const lobehubSkillItems = lobehubSkillPlugins
.map((p) => {
const desc = p.description ? ` - ${escapeXml(p.description)}` : '';
return ` <plugin id="${p.identifier}">${escapeXml(p.name)}${desc}</plugin>`;
})
.join('\n');
pluginsSections.push(
` <lobehub_skill_plugins>\n${lobehubSkillItems}\n </lobehub_skill_plugins>`,
);
}
if (pluginsSections.length > 0) {
parts.push(`<available_plugins>\n${pluginsSections.join('\n')}\n</available_plugins>`);
}
}
if (parts.length === 0) {
return '';
}
return `<agent_management_context>
<instruction>When creating or updating agents using the Agent Management tools, you can select from these available models and plugins. Use the exact IDs from this context when specifying model/provider/plugins parameters.</instruction>
${parts.join('\n')}
</agent_management_context>`;
};
/**
* Agent Management Context Injector
* Responsible for injecting available models and plugins when Agent Management tool is enabled
*/
export class AgentManagementContextInjector extends BaseProvider {
readonly name = 'AgentManagementContextInjector';
constructor(
private config: AgentManagementContextInjectorConfig,
options: ProcessorOptions = {},
) {
super(options);
}
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
const clonedContext = this.cloneContext(context);
// Skip if Agent Management is not enabled
if (!this.config.enabled) {
log('Agent Management not enabled, skipping injection');
return this.markAsExecuted(clonedContext);
}
// Skip if no context data
if (!this.config.context) {
log('No Agent Management context provided, skipping injection');
return this.markAsExecuted(clonedContext);
}
// Format context
const formatFn = this.config.formatContext || defaultFormatContext;
const formattedContent = formatFn(this.config.context);
// Skip if no content to inject
if (!formattedContent) {
log('No content to inject after formatting');
return this.markAsExecuted(clonedContext);
}
// Find the first user message index
const firstUserIndex = clonedContext.messages.findIndex((msg) => msg.role === 'user');
if (firstUserIndex === -1) {
log('No user messages found, skipping injection');
return this.markAsExecuted(clonedContext);
}
// Insert a new user message with context before the first user message
const contextMessage = {
content: formattedContent,
createdAt: Date.now(),
id: `agent-management-context-${Date.now()}`,
meta: { injectType: 'agent-management-context', systemInjection: true },
role: 'user' as const,
updatedAt: Date.now(),
};
clonedContext.messages.splice(firstUserIndex, 0, contextMessage);
// Update metadata
clonedContext.metadata.agentManagementContextInjected = true;
log('Agent Management context injected as new user message');
return this.markAsExecuted(clonedContext);
}
}

View file

@ -2,6 +2,7 @@
export { AgentBuilderContextInjector } from './AgentBuilderContextInjector';
export { EvalContextSystemInjector } from './EvalContextSystemInjector';
export { ForceFinishSummaryInjector } from './ForceFinishSummaryInjector';
export { AgentManagementContextInjector } from './AgentManagementContextInjector';
export { GroupAgentBuilderContextInjector } from './GroupAgentBuilderContextInjector';
export { GroupContextInjector } from './GroupContextInjector';
export { GTDPlanInjector } from './GTDPlanInjector';
@ -25,6 +26,13 @@ export type {
} from './AgentBuilderContextInjector';
export type { EvalContext, EvalContextSystemInjectorConfig } from './EvalContextSystemInjector';
export type { ForceFinishSummaryInjectorConfig } from './ForceFinishSummaryInjector';
export type {
AgentManagementContext,
AgentManagementContextInjectorConfig,
AvailableModelInfo,
AvailablePluginInfo,
AvailableProviderInfo,
} from './AgentManagementContextInjector';
export type {
GroupAgentBuilderContext,
GroupAgentBuilderContextInjectorConfig,

View file

@ -21,9 +21,20 @@ import type { Message, MessageGroupMetadata, ParseResult } from './types';
* @returns ParseResult containing messageMap, displayTree, and flatList
*/
export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[]): ParseResult {
// Pre-processing: Transform sub_agent messages before building helper maps
// This ensures FlatListBuilder and MessageCollector see the correct agentId
// and won't merge messages from different agents into the same group
// Only applies to scope: 'sub_agent' (agent-to-agent calls, not group orchestration)
const processedMessages = messages.map((msg) => {
if (msg.metadata?.scope === 'sub_agent' && msg.metadata?.subAgentId) {
return { ...msg, agentId: msg.metadata.subAgentId };
}
return msg;
});
// Phase 1: Indexing
// Build helper maps for O(1) access patterns
const helperMaps = buildHelperMaps(messages, messageGroups);
const helperMaps = buildHelperMaps(processedMessages, messageGroups);
// Phase 2: Structuring
// Convert flat parent-child relationships to tree structure
@ -37,7 +48,7 @@ export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[
// Phase 3b: Generate flatList for virtual list rendering
// Implements RFC priority-based pattern matching
const flatList = transformer.flatten(messages);
const flatList = transformer.flatten(processedMessages);
// Convert messageMap from Map to plain object for serialization
// Clean up metadata for assistant messages with tools
@ -76,6 +87,9 @@ export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[
processedMessage = { ...message, role: 'supervisor' as const };
}
// Note: sub_agent scope transformation is done in pre-processing phase (before buildHelperMaps)
// No need to transform agentId here since it's already been transformed
// For assistant messages with tools, clean metadata to keep only usage/performance fields
if (
processedMessage.role === 'assistant' &&
@ -100,7 +114,9 @@ export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[
// Transform supervisor messages in flatList
// For non-grouped supervisor messages (e.g., supervisor summary without tools)
// Note: sub_agent scope transformation is done in pre-processing phase (before buildHelperMaps)
const processedFlatList = flatList.map((msg) => {
// Transform supervisor messages
if (msg.role === 'assistant' && msg.metadata?.isSupervisor) {
return { ...msg, role: 'supervisor' as const };
}

View file

@ -66,6 +66,7 @@ export class MessageCollector {
const nextMessages = allMessages.filter((m) => m.parentId === toolMsg.id);
// Stop if there are task children - they should be handled separately, not part of AssistantGroup
// This ensures that messages after a task are not merged into the AssistantGroup before the task
const taskChildren = nextMessages.filter((m) => m.role === 'task');
if (taskChildren.length > 0) {
continue;
@ -142,7 +143,8 @@ export class MessageCollector {
continue;
}
// Stop if there are task children - they should be handled separately, not part of AssistantGroup
// Stop if there are ANY task children - they should be processed separately, not part of AssistantGroup
// This ensures that messages after a task are not merged into the AssistantGroup before the task
const taskChildren = toolNode.children.filter((child) => {
const childMsg = this.messageMap.get(child.id);
return childMsg?.role === 'task';
@ -181,7 +183,7 @@ export class MessageCollector {
return lastNode;
}
// Check if lastNode is a tool with task children
// Check if lastNode is a tool with ANY task children
// In this case, return the tool node itself so ContextTreeBuilder can process tasks
if (lastMsg?.role === 'tool') {
const taskChildren = lastNode.children.filter((child) => {
@ -224,7 +226,8 @@ export class MessageCollector {
continue;
}
// Stop if there are task children - they should be handled separately, not part of AssistantGroup
// Stop if there are ANY task children - they should be processed separately, not part of AssistantGroup
// This ensures that messages after a task are not merged into the AssistantGroup before the task
const taskNodes = toolNode.children.filter((child) => {
const childMsg = this.messageMap.get(child.id);
return childMsg?.role === 'task';

View file

@ -6,6 +6,7 @@ import type { IThreadType } from './topic/thread';
* - thread: Agent thread conversation
* - group: Group main conversation
* - group_agent: Agent conversation within a group
* - sub_agent: Agent-to-agent communication (non-group, uses subAgentId for config/display only)
*/
export type MessageMapScope =
| 'main'
@ -14,7 +15,8 @@ export type MessageMapScope =
| 'group_agent'
| 'group_agent_builder'
| 'page'
| 'agent_builder';
| 'agent_builder'
| 'sub_agent';
/**
* Context for generating message map key with scope-driven architecture

View file

@ -98,6 +98,8 @@ export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSche
isSupervisor: z.boolean().optional(),
pageSelections: z.array(PageSelectionSchema).optional(),
reactions: z.array(EmojiReactionSchema).optional(),
scope: z.string().optional(),
subAgentId: z.string().optional(),
});
export interface ModelUsage extends ModelTokensUsage {
@ -154,6 +156,19 @@ export interface MessageMetadata extends ModelUsage, ModelPerformance {
* Used by conversation-flow to transform role to 'supervisor' for UI rendering
*/
isSupervisor?: boolean;
/**
* Message scope - indicates the context in which this message was created
* Used by conversation-flow to determine how to handle message grouping and display
* See MessageMapScope for available values
*/
scope?: string;
/**
* Sub Agent ID - behavior depends on scope
* - scope: 'sub_agent': conversation-flow will transform message.agentId to this value for display
* - scope: 'group' | 'group_agent': indicates the agent that generated this message in group mode
* Used by callAgent tool (sub_agent) and group orchestration (group modes)
*/
subAgentId?: string;
/**
* Flag indicating if message is pinned (excluded from compression)
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View file

@ -24,6 +24,20 @@ export default {
'builtins.lobe-agent-builder.inspector.noResults': 'No results',
'builtins.lobe-agent-builder.inspector.togglePlugin': 'Toggle',
'builtins.lobe-agent-builder.title': 'Agent Builder Expert',
'builtins.lobe-agent-management.apiName.callAgent': 'Call agent',
'builtins.lobe-agent-management.apiName.createAgent': 'Create agent',
'builtins.lobe-agent-management.apiName.deleteAgent': 'Delete agent',
'builtins.lobe-agent-management.apiName.searchAgent': 'Search agents',
'builtins.lobe-agent-management.apiName.updateAgent': 'Update agent',
'builtins.lobe-agent-management.inspector.callAgent.sync': 'Calling:',
'builtins.lobe-agent-management.inspector.callAgent.task': 'Assigning task to:',
'builtins.lobe-agent-management.inspector.createAgent.title': 'Creating agent:',
'builtins.lobe-agent-management.inspector.searchAgent.all': 'Search agents:',
'builtins.lobe-agent-management.inspector.searchAgent.market': 'Search market:',
'builtins.lobe-agent-management.inspector.searchAgent.results': '{{count}} results',
'builtins.lobe-agent-management.inspector.searchAgent.user': 'Search my agents:',
'builtins.lobe-agent-management.inspector.updateAgent.title': 'Updating agent:',
'builtins.lobe-agent-management.title': 'Agent Manager',
'builtins.lobe-cloud-sandbox.apiName.editLocalFile': 'Edit file',
'builtins.lobe-cloud-sandbox.apiName.executeCode': 'Execute code',
'builtins.lobe-cloud-sandbox.apiName.exportFile': 'Export file',

View file

@ -55,6 +55,7 @@ export const serverMessagesEngine = async ({
userMemory,
agentBuilderContext,
evalContext,
agentManagementContext,
pageContentContext,
}: ServerMessagesEngineParams): Promise<OpenAIChatMessage[]> => {
const engine = new MessagesEngine({
@ -118,6 +119,7 @@ export const serverMessagesEngine = async ({
// Extended contexts
...(agentBuilderContext && { agentBuilderContext }),
...(evalContext && { evalContext }),
...(agentManagementContext && { agentManagementContext }),
...(pageContentContext && { pageContentContext }),
});

View file

@ -1,6 +1,7 @@
/* eslint-disable perfectionist/sort-interfaces */
import type {
AgentBuilderContext,
AgentManagementContext,
EvalContext,
FileContent,
KnowledgeBaseInfo,
@ -62,6 +63,8 @@ export interface ServerMessagesEngineParams {
// ========== Extended contexts ==========
/** Agent Builder context (optional, for editing agents) */
agentBuilderContext?: AgentBuilderContext;
/** Agent Management context (optional, available models and plugins) */
agentManagementContext?: AgentManagementContext;
// ========== Capability injection ==========
/** Model capability checkers */
capabilities?: ServerModelCapabilities;
@ -116,6 +119,7 @@ export interface ServerMessagesEngineParams {
export {
type AgentBuilderContext,
type EvalContext,
type AgentManagementContext,
type FileContent,
type KnowledgeBaseInfo,
type UserMemoryData,

View file

@ -55,7 +55,6 @@ export const agentRouter = router({
chatConfig: true,
openingMessage: true,
openingQuestions: true,
plugins: true,
tags: true,
tts: true,
})
@ -67,7 +66,7 @@ export const agentRouter = router({
)
.mutation(async ({ input, ctx }) => {
const session = await ctx.sessionModel.create({
config: input.config,
config: input.config as any,
session: { groupId: input.groupId },
type: 'agent',
});

View file

@ -855,12 +855,25 @@ export const aiAgentRouter = router({
log('getSubAgentTaskStatus: marked thread %s as completed', threadId);
} else if (realtimeStatus.hasError || redisState.status === 'error') {
updatedMetadata.error = redisState.error;
// Format error properly to avoid [object Object] in serialization
const errorObj = redisState.error as any;
const formattedError = errorObj
? typeof errorObj === 'object' && 'message' in errorObj
? { message: errorObj.message, ...errorObj }
: { message: String(errorObj) }
: undefined;
updatedMetadata.error = formattedError;
updatedMetadata.completedAt = new Date().toISOString();
if (metadata?.startedAt) {
updatedMetadata.duration = Date.now() - new Date(metadata.startedAt).getTime();
}
log('getSubAgentTaskStatus: error formatting for thread %s: %O', threadId, {
originalError: redisState.error,
formattedError,
});
await ctx.threadModel.update(threadId, {
metadata: updatedMetadata,
status: ThreadStatus.Failed,
@ -888,12 +901,10 @@ export const aiAgentRouter = router({
const updatedStatus = updatedThread?.status ?? thread.status;
const updatedTaskStatus = threadStatusToTaskStatus[updatedStatus] || 'processing';
// DEBUG: Log metadata for failed tasks
if (updatedTaskStatus === 'failed') {
console.log('[DEBUG] getSubAgentTaskStatus - failed task metadata:', {
threadId,
console.error('getSubAgentTaskStatus: failed task metadata for thread %s: %O', threadId, {
updatedMetadata,
'updatedMetadata?.error': updatedMetadata?.error,
error: updatedMetadata?.error,
updatedStatus,
});
}

View file

@ -48,6 +48,19 @@ vi.mock('@/database/models/topic', () => ({
})),
}));
// Mock AgentService
vi.mock('@/server/services/agent', () => ({
AgentService: vi.fn().mockImplementation(() => ({
getAgentConfig: vi.fn().mockResolvedValue({
chatConfig: {},
id: 'agent-1',
model: 'gpt-4',
plugins: [],
provider: 'openai',
}),
})),
}));
// Mock AgentRuntimeService
vi.mock('@/server/services/agentRuntime', () => ({
AgentRuntimeService: vi.fn().mockImplementation(() => ({
@ -60,6 +73,20 @@ vi.mock('@/server/services/agentRuntime', () => ({
})),
}));
// Mock MarketService
vi.mock('@/server/services/market', () => ({
MarketService: vi.fn().mockImplementation(() => ({
getLobehubSkillManifests: vi.fn().mockResolvedValue([]),
})),
}));
// Mock KlavisService
vi.mock('@/server/services/klavis', () => ({
KlavisService: vi.fn().mockImplementation(() => ({
getKlavisManifests: vi.fn().mockResolvedValue([]),
})),
}));
describe('AiAgentService.execSubAgentTask', () => {
let service: AiAgentService;
const mockDb = {} as any;

View file

@ -1,4 +1,6 @@
import { type AgentRuntimeContext, type AgentState } from '@lobechat/agent-runtime';
import { builtinTools } from '@lobechat/builtin-tools';
import { LOADING_FLAT } from '@lobechat/const';
import { type LobeToolManifest } from '@lobechat/context-engine';
import { type LobeChatDatabase } from '@lobechat/database';
import {
@ -14,14 +16,17 @@ import { ThreadStatus, ThreadType } from '@lobechat/types';
import { nanoid } from '@lobechat/utils';
import debug from 'debug';
import { LOADING_FLAT } from '@/const/message';
import { AgentModel } from '@/database/models/agent';
import { AiModelModel } from '@/database/models/aiModel';
import { MessageModel } from '@/database/models/message';
import { PluginModel } from '@/database/models/plugin';
import { ThreadModel } from '@/database/models/thread';
import { TopicModel } from '@/database/models/topic';
import { type EvalContext, type ServerAgentToolsContext } from '@/server/modules/Mecha';
import { createServerAgentToolsEngine } from '@/server/modules/Mecha';
import {
createServerAgentToolsEngine,
type EvalContext,
type ServerAgentToolsContext,
} from '@/server/modules/Mecha';
import { AgentService } from '@/server/services/agent';
import { AgentRuntimeService } from '@/server/services/agentRuntime';
import { type StepLifecycleCallbacks } from '@/server/services/agentRuntime/types';
@ -168,7 +173,13 @@ export class AiAgentService {
// Use actual agent ID from config for subsequent operations
const resolvedAgentId = agentConfig.id;
log('execAgent: got agent config for %s (id: %s)', identifier, resolvedAgentId);
log(
'execAgent: got agent config for %s (id: %s), model: %s, provider: %s',
identifier,
resolvedAgentId,
agentConfig.model,
agentConfig.provider,
);
// 2. Handle topic creation: if no topicId provided, create a new topic; otherwise reuse existing
let topicId = appContext?.topicId;
@ -203,9 +214,6 @@ export class AiAgentService {
// 4. Get model abilities from model-bank for function calling support check
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
const modelInfo = LOBE_DEFAULT_MODEL_LIST.find(
(m) => m.id === model && m.providerId === provider,
);
const isModelSupportToolUse = (m: string, p: string) => {
const info = LOBE_DEFAULT_MODEL_LIST.find((item) => item.id === m && item.providerId === p);
return info?.abilities?.functionCall ?? true;
@ -243,7 +251,7 @@ export class AiAgentService {
additionalManifests: [...lobehubSkillManifests, ...klavisManifests],
agentConfig: {
chatConfig: agentConfig.chatConfig ?? undefined,
plugins: agentConfig.plugins ?? undefined,
plugins: agentConfig?.plugins ?? undefined,
},
hasEnabledKnowledgeBases,
model,
@ -291,6 +299,98 @@ export class AiAgentService {
klavisManifests.length,
);
// 7.5. Build Agent Management context if agent-management tool is enabled
const isAgentManagementEnabled = toolsResult.enabledToolIds?.includes('lobe-agent-management');
let agentManagementContext;
if (isAgentManagementEnabled) {
// Query user's enabled models from database
const aiModelModel = new AiModelModel(this.db, this.userId);
const allUserModels = await aiModelModel.getAllModels();
// Filter only enabled chat models and group by provider
const providerMap = new Map<
string,
{
id: string;
models: Array<{ abilities?: any; description?: string; id: string; name: string }>;
name: string;
}
>();
for (const userModel of allUserModels) {
// Only include enabled chat models
if (!userModel.enabled || userModel.type !== 'chat') continue;
// Get model info from LOBE_DEFAULT_MODEL_LIST for full metadata
const modelInfo = LOBE_DEFAULT_MODEL_LIST.find(
(m) => m.id === userModel.id && m.providerId === userModel.providerId,
);
if (!providerMap.has(userModel.providerId)) {
providerMap.set(userModel.providerId, {
id: userModel.providerId,
models: [],
name: userModel.providerId, // TODO: Map to friendly provider name
});
}
const provider = providerMap.get(userModel.providerId)!;
provider.models.push({
abilities: userModel.abilities || modelInfo?.abilities,
description: modelInfo?.description,
id: userModel.id,
name: userModel.displayName || modelInfo?.displayName || userModel.id,
});
}
// Build availablePlugins from all plugin sources
// Exclude only truly internal tools (agent-management itself, agent-builder, page-agent)
const INTERNAL_TOOLS = new Set([
'lobe-agent-management', // Don't show agent-management in its own context
'lobe-agent-builder', // Used for editing current agent, not for creating new agents
'lobe-group-agent-builder', // Used for editing current group, not for creating new agents
'lobe-page-agent', // Page-editor specific tool
]);
const availablePlugins = [
// All builtin tools (including hidden ones like web-browsing, cloud-sandbox)
...builtinTools
.filter((tool) => !INTERNAL_TOOLS.has(tool.identifier))
.map((tool) => ({
description: tool.manifest.meta?.description,
identifier: tool.identifier,
name: tool.manifest.meta?.title || tool.identifier,
type: 'builtin' as const,
})),
// Lobehub Skills
...lobehubSkillManifests.map((manifest) => ({
description: manifest.meta?.description,
identifier: manifest.identifier,
name: manifest.meta?.title || manifest.identifier,
type: 'lobehub-skill' as const,
})),
// Klavis tools
...klavisManifests.map((manifest) => ({
description: manifest.meta?.description,
identifier: manifest.identifier,
name: manifest.meta?.title || manifest.identifier,
type: 'klavis' as const,
})),
];
agentManagementContext = {
availablePlugins,
// Limit to first 5 providers to avoid context bloat
availableProviders: Array.from(providerMap.values()).slice(0, 5),
};
log(
'execAgent: built agentManagementContext with %d providers and %d plugins',
agentManagementContext.availableProviders.length,
agentManagementContext.availablePlugins.length,
);
}
// 8. Get existing messages if provided
let historyMessages: any[] = [];
if (existingMessageIds.length > 0) {

View file

@ -44,7 +44,7 @@ const applyParamsFromChatConfig = (
chatConfig: LobeAgentChatConfig,
): LobeAgentConfig => {
// If params is not defined, return agentConfig as-is
if (!agentConfig.params) {
if (!agentConfig?.params) {
return agentConfig;
}
@ -168,7 +168,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
const chatConfig = chatConfigByIdSelectors.getChatConfigById(agentId)(agentStoreState);
// Base plugins from agent config
const basePlugins = agentConfig.plugins ?? [];
const basePlugins = agentConfig?.plugins ?? [];
// Check if this is a builtin agent
// Priority: supervisor check (when in group scope) > agent store slug
@ -186,10 +186,10 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
ctx.groupId,
group
? {
groupId: group.id,
supervisorAgentId: group.supervisorAgentId,
title: group.title,
}
groupId: group.id,
supervisorAgentId: group.supervisorAgentId,
title: group.title,
}
: null,
agentId,
);
@ -292,11 +292,11 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
ctx.groupId,
group
? {
agentsCount: group.agents?.length,
groupId: group.id,
supervisorAgentId: group.supervisorAgentId,
title: group.title,
}
agentsCount: group.agents?.length,
groupId: group.id,
supervisorAgentId: group.supervisorAgentId,
title: group.title,
}
: null,
);

View file

@ -1,26 +1,28 @@
import { AgentBuilderIdentifier } from '@lobechat/builtin-tool-agent-builder';
import { AgentManagementIdentifier } from '@lobechat/builtin-tool-agent-management';
import { GroupAgentBuilderIdentifier } from '@lobechat/builtin-tool-group-agent-builder';
import { GTDIdentifier } from '@lobechat/builtin-tool-gtd';
import { LobeToolIdentifier } from '@lobechat/builtin-tool-tools';
import { isDesktop, KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
import {
type AgentBuilderContext,
type AgentGroupConfig,
type GroupAgentBuilderContext,
type GroupOfficialToolItem,
type GTDConfig,
type LobeToolManifest,
type MemoryContext,
type ToolDiscoveryConfig,
type UserMemoryData,
import type {
AgentBuilderContext,
AgentGroupConfig,
AgentManagementContext,
GroupAgentBuilderContext,
GroupOfficialToolItem,
GTDConfig,
LobeToolManifest,
MemoryContext,
ToolDiscoveryConfig,
UserMemoryData,
} from '@lobechat/context-engine';
import { MessagesEngine } from '@lobechat/context-engine';
import { historySummaryPrompt } from '@lobechat/prompts';
import {
type OpenAIChatMessage,
type RuntimeInitialContext,
type RuntimeStepContext,
type UIChatMessage,
import type {
OpenAIChatMessage,
RuntimeInitialContext,
RuntimeStepContext,
UIChatMessage,
} from '@lobechat/types';
import debug from 'debug';
@ -31,6 +33,7 @@ import { getAgentStoreState } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { getChatGroupStoreState } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import { getAiInfraStoreState } from '@/store/aiInfra';
import { getChatStoreState } from '@/store/chat';
import { getToolStoreState } from '@/store/tool';
import {
@ -116,9 +119,12 @@ export const contextEngineering = async ({
const isAgentBuilderEnabled = tools?.includes(AgentBuilderIdentifier) ?? false;
// Check if Group Agent Builder tool is enabled
const isGroupAgentBuilderEnabled = tools?.includes(GroupAgentBuilderIdentifier) ?? false;
// Check if Agent Management tool is enabled
const isAgentManagementEnabled = tools?.includes(AgentManagementIdentifier) ?? false;
log('isAgentBuilderEnabled: %s', isAgentBuilderEnabled);
log('isGroupAgentBuilderEnabled: %s', isGroupAgentBuilderEnabled);
log('isAgentManagementEnabled: %s', isAgentManagementEnabled);
// Build agent group configuration if groupId is provided
let agentGroup: AgentGroupConfig | undefined;
@ -368,6 +374,101 @@ export const contextEngineering = async ({
}
}
// Build Agent Management context if Agent Management tool is enabled
let agentManagementContext: AgentManagementContext | undefined;
if (isAgentManagementEnabled) {
// Get enabled providers and models from aiInfra store
const aiProviderState = getAiInfraStoreState();
const enabledChatModelList = aiProviderState.enabledChatModelList || [];
// Build availableProviders from enabled chat models (only user-enabled providers)
// Limit to first 5 providers to avoid context bloat
const availableProviders = enabledChatModelList.slice(0, 5).map((provider) => ({
id: provider.id,
models: provider.children.map((model) => ({
abilities: model.abilities,
description: model.description,
id: model.id,
name: model.displayName || model.id,
})),
name: provider.name,
}));
// Get tool state for plugins
const toolState = getToolStoreState();
// Build availablePlugins from all plugin sources
const availablePlugins = [];
// Builtin tools (use allMetaList to include hidden tools like web-browsing, cloud-sandbox, etc.)
// Exclude only truly internal tools (agent-management itself, agent-builder, page-agent)
const allBuiltinTools = builtinToolSelectors.allMetaList(toolState);
const klavisIdentifiers = new Set(KLAVIS_SERVER_TYPES.map((t) => t.identifier));
const INTERNAL_TOOLS = new Set([
'lobe-agent-management', // Don't show agent-management in its own context
'lobe-agent-builder', // Used for editing current agent, not for creating new agents
'lobe-group-agent-builder', // Used for editing current group, not for creating new agents
'lobe-page-agent', // Page-editor specific tool
]);
for (const tool of allBuiltinTools) {
// Skip Klavis tools in builtin list (they'll be shown separately)
if (klavisIdentifiers.has(tool.identifier)) continue;
// Skip internal tools
if (INTERNAL_TOOLS.has(tool.identifier)) continue;
availablePlugins.push({
description: tool.meta?.description,
identifier: tool.identifier,
name: tool.meta?.title || tool.identifier,
type: 'builtin' as const,
});
}
// Klavis tools (if enabled)
const isKlavisEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableKlavis;
if (isKlavisEnabled) {
for (const klavisType of KLAVIS_SERVER_TYPES) {
availablePlugins.push({
description: klavisType.description,
identifier: klavisType.identifier,
name: klavisType.label,
type: 'klavis' as const,
});
}
}
// LobehubSkill providers (if enabled)
const isLobehubSkillEnabled =
typeof window !== 'undefined' &&
window.global_serverConfigStore?.getState()?.serverConfig?.enableLobehubSkill;
if (isLobehubSkillEnabled) {
for (const provider of LOBEHUB_SKILL_PROVIDERS) {
availablePlugins.push({
description: provider.description,
identifier: provider.id,
name: provider.label,
type: 'lobehub-skill' as const,
});
}
}
agentManagementContext = {
availablePlugins,
availableProviders,
};
log(
'agentManagementContext built: %d providers, %d plugins',
agentManagementContext.availableProviders?.length ?? 0,
agentManagementContext.availablePlugins?.length ?? 0,
);
}
// Create MessagesEngine with injected dependencies
const engine = new MessagesEngine({
// Agent configuration
@ -432,6 +533,7 @@ export const contextEngineering = async ({
// Extended contexts - only pass when enabled
...(isAgentBuilderEnabled && { agentBuilderContext }),
...(isGroupAgentBuilderEnabled && { groupAgentBuilderContext }),
...(isAgentManagementEnabled && { agentManagementContext }),
...(agentGroup && { agentGroup }),
...(gtdConfig && { gtd: gtdConfig }),
});

View file

@ -1200,8 +1200,9 @@ describe('call_llm executor', () => {
// Given
const mockStore = createMockStore();
const context = createTestContext({
agentId: 'supervisor-agent',
subAgentId: 'worker-agent',
agentId: 'supervisor-agent', // Main agent (supervisor)
scope: 'group_agent',
subAgentId: 'worker-agent', // Actual executing agent
topicId: 'group-topic',
});
const instruction = createCallLLMInstruction({
@ -1278,6 +1279,7 @@ describe('call_llm executor', () => {
const mockStore = createMockStore();
const context = createTestContext({
agentId: 'supervisor-agent',
scope: 'group_agent',
subAgentId: 'worker-agent',
groupId: 'group-123',
topicId: 'group-topic',
@ -1314,6 +1316,48 @@ describe('call_llm executor', () => {
);
});
it('should use subAgentId even without explicit scope when groupId is present (backward compatibility)', async () => {
// Given - Group scenario without explicit scope (backward compatibility test)
const mockStore = createMockStore();
const context = createTestContext({
agentId: 'supervisor-agent',
subAgentId: 'worker-agent',
groupId: 'group-123',
topicId: 'group-topic',
// No explicit scope - should infer from groupId
});
const instruction = createCallLLMInstruction({
model: 'gpt-4',
provider: 'openai',
messages: [createUserMessage()],
});
const state = createInitialState({ operationId: context.operationId });
mockStreamResponse({ content: 'AI response' });
mockStore.dbMessagesMap[context.messageKey] = [];
// When
await executeWithMockContext({
executor: 'call_llm',
instruction,
state,
mockStore,
context,
});
// Then - should still use subAgentId for backward compatibility
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
expect.objectContaining({
agentId: 'worker-agent', // Should use subAgentId even without explicit scope
groupId: 'group-123',
role: 'assistant',
}),
expect.objectContaining({
operationId: expect.any(String),
}),
);
});
it('should not include groupId when not in group chat context', async () => {
// Given
const mockStore = createMockStore();

View file

@ -2338,6 +2338,7 @@ describe('call_tool executor', () => {
const mockStore = createMockStore();
const context = createTestContext({
agentId: 'supervisor-agent',
scope: 'group_agent',
subAgentId: 'worker-agent',
topicId: 'group-topic',
});

View file

@ -1,4 +1,5 @@
import { type AgentInstruction, type AgentState } from '@lobechat/agent-runtime';
import type { AgentInstruction, AgentState } from '@lobechat/agent-runtime';
import type { MessageMapScope } from '@lobechat/types';
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_AGENT_CONFIG } from '@/const/settings';
import { type ResolvedAgentConfig } from '@/services/chat/mecha';
@ -42,6 +43,7 @@ export const executeWithMockContext = async ({
messageKey: string;
operationId: string;
parentId: string;
scope?: MessageMapScope;
subAgentId?: string;
topicId?: string | null;
};
@ -60,6 +62,7 @@ export const executeWithMockContext = async ({
agentId: context.agentId || 'test-session',
groupId: context.groupId,
messageId: context.parentId,
scope: context.scope,
subAgentId: context.subAgentId,
topicId: context.topicId !== undefined ? context.topicId : 'test-topic',
},
@ -141,6 +144,7 @@ export const createTestContext = (
messageKey?: string;
operationId?: string;
parentId?: string;
scope?: MessageMapScope;
subAgentId?: string;
topicId?: string | null;
} = {},
@ -153,6 +157,7 @@ export const createTestContext = (
`${overrides.agentId || 'test-session'}_${overrides.topicId !== undefined ? overrides.topicId : 'test-topic'}`,
operationId: overrides.operationId || 'op_test',
parentId: overrides.parentId || 'msg_parent',
scope: overrides.scope,
subAgentId: overrides.subAgentId,
topicId: overrides.topicId !== undefined ? overrides.topicId : 'test-topic',
};

View file

@ -92,13 +92,35 @@ export const createAgentExecutors = (context: {
};
/**
* Get effective agentId for message creation
* In Group Orchestration scenarios, subAgentId is the actual executing agent
* Falls back to agentId for normal scenarios
* Get effective agentId for message creation - depends on scope
* - scope: 'sub_agent': agentId stays unchanged (subAgentId only for config/display)
* - Other scopes with subAgentId: use subAgentId for message ownership (e.g., Group mode)
* - Default: use agentId
*/
const getEffectiveAgentId = () => {
const opContext = getOperationContext();
return opContext.subAgentId || opContext.agentId;
// Use subAgentId for message ownership except in sub_agent scope
// - sub_agent scope: callAgent scenario, message.agentId should stay unchanged
// - Other scopes with subAgentId: Group mode, message.agentId should be subAgentId
return opContext.subAgentId && opContext.scope !== 'sub_agent'
? opContext.subAgentId
: opContext.agentId;
};
/**
* Get subAgentId and scope for metadata (when scope is 'sub_agent')
*/
const getMetadataForSubAgent = () => {
const opContext = getOperationContext();
if (opContext.scope === 'sub_agent' && opContext.subAgentId) {
return {
subAgentId: opContext.subAgentId,
scope: opContext.scope,
};
}
return null;
};
const executors: Partial<Record<AgentInstruction['type'], InstructionExecutor>> = {
@ -133,8 +155,10 @@ export const createAgentExecutors = (context: {
} else {
// Get context from operation
const opContext = getOperationContext();
// Get effective agentId (subAgentId for group orchestration, agentId otherwise)
// Get effective agentId (depends on scope)
const effectiveAgentId = getEffectiveAgentId();
// Get subAgentId metadata (for sub_agent scope)
const subAgentMetadata = getMetadataForSubAgent();
// If this is the first regenerated creation of userMessage, llmPayload doesn't have parentMessageId
// So we assign it this way
@ -142,13 +166,24 @@ export const createAgentExecutors = (context: {
if (!llmPayload.parentMessageId) {
llmPayload.parentMessageId = context.parentId;
}
// Build metadata
const metadata: Record<string, any> = {};
if (opContext.isSupervisor) {
metadata.isSupervisor = true;
}
if (subAgentMetadata) {
// Store subAgentId and scope in metadata for sub_agent mode
// This will be used by conversation-flow to transform agentId for display
Object.assign(metadata, subAgentMetadata);
}
// Create assistant message (following server-side pattern)
// If isSupervisor is true, add metadata.isSupervisor for UI rendering
const assistantMessageItem = await context.get().optimisticCreateMessage(
{
content: LOADING_FLAT,
groupId: opContext.groupId,
metadata: opContext.isSupervisor ? { isSupervisor: true } : undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
model: llmPayload.model,
parentId: llmPayload.parentMessageId,
provider: llmPayload.provider,
@ -1142,7 +1177,11 @@ export const createAgentExecutors = (context: {
const opContext = getOperationContext();
const { agentId, topicId } = opContext;
if (!agentId || !topicId) {
// Check for targetAgentId (callAgent mode)
const targetAgentId = (task as any).targetAgentId;
const executionAgentId = targetAgentId || agentId;
if (!agentId || !topicId || !executionAgentId) {
log('[%s][exec_task] No valid context, cannot execute task', sessionLogId);
return {
events,
@ -1168,15 +1207,31 @@ export const createAgentExecutors = (context: {
};
}
if (targetAgentId) {
log(
'[%s][exec_task] callAgent mode - current agent: %s, target agent: %s',
sessionLogId,
agentId,
targetAgentId,
);
}
const taskLogId = `${sessionLogId}:task`;
try {
// 1. Create task message as placeholder
// IMPORTANT: Use operation context's agentId (current agent) for message creation
// This ensures the task message appears in the current conversation
const taskMessageResult = await context.get().optimisticCreateMessage(
{
agentId,
agentId, // Use current agent's ID (not targetAgentId)
content: '',
metadata: { instruction: task.instruction, taskTitle: task.description },
metadata: {
instruction: task.instruction,
taskTitle: task.description,
// Store targetAgentId in metadata for UI display
...(targetAgentId && { targetAgentId }),
},
parentId: parentMessageId,
role: 'task',
topicId,
@ -1214,9 +1269,11 @@ export const createAgentExecutors = (context: {
log('[%s] Created task message: %s', taskLogId, taskMessageId);
// 2. Create and execute task on server
log('[%s] Using server-side execution', taskLogId);
// IMPORTANT: Use executionAgentId here (targetAgentId if in callAgent mode)
// This ensures the task executes with the correct agent's config
log('[%s] Using server-side execution with agentId: %s', taskLogId, executionAgentId);
const createResult = await aiAgentService.execSubAgentTask({
agentId,
agentId: executionAgentId, // Use targetAgentId for callAgent, or current agentId for GTD
instruction: task.instruction,
parentMessageId: taskMessageId,
title: task.description,

View file

@ -83,6 +83,11 @@ export class StreamingExecutorActionImpl {
operationId?: string;
initialState?: AgentState;
initialContext?: AgentRuntimeContext;
/**
* Sub Agent ID - behavior depends on scope
* - scope: 'group' | 'group_agent': Used for agent config and changes message ownership
* - scope: 'sub_agent': Used for agent config but doesn't change message ownership
*/
subAgentId?: string;
isSubTask?: boolean;
}): {
@ -97,9 +102,9 @@ export class StreamingExecutorActionImpl {
const agentId = paramAgentId || activeAgentId;
const topicId = paramTopicId !== undefined ? paramTopicId : activeTopicId;
// For group orchestration scenarios:
// - subAgentId is used for agent config retrieval (model, provider, plugins)
// - agentId is used for session ID (message storage location)
// Determine effectiveAgentId for agent config retrieval:
// - paramSubAgentId: Used for agent config (behavior depends on scope)
// - agentId: Default
const effectiveAgentId = paramSubAgentId || agentId;
// Get scope and groupId from operation context if available
@ -118,8 +123,13 @@ export class StreamingExecutorActionImpl {
isSubTask, // Filter out lobe-gtd in sub-task context
scope, // Pass scope from operation context
});
const { agentConfig: agentConfigData, plugins: pluginIds } = agentConfig;
if (!agentConfigData || !agentConfigData.model) {
throw new Error(`[internal_createAgentState] Agent config not found or incomplete for agentId: ${effectiveAgentId}, scope: ${scope}`);
}
log(
'[internal_createAgentState] resolved plugins=%o, isSubTask=%s, disableTools=%s',
pluginIds,
@ -276,11 +286,11 @@ export class StreamingExecutorActionImpl {
} = params;
// Extract values from context
const { agentId, topicId, threadId, subAgentId, groupId } = context;
const { agentId, topicId, threadId, subAgentId, groupId, scope } = context;
// For group orchestration scenarios:
// - subAgentId is used for agent config retrieval (model, provider, plugins)
// - agentId is used for message storage location (via messageMapKey)
// Determine effectiveAgentId for agent config retrieval:
// - subAgentId is used when present (behavior depends on scope)
// - agentId: Default
const effectiveAgentId = subAgentId || agentId;
// Generate message key from context
@ -307,10 +317,11 @@ export class StreamingExecutorActionImpl {
}
log(
'[internal_execAgentRuntime] start, operationId: %s, agentId: %s, subAgentId: %s, effectiveAgentId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d, disableTools: %s',
'[internal_execAgentRuntime] start, operationId: %s, agentId: %s, subAgentId: %s, scope: %s, effectiveAgentId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d, disableTools: %s',
operationId,
agentId,
subAgentId,
scope,
effectiveAgentId,
topicId,
messageKey,
@ -342,7 +353,7 @@ export class StreamingExecutorActionImpl {
initialState: params.initialState,
initialContext: params.initialContext,
operationId,
subAgentId, // Pass subAgentId for agent config retrieval
subAgentId, // Pass subAgentId for agent config retrieval (behavior depends on scope)
isSubTask, // Pass isSubTask to filter out lobe-gtd tools in sub-task context
});

View file

@ -85,9 +85,10 @@ const toMessageMapContext = (input: MessageMapKeyInput): MessageMapContext => {
// Default scope (main if not specified)
// isNew can be used with any scope (main for new topic, thread for new thread with explicit scope)
// Note: sub_agent scope uses same key as main scope (same conversation, just different display)
return {
isNew,
scope: scope ?? 'main',
scope: scope === 'sub_agent' ? 'main' : (scope ?? 'main'),
scopeId: agentId,
topicId,
};

View file

@ -5,6 +5,7 @@
* Executors are registered as class instances by identifier.
*/
import { agentBuilderExecutor } from '@lobechat/builtin-tool-agent-builder/executor';
import { agentManagementExecutor } from '@lobechat/builtin-tool-agent-management/executor';
import { calculatorExecutor } from '@lobechat/builtin-tool-calculator/executor';
import { cloudSandboxExecutor } from '@lobechat/builtin-tool-cloud-sandbox/executor';
import { groupAgentBuilderExecutor } from '@lobechat/builtin-tool-group-agent-builder/executor';
@ -125,6 +126,7 @@ const registerExecutors = (executors: IBuiltinToolExecutor[]): void => {
// Register all executor instances
registerExecutors([
agentBuilderExecutor,
agentManagementExecutor,
calculatorExecutor,
cloudSandboxExecutor,
groupAgentBuilderExecutor,