From 359b3489891323cccae701060c19a897e2249361 Mon Sep 17 00:00:00 2001 From: LiJian Date: Tue, 2 Jun 2026 17:10:30 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(agent-builder):=20add=20skill?= =?UTF-8?q?=20priority=20instruction=20and=20server=20runtime=20(#15409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat(agent-builder): add skill priority instruction and server runtime - Add 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 * ✨ feat(agent-builder): fix identity confusion when user provides agent name/purpose Add 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 * 🐛 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 --------- Co-authored-by: Claude Sonnet 4.6 --- .../src/systemRole.ts | 29 ++ .../serverRuntimes/agentBuilder.ts | 362 ++++++++++++++++++ .../toolExecution/serverRuntimes/index.ts | 2 + 3 files changed, 393 insertions(+) create mode 100644 src/server/services/toolExecution/serverRuntimes/agentBuilder.ts diff --git a/packages/builtin-tool-agent-builder/src/systemRole.ts b/packages/builtin-tool-agent-builder/src/systemRole.ts index bc25d4f8e0..8a3ee0fbfa 100644 --- a/packages/builtin-tool-agent-builder/src/systemRole.ts +++ b/packages/builtin-tool-agent-builder/src/systemRole.ts @@ -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 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. + + + +When LobeHub skills appear in the system context (listed under \`\`), 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. + + 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. +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" } diff --git a/src/server/services/toolExecution/serverRuntimes/agentBuilder.ts b/src/server/services/toolExecution/serverRuntimes/agentBuilder.ts new file mode 100644 index 0000000000..6ce6ff7596 --- /dev/null +++ b/src/server/services/toolExecution/serverRuntimes/agentBuilder.ts @@ -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 => { + 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 => { + 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 => { + 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); + 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 => { + 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); + + 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 => { + 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, +}; diff --git a/src/server/services/toolExecution/serverRuntimes/index.ts b/src/server/services/toolExecution/serverRuntimes/index.ts index f63af9434a..e66b95e120 100644 --- a/src/server/services/toolExecution/serverRuntimes/index.ts +++ b/src/server/services/toolExecution/serverRuntimes/index.ts @@ -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,