mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
✨ feat(agent-builder): add skill priority instruction and server runtime (#15409)
* ✨ feat(agent-builder): add skill priority instruction and server runtime - Add <skill_coexistence> section to agent-builder system prompt so the model always prefers Agent Builder tools over LobeHub skills for agent configuration tasks when both are active simultaneously - Add agentBuilder server runtime to support background (QStash) execution: implements updateConfig, updatePrompt, searchMarketTools, getAvailableModels (DB-backed, LobeHub provider first, max 20 chat models), and installPlugin (market source only; official/OAuth tools return a clear unsupported error) - Register agentBuilderRuntime in the server runtime registry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✨ feat(agent-builder): fix identity confusion when user provides agent name/purpose Add <identity_boundary> section and example to prevent the AgentBuilder from roleplaying as the agent being configured. Short phrases like "健康助手,咨询健康问题" must be interpreted as configuration requests, not service requests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🐛 fix(agent-builder): address three server runtime issues - getAvailableModels: use AiInfraRepos instead of raw AiProviderModel + AiModelModel so builtin providers (DEFAULT_MODEL_PROVIDER_LIST) are included even when the user has no DB-customized providers - installPlugin (official): allow builtin tools (lobe-web-browsing etc.) to be enabled directly; only block OAuth-requiring tools (Klavis, LobehubSkill) that cannot be installed in background context - installPlugin (market): fetch and persist the marketplace MCP manifest on install so server tool discovery can find and execute the plugin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,28 @@ The injected context includes:
|
||||
You should use this context to understand the current state of the agent and available tools before making any modifications.
|
||||
</context_awareness>
|
||||
|
||||
<identity_boundary>
|
||||
**You are always the Agent Configuration Assistant — never the agent being configured.**
|
||||
|
||||
Your sole role is to help users build, configure, and optimize agents. You do not become or roleplay as any agent under any circumstances.
|
||||
|
||||
**Interpreting ambiguous short inputs:**
|
||||
When a user's message is a short phrase that could be read as either (a) a request for service from a domain expert, or (b) a description of an agent to create/configure — always choose interpretation (b).
|
||||
|
||||
Examples of ambiguous inputs that should be treated as configuration requests:
|
||||
- "健康助手,咨询健康问题" → The user wants to create/configure an agent titled "健康助手" (Health Assistant) for "咨询健康问题" (health consultation) — NOT asking you to give health advice.
|
||||
- "客服机器人,处理售后问题" → Configure a customer-service agent for post-sales issues — NOT asking you to handle after-sales queries yourself.
|
||||
- "旅行助手" → Create/configure a travel assistant agent — NOT asking you for travel tips.
|
||||
|
||||
The distinction is simple: **you configure agents; you do not act as them.** If the user genuinely wants health/travel/customer-service help, they would be talking to those agents directly — not to you, the Agent Configuration Assistant.
|
||||
</identity_boundary>
|
||||
|
||||
<skill_coexistence>
|
||||
When LobeHub skills appear in the system context (listed under \`<available_skills>\`), those skills provide task-execution capabilities (e.g., web search, calendar access, coding assistance). However, for all agent **configuration** tasks — updating the agent's model, system prompt, plugins, metadata, or any other settings — always use the Agent Builder tools directly (\`updateConfig\`, \`updatePrompt\`, \`installPlugin\`, etc.).
|
||||
|
||||
Do not delegate agent configuration to a LobeHub skill, even if the skill's name or description appears to overlap. Agent Builder tools apply changes immediately and directly to the current agent's stored configuration; LobeHub skills do not modify agent configuration.
|
||||
</skill_coexistence>
|
||||
|
||||
<capabilities>
|
||||
You have access to tools that can modify agent configurations:
|
||||
|
||||
@@ -142,6 +164,13 @@ Always adapt to user's language. Use natural descriptions, not raw field names.
|
||||
</configuration_knowledge>
|
||||
|
||||
<examples>
|
||||
User: "健康助手,咨询健康问题" (short phrase — agent name + purpose)
|
||||
Action: Treat as a configuration request, NOT a health consultation. Follow the modification sequence:
|
||||
1. Use updateMeta to set identity: { avatar: "🏥", title: "健康助手", description: "专注于健康咨询的 AI 助手" }
|
||||
2. Use updateConfig to set a suitable model
|
||||
3. Use updatePrompt to write a system prompt for a health consultant
|
||||
Do NOT respond as a health assistant or provide health advice. You are configuring the agent on the left panel to become a health assistant.
|
||||
|
||||
User: "帮我创建一个代码助手" / "Help me create a coding assistant"
|
||||
Action: Follow the modification sequence:
|
||||
1. First, use updateMeta to set identity: { avatar: "👨💻", title: "Code Assistant", description: "A helpful coding assistant for debugging and writing code" }
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
import {
|
||||
AgentBuilderIdentifier,
|
||||
type GetAvailableModelsParams,
|
||||
type InstallPluginParams,
|
||||
type SearchMarketToolsParams,
|
||||
type UpdateAgentConfigParams,
|
||||
type UpdatePromptParams,
|
||||
} from '@lobechat/builtin-tool-agent-builder';
|
||||
import { builtinTools } from '@lobechat/builtin-tools';
|
||||
import { BRANDING_PROVIDER } from '@lobechat/business-const';
|
||||
import { modelsResultsPrompt } from '@lobechat/prompts';
|
||||
|
||||
import { AgentModel } from '@/database/models/agent';
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
||||
import { DiscoverService } from '@/server/services/discover';
|
||||
|
||||
import { type ToolExecutionContext, type ToolExecutionResult } from '../types';
|
||||
import { type ServerRuntimeRegistration } from './types';
|
||||
|
||||
const MAX_MODELS = 20;
|
||||
|
||||
const handleError = (error: unknown, message: string): ToolExecutionResult => {
|
||||
const err = error as Error;
|
||||
return { content: `${message}: ${err.message}`, success: false };
|
||||
};
|
||||
|
||||
export const agentBuilderRuntime: ServerRuntimeRegistration = {
|
||||
factory: (context: ToolExecutionContext) => {
|
||||
if (!context.userId || !context.serverDB) {
|
||||
throw new Error('userId and serverDB are required for Agent Builder execution');
|
||||
}
|
||||
|
||||
const agentModel = new AgentModel(context.serverDB, context.userId);
|
||||
const pluginModel = new PluginModel(context.serverDB, context.userId);
|
||||
const aiInfraRepos = new AiInfraRepos(context.serverDB, context.userId, {});
|
||||
const discoverService = new DiscoverService();
|
||||
|
||||
return {
|
||||
getAvailableModels: async (
|
||||
params: GetAvailableModelsParams,
|
||||
): Promise<ToolExecutionResult> => {
|
||||
try {
|
||||
const allProviders = await aiInfraRepos.getAiProviderList();
|
||||
const enabledProviders = allProviders.filter((p) => p.enabled);
|
||||
|
||||
// LobeHub provider first, then by sort order
|
||||
enabledProviders.sort((a, b) => {
|
||||
if (a.id === BRANDING_PROVIDER) return -1;
|
||||
if (b.id === BRANDING_PROVIDER) return 1;
|
||||
return (a.sort ?? 999) - (b.sort ?? 999);
|
||||
});
|
||||
|
||||
// Apply optional provider filter
|
||||
const filteredProviders = params.providerId
|
||||
? enabledProviders.filter((p) => p.id === params.providerId)
|
||||
: enabledProviders;
|
||||
|
||||
const providerResults: Array<{
|
||||
id: string;
|
||||
models: Array<{
|
||||
abilities?: {
|
||||
files?: boolean;
|
||||
functionCall?: boolean;
|
||||
reasoning?: boolean;
|
||||
vision?: boolean;
|
||||
};
|
||||
description?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
name: string;
|
||||
}> = [];
|
||||
|
||||
let totalModels = 0;
|
||||
|
||||
for (const provider of filteredProviders) {
|
||||
if (totalModels >= MAX_MODELS) break;
|
||||
|
||||
const enabledChatModels = await aiInfraRepos.getAiProviderModelList(provider.id, {
|
||||
enabled: true,
|
||||
type: 'chat',
|
||||
});
|
||||
|
||||
const remaining = MAX_MODELS - totalModels;
|
||||
const sliced = enabledChatModels.slice(0, remaining);
|
||||
|
||||
if (sliced.length === 0) continue;
|
||||
|
||||
providerResults.push({
|
||||
id: provider.id,
|
||||
models: sliced.map((m) => ({
|
||||
abilities: (m.abilities as any) ?? undefined,
|
||||
id: m.id,
|
||||
name: m.displayName || m.id,
|
||||
})),
|
||||
name: provider.name || provider.id,
|
||||
});
|
||||
|
||||
totalModels += sliced.length;
|
||||
}
|
||||
|
||||
const xmlContent = modelsResultsPrompt(providerResults);
|
||||
const summary = `Found ${providerResults.length} enabled provider(s) with ${totalModels} model(s).\n\n${xmlContent}`;
|
||||
|
||||
return {
|
||||
content: summary,
|
||||
state: { providers: providerResults },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to get available models');
|
||||
}
|
||||
},
|
||||
|
||||
searchMarketTools: async (params: SearchMarketToolsParams): Promise<ToolExecutionResult> => {
|
||||
try {
|
||||
const response = await discoverService.getMcpList({
|
||||
category: params.category,
|
||||
pageSize: params.pageSize || 10,
|
||||
q: params.query,
|
||||
});
|
||||
|
||||
const tools = response.items.map((item) => ({
|
||||
author: item.author,
|
||||
description: item.description,
|
||||
identifier: item.identifier,
|
||||
name: item.name,
|
||||
tags: item.tags,
|
||||
}));
|
||||
|
||||
let summary = `Found ${response.totalCount} tool(s) in the marketplace.`;
|
||||
if (params.query) {
|
||||
summary = `Found ${response.totalCount} tool(s) matching "${params.query}".`;
|
||||
}
|
||||
|
||||
const toolLines = tools
|
||||
.map((t) => `- ${t.name} (${t.identifier})${t.description ? ': ' + t.description : ''}`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
content: `${summary}\n\n${toolLines}`,
|
||||
state: { query: params.query, tools, totalCount: response.totalCount },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to search market tools');
|
||||
}
|
||||
},
|
||||
|
||||
updateConfig: async (
|
||||
params: UpdateAgentConfigParams,
|
||||
ctx: ToolExecutionContext,
|
||||
): Promise<ToolExecutionResult> => {
|
||||
const agentId = ctx.agentId;
|
||||
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'No active agent found',
|
||||
error: { message: 'No active agent found', type: 'NoAgentContext' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const agent = await agentModel.getAgentConfigById(agentId);
|
||||
if (!agent) {
|
||||
return { content: `Agent "${agentId}" not found.`, success: false };
|
||||
}
|
||||
|
||||
let rawConfig: any = params.config;
|
||||
if (typeof rawConfig === 'string') {
|
||||
try {
|
||||
rawConfig = JSON.parse(rawConfig);
|
||||
} catch {
|
||||
rawConfig = undefined;
|
||||
}
|
||||
}
|
||||
let rawMeta: any = params.meta;
|
||||
if (typeof rawMeta === 'string') {
|
||||
try {
|
||||
rawMeta = JSON.parse(rawMeta);
|
||||
} catch {
|
||||
rawMeta = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let finalConfig = rawConfig ? { ...rawConfig } : {};
|
||||
const updatedParts: string[] = [];
|
||||
|
||||
if (params.togglePlugin) {
|
||||
const { pluginId, enabled } = params.togglePlugin;
|
||||
const currentPlugins = (agent.plugins as string[] | null) || [];
|
||||
const isEnabled = currentPlugins.includes(pluginId);
|
||||
const shouldEnable = enabled !== undefined ? enabled : !isEnabled;
|
||||
|
||||
const newPlugins =
|
||||
shouldEnable && !isEnabled
|
||||
? [...currentPlugins, pluginId]
|
||||
: !shouldEnable && isEnabled
|
||||
? currentPlugins.filter((id: string) => id !== pluginId)
|
||||
: currentPlugins;
|
||||
|
||||
finalConfig = { ...finalConfig, plugins: newPlugins };
|
||||
updatedParts.push(`plugin ${pluginId} ${shouldEnable ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
if ('systemRole' in finalConfig && !('editorData' in finalConfig)) {
|
||||
finalConfig = { ...finalConfig, editorData: null };
|
||||
}
|
||||
|
||||
if (Object.keys(finalConfig).length > 0) {
|
||||
await agentModel.updateConfig(agentId, finalConfig);
|
||||
const nonPluginFields = Object.keys(finalConfig).filter((f) => f !== 'plugins');
|
||||
if (nonPluginFields.length > 0) {
|
||||
updatedParts.push(`config fields: ${nonPluginFields.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rawMeta && Object.keys(rawMeta).length > 0) {
|
||||
await agentModel.update(agentId, rawMeta as Record<string, unknown>);
|
||||
updatedParts.push(`meta fields: ${Object.keys(rawMeta).join(', ')}`);
|
||||
}
|
||||
|
||||
if (updatedParts.length === 0) {
|
||||
return { content: 'No fields to update.', state: { success: true }, success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
content: `Successfully updated agent. Updated ${updatedParts.join('; ')}`,
|
||||
state: { agentId, success: true },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to update agent config');
|
||||
}
|
||||
},
|
||||
|
||||
updatePrompt: async (
|
||||
params: UpdatePromptParams,
|
||||
ctx: ToolExecutionContext,
|
||||
): Promise<ToolExecutionResult> => {
|
||||
const agentId = ctx.agentId;
|
||||
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'No active agent found',
|
||||
error: { message: 'No active agent found', type: 'NoAgentContext' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await agentModel.update(agentId, {
|
||||
editorData: null,
|
||||
systemRole: params.prompt,
|
||||
} as Record<string, unknown>);
|
||||
|
||||
return {
|
||||
content: params.prompt
|
||||
? `Successfully updated system prompt (${params.prompt.length} characters)`
|
||||
: 'Successfully cleared system prompt',
|
||||
state: { newPrompt: params.prompt, success: true },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to update prompt');
|
||||
}
|
||||
},
|
||||
|
||||
installPlugin: async (
|
||||
params: InstallPluginParams,
|
||||
ctx: ToolExecutionContext,
|
||||
): Promise<ToolExecutionResult> => {
|
||||
const agentId = ctx.agentId;
|
||||
|
||||
if (!agentId) {
|
||||
return {
|
||||
content: 'No active agent found',
|
||||
error: { message: 'No active agent found', type: 'NoAgentContext' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { identifier, source } = params;
|
||||
|
||||
if (source === 'official') {
|
||||
if (builtinTools.some((t) => t.identifier === identifier)) {
|
||||
// Builtin tools (lobe-web-browsing, lobe-image-generation, etc.) need no OAuth
|
||||
try {
|
||||
const agent = await agentModel.getAgentConfigById(agentId);
|
||||
if (!agent) return { content: `Agent "${agentId}" not found.`, success: false };
|
||||
|
||||
const currentPlugins = (agent.plugins as string[] | null) || [];
|
||||
if (!currentPlugins.includes(identifier)) {
|
||||
await agentModel.updateConfig(agentId, {
|
||||
plugins: [...currentPlugins, identifier],
|
||||
});
|
||||
}
|
||||
return {
|
||||
content: `Successfully enabled "${identifier}" for agent "${agentId}"`,
|
||||
state: { installed: true, pluginId: identifier, success: true },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to enable builtin tool');
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth-based tools (Klavis, LobehubSkill) cannot be installed in background context
|
||||
return {
|
||||
content: `Installing official integrations that require OAuth (Klavis, LobehubSkill) is not supported in background execution. Please install "${identifier}" from the Agent Builder UI instead.`,
|
||||
error: { message: 'OAuth not available in background context', type: 'NotSupported' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
// source === 'market' — MCP marketplace plugin
|
||||
try {
|
||||
const agent = await agentModel.getAgentConfigById(agentId);
|
||||
if (!agent) {
|
||||
return { content: `Agent "${agentId}" not found.`, success: false };
|
||||
}
|
||||
|
||||
const existing = await pluginModel.findById(identifier);
|
||||
if (!existing) {
|
||||
let manifest: any;
|
||||
try {
|
||||
manifest = await discoverService.getMcpManifest({ identifier });
|
||||
} catch {
|
||||
// proceed without manifest if fetch fails; tool will be unusable until manifest loads
|
||||
}
|
||||
await pluginModel.create({ identifier, manifest: manifest as any, type: 'plugin' });
|
||||
} else if (!existing.manifest) {
|
||||
try {
|
||||
const manifest = await discoverService.getMcpManifest({ identifier });
|
||||
await pluginModel.update(identifier, { manifest: manifest as any });
|
||||
} catch {
|
||||
// best-effort backfill
|
||||
}
|
||||
}
|
||||
|
||||
const currentPlugins = (agent.plugins as string[] | null) || [];
|
||||
if (!currentPlugins.includes(identifier)) {
|
||||
await agentModel.updateConfig(agentId, {
|
||||
plugins: [...currentPlugins, identifier],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
content: `Successfully enabled plugin "${identifier}" for agent "${agentId}"`,
|
||||
state: { installed: true, pluginId: identifier, success: true },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleError(error, 'Failed to install plugin');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
identifier: AgentBuilderIdentifier,
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import type { ToolExecutionContext } from '../types';
|
||||
import { activatorRuntime } from './activator';
|
||||
import { agentBuilderRuntime } from './agentBuilder';
|
||||
import { agentDocumentsRuntime } from './agentDocuments';
|
||||
import { agentManagementRuntime } from './agentManagement';
|
||||
import { briefRuntime } from './brief';
|
||||
@@ -48,6 +49,7 @@ const registerRuntimes = (runtimes: ServerRuntimeRegistration[]) => {
|
||||
|
||||
// Register all server runtimes
|
||||
registerRuntimes([
|
||||
agentBuilderRuntime,
|
||||
webBrowsingRuntime,
|
||||
cloudSandboxRuntime,
|
||||
calculatorRuntime,
|
||||
|
||||
Reference in New Issue
Block a user