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:
LiJian
2026-06-02 17:10:30 +08:00
committed by GitHub
parent 0c3450de7c
commit 359b348989
3 changed files with 393 additions and 0 deletions
@@ -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,