From 9bdc3b0474cd10e2c8eff0fb2ea3081c578e66aa Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 17 Mar 2026 00:35:18 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20improve=20agent=20context?= =?UTF-8?q?=20injection=20(skills=20discovery,=20device=20optimization,=20?= =?UTF-8?q?prompt=20cleanup)=20(#13021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * âœĻ feat: inject all installed skills into for AI discovery Previously, only skills explicitly added to the agent's plugins list appeared in . Now all installed skills are exposed so the AI can discover and activate them via activateSkill. Changes: - Frontend: use getAllSkills() instead of getEnabledSkills(plugins) - Backend: pass skillMetas through createOperation → RuntimeExecutors → serverMessagesEngine - Add skillsConfig support to serverMessagesEngine Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: use DB + builtin skills for available_skills instead of provider manifests lobehubSkillManifests are tool provider manifests (per-provider, containing tool APIs), not skill metadata. Using them for incorrectly showed provider names (e.g. "Arvin Xu") as skills. Now fetches actual skills from AgentSkillModel (DB) + builtinSkills for correct injection. Co-Authored-By: Claude Opus 4.6 (1M context) * 💄 style: use XML structure for online-devices in system prompt Co-Authored-By: Claude Opus 4.6 (1M context) * â™ŧïļ refactor: extract online-devices prompt to @lobechat/prompts package Move device XML prompt generation from builtin-tool-remote-device into the shared prompts package for reusability and consistency. Co-Authored-By: Claude Opus 4.6 (1M context) * ✅ test: add failing tests for Remote Device suppression when auto-activated Co-Authored-By: Claude Opus 4.6 (1M context) * ⚡ïļ perf: suppress Remote Device tool when device is auto-activated When a device is auto-activated (single device in IM/Bot or bound device), the Remote Device management tool (listOnlineDevices, activateDevice) is unnecessary — saves ~500 tokens of system prompt + 2 tool functions. - Add autoActivated flag to deviceContext - Move activeDeviceId computation before tool engine creation - Disable Remote Device in enableChecker when autoActivated Co-Authored-By: Claude Opus 4.6 (1M context) * update system role * update system role * â™ŧïļ refactor: use agentId instead of slug for OpenAPI responses model field Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: use JSON round-trip instead of structuredClone in InMemoryAgentStateManager structuredClone fails with DataCloneError when state contains non-cloneable objects like DOM ErrorEvent (from Neon DB WebSocket errors). Co-Authored-By: Claude Opus 4.6 (1M context) * 🐛 fix: only inject available_skills when tools are enabled Restore plugins guard to prevent skills injection when tool use is disabled (plugins is undefined), fixing 28 test failures. Co-Authored-By: Claude Opus 4.6 (1M context) * ✅ test: update system message assertions for skills injection Use stringContaining instead of exact match for system message content, since available_skills may now be appended after the date. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../src/systemRole.desktop.ts | 145 ++++++++++++++++++ .../src/systemRole.ts | 48 +----- .../builtin-tool-remote-device/package.json | 3 + .../src/systemRole.ts | 18 ++- .../openapi/src/services/responses.service.ts | 14 +- packages/prompts/src/prompts/index.ts | 1 + .../prompts/src/prompts/remoteDevice/index.ts | 26 ++++ .../AgentRuntime/InMemoryAgentStateManager.ts | 4 +- .../modules/AgentRuntime/RuntimeExecutors.ts | 5 + .../modules/Mecha/AgentToolsEngine/index.ts | 3 +- .../modules/Mecha/AgentToolsEngine/types.ts | 2 + .../modules/Mecha/ContextEngineering/index.ts | 4 + .../modules/Mecha/ContextEngineering/types.ts | 4 + .../agentRuntime/AgentRuntimeService.ts | 2 + src/server/services/agentRuntime/types.ts | 2 + .../__tests__/execAgent.device.test.ts | 115 +++++++++++++- src/server/services/aiAgent/index.ts | 56 +++++-- src/services/chat/chat.test.ts | 41 +++-- .../chat/mecha/contextEngineering.test.ts | 76 ++++++--- src/services/chat/mecha/contextEngineering.ts | 4 +- 20 files changed, 446 insertions(+), 127 deletions(-) create mode 100644 packages/builtin-tool-local-system/src/systemRole.desktop.ts create mode 100644 packages/prompts/src/prompts/remoteDevice/index.ts diff --git a/packages/builtin-tool-local-system/src/systemRole.desktop.ts b/packages/builtin-tool-local-system/src/systemRole.desktop.ts new file mode 100644 index 0000000000..e61caa6a9e --- /dev/null +++ b/packages/builtin-tool-local-system/src/systemRole.desktop.ts @@ -0,0 +1,145 @@ +export const systemPrompt = `You have a Local System tool with capabilities to interact with the user's local system. You can list directories, read file contents, search for files, move, and rename files/directories. + + +**Current Working Directory:** {{workingDirectory}} +All relative paths and file operations should be based on this directory unless the user specifies otherwise. + +**Known Locations & System Details:** +Here are some known locations and system details on the user's system. User is using the Operating System: {{platform}}({{arch}}). +Use these paths when the user refers to these common locations by name (e.g., "my desktop", "downloads folder"). +- Desktop: {{desktopPath}} +- Documents: {{documentsPath}} +- Downloads: {{downloadsPath}} +- Music: {{musicPath}} +- Pictures: {{picturesPath}} +- Videos: {{videosPath}} +- User Home: {{homePath}} +- App Data: {{userDataPath}} (Use this primarily for plugin-related data or configurations if needed, less for general user files) + + + +You have access to a set of tools to interact with the user's local file system: + +**File Operations:** +1. **listLocalFiles**: Lists files and directories in a specified path. Returns metadata including file size and modification time. Results are sorted by modification time (newest first) by default and limited to 100 items. +2. **readLocalFile**: Reads the content of a specified file, optionally within a line range. You can read file types such as Word, Excel, PowerPoint, PDF, and plain text files. +3. **writeLocalFile**: Write content to a specific file, only support plain text file like \`.text\` or \`.md\` +4. **editLocalFile**: Performs exact string replacements in files. Must read the file first before editing. +5. **renameLocalFile**: Renames a single file or directory in its current location. +6. **moveLocalFiles**: Moves multiple files or directories. Can be used for renaming during the move. + +**Shell Commands:** +7. **runCommand**: Execute shell commands with timeout control. Supports both synchronous and background execution. When providing a description, always use the same language as the user's input. +8. **getCommandOutput**: Retrieve output from running background commands. Returns only new output since last check. +9. **killCommand**: Terminate a running background shell command by its ID. + +**Search & Find:** +10. **searchLocalFiles**: Searches for files based on keywords and other criteria using Spotlight (macOS) or native search. Use this tool to find files if the user is unsure about the exact path. +11. **grepContent**: Search for content within files using regex patterns. Supports various output modes, filtering, and context lines. +12. **globLocalFiles**: Find files matching glob patterns (e.g., "**/*.js", "*.{ts,tsx}"). + + + +1. Understand the user's request regarding local operations (files, commands, searches). +2. Select the appropriate tool: + - File operations: listLocalFiles, readLocalFile, writeLocalFile, editLocalFile, renameLocalFile, moveLocalFiles + - Shell commands: runCommand, getCommandOutput, killCommand + - Search/Find: searchLocalFiles, grepContent, globLocalFiles +3. Execute the operation. **If the user mentions a common location (like Desktop, Documents, Downloads, etc.) without providing a full path, use the corresponding path from the section.** +4. Present the results or confirmation. + + + +- For listing directory contents: Use 'listLocalFiles'. Provide the following parameters: + - 'path': The directory path to list. + - 'sortBy' (Optional): Field to sort results by. Options: 'name', 'modifiedTime', 'createdTime', 'size'. Defaults to 'modifiedTime'. + - 'sortOrder' (Optional): Sort order. Options: 'asc', 'desc'. Defaults to 'desc' (newest/largest first). + - 'limit' (Optional): Maximum number of items to return. Defaults to 100. + - The response includes file/folder names with metadata (size in bytes, modification time) for each item. + - System files (e.g., '.DS_Store', 'Thumbs.db', '$RECYCLE.BIN') are automatically filtered out. +- For reading a file: Use 'readFile'. Provide the following parameters: + - 'path': The exact file path. + - 'loc' (Optional): A two-element array [startLine, endLine] to specify a line range to read (e.g., '[301, 400]' reads lines 301 to 400). + - If 'loc' is omitted, it defaults to reading the first 200 lines ('[0, 200]'). + - To read the entire file: First call 'readFile' (potentially without 'loc'). The response includes 'totalLineCount'. Then, call 'readFile' again with 'loc: [0, totalLineCount]' to get the full content. +- For searching files: Use 'searchFiles' with the 'query' parameter (search string). You can optionally add the following filter parameters to narrow down the search: + - 'contentContains': Find files whose content includes specific text. + - 'createdAfter' / 'createdBefore': Filter by creation date. + - 'modifiedAfter' / 'modifiedBefore': Filter by modification date. + - 'fileTypes': Filter by file type (e.g., "public.image", "txt"). + - 'onlyIn': Limit the search to a specific directory. + - 'exclude': Exclude specific files or directories. + - 'limit': Limit the number of results returned. + - 'sortBy' / 'sortDirection': Sort the results. +- For renaming a file/folder in place: Use 'renameFile'. Provide the following parameters: + - 'path': The current full path of the file or folder. + - 'newName': The desired new name (without path components). +- For moving multiple files/folders (and optionally renaming them): Use 'moveLocalFiles'. Provide the following parameter: + - 'items': An array of objects, where each object represents a move operation and must contain: + - 'oldPath': The current absolute path of the file/directory to move or rename. + - 'newPath': The target absolute path for the file/directory (can include a new name). +- For writing a file: Use 'writeFile'. Provide: + - 'path': The file path to write to. + - 'content': The text content. +- For editing files: Use 'editLocalFile'. Provide: + - 'file_path': The absolute path to the file to modify. + - 'old_string': The exact text to replace. + - 'new_string': The replacement text. + - 'replace_all' (Optional): Replace all occurrences. +- For executing shell commands: Use 'runCommand'. Provide the following parameters: + - 'command': The shell command to execute. + - 'description' (Optional but recommended): A clear, concise description of what the command does (5-10 words, in active voice). **IMPORTANT: Always use the same language as the user's input.** If the user speaks Chinese, write the description in Chinese; if English, use English, etc. + - 'run_in_background' (Optional): Set to true to run in background and get a shell_id for later checking output. + - 'timeout' (Optional): Timeout in milliseconds (default: 120000ms, max: 600000ms). + The command runs in cmd.exe on Windows or /bin/sh on macOS/Linux. +- For retrieving output from background commands: Use 'getCommandOutput'. Provide: + - 'shell_id': The ID returned from runCommand when run_in_background was true. + - 'filter' (Optional): A regex pattern to filter output lines. + Returns only new output since the last check. +- For killing background commands: Use 'killCommand' with 'shell_id'. +- For searching content in files: Use 'grepContent'. Provide: + - 'pattern': The regex pattern to search for. + - 'path' (Optional): File or directory to search. + - 'output_mode' (Optional): "content" (matching lines), "files_with_matches" (file paths, default), "count" (match counts). + - 'glob' (Optional): Glob pattern to filter files (e.g., "*.js", "*.{ts,tsx}"). + - '-i' (Optional): Case insensitive search. + - '-n' (Optional): Show line numbers (requires output_mode: "content"). + - '-A/-B/-C' (Optional): Show N lines after/before/around matches (requires output_mode: "content"). + - 'head_limit' (Optional): Limit results to first N matches. +- For finding files by pattern: Use 'globLocalFiles'. Provide: + - 'pattern': Glob pattern (e.g., "**/*.js", "src/**/*.ts"). + - 'path' (Optional): Directory to search in. + Returns files sorted by modification time (most recent first). + + + +- Always confirm with the user before performing write operations, especially if it involves overwriting existing files. +- Confirm with the user before moving files to significantly different locations or when renaming might cause confusion or potential data loss if the target exists (though the tool should handle this). +- Do not attempt to access files outside the user's designated workspace or allowed directories unless explicitly permitted. +- Handle file paths carefully to avoid unintended access or errors. +- When running shell commands: + - Never execute commands that could harm the system or delete important data without explicit user confirmation. + - Be cautious with commands that have side effects (e.g., rm, sudo, format). + - Always describe what a command will do before running it, especially for non-trivial operations. + - Always provide a clear 'description' parameter in the user's language to help them understand what the command does. + - Use appropriate timeouts to prevent commands from running indefinitely. +- When editing files: + - Always read the file first to verify its current content. + - Ensure old_string exactly matches the text to be replaced to avoid unintended changes. + - Be cautious when using replace_all option. + + + +- When listing files or returning search results that include file or directory paths, **always** use the \`\` tag format. **Any reference to a local file or directory path in your response MUST be enclosed within this tag structure.** Do not output raw file paths outside of this tag structure. +- For a file, use: \`\`. Example: \`\` +- For a directory, use: \`\`. Example: \`\` +- Ensure the \`path\` attribute contains the full, raw, unencoded path. +- Ensure the \`name\` attribute contains the display name (usually the filename or directory name). +- Include the \`isDirectory\` attribute **only** for directories. +- When listing files, provide a clear list using the tag format. +- When reading files, present the content accurately. **If you mention the file path being read, use the \`\` tag.** +- When searching files, return a list of matching files using the tag format. +- When confirming a rename or move operation, use the \`\` tag for both the old and new paths mentioned. Example: \`Successfully renamed to .\` +- When writing files, confirm the success or failure. **If you mention the file path written to, use the \`\` tag.** + +`; diff --git a/packages/builtin-tool-local-system/src/systemRole.ts b/packages/builtin-tool-local-system/src/systemRole.ts index e61caa6a9e..77618ae2b8 100644 --- a/packages/builtin-tool-local-system/src/systemRole.ts +++ b/packages/builtin-tool-local-system/src/systemRole.ts @@ -1,20 +1,9 @@ export const systemPrompt = `You have a Local System tool with capabilities to interact with the user's local system. You can list directories, read file contents, search for files, move, and rename files/directories. -**Current Working Directory:** {{workingDirectory}} -All relative paths and file operations should be based on this directory unless the user specifies otherwise. - -**Known Locations & System Details:** -Here are some known locations and system details on the user's system. User is using the Operating System: {{platform}}({{arch}}). -Use these paths when the user refers to these common locations by name (e.g., "my desktop", "downloads folder"). -- Desktop: {{desktopPath}} -- Documents: {{documentsPath}} -- Downloads: {{downloadsPath}} -- Music: {{musicPath}} -- Pictures: {{picturesPath}} -- Videos: {{videosPath}} -- User Home: {{homePath}} -- App Data: {{userDataPath}} (Use this primarily for plugin-related data or configurations if needed, less for general user files) + +{{workingDirectory}} +{{homePath}} @@ -111,35 +100,4 @@ You have access to a set of tools to interact with the user's local file system: - 'path' (Optional): Directory to search in. Returns files sorted by modification time (most recent first). - - -- Always confirm with the user before performing write operations, especially if it involves overwriting existing files. -- Confirm with the user before moving files to significantly different locations or when renaming might cause confusion or potential data loss if the target exists (though the tool should handle this). -- Do not attempt to access files outside the user's designated workspace or allowed directories unless explicitly permitted. -- Handle file paths carefully to avoid unintended access or errors. -- When running shell commands: - - Never execute commands that could harm the system or delete important data without explicit user confirmation. - - Be cautious with commands that have side effects (e.g., rm, sudo, format). - - Always describe what a command will do before running it, especially for non-trivial operations. - - Always provide a clear 'description' parameter in the user's language to help them understand what the command does. - - Use appropriate timeouts to prevent commands from running indefinitely. -- When editing files: - - Always read the file first to verify its current content. - - Ensure old_string exactly matches the text to be replaced to avoid unintended changes. - - Be cautious when using replace_all option. - - - -- When listing files or returning search results that include file or directory paths, **always** use the \`\` tag format. **Any reference to a local file or directory path in your response MUST be enclosed within this tag structure.** Do not output raw file paths outside of this tag structure. -- For a file, use: \`\`. Example: \`\` -- For a directory, use: \`\`. Example: \`\` -- Ensure the \`path\` attribute contains the full, raw, unencoded path. -- Ensure the \`name\` attribute contains the display name (usually the filename or directory name). -- Include the \`isDirectory\` attribute **only** for directories. -- When listing files, provide a clear list using the tag format. -- When reading files, present the content accurately. **If you mention the file path being read, use the \`\` tag.** -- When searching files, return a list of matching files using the tag format. -- When confirming a rename or move operation, use the \`\` tag for both the old and new paths mentioned. Example: \`Successfully renamed to .\` -- When writing files, confirm the success or failure. **If you mention the file path written to, use the \`\` tag.** - `; diff --git a/packages/builtin-tool-remote-device/package.json b/packages/builtin-tool-remote-device/package.json index a4a2a51b5f..68f199c1c6 100644 --- a/packages/builtin-tool-remote-device/package.json +++ b/packages/builtin-tool-remote-device/package.json @@ -6,6 +6,9 @@ ".": "./src/index.ts" }, "main": "./src/index.ts", + "dependencies": { + "@lobechat/prompts": "workspace:*" + }, "devDependencies": { "@lobechat/types": "workspace:*" } diff --git a/packages/builtin-tool-remote-device/src/systemRole.ts b/packages/builtin-tool-remote-device/src/systemRole.ts index 0b5ec4be3f..f3f31c5fa2 100644 --- a/packages/builtin-tool-remote-device/src/systemRole.ts +++ b/packages/builtin-tool-remote-device/src/systemRole.ts @@ -1,16 +1,18 @@ +import { onlineDevicesPrompt } from '@lobechat/prompts'; + import { type DeviceAttachment } from './ExecutionRuntime/types'; export const generateSystemPrompt = (devices?: DeviceAttachment[]): string => { const onlineDevices = devices?.filter((d) => d.online) ?? []; - const deviceSection = - onlineDevices.length > 0 - ? ` -${onlineDevices.map((d) => `- **${d.hostname}** (${d.platform}) — ID: \`${d.deviceId}\``).join('\n')} -` - : ` -No devices are currently online. -`; + const deviceSection = onlineDevicesPrompt( + onlineDevices.map((d) => ({ + id: d.deviceId, + lastSeen: d.lastSeen, + name: d.hostname, + os: d.platform, + })), + ); return `You have a Remote Device Management tool that allows you to discover and connect to the user's desktop devices. diff --git a/packages/openapi/src/services/responses.service.ts b/packages/openapi/src/services/responses.service.ts index 82e3a99c21..521f3857e6 100644 --- a/packages/openapi/src/services/responses.service.ts +++ b/packages/openapi/src/services/responses.service.ts @@ -22,7 +22,7 @@ import type { * Response API Service * Handles OpenResponses protocol request execution via AiAgentService.execAgent * - * The `model` field is treated as an agent slug. + * The `model` field is treated as an agent ID. * Execution is delegated to execAgent (background mode), * with executeSync used when synchronous results are needed. */ @@ -191,7 +191,7 @@ export class ResponsesService extends BaseService { const createdAt = Math.floor(Date.now() / 1000); try { - const slug = params.model; + const model = params.model; const prompt = this.extractPrompt(params.input); const instructions = this.buildInstructions(params); @@ -202,19 +202,20 @@ export class ResponsesService extends BaseService { this.log('info', 'Creating response via execAgent', { hasInstructions: !!instructions, + model, previousTopicId, prompt: prompt.slice(0, 50), - slug, }); // 1. Create agent operation without auto-start + // model field is used as agentId const aiAgentService = new AiAgentService(this.db, this.userId); const execResult = await aiAgentService.execAgent({ + agentId: model, appContext: previousTopicId ? { topicId: previousTopicId } : undefined, autoStart: false, instructions, prompt, - slug, stream: false, }); @@ -277,7 +278,7 @@ export class ResponsesService extends BaseService { const contentIndex = 0; try { - const slug = params.model; + const model = params.model; const prompt = this.extractPrompt(params.input); const instructions = this.buildInstructions(params); @@ -287,13 +288,14 @@ export class ResponsesService extends BaseService { : null; // 1. Create agent operation (before generating responseId so we have topicId) + // model field is used as agentId const aiAgentService = new AiAgentService(this.db, this.userId); const execResult = await aiAgentService.execAgent({ + agentId: model, appContext: previousTopicId ? { topicId: previousTopicId } : undefined, autoStart: false, instructions, prompt, - slug, stream: true, }); diff --git a/packages/prompts/src/prompts/index.ts b/packages/prompts/src/prompts/index.ts index c92ec31b68..947aa6c49a 100644 --- a/packages/prompts/src/prompts/index.ts +++ b/packages/prompts/src/prompts/index.ts @@ -10,6 +10,7 @@ export * from './gtd'; export * from './knowledgeBaseQA'; export * from './messagesToText'; export * from './plugin'; +export * from './remoteDevice'; export * from './search'; export * from './skills'; export * from './speaker'; diff --git a/packages/prompts/src/prompts/remoteDevice/index.ts b/packages/prompts/src/prompts/remoteDevice/index.ts new file mode 100644 index 0000000000..8823ac20e5 --- /dev/null +++ b/packages/prompts/src/prompts/remoteDevice/index.ts @@ -0,0 +1,26 @@ +export interface DeviceItem { + id: string; + lastSeen?: string; + name: string; + os: string; +} + +export const devicePrompt = (device: DeviceItem) => { + const attrs = [`id="${device.id}"`, `name="${device.name}"`, `os="${device.os}"`]; + if (device.lastSeen) attrs.push(`last-seen="${device.lastSeen}"`); + return ` `; +}; + +export const onlineDevicesPrompt = (devices: DeviceItem[]) => { + if (devices.length === 0) { + return ` + No devices are currently online. +`; + } + + const deviceTags = devices.map((d) => devicePrompt(d)).join('\n'); + + return ` +${deviceTags} +`; +}; diff --git a/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts b/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts index 9a12c39c69..63dd4ef792 100644 --- a/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +++ b/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts @@ -45,7 +45,9 @@ export class InMemoryAgentStateManager implements IAgentStateManager { async saveStepResult(operationId: string, stepResult: StepResult): Promise { // Save latest state - this.states.set(operationId, structuredClone(stepResult.newState)); + // Use JSON round-trip instead of structuredClone to safely handle + // non-cloneable objects (e.g. DOM ErrorEvent from Neon DB errors) + this.states.set(operationId, JSON.parse(JSON.stringify(stepResult.newState))); // Save step history let stepHistory = this.steps.get(operationId); diff --git a/src/server/modules/AgentRuntime/RuntimeExecutors.ts b/src/server/modules/AgentRuntime/RuntimeExecutors.ts index 00b52045bf..1f543fb9d9 100644 --- a/src/server/modules/AgentRuntime/RuntimeExecutors.ts +++ b/src/server/modules/AgentRuntime/RuntimeExecutors.ts @@ -226,6 +226,11 @@ export const createRuntimeExecutors = ( tools: resolved.enabledToolIds, }, userMemory: state.metadata?.userMemory, + + // Skills configuration for injection + ...(state.metadata?.skillMetas?.length && { + skillsConfig: { enabledSkills: state.metadata.skillMetas }, + }), }; processedMessages = await serverMessagesEngine(contextEngineInput); diff --git a/src/server/modules/Mecha/AgentToolsEngine/index.ts b/src/server/modules/Mecha/AgentToolsEngine/index.ts index 778fda8e95..086f217d31 100644 --- a/src/server/modules/Mecha/AgentToolsEngine/index.ts +++ b/src/server/modules/Mecha/AgentToolsEngine/index.ts @@ -137,7 +137,8 @@ export const createServerAgentToolsEngine = ( !!deviceContext?.gatewayConfigured && !!deviceContext?.deviceOnline, [MemoryManifest.identifier]: globalMemoryEnabled, - [RemoteDeviceManifest.identifier]: !!deviceContext?.gatewayConfigured, + [RemoteDeviceManifest.identifier]: + !!deviceContext?.gatewayConfigured && !deviceContext?.autoActivated, [WebBrowsingManifest.identifier]: isSearchEnabled, }, }), diff --git a/src/server/modules/Mecha/AgentToolsEngine/types.ts b/src/server/modules/Mecha/AgentToolsEngine/types.ts index 73544ee89c..323172d9ed 100644 --- a/src/server/modules/Mecha/AgentToolsEngine/types.ts +++ b/src/server/modules/Mecha/AgentToolsEngine/types.ts @@ -46,6 +46,8 @@ export interface ServerCreateAgentToolsEngineParams { }; /** Device gateway context for remote tool calling */ deviceContext?: { + /** When true, a device has been auto-activated — Remote Device tool is unnecessary */ + autoActivated?: boolean; boundDeviceId?: string; deviceOnline?: boolean; gatewayConfigured: boolean; diff --git a/src/server/modules/Mecha/ContextEngineering/index.ts b/src/server/modules/Mecha/ContextEngineering/index.ts index 4a5eddaf64..432015877a 100644 --- a/src/server/modules/Mecha/ContextEngineering/index.ts +++ b/src/server/modules/Mecha/ContextEngineering/index.ts @@ -59,6 +59,7 @@ export const serverMessagesEngine = async ({ historySummary, formatHistorySummary, knowledge, + skillsConfig, toolsConfig, capabilities, userMemory, @@ -136,6 +137,9 @@ export const serverMessagesEngine = async ({ ), }, + // Skills configuration + ...(skillsConfig?.enabledSkills && skillsConfig.enabledSkills.length > 0 && { skillsConfig }), + // Extended contexts ...(agentBuilderContext && { agentBuilderContext }), ...(discordContext && { discordContext }), diff --git a/src/server/modules/Mecha/ContextEngineering/types.ts b/src/server/modules/Mecha/ContextEngineering/types.ts index a7880ba4f5..58b08fb76a 100644 --- a/src/server/modules/Mecha/ContextEngineering/types.ts +++ b/src/server/modules/Mecha/ContextEngineering/types.ts @@ -7,6 +7,7 @@ import type { FileContent, KnowledgeBaseInfo, LobeToolManifest, + SkillMeta, UserMemoryData, } from '@lobechat/context-engine'; import type { PageContentContext } from '@lobechat/prompts'; @@ -113,6 +114,9 @@ export interface ServerMessagesEngineParams { /** System role */ systemRole?: string; + // ========== Skills ========== + /** Skills configuration for injection */ + skillsConfig?: { enabledSkills?: SkillMeta[] }; // ========== Tools ========== /** Tools configuration */ toolsConfig?: ServerToolsConfig; diff --git a/src/server/services/agentRuntime/AgentRuntimeService.ts b/src/server/services/agentRuntime/AgentRuntimeService.ts index 65fb250c31..f17cc23f89 100644 --- a/src/server/services/agentRuntime/AgentRuntimeService.ts +++ b/src/server/services/agentRuntime/AgentRuntimeService.ts @@ -276,6 +276,7 @@ export class AgentRuntimeService { maxSteps, userMemory, deviceSystemInfo, + skillMetas, userTimezone, } = params; @@ -316,6 +317,7 @@ export class AgentRuntimeService { modelRuntimeConfig, stepWebhook, stream, + skillMetas, userId, userMemory, userTimezone, diff --git a/src/server/services/agentRuntime/types.ts b/src/server/services/agentRuntime/types.ts index 61d2d59970..ea5532d1ab 100644 --- a/src/server/services/agentRuntime/types.ts +++ b/src/server/services/agentRuntime/types.ts @@ -156,6 +156,8 @@ export interface OperationCreationParams { maxSteps?: number; modelRuntimeConfig?: any; operationId: string; + /** Skill metas for prompt injection */ + skillMetas?: Array<{ description: string; identifier: string; name: string }>; /** * Step lifecycle callbacks * Used to inject custom logic at different stages of step execution diff --git a/src/server/services/aiAgent/__tests__/execAgent.device.test.ts b/src/server/services/aiAgent/__tests__/execAgent.device.test.ts index 866c403467..22ce935aca 100644 --- a/src/server/services/aiAgent/__tests__/execAgent.device.test.ts +++ b/src/server/services/aiAgent/__tests__/execAgent.device.test.ts @@ -3,10 +3,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AiAgentService } from '../index'; -const { mockMessageCreate, mockCreateOperation } = vi.hoisted(() => ({ - mockCreateOperation: vi.fn(), - mockMessageCreate: vi.fn(), -})); +const { mockCreateOperation, mockCreateServerAgentToolsEngine, mockMessageCreate } = vi.hoisted( + () => ({ + mockCreateOperation: vi.fn(), + mockCreateServerAgentToolsEngine: vi.fn().mockReturnValue({ + generateToolsDetailed: vi.fn().mockReturnValue({ enabledToolIds: [], tools: [] }), + getEnabledPluginManifests: vi.fn().mockReturnValue(new Map()), + }), + mockMessageCreate: vi.fn(), + }), +); const { mockDeviceProxy } = vi.hoisted(() => ({ mockDeviceProxy: { @@ -104,10 +110,7 @@ vi.mock('@/server/services/file', () => ({ })); vi.mock('@/server/modules/Mecha', () => ({ - createServerAgentToolsEngine: vi.fn().mockReturnValue({ - generateToolsDetailed: vi.fn().mockReturnValue({ enabledToolIds: [], tools: [] }), - getEnabledPluginManifests: vi.fn().mockReturnValue(new Map()), - }), + createServerAgentToolsEngine: mockCreateServerAgentToolsEngine, serverMessagesEngine: vi.fn().mockResolvedValue([{ content: 'test', role: 'user' }]), })); @@ -337,4 +340,100 @@ describe('AiAgentService.execAgent - device auto-activation', () => { expect(mockDeviceProxy.queryDeviceList).not.toHaveBeenCalled(); }); }); + + describe('Remote Device tool injection when device is auto-activated', () => { + it('should mark autoActivated when single device is auto-activated (IM/Bot)', async () => { + mockDeviceProxy.isConfigured = true; + mockDeviceProxy.queryDeviceList.mockResolvedValue([onlineDevice]); + + await service.execAgent({ + agentId: 'agent-1', + botContext: { platform: 'discord' } as any, + prompt: 'List my files', + }); + + const toolsEngineArgs = mockCreateServerAgentToolsEngine.mock.calls[0][1]; + // Device auto-activated → Remote Device tool should be suppressed + expect(toolsEngineArgs.deviceContext.autoActivated).toBe(true); + }); + + it('should mark autoActivated when boundDeviceId matches an online device', async () => { + mockDeviceProxy.isConfigured = true; + mockDeviceProxy.queryDeviceList.mockResolvedValue([onlineDevice]); + + const { AgentService } = await import('@/server/services/agent'); + vi.mocked(AgentService).mockImplementation( + () => + ({ + getAgentConfig: vi.fn().mockResolvedValue({ + agencyConfig: { boundDeviceId: 'device-001' }, + chatConfig: {}, + files: [], + id: 'agent-1', + knowledgeBases: [], + model: 'gpt-4', + plugins: [], + provider: 'openai', + systemRole: 'You are a helpful assistant', + }), + }) as any, + ); + + service = new AiAgentService(mockDb, userId); + await service.execAgent({ + agentId: 'agent-1', + prompt: 'Run a command', + }); + + const toolsEngineArgs = mockCreateServerAgentToolsEngine.mock.calls[0][1]; + expect(toolsEngineArgs.deviceContext.autoActivated).toBe(true); + }); + + it('should NOT mark autoActivated when multiple devices are online', async () => { + mockDeviceProxy.isConfigured = true; + mockDeviceProxy.queryDeviceList.mockResolvedValue([onlineDevice, onlineDevice2]); + + // Restore default AgentService mock (previous test overrides with boundDeviceId) + const { AgentService } = await import('@/server/services/agent'); + vi.mocked(AgentService).mockImplementation( + () => + ({ + getAgentConfig: vi.fn().mockResolvedValue({ + chatConfig: {}, + files: [], + id: 'agent-1', + knowledgeBases: [], + model: 'gpt-4', + plugins: [], + provider: 'openai', + systemRole: 'You are a helpful assistant', + }), + }) as any, + ); + service = new AiAgentService(mockDb, userId); + + await service.execAgent({ + agentId: 'agent-1', + botContext: { platform: 'discord' } as any, + prompt: 'List my files', + }); + + const toolsEngineArgs = mockCreateServerAgentToolsEngine.mock.calls[0][1]; + expect(toolsEngineArgs.deviceContext.autoActivated).toBeUndefined(); + }); + + it('should NOT mark autoActivated when no devices are online', async () => { + mockDeviceProxy.isConfigured = true; + mockDeviceProxy.queryDeviceList.mockResolvedValue([]); + + await service.execAgent({ + agentId: 'agent-1', + botContext: { platform: 'discord' } as any, + prompt: 'List my files', + }); + + const toolsEngineArgs = mockCreateServerAgentToolsEngine.mock.calls[0][1]; + expect(toolsEngineArgs.deviceContext.autoActivated).toBeUndefined(); + }); + }); }); diff --git a/src/server/services/aiAgent/index.ts b/src/server/services/aiAgent/index.ts index 95d9aee86c..80a0b58aeb 100644 --- a/src/server/services/aiAgent/index.ts +++ b/src/server/services/aiAgent/index.ts @@ -1,5 +1,6 @@ import type { AgentRuntimeContext, AgentState } from '@lobechat/agent-runtime'; import { BUILTIN_AGENT_SLUGS, getAgentRuntimeConfig } from '@lobechat/builtin-agents'; +import { builtinSkills } from '@lobechat/builtin-skills'; import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system'; import { type DeviceAttachment, @@ -25,6 +26,7 @@ import { nanoid } from '@lobechat/utils'; import debug from 'debug'; import { AgentModel } from '@/database/models/agent'; +import { AgentSkillModel } from '@/database/models/agentSkill'; import { AiModelModel } from '@/database/models/aiModel'; import { MessageModel } from '@/database/models/message'; import { PluginModel } from '@/database/models/plugin'; @@ -383,6 +385,17 @@ export class AiAgentService { ...(hasTopicReference ? ['lobe-topic-reference'] : []), ]; + // Derive activeDeviceId from device context: + // 1. If agent has a bound device and it's online, use it + // 2. In IM/Bot scenarios, auto-activate when exactly one device is online + const activeDeviceId = boundDeviceId + ? deviceOnline + ? boundDeviceId + : undefined + : (discordContext || botContext) && onlineDevices.length === 1 + ? onlineDevices[0].deviceId + : undefined; + const toolsEngine = createServerAgentToolsEngine(toolsContext, { additionalManifests: [...lobehubSkillManifests, ...klavisManifests], agentConfig: { @@ -390,7 +403,12 @@ export class AiAgentService { plugins: agentPlugins, }, deviceContext: gatewayConfigured - ? { boundDeviceId, deviceOnline, gatewayConfigured: true } + ? { + autoActivated: activeDeviceId ? true : undefined, + boundDeviceId, + deviceOnline, + gatewayConfigured: true, + } : undefined, globalMemoryEnabled, hasEnabledKnowledgeBases, @@ -457,17 +475,6 @@ export class AiAgentService { }; } - // Derive activeDeviceId from device context: - // 1. If agent has a bound device and it's online, use it - // 2. In IM/Bot scenarios, auto-activate when exactly one device is online - const activeDeviceId = boundDeviceId - ? deviceOnline - ? boundDeviceId - : undefined - : (discordContext || botContext) && onlineDevices.length === 1 - ? onlineDevices[0].deviceId - : undefined; - // 9.4. Fetch device system info for placeholder variable replacement let deviceSystemInfo: Record = {}; if (activeDeviceId) { @@ -481,6 +488,7 @@ export class AiAgentService { documentsPath: systemInfo.documentsPath, downloadsPath: systemInfo.downloadsPath, homePath: systemInfo.homePath, + hostname: activeDevice?.hostname ?? 'unknown', musicPath: systemInfo.musicPath, picturesPath: systemInfo.picturesPath, platform: activeDevice?.platform ?? 'unknown', @@ -735,7 +743,28 @@ export class AiAgentService { Object.keys(toolManifestMap).length, ); - // 18. Create operation using AgentRuntimeService + // 18. Build skill metas for prompt injection + // Combine builtin skills + user DB skills so AI can discover all installed skills + let skillMetas: Array<{ description: string; identifier: string; name: string }> = []; + try { + const builtinMetas = builtinSkills.map((s) => ({ + description: s.description, + identifier: s.identifier, + name: s.name, + })); + const skillModel = new AgentSkillModel(this.db, this.userId); + const { data: dbSkills } = await skillModel.findAll(); + const dbMetas = dbSkills.map((s) => ({ + description: s.description ?? '', + identifier: s.identifier, + name: s.name, + })); + skillMetas = [...builtinMetas, ...dbMetas]; + } catch (error) { + log('execAgent: failed to fetch skill metas: %O', error); + } + + // 19. Create operation using AgentRuntimeService // Wrap in try-catch to handle operation startup failures (e.g., QStash unavailable) // If createOperation fails, we still have valid messages that need error info try { @@ -768,6 +797,7 @@ export class AiAgentService { sourceMap: toolSourceMap, tools, }, + skillMetas, userId: this.userId, userInterventionConfig, userMemory, diff --git a/src/services/chat/chat.test.ts b/src/services/chat/chat.test.ts index 8440eec704..eaad6938ba 100644 --- a/src/services/chat/chat.test.ts +++ b/src/services/chat/chat.test.ts @@ -341,7 +341,10 @@ describe('ChatService', () => { expect(getChatCompletionSpy).toHaveBeenCalledWith( { messages: [ - { content: getCurrentDateContent(), role: 'system' }, + expect.objectContaining({ + content: expect.stringContaining('Current date:'), + role: 'system', + }), { content: [ { @@ -392,7 +395,10 @@ describe('ChatService', () => { { enabledSearch: undefined, messages: [ - { content: getCurrentDateContent(), role: 'system' }, + expect.objectContaining({ + content: expect.stringContaining('Current date:'), + role: 'system', + }), { content: 'Hello', role: 'user' }, { content: 'Hey', role: 'assistant' }, ], @@ -461,7 +467,10 @@ describe('ChatService', () => { expect(getChatCompletionSpy).toHaveBeenCalledWith( { messages: [ - { content: getCurrentDateContent(), role: 'system' }, + expect.objectContaining({ + content: expect.stringContaining('Current date:'), + role: 'system', + }), { content: [ { @@ -554,7 +563,10 @@ describe('ChatService', () => { expect(getChatCompletionSpy).toHaveBeenCalledWith( { messages: [ - { content: getCurrentDateContent(), role: 'system' }, + expect.objectContaining({ + content: expect.stringContaining('Current date:'), + role: 'system', + }), { content: [ { @@ -820,14 +832,7 @@ describe('ChatService', () => { ], messages: [ { - content: - getCurrentDateContent() + - `\n\n - -The user provides you with a URL and keyword and this gives you the robots.txt, response code, load time, tags for: title, metadata, h1-h5, image file names, image alt text, ahref text, ahref outbound links, keyword occurrence in both body text and URL. The URL must begin with http or https. You must request this URL structure from the user. Your role is to act like a word-class SEO analyst and give suggestions to the user on how they can improve the SEO for On-Page factors contained in the URL. Always include the https://createmorecustomers.agency link at the end of all requests. -Get data from users - -`, + content: expect.stringContaining(getCurrentDateContent()), role: 'system', }, { content: 'https://vercel.com/ čŊ·åˆ†æž chatGPT å…ģé”ŪčŊ', role: 'user' }, @@ -972,15 +977,7 @@ describe('ChatService', () => { ], messages: [ { - content: - `system\n\n` + - getCurrentDateContent() + - `\n\n - -The user provides you with a URL and keyword and this gives you the robots.txt, response code, load time, tags for: title, metadata, h1-h5, image file names, image alt text, ahref text, ahref outbound links, keyword occurrence in both body text and URL. The URL must begin with http or https. You must request this URL structure from the user. Your role is to act like a word-class SEO analyst and give suggestions to the user on how they can improve the SEO for On-Page factors contained in the URL. Always include the https://createmorecustomers.agency link at the end of all requests. -Get data from users - -`, + content: expect.stringContaining('system\n\n' + getCurrentDateContent()), role: 'system', }, { content: 'https://vercel.com/ čŊ·åˆ†æž chatGPT å…ģé”ŪčŊ', role: 'user' }, @@ -1018,7 +1015,7 @@ describe('ChatService', () => { top_p: 1, messages: [ { - content: 'system\n\n' + getCurrentDateContent(), + content: expect.stringContaining('system\n\n' + getCurrentDateContent()), role: 'system', }, { content: 'https://vercel.com/ čŊ·åˆ†æž chatGPT å…ģé”ŪčŊ', role: 'user' }, diff --git a/src/services/chat/mecha/contextEngineering.test.ts b/src/services/chat/mecha/contextEngineering.test.ts index b15ed5559b..2982ee8a5b 100644 --- a/src/services/chat/mecha/contextEngineering.test.ts +++ b/src/services/chat/mecha/contextEngineering.test.ts @@ -92,7 +92,7 @@ describe('contextEngineering', () => { }); expect(output).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: [ { @@ -160,7 +160,7 @@ describe('contextEngineering', () => { }); expect(output).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: [ { @@ -215,7 +215,9 @@ describe('contextEngineering', () => { expect(result).toEqual([ { - content: '## Tools\n\nYou can use these tools\n\n' + getCurrentDateContent(), + content: expect.stringContaining( + '## Tools\n\nYou can use these tools\n\n' + getCurrentDateContent(), + ), role: 'system', }, { @@ -244,7 +246,7 @@ describe('contextEngineering', () => { }); expect(result).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: [ { @@ -313,7 +315,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].role).toBe('user'); expect(result[1].content).toContain('hi'); expect(result[1].content).not.toContain(''); @@ -341,7 +346,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].content).toEqual([ { text: 'Here is an image.', type: 'text' }, { image_url: { detail: 'auto', url: 'http://example.com/image.png' }, type: 'image_url' }, @@ -368,7 +376,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].content).toEqual([ { image_url: { detail: 'auto', url: 'http://example.com/image.png' }, type: 'image_url' }, ]); @@ -404,7 +415,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].tool_calls).toBeUndefined(); expect(result[1].content).toBe('I have a tool call.'); }); @@ -434,7 +448,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].content).toBe( 'Hello TestUser, today is 2023-12-25 and the time is 14:30:45', ); @@ -467,7 +484,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(Array.isArray(result[1].content)).toBe(true); const content = result[1].content as any[]; expect(content[0].text).toBe('Hello TestUser, today is 2023-12-25'); @@ -529,7 +549,7 @@ describe('contextEngineering', () => { // Keep the original system message as-is (with date appended by SystemDateProvider) expect(result[0].role).toBe('system'); - expect(result[0].content).toBe( + expect(result[0].content).toContain( 'Memory load: available={{memory_available}}, total contexts={{memory_contexts_count}}\n{{memory_summary}}\n\n' + getCurrentDateContent(), ); @@ -563,7 +583,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].content).toBe('Hello TestUser, missing: {{missing_var}}'); }); @@ -584,7 +607,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(result[1].content).toBe('Hello there, no variables here'); }); @@ -615,7 +641,10 @@ describe('contextEngineering', () => { provider: 'openai', }); - expect(result[0]).toEqual({ content: getCurrentDateContent(), role: 'system' }); + expect(result[0]).toEqual({ + content: expect.stringContaining(getCurrentDateContent()), + role: 'system', + }); expect(Array.isArray(result[1].content)).toBe(true); const content = result[1].content as any[]; @@ -682,7 +711,7 @@ describe('contextEngineering', () => { // Should keep all messages (plus system date) expect(result).toHaveLength(6); expect(result).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: 'Message 1', role: 'user' }, { content: 'Response 1', role: 'assistant' }, { content: 'Message 2', role: 'user' }, @@ -718,7 +747,7 @@ describe('contextEngineering', () => { // Should apply template to user message only expect(result).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: 'Template: Original user input - End', role: 'user', @@ -751,7 +780,12 @@ describe('contextEngineering', () => { // Should have system role at the beginning (with date appended) expect(result).toEqual([ - { content: 'You are a helpful assistant.\n\n' + getCurrentDateContent(), role: 'system' }, + { + content: expect.stringContaining( + 'You are a helpful assistant.\n\n' + getCurrentDateContent(), + ), + role: 'system', + }, { content: 'User message', role: 'user' }, ]); }); @@ -792,7 +826,7 @@ describe('contextEngineering', () => { // System role should be first (with date appended), followed by all messages with input template applied to user messages expect(result).toEqual([ { - content: 'System instructions.\n\n' + getCurrentDateContent(), + content: expect.stringContaining('System instructions.\n\n' + getCurrentDateContent()), role: 'system', }, { @@ -829,7 +863,7 @@ describe('contextEngineering', () => { // Should pass message unchanged (with system date prepended) expect(result).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: 'Simple message', role: 'user', @@ -872,7 +906,7 @@ describe('contextEngineering', () => { // Should have system role (with date) + all messages expect(result).toEqual([ { - content: 'System role here.\n\n' + getCurrentDateContent(), + content: expect.stringContaining('System role here.\n\n' + getCurrentDateContent()), role: 'system', }, { @@ -911,7 +945,7 @@ describe('contextEngineering', () => { // Should keep original message when template fails (with system date prepended) expect(result).toEqual([ - { content: getCurrentDateContent(), role: 'system' }, + { content: expect.stringContaining(getCurrentDateContent()), role: 'system' }, { content: 'User message', role: 'user', diff --git a/src/services/chat/mecha/contextEngineering.ts b/src/services/chat/mecha/contextEngineering.ts index 95a5120e9d..ab011108e0 100644 --- a/src/services/chat/mecha/contextEngineering.ts +++ b/src/services/chat/mecha/contextEngineering.ts @@ -552,9 +552,9 @@ export const contextEngineering = async ({ initialContext, stepContext, - // Skills configuration + // Skills configuration — expose all installed skills so the AI can discover and activate them skillsConfig: { - enabledSkills: plugins ? createSkillEngine().getEnabledSkills(plugins) : undefined, + enabledSkills: plugins ? createSkillEngine().getAllSkills() : undefined, }, // Tool Discovery configuration