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. You should use this context to understand the current state of the agent and available tools before making any modifications.
</context_awareness> </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> <capabilities>
You have access to tools that can modify agent configurations: 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> </configuration_knowledge>
<examples> <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" User: "帮我创建一个代码助手" / "Help me create a coding assistant"
Action: Follow the modification sequence: 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" } 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 type { ToolExecutionContext } from '../types';
import { activatorRuntime } from './activator'; import { activatorRuntime } from './activator';
import { agentBuilderRuntime } from './agentBuilder';
import { agentDocumentsRuntime } from './agentDocuments'; import { agentDocumentsRuntime } from './agentDocuments';
import { agentManagementRuntime } from './agentManagement'; import { agentManagementRuntime } from './agentManagement';
import { briefRuntime } from './brief'; import { briefRuntime } from './brief';
@@ -48,6 +49,7 @@ const registerRuntimes = (runtimes: ServerRuntimeRegistration[]) => {
// Register all server runtimes // Register all server runtimes
registerRuntimes([ registerRuntimes([
agentBuilderRuntime,
webBrowsingRuntime, webBrowsingRuntime,
cloudSandboxRuntime, cloudSandboxRuntime,
calculatorRuntime, calculatorRuntime,