diff --git a/packages/@n8n/agents/src/skills/__tests__/runtime-skills.test.ts b/packages/@n8n/agents/src/skills/__tests__/runtime-skills.test.ts index 7885caf2499..8f208f0b259 100644 --- a/packages/@n8n/agents/src/skills/__tests__/runtime-skills.test.ts +++ b/packages/@n8n/agents/src/skills/__tests__/runtime-skills.test.ts @@ -277,6 +277,7 @@ Use the workflow SDK.`, expect(prompt).toContain('Skill loading protocol:'); expect(prompt).toContain('name: "Summarize notes"'); expect(prompt).toContain('id: "summarize_notes"'); + expect(prompt).toContain('load_skill once with `{ "skillId": "" }`'); expect(prompt).not.toContain('Extract private decisions.'); }); diff --git a/packages/@n8n/agents/src/skills/prompt.ts b/packages/@n8n/agents/src/skills/prompt.ts index 7d12b6e3188..d23aa91b932 100644 --- a/packages/@n8n/agents/src/skills/prompt.ts +++ b/packages/@n8n/agents/src/skills/prompt.ts @@ -35,8 +35,8 @@ ${catalog} When deciding whether to load a skill: - Match the user's request against the skill name and description. - Call list_skills when you need to inspect available categories or installed skill metadata. -- If one skill clearly matches, call load_skill once with that skill's id, then follow the returned instructions. -- If a loaded skill references a supporting file, call load_skill with that skill id and relative filePath. +- If one skill clearly matches, call load_skill once with \`{ "skillId": "" }\`, then follow the returned instructions. +- If a loaded skill references a supporting file, call load_skill with \`{ "skillId": "", "filePath": "" }\`. - If the relevant skill was already loaded for this request, do not call load_skill again. - If no skill clearly matches, do not call load_skill. - Do not load a skill just because it is listed here.`; diff --git a/packages/cli/src/modules/agents/__tests__/agents-builder-prompts.test.ts b/packages/cli/src/modules/agents/__tests__/agents-builder-prompts.test.ts deleted file mode 100644 index 8f927708c03..00000000000 --- a/packages/cli/src/modules/agents/__tests__/agents-builder-prompts.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AgentJsonConfigSchema } from '@n8n/api-types'; - -import { - getSchemaReferenceSection, - MEMORY_PRESETS_SECTION, -} from '../builder/agents-builder-prompts'; - -const baseConfig = { - name: 'Test Agent', - model: 'anthropic/claude-sonnet-4-5', - instructions: 'Be helpful', -}; - -describe('agents builder prompt', () => { - it('only documents memory storage values accepted by the JSON config schema', () => { - expect( - AgentJsonConfigSchema.safeParse({ - ...baseConfig, - memory: { enabled: true, storage: 'n8n' }, - }).success, - ).toBe(true); - - for (const storage of ['sqlite', 'postgres']) { - expect( - AgentJsonConfigSchema.safeParse({ - ...baseConfig, - memory: { enabled: true, storage }, - }).success, - ).toBe(false); - expect(MEMORY_PRESETS_SECTION).not.toContain(`| ${storage}`); - } - expect(MEMORY_PRESETS_SECTION).not.toContain('connection.path'); - expect(MEMORY_PRESETS_SECTION).not.toContain('connection.credential'); - }); - - it('describes observation-log memory', () => { - expect(MEMORY_PRESETS_SECTION).toContain('observation log'); - expect(MEMORY_PRESETS_SECTION).toContain('renderTokenBudget'); - }); - - it('describes episodic memory credential selection', () => { - expect(MEMORY_PRESETS_SECTION).toContain('Episodic Memory'); - expect(MEMORY_PRESETS_SECTION).toContain('memory.episodicMemory'); - expect(MEMORY_PRESETS_SECTION).toContain('ask_credential'); - expect(MEMORY_PRESETS_SECTION).toContain('openAiApi'); - expect(MEMORY_PRESETS_SECTION).toContain('runtime handles memory extraction and indexing'); - expect(MEMORY_PRESETS_SECTION).toContain('Use recalled prior context'); - expect(getSchemaReferenceSection()).toContain('episodicMemory'); - }); -}); diff --git a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-model-recommendations.test.ts b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-model-recommendations.test.ts index eb744c9be44..998b0b76864 100644 --- a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-model-recommendations.test.ts +++ b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-model-recommendations.test.ts @@ -2,6 +2,7 @@ import type { ProviderCatalog } from '@n8n/agents'; import { buildBuilderPrompt } from '../agents-builder-prompts'; import { buildModelRecommendationsSection } from '../agents-builder-model-recommendations'; +import { getBuilderRuntimeSkills } from '../skills'; const catalog: ProviderCatalog = { anthropic: { @@ -111,9 +112,17 @@ describe('builder model recommendations', () => { it('injects the recommendation section only when catalog recommendations are available', () => { const section = buildModelRecommendationsSection(catalog); + const llmSkillWithRecommendations = getBuilderRuntimeSkills({ + modelRecommendationsSection: section, + }).find((skill) => skill.id === 'agent-builder-llm-selection'); + const llmSkillWithoutRecommendations = getBuilderRuntimeSkills({ + modelRecommendationsSection: null, + }).find((skill) => skill.id === 'agent-builder-llm-selection'); - expect(buildPrompt(section)).toContain('## Recommended LLM models'); + expect(llmSkillWithRecommendations?.instructions).toContain('## Recommended LLM models'); + expect(llmSkillWithoutRecommendations?.instructions).not.toContain('## Recommended LLM models'); + expect(llmSkillWithoutRecommendations?.instructions).toContain('do not recommend or name'); + expect(buildPrompt(section)).not.toContain('## Recommended LLM models'); expect(buildPrompt(null)).not.toContain('## Recommended LLM models'); - expect(buildPrompt(null)).toContain('do not recommend or name'); }); }); diff --git a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts index 608e240d2ba..efa66dd422e 100644 --- a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts +++ b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts @@ -1,8 +1,12 @@ -import { INTEGRATIONS_SECTION } from '../agents-builder-prompts'; +import { getBuilderRuntimeSkills } from '../skills'; describe('agents builder integrations prompt', () => { it('does not tell the builder to prefer Slack OAuth credentials for chat integrations', () => { - expect(INTEGRATIONS_SECTION).not.toContain('slackOAuth2Api'); - expect(INTEGRATIONS_SECTION).not.toContain('prefer the OAuth variant'); + const integrationsSkill = getBuilderRuntimeSkills({ modelRecommendationsSection: null }).find( + (skill) => skill.id === 'agent-builder-integrations', + ); + + expect(integrationsSkill?.instructions).not.toContain('slackOAuth2Api'); + expect(integrationsSkill?.instructions).not.toContain('prefer the OAuth variant'); }); }); diff --git a/packages/cli/src/modules/agents/builder/agents-builder-prompts.ts b/packages/cli/src/modules/agents/builder/agents-builder-prompts.ts index 8de2e24d9ab..ca81b2bba7c 100644 --- a/packages/cli/src/modules/agents/builder/agents-builder-prompts.ts +++ b/packages/cli/src/modules/agents/builder/agents-builder-prompts.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { RunnableAgentJsonConfigSchema } from '@n8n/api-types'; + import { jsonSchemaToCompactText } from '../json-config/schema-text-serializer'; const BuilderPromptMemoryConfigSchema = z.object({ @@ -36,10 +37,6 @@ const BuilderPromptAgentJsonConfigSchema = RunnableAgentJsonConfigSchema.extend( memory: BuilderPromptMemoryConfigSchema.optional(), }); -// --------------------------------------------------------------------------- -// Context sections — dynamic, injected at runtime -// --------------------------------------------------------------------------- - export function getAgentStateSection( configJson: string, configHash: string | null, @@ -66,579 +63,96 @@ Treat this config as a starting snapshot only. Before any \`write_config\` or ${toolList}`; } -// --------------------------------------------------------------------------- -// Reference sections — static -// --------------------------------------------------------------------------- +export const TARGET_AGENT_SECTION = `\ +## Builder vs target agent -export const TOOL_TYPES_SECTION = `\ -## Tool types - -### Workflow tools (preferred) -Reference existing n8n workflows by name. Call list_workflows to see available ones. -\`\`\`json -{ "type": "workflow", "workflow": "Send Welcome Email" } -\`\`\` - -### Node tools -Run a single n8n node as a tool. Use search_nodes to find available nodes, then -get_node_types to see their parameters. Add the node to the config with nodeType, -nodeTypeVersion, and nodeParameters. - -get_node_types return typescript references, but you must supply json fields in node config - -Flow: search_nodes → get_node_types → ask_credential (per slot) → write/update config - -\`\`\`json -{ - "type": "node", - "name": "http_request", - "description": "Make an HTTP request to any URL", - "node": { - "nodeType": "n8n-nodes-base.httpRequestTool", - "nodeTypeVersion": 4, - "nodeParameters": { - "method": "GET", - "url": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('url', 'The URL to request', 'string') }}" - } - } -} -\`\`\` - -Rules for node tools: -- \`nodeType\` and \`nodeTypeVersion\` come from get_node_types results. Use the tool node ID from search_nodes (usually ending in \`Tool\`, e.g. \`n8n-nodes-base.httpRequestTool\`), not the base node ID. -- \`nodeParameters\` sets fixed parameters (resource, operation, etc.). For any value the AI should choose at runtime, use \`$fromAI\`: \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('key', 'description', 'type') }}\`. -- Match the \`$fromAI\` type to the node parameter type from get_node_types: use \`string\`, \`number\`, \`boolean\`, or \`json\`. -- Do NOT pipe AI-chosen node-tool fields through \`$json\`; use \`$fromAI\` for those fields instead. -- Do NOT include \`inputSchema\` for node tools. It is derived automatically from the \`$fromAI\` expressions in \`nodeParameters\`. -- Do NOT include \`toolDescription\` in \`nodeParameters\`. Use the top-level tool \`description\` only. -- For resource locator parameters (objects with \`"__rl": true\`), keep the locator shape and put the \`$fromAI\` expression in its \`value\` field. -- For every credential slot the node requires, you MUST first call ask_credential. If it returns { credentialId, credentialName }, use the returned values in \`credentials[slotName]\`. Never copy ids from list_credentials directly; never invent ids; never write empty credential values. -- Call ask_credential ONCE per slot, before the write_config / patch_config that introduces the node tool. If it returns { skipped: true }, DO NOT abort and DO NOT refuse to add the tool. Continue adding the node tool, omit that credential slot entirely, and tell the user they can configure the credential later. -- Use search_nodes first, never guess node type names - -### Custom tools -Write TypeScript using the Tool builder, validate via build_custom_tool, then register the returned id. -\`\`\`json -{ "type": "custom", "id": "tool_7fGh2Lm9Qx0Ba8Ts" } -\`\`\` - -The tool code must follow this pattern: -\`\`\`typescript -import { Tool } from '@n8n/agents'; -import { z } from 'zod'; - -export default new Tool('tool_name') - .description('What the tool does') - .input(z.object({ query: z.string() })) - .handler(async ({ query }) => { - return { result: query.toUpperCase() }; - }); -\`\`\` - -Custom tools run inside a V8 isolate sandbox. Treat every handler as a pure -function: take \`input\`, compute, return a JSON-serialisable value. - -- Must use \`export default new Tool(...)\` pattern. -- Imports at the top of the file: only '@n8n/agents' and 'zod'. No other - modules resolve. -- No I/O of any kind — no network, no filesystem, no waiting for wall-clock - time. Host globals like \`crypto\`, \`process\`, \`Buffer\`, \`fetch\`, \`atob\`, - \`XMLHttpRequest\` are not present and will throw \`ReferenceError\` at runtime. -- Some web APIs appear defined but are no-op stubs (\`setTimeout\` fires - synchronously, \`console.log\` goes nowhere, \`TextEncoder.encode\` returns - its input unchanged). Don't rely on their real behaviour. -- Free to use: \`Math\`, \`Date\`, \`JSON\`, \`RegExp\`, \`Array\`, \`Object\`, \`Map\`, - \`Set\`, \`Promise\`, typed arrays, and any method on values you already have. -- The handler is async and receives \`(input, ctx)\`. - - \`input\` is already validated against your zod schema. - - \`ctx.suspend(payload)\` pauses the tool until the caller resumes it — - use it for human-in-the-loop flows that need to ask the user something. - Otherwise ignore \`ctx\`. -- Return a JSON-serialisable value. Execution is capped at 5 seconds and - ~32 MB of memory. -- If something fails at runtime, the error message is handed back to you on - the next turn — fix the code and try again. -- Do NOT call \`.build()\` — the engine handles it. - -### Skills -Use skills for reusable instructions, playbooks, style guides, policies, or -domain knowledge the agent should follow. Call create_skill with the skill -\`name\`, \`description\`, and \`body\`; the tool returns the generated skill -\`id\`. Skill descriptions should describe the task/situation that should -trigger loading the skill. create_skill stores the skill body only; it does not -attach the skill to the agent config. After create_skill, call read_config and -use patch_config (or write_config) to add -\`{ "type": "skill", "id": "" }\` to \`skills\`.`; - -export const INTERACTIVE_TOOLS_SECTION = `\ -## Interactive tools (user-facing) - -These tools render a UI card in the chat and SUSPEND your run until the user -responds. Treat the resume value as authoritative — it is the user's choice and -must be persisted into the config exactly as returned. - -### ask_llm -When: the user must choose a model/credential because the request is ambiguous, -resolve_llm returned an ambiguous/missing credential result, or the user asks -to pick/change/use a different model. Call AT MOST ONCE per build turn unless -the user changes their mind. -Never ask the user in plain text to choose, confirm, configure, or change the -agent main LLM, provider, model, or main LLM credential. If the user needs to -make that choice, call ask_llm so the picker card is shown. -Returns: { provider, model, credentialId, credentialName }. -After: set \`model = "{provider}/{model}"\` and \`credential = credentialId\` -via write_config or patch_config. - -### ask_credential -When: about to add (or change) a node tool whose node requires credentials, or -when another section explicitly tells you to select a credential before -writing config. -Call ONCE per slot, BEFORE write_config / patch_config that introduces the -tool. Pass \`credentialType\` (a single credential type name picked from the -slot's accepted types in get_node_types — when the slot accepts multiple, -choose the most appropriate one, typically OAuth or the first listed) and -\`purpose\` (one short sentence, e.g. "Slack credential for posting messages"). -For Episodic Memory, pass exactly \`credentialType: "openAiApi"\`. -Returns: { credentialId, credentialName } or { skipped: true }. -After (success): for node tools, set \`tools[i].node.credentials. = { -id: credentialId, name: credentialName }\`. For section-specific credential -flows, follow that section's success instructions. After (skipped): for node -tools, DO NOT abort and DO NOT refuse to add the node tool. Still add the node -tool, omit that credential slot, and tell the user they can configure the -credential later. For section-specific credential flows, follow that section's -skipped instructions. - -### ask_question -When: you would otherwise ask a clarifying question whose answer is one (or -more) of a known list. Examples: pick a Slack channel from a list, -read-only vs read-write, which workflow to wrap. -Inputs: \`question\`, \`options[{label,value,description?}]\`, \`allowMultiple?\`. -Returns: { values: string[] }. Values are selected option values unless the -user types into the card's Other field, in which case the freeform text appears -in \`values\`. - -### Rules -- Never call two interactive tools in parallel. The run suspends on the first. -- Never re-ask a question the user already answered in this thread. -- After resume, continue with the next concrete action (write_config / - patch_config / next ask_*). Do not narrate the answer back to the user. -- list_credentials remains available but is for read-only inspection only. - Never copy ids from it into the config.`; - -export const LLM_RESOLUTION_SECTION = `\ -## LLM model and credential resolution - -Use resolve_llm before ask_llm whenever the user's request contains enough -information to resolve the main LLM without a picker. - -### resolve_llm -When: the user explicitly names a provider/model, or a fresh agent needs its -main LLM set and the user did not ask to choose. - -Inputs: optional \`provider\`, optional \`model\`. -- If the user says "Anthropic via OpenRouter", pass - \`provider: "openrouter"\` and omit \`model\` unless they named a concrete - OpenRouter model id. -- If the user names a concrete model, pass \`model\` without the selected - provider prefix. For OpenRouter, use the routed model id, e.g. - \`"anthropic/claude-sonnet-4.6"\`. - -On \`{ ok: true, provider, model, credentialId, credentialName }\`: set -\`model = "{provider}/{model}"\` and \`credential = credentialId\`. The -returned \`model\` is the canonical id resolved against the provider's live -list, so use it as-is — do not transform or "correct" it. - -On \`ok: false\`: your NEXT action is another tool call — never reply with -plain text asking the user to clarify. Do not guess credential names from -list_credentials. Pick the action by reason: -- \`missing_credential\` / \`ambiguous_credential\` / \`ambiguous_provider_or_credential\` → - call ask_llm (the picker handles credential selection). -- \`unknown_model\` → the response includes \`availableModels: [{ name, value }]\` - (or a narrowed candidate list when the user's hint matched several). If - one entry plausibly matches what the user named, re-call resolve_llm - with \`model\` set to that exact \`value\`. Otherwise call ask_llm. -- \`model_lookup_failed\` (the live list could not be fetched, e.g. invalid - credentials) → call ask_llm. -- \`unsupported_provider\` → call ask_llm. Do not list the supported - providers back to the user; the picker UI handles that. - -Rules: -- Explicit provider/model request → resolve_llm first, not ask_llm. -- User does not know which model to use and the Recommended LLM models section - is present → choose from that section, then pass that provider/model to - resolve_llm. Prefer a provider the user already has credentials for. -- If the Recommended LLM models section is absent, do not recommend or name - current, best, latest, or fallback model IDs from memory. Call ask_llm when - the user needs model guidance or choice. -- User asks to pick/change/use a different model → ask_llm. -- User needs to choose/confirm/configure a model or main LLM credential → - ask_llm, never a plain-text question. -- No provider specified and resolve_llm reports ambiguity → ask_llm.`; - -export const N8N_EXPRESSIONS_SECTION = `\ -## n8n expressions - -Node tool parameters inside \`nodeParameters\` can use n8n expressions. -For node tools, prefer \`$fromAI\` whenever the agent should decide a value at runtime. - -- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('fieldName', 'What value to provide', 'string') }}\` — let the AI provide a string -- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('count', 'How many items', 'number') }}\` — let the AI provide a number -- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('enabled', 'Whether to enable this option', 'boolean') }}\` — let the AI provide a boolean -- \`={{ $now.toISO() }}\` — current date/time (Luxon DateTime) -- \`={{ $today }}\` — start of today (Luxon DateTime) - -Always wrap expressions in \`={{ }}\`. Never use bare JS variables outside the braces.`; - -export const PROVIDER_TOOLS_SECTION = `\ -## Provider tools - -Built-in capabilities offered by the model provider. Pick the entry that -matches the agent's configured \`model\` provider — Anthropic tools work with -\`anthropic/*\` models, OpenAI tools work with \`openai/*\` models. - -Anthropic web search: -\`\`\`json -{ "providerTools": { "anthropic.web_search": { "maxUses": 5 } } } -\`\`\` - -OpenAI web search (requires a Responses-API-compatible model, e.g. \`openai/gpt-4o\`): -\`\`\`json -{ "providerTools": { "openai.web_search": { "searchContextSize": "medium" } } } -\`\`\` - -OpenAI image generation: -\`\`\`json -{ "providerTools": { "openai.image_generation": {} } } -\`\`\``; +You are the builder agent, not the target agent. +The target agent is the AI agent you are configuring for the user. Changes to +config, tools, memory, integrations, and target-agent skills affect the target +agent, not your own builder behavior.`; export function getConversationModeSection(agentPreviewPath: string): string { return `\ ## When to build vs when to converse -Not every user message is a build request. Before calling \`write_config\`, -\`patch_config\`, or \`build_custom_tool\`, check: has the user given you a -concrete goal the agent should accomplish? +Not every user message is a build request. Before changing config or creating +tools, check whether the user gave a concrete goal for the target agent. -If the user just said "hi", asked what you do, gave a vague intent ("build me -something cool"), or asked a question — reply conversationally. Ask what they -want the agent to do, what systems it needs to touch, what triggers it. Only -start building once you have a real goal. +If the user just says hi, asks what you do, gives a vague intent, or asks a +question, reply conversationally and ask for the missing goal/systems/triggers. If the user tries to test, run, chat with, or interact with the newly built agent in this Build chat, do not call tools. Reply exactly: "Head to the [Preview](${agentPreviewPath}) section to chat with your agent." -Do not say anything else. Keep the Preview link as a relative app path; do not -expand it to an absolute URL. +Do not say anything else. Keep the Preview link as a relative app path. -Never call \`write_config\` with empty, placeholder, or guessed \`instructions\`. -An agent without real instructions is broken and can't chat. If you don't have +Never write empty, placeholder, or guessed \`instructions\`. If you do not have enough detail to write meaningful instructions, ask the user first.`; } -export const RESEARCH_SECTION = `\ -## Research +export const BUILDER_SKILL_ROUTING_SECTION = `\ +## Builder runtime skills -You have access to Anthropic's web search tool. Use it when you encounter an -API, service, product, or concept you don't fully understand. Better to search -once and be correct than to guess at endpoint shapes, auth methods, or node -parameters. +Detailed builder guidance is available through runtime skills. Before +specialized work, call \`load_skill\` with \`{ "skillId": "" }\` and follow +the returned instructions. -Good reasons to search: -- The user named an API or service you're unsure about -- You're unsure of an endpoint's URL shape, auth method, or request format -- The user referenced a recent or external product, standard, or spec +- \`agent-builder-config-mutation\`: reading/writing JSON config, schema, patch paths, stale retries. +- \`agent-builder-llm-selection\`: resolving or asking for the target agent's main LLM. +- \`agent-builder-tools\`: workflow, node, custom, provider tools, and expressions. +- \`agent-builder-memory\`: n8n session memory, observation log, Episodic Memory. +- \`agent-builder-integrations\`: schedule and chat integrations. +- \`agent-builder-target-skills\`: creating skills for the target agent. +- \`agent-builder-research\`: when to use web search for external APIs/services. -Don't search for things you already know (n8n internals, common JS/TS -patterns, widely-known public APIs you've configured many times).`; +Do not use \`create_skill\` for your own builder guidance. \`create_skill\` +creates a skill for the target agent only.`; -export const MEMORY_PRESETS_SECTION = `\ -## Memory +export const READ_CONFIG_FRESHNESS_SECTION = `\ +## Config freshness -Use n8n session-scoped memory only. It keeps recent conversation context and -an observation log for the current chat session. The agent reads the rendered -observation log directly as private context. +\`read_config\` is mandatory before every \`write_config\` or \`patch_config\`. +Use only the returned \`config\` and \`configHash\` as the write base. Do not +patch from memory, conversation state, or the prompt snapshot. -Shape: -\`\`\`json -{ "enabled": true, "storage": "n8n", "lastMessages": 50, "observationalMemory": { "renderTokenBudget": 4500 } } -\`\`\` - -Rules: -- Set \`storage\` to "n8n". -- \`lastMessages\` default: 50. - -### Observation-log memory - -- Observation-log memory is enabled by default when memory is enabled. -- Keep \`observationalMemory\` optional; use it only for explicit memory tuning. -- Supported observation-log tuning fields: \`enabled\`, \`observerThresholdTokens\`, \`reflectorThresholdTokens\`, \`renderTokenBudget\`, \`observationLogTailLimit\`, and \`lockTtlMs\`. - -### Episodic Memory - -Episodic Memory stores source-backed memories from previous conversations and -exposes them through \`recall_memory\`. Use it only when the user wants -long-term memory across conversations. - -- Enable \`memory.episodicMemory\` only when the user asks for Episodic Memory, long-term memory, prior conversations, remembered decisions, exact artifacts, or cross-session memory. -- Before enabling Episodic Memory, call \`ask_credential({ credentialType: "openAiApi", purpose: "OpenAI credential for Episodic Memory embeddings" })\`. -- On success, set \`memory.episodicMemory = { "enabled": true, "credential": "" }\`. Preserve existing \`topK\` and \`maxEntriesPerRun\` values if they are already configured. -- If credential selection is skipped, do not enable \`memory.episodicMemory\`; explain that Episodic Memory needs an OpenAI credential for embeddings. -- Do not add agent instructions that say the agent should remember, store, save, or decide what context is important from previous interactions. The runtime handles memory extraction and indexing. -- If agent instructions mention Episodic Memory, phrase it as retrieval/use only, e.g. "Use recalled prior context when relevant to the user's request." -- Do not invent Episodic Memory credential IDs, copy IDs from \`list_credentials\`, or reuse the main model credential unless it was returned by \`ask_credential\` for this purpose.`; - -export const INTEGRATIONS_SECTION = `\ -## Integrations (triggers) - -The \`integrations\` array on the agent config defines how the agent gets triggered. -Two kinds: - -1. **Schedule trigger** — runs the agent on a cron schedule. One per agent. - Shape: - \`\`\`json - { "type": "schedule", "active": false, "cronExpression": "0 9 * * *", "wakeUpPrompt": "Daily standup ping" } - \`\`\` - - \`active\` stays false until the agent is published. The schedule only fires once \`active: true\` AND the agent has a published version. - - \`cronExpression\` is standard 5-field cron. - - \`wakeUpPrompt\` is the message the agent receives when it fires. - -2. **Chat integrations** — connect the agent to a messaging platform. Multiple allowed. - Shape: - \`\`\`json - { "type": "slack", "credentialId": "" } - \`\`\` - -### Workflow for adding integrations - -1. Call \`list_integration_types\` to discover available platforms and their \`credentialTypes\`. -2. For chat integrations: pick **one** entry from the \`credentialTypes\` array returned by \`list_integration_types\` and pass it to \`ask_credential\` as the singular \`credentialType\` arg. It returns \`{ credentialId, credentialName }\`. -3. Use \`patch_config\` (or \`write_config\`) to add an entry to \`integrations\`. For chat integrations, only persist \`type\` and \`credentialId\`. For schedule, write the cron expression directly. - -Never invent credential IDs or names. Always go through \`ask_credential\`.`; - -export const WRITE_CONFIG_SECTION = `\ -## write_config — full replace - -Before calling write_config, call \`read_config\` and build the full replacement -from the returned \`config\`. Call write_config with the complete agent -configuration as a JSON string and the \`baseConfigHash\` from that same -\`read_config\` result: -\`\`\`json -{ - "baseConfigHash": "", - "json": "{ \\"name\\": \\"My Agent\\", \\"model\\": \\"{provider}/{model}\\", \\"credential\\": \\"\\", \\"instructions\\": \\"You are a helpful assistant.\\", \\"memory\\": { \\"enabled\\": true, \\"storage\\": \\"n8n\\", \\"lastMessages\\": 50 } }" -} -\`\`\` - -Do not use the prompt's config snapshot or your remembered state as the base -for write_config. The only retry exception is when write_config returns -\`stage: "stale"\`; in that case, use the returned \`config\` and \`configHash\` -to retry once. Do not retry from memory.`; - -export const PATCH_CONFIG_SECTION = `\ -## patch_config — RFC 6902 JSON Patch - -Before calling patch_config, call \`read_config\` and derive the patch from the -returned \`config\`. Send an array of RFC 6902 patch operations as a JSON string -plus the \`baseConfigHash\` from that same \`read_config\` result. Each operation -targets a field by its JSON Pointer path. - -| op | description | -|---------|------------------------------------------| -| add | Add or set a value at path | -| remove | Remove the value at path | -| replace | Replace the value at path | -| move | Move value from \`from\` path to \`path\` | -| copy | Copy value from \`from\` path to \`path\` | -| test | Assert a value at path (aborts if wrong) | - -Examples: -\`\`\`json -{ - "baseConfigHash": "", - "operations": "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/model\\", \\"value\\": \\"{provider}/{model}\\" }, { \\"op\\": \\"replace\\", \\"path\\": \\"/credential\\", \\"value\\": \\"\\" }]" -} -\`\`\` -\`\`\`json -{ - "baseConfigHash": "", - "operations": "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/memory/lastMessages\\", \\"value\\": 50 }, { \\"op\\": \\"add\\", \\"path\\": \\"/tools/-\\", \\"value\\": { \\"type\\": \\"workflow\\", \\"workflow\\": \\"Send Email\\" } }]" -} -\`\`\` -\`\`\`json -{ - "baseConfigHash": "", - "operations": "[{ \\"op\\": \\"remove\\", \\"path\\": \\"/description\\" }]" -} -\`\`\` - -Path syntax: \`/field\` for top-level fields, \`/nested/field\` for nested, \`/array/0\` for index, \`/array/-\` to append. - -When attaching a skill, append to \`/skills/-\` if \`skills\` exists; otherwise -add \`/skills\` with an array containing the skill ref. - -If patch_config returns \`stage: "stale"\`, use the returned \`config\` and -\`configHash\` to retry once. Do not retry from memory. - -On error, the response includes a \`stage\` field: "parse" (invalid JSON), "stale" (config changed), "patch" (operation failed), or "schema" (config fails validation).`; - -export const READ_CONFIG_SECTION = `\ -## read_config — mandatory freshness check - -Call \`read_config\` before every \`write_config\` or \`patch_config\` call. Call -it after any interactive tool returns and immediately before composing the -write or patch payload. - -Use the returned \`config\` as the only source of truth and pass the returned -\`configHash\` as \`baseConfigHash\`. Do not patch from memory, the conversation, -or the prompt snapshot. Do not skip this just because the prompt already -contains a \`configHash\`. - -If a write_config or patch_config call returns \`stage: "stale"\`, retry once -from the returned \`config\` and \`configHash\`. For any later independent config -change, call \`read_config\` again. - -\`create_skill\` stores a skill body but does not attach it. To make the agent -use the skill, call \`read_config\` after create_skill and then attach the -returned id through \`patch_config\` or \`write_config\`.`; - -export const WORKFLOW_SECTION = `\ -## Workflow - -1. If the agent has no \`instructions\` and \`credential\` yet (fresh agent), FIRST call resolve_llm - when the user specified a provider/model or left model choice to the builder. If - resolve_llm reports ambiguity, or the user asks to choose/change/use a - different model, call ask_llm. Then call read_config and write_config - with the chosen \`model\` and \`credential\` plus a draft \`instructions\`. - Never ask for the main LLM/model/credential in plain text; call ask_llm so - the picker card is shown. -2. Use ask_question whenever you have a clarifying question with discrete - options (e.g. "Which Slack channel?" → list channels, "Read-only or - read-write?"). Never put the question in plain text if options are known. -3. Before adding any node tool that needs credentials, call ask_credential for - each slot. -4. PREFER attaching existing workflows or nodes as tools over custom tools. -5. Use create_skill for reusable instruction bundles, then read_config and - patch_config to add the returned skill id to \`skills\`. -6. Before every write_config or patch_config, call read_config in the same turn - and use the returned configHash as baseConfigHash. -7. Use patch_config for targeted changes; write_config to replace the full config.`; - -export const FEW_SHOT_FLOWS_SECTION = `\ -## Example flows - -### New agent (no instructions yet), user says "Build me a Slack triage agent" -1. resolve_llm({}) - → { ok: true, provider: "anthropic", model: "claude-sonnet-4-5", - credentialId: "abc", credentialName: "My Anthropic" } -2. search_nodes({ query: "slack" }) → ... -3. get_node_types({ nodeType: "n8n-nodes-base.slackTool" }) → ... -4. ask_credential({ purpose: "Slack workspace to read/post messages", - nodeType: "n8n-nodes-base.slackTool", credentialType: "slackApi", - slot: "slackApi" }) - → { credentialId: "xyz", credentialName: "Acme Slack" } -5. read_config() → { configHash: "hash1", config: { ... } } -6. write_config({ baseConfigHash: "hash1", json: "{ ...complete config with model, credential, instructions, and Slack tool... }" }) -7. Reply: "Done." - -### New agent, user says "Use Anthropic via OpenRouter" -1. resolve_llm({ provider: "openrouter" }) - → { ok: true, provider: "openrouter", - model: "anthropic/claude-sonnet-4.6", - credentialId: "or1", credentialName: "OpenRouter" } -2. read_config() → { configHash: "hash1", config: { ... } } -3. write_config({ baseConfigHash: "hash1", json: "{ ...complete config with model: \\"openrouter/anthropic/claude-sonnet-4.6\\", credential: \\"or1\\", and the requested instructions... }" }) - -### User says "Use a different OpenRouter model" -1. ask_llm({ purpose: "Choose a different OpenRouter model" }) -2. read_config() → { configHash: "hash1", config: { ... } } -3. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/model\\", \\"value\\": \\"{provider}/{model}\\" }, { \\"op\\": \\"replace\\", \\"path\\": \\"/credential\\", \\"value\\": \\"\\" }]" }\`. - -### Adding a new node tool to an existing agent -1. (skip ask_llm — already set) -2. search_nodes / get_node_types -3. ask_credential per required slot -4. read_config() → { configHash: "hash1", config: { ... } } -5. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ op: \\"add\\", path: \\"/tools/-\\", value: { ... credentials: {...} } }]" }\` - -### Adding a node tool when credential setup is skipped -1. search_nodes / get_node_types -2. ask_credential({ purpose: "Salesforce credential for creating leads", - nodeType: "n8n-nodes-base.salesforceTool", credentialType: "salesforceOAuth2Api", - slot: "salesforceOAuth2Api" }) - → { skipped: true } -3. read_config() → { configHash: "hash1", config: { ... } } -4. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ op: \\"add\\", path: \\"/tools/-\\", value: { type: \\"node\\", - name: "salesforce_create_lead", description: "...", node: { - nodeType: "n8n-nodes-base.salesforceTool", nodeTypeVersion: 1, - nodeParameters: { ... } } } }]" }\` - IMPORTANT: omit \`node.credentials\` or omit only the skipped credential slot. - Do not stop. Do not say you will not add the tool. -5. Reply: "Done. I added the Salesforce tool without credentials; configure - the credential later before using it." - -### Adding a skill to an existing agent -1. create_skill({ name: "Summarize Meetings", description: "Use when summarizing meeting notes or transcripts", body: "Extract decisions, risks, and action items." }) - → { id: "skill_0Ab9ZkLm3Pq7Xy2N", ... } -2. read_config() → { configHash: "hash1", config: { ... } } -3. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ \\"op\\": \\"add\\", \\"path\\": \\"/skills/-\\", \\"value\\": { \\"type\\": \\"skill\\", \\"id\\": \\"skill_0Ab9ZkLm3Pq7Xy2N\\" } }]" }\` -4. Reply: "Done. I added the skill." - -### Ambiguous request: "Make it post somewhere" -1. ask_question({ question: "Where should the agent post?", - options: [ - { label: "Slack", value: "slack" }, - { label: "Discord", value: "discord" }, - { label: "Email", value: "email" } ] }) -2. Continue with the chosen branch (search_nodes → ask_credential → read_config → patch_config).`; +If \`write_config\` or \`patch_config\` returns \`stage: "stale"\`, retry once +from the returned \`config\` and \`configHash\`. For any independent later +change, call \`read_config\` again.`; export const IMPORTANT_SECTION = `\ ## Important -- Credentials are user-controlled. ALWAYS use ask_llm (for the agent's main - LLM picker), resolve_llm (for explicit/default main LLM resolution), and - ask_credential (for every node-tool credential slot). - Never read credential ids from list_credentials into the config. -- When you need to clarify an ambiguous user request and the answer is a - choice from a small set, use ask_question instead of asking in prose. -- Use search_nodes + get_node_types to discover nodes before adding node tools -- Prefer workflow tools and node tools over custom tools for real-world interactions -- n8n session-scoped memory is the default -- always enable it unless told otherwise -- \`build_custom_tool\` generates an opaque custom tool id, then compiles and stores the tool code. Register the returned id in the config separately by adding a \`{ type: "custom", id }\` entry to \`tools\` via write_config or patch_config -- \`create_skill\` stores the skill body only. It is not active until you add a \`{ type: "skill", id }\` entry to \`skills\` via read_config and patch_config/write_config.`; +- Credentials are user-controlled. Use \`resolve_llm\` or \`ask_llm\` for the + target agent's main model, and \`ask_credential\` for node-tool, + integration, or Episodic Memory credentials. Never copy credential IDs from + \`list_credentials\` into config. +- Use \`ask_question\` instead of prose when the answer is a known small set. +- Prefer existing workflow and node tools over custom tools for real-world + integrations. +- \`build_custom_tool\` stores code only; register the returned id in config. +- \`create_skill\` stores a target-agent skill body only. It is active only + after \`read_config\` plus \`patch_config\` or \`write_config\` adds + \`{ "type": "skill", "id": "" }\` to \`skills\`. +- n8n session-scoped memory is the default unless the user says otherwise.`; export const RESPONSE_STYLE_SECTION = `\ ## Response style -Be concise but informative. - -- After a build step (write_config, patch_config, build_custom_tool), give a - 1–2 sentence summary of what you changed and, if useful, one thing the user - might try next. No field-by-field narration, no JSON repetition, no - re-stating the user's request back to them. -- Do not narrate your reasoning before a tool call (no "Let me check the - credentials first…"). Just do it, then summarise the result. -- The config and tools speak for themselves — the user can inspect them - directly, so don't re-list what's visible in the sidebar.`; - -// --------------------------------------------------------------------------- -// Dynamic sections — depend on runtime values -// --------------------------------------------------------------------------- +Be concise. After a build step, give a 1-2 sentence summary of what changed and +one useful next step if there is one. Do not narrate reasoning before tool +calls, reprint JSON, or list what is already visible in the sidebar.`; export function getConfigRulesSection(): string { return `\ ## Agent config rules - - \`model\` must be "provider/model-name" format (e.g. "anthropic/claude-sonnet-4-5") - - \`credential\` must be the \`credentialId\` returned by a prior resolve_llm or ask_llm tool call. Do not guess. - - \`memory.storage\` must be "n8n" - - \`memory.lastMessages\` default: 50 - - Use "n8n" as the default memory storage for all agents - - \`memory.observationalMemory\` tunes observation-log memory. It is enabled by default whenever memory is enabled; use \`{ enabled: false }\` only when the user explicitly does not want automatic memory updates. - - Defaults: \`observerThresholdTokens: 500\`, \`reflectorThresholdTokens: 4000\`, \`renderTokenBudget: 4500\`, \`observationLogTailLimit: 20\`. - - Cost: observing and reflecting use background LLM calls on the agent's main model. Mention this if the user asks about cost. - - \`memory.episodicMemory\` enables Episodic Memory. It requires \`credential\` from \`ask_credential\` with \`credentialType: "openAiApi"\`; never guess or reuse credential IDs. - - If the agent has no \`model\`/\`credential\` yet, call resolve_llm or ask_llm before writing config. Do not write a placeholder/default model without a credential.`; +- \`model\` must be "provider/model-name". +- \`credential\` must be the id returned by \`resolve_llm\` or \`ask_llm\`. +- \`memory.storage\` must be "n8n"; \`memory.lastMessages\` defaults to 50. +- \`memory.episodicMemory\` requires \`ask_credential\` with + \`credentialType: "openAiApi"\`. +- Fresh agents need a real model, credential, and instructions before config + is written.`; } export function getSchemaReferenceSection(): string { @@ -653,10 +167,6 @@ ${jsonSchemaText} \`\`\``; } -// --------------------------------------------------------------------------- -// Prompt assembler -// --------------------------------------------------------------------------- - export interface BuilderPromptContext { configJson: string; configHash: string | null; @@ -667,38 +177,18 @@ export interface BuilderPromptContext { } export function buildBuilderPrompt(ctx: BuilderPromptContext): string { - const { - configJson, - configHash, - configUpdatedAt, - toolList, - agentPreviewPath, - modelRecommendationsSection, - } = ctx; + const { configJson, configHash, configUpdatedAt, toolList, agentPreviewPath } = ctx; const sections = [ 'You are an expert agent builder. You help users create and configure AI agents by writing raw JSON configuration and building custom tools.', + TARGET_AGENT_SECTION, getAgentStateSection(configJson, configHash, configUpdatedAt, toolList), - READ_CONFIG_SECTION, getConversationModeSection(agentPreviewPath), - TOOL_TYPES_SECTION, - LLM_RESOLUTION_SECTION, - modelRecommendationsSection, - INTERACTIVE_TOOLS_SECTION, - N8N_EXPRESSIONS_SECTION, - PROVIDER_TOOLS_SECTION, - MEMORY_PRESETS_SECTION, - INTEGRATIONS_SECTION, - RESEARCH_SECTION, - getConfigRulesSection(), - getSchemaReferenceSection(), - WORKFLOW_SECTION, - WRITE_CONFIG_SECTION, - PATCH_CONFIG_SECTION, - FEW_SHOT_FLOWS_SECTION, + BUILDER_SKILL_ROUTING_SECTION, + READ_CONFIG_FRESHNESS_SECTION, IMPORTANT_SECTION, RESPONSE_STYLE_SECTION, ]; - return sections.filter((section): section is string => section !== null).join('\n\n'); + return sections.join('\n\n'); } diff --git a/packages/cli/src/modules/agents/builder/agents-builder.service.ts b/packages/cli/src/modules/agents/builder/agents-builder.service.ts index 3c12d9f46e5..841c2793f6c 100644 --- a/packages/cli/src/modules/agents/builder/agents-builder.service.ts +++ b/packages/cli/src/modules/agents/builder/agents-builder.service.ts @@ -26,6 +26,7 @@ import { AGENT_THREAD_PREFIX } from './builder-tool-names'; import { AgentsBuilderSettingsService } from './agents-builder-settings.service'; import { buildBuilderTelemetry } from '../tracing/builder-telemetry'; import { getModelRecommendationsSection } from './agents-builder-model-recommendations'; +import { getBuilderRuntimeSkills } from './skills'; /** Derive a stable thread ID for the builder chat of a given agent. */ function builderThreadId(agentId: string): string { @@ -183,6 +184,7 @@ export class AgentsBuilderService { agentPreviewPath: buildAgentPreviewPath(projectId, agentId), modelRecommendationsSection, }); + const runtimeSkills = getBuilderRuntimeSkills({ modelRecommendationsSection }); const tools = this.agentsBuilderToolsService.getTools( agentId, @@ -201,6 +203,7 @@ export class AgentsBuilderService { const builder = new Agent('agent-builder') .model(modelConfig) .instructions(instructions) + .skills(runtimeSkills) .memory(builderMemory) .checkpoint(this.n8nCheckpointStorage.getStorage(agentId)); diff --git a/packages/cli/src/modules/agents/builder/skills/config-mutation.skill.ts b/packages/cli/src/modules/agents/builder/skills/config-mutation.skill.ts new file mode 100644 index 00000000000..31d51016fa7 --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/config-mutation.skill.ts @@ -0,0 +1,27 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +import { getConfigRulesSection, getSchemaReferenceSection } from '../agents-builder-prompts'; + +export function configMutationSkill(): RuntimeSkill { + return { + id: 'agent-builder-config-mutation', + name: 'Agent builder config mutation', + description: + 'Use before reading, writing, replacing, or patching the target agent JSON config.', + instructions: `\ +Use this skill whenever you call \`read_config\`, \`write_config\`, or \`patch_config\`. + +${getConfigRulesSection()} + +${getSchemaReferenceSection()} + +Mutation protocol: +- Call \`read_config\` immediately before every \`write_config\` or \`patch_config\`. +- For \`write_config\`, send the complete config JSON string and the \`baseConfigHash\` returned by that same \`read_config\`. +- For \`patch_config\`, send RFC 6902 operations as a JSON string plus that \`baseConfigHash\`. +- Use \`/field\`, \`/nested/field\`, \`/array/0\`, and \`/array/-\` JSON Pointer paths. +- On \`stage: "stale"\`, retry once from the returned \`config\` and \`configHash\`; never retry from memory. +- On parse, patch, or schema errors, fix the payload before trying again. +- When attaching a target-agent skill, append to \`/skills/-\` if \`skills\` exists; otherwise add \`/skills\` with an array containing the skill ref.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/index.ts b/packages/cli/src/modules/agents/builder/skills/index.ts new file mode 100644 index 00000000000..f074b00e7ed --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/index.ts @@ -0,0 +1,24 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +import { configMutationSkill } from './config-mutation.skill'; +import { integrationsSkill } from './integrations.skill'; +import { llmSelectionSkill } from './llm-selection.skill'; +import { memorySkill } from './memory.skill'; +import { researchSkill } from './research.skill'; +import { targetSkillsSkill } from './target-skills.skill'; +import { toolsSkill } from './tools.skill'; +import type { BuilderRuntimeSkillsOptions } from './types'; + +export function getBuilderRuntimeSkills({ + modelRecommendationsSection, +}: BuilderRuntimeSkillsOptions): RuntimeSkill[] { + return [ + configMutationSkill(), + llmSelectionSkill(modelRecommendationsSection), + toolsSkill(), + memorySkill(), + integrationsSkill(), + targetSkillsSkill(), + researchSkill(), + ]; +} diff --git a/packages/cli/src/modules/agents/builder/skills/integrations.skill.ts b/packages/cli/src/modules/agents/builder/skills/integrations.skill.ts new file mode 100644 index 00000000000..afb7f0605de --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/integrations.skill.ts @@ -0,0 +1,23 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function integrationsSkill(): RuntimeSkill { + return { + id: 'agent-builder-integrations', + name: 'Agent builder integrations', + description: + 'Use when adding schedule triggers or connected chat integrations to the target agent.', + instructions: `\ +The \`integrations\` array controls how the target agent is triggered. + +Schedule: +- One schedule integration per agent. +- Use \`{ "type": "schedule", "active": false, "cronExpression": "0 9 * * *", "wakeUpPrompt": "..." }\`. +- Keep \`active: false\`; schedules run only after publish and activation. +- Use standard 5-field cron. + +Chat integrations: +- Call \`list_integration_types\` first. +- Pick one returned \`credentialTypes\` entry and pass it to \`ask_credential\`. +- Persist only \`type\` and \`credentialId\`; never invent credential IDs or names.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/llm-selection.skill.ts b/packages/cli/src/modules/agents/builder/skills/llm-selection.skill.ts new file mode 100644 index 00000000000..f9c67a55aca --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/llm-selection.skill.ts @@ -0,0 +1,26 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function llmSelectionSkill(modelRecommendationsSection: string | null): RuntimeSkill { + const recommendationGuidance = modelRecommendationsSection + ? `\n\n${modelRecommendationsSection}` + : '\n\nNo Recommended LLM models section is available; do not recommend or name current, best, latest, or fallback model IDs from memory. Call ask_llm when the user needs model guidance or choice.'; + + return { + id: 'agent-builder-llm-selection', + name: 'Agent builder LLM selection', + description: + 'Use when setting, changing, resolving, or asking for the target agent main model and credential.', + instructions: `\ +Use \`resolve_llm\` before \`ask_llm\` when the request contains enough provider/model detail. + +Rules: +- Fresh agents need a resolved \`model\` and \`credential\` before config is written. +- Explicit provider/model requests go to \`resolve_llm\` first. +- If the user asks to pick, change, confirm, or configure a model or main credential, call \`ask_llm\`; do not ask in prose. +- If \`resolve_llm\` succeeds, persist \`model = "{provider}/{model}"\` and \`credential = credentialId\`. +- If \`resolve_llm\` reports missing or ambiguous credentials/provider, call \`ask_llm\`. +- If it reports \`unknown_model\`, retry with a plausible returned model value or call \`ask_llm\`. +- For "Anthropic via OpenRouter", pass \`provider: "openrouter"\`; if the user names a routed model, pass the routed id without adding another provider prefix. +- Prefer a provider the user already has credentials for when choosing from recommendations.${recommendationGuidance}`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/memory.skill.ts b/packages/cli/src/modules/agents/builder/skills/memory.skill.ts new file mode 100644 index 00000000000..770996b9fac --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/memory.skill.ts @@ -0,0 +1,31 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function memorySkill(): RuntimeSkill { + return { + id: 'agent-builder-memory', + name: 'Agent builder memory', + description: + 'Use when configuring target agent memory, observation-log memory, or Episodic Memory.', + instructions: `\ +Use n8n session-scoped memory by default: +\`\`\`json +{ "enabled": true, "storage": "n8n", "lastMessages": 50 } +\`\`\` + +Rules: +- Set \`storage\` to "n8n". +- \`lastMessages\` defaults to 50. +- Observation-log memory is enabled by default when memory is enabled. +- Keep \`observationalMemory\` optional; use it only for explicit tuning. +- Supported observation log tuning fields: \`enabled\`, \`observerThresholdTokens\`, \`reflectorThresholdTokens\`, \`renderTokenBudget\`, \`observationLogTailLimit\`, and \`lockTtlMs\`. + +Episodic Memory: +- Enable \`memory.episodicMemory\` only when the user asks for Episodic Memory, long-term memory, prior conversations, remembered decisions, exact artifacts, or cross-session memory. +- Before enabling it, call \`ask_credential({ credentialType: "openAiApi", purpose: "OpenAI credential for Episodic Memory embeddings" })\`. +- On success, set \`memory.episodicMemory = { "enabled": true, "credential": "" }\` and preserve existing \`topK\` or \`maxEntriesPerRun\`. +- If credential selection is skipped, do not enable Episodic Memory; explain that it needs an OpenAI credential for embeddings. +- Do not add instructions saying the agent should remember, store, save, or decide what context matters. The runtime handles memory extraction and indexing. +- If instructions mention Episodic Memory, phrase it as retrieval/use only, e.g. "Use recalled prior context when relevant to the user's request." +- Do not invent Episodic Memory credential IDs or reuse the main model credential unless \`ask_credential\` returned it for this purpose.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/research.skill.ts b/packages/cli/src/modules/agents/builder/skills/research.skill.ts new file mode 100644 index 00000000000..9b0c8b645e1 --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/research.skill.ts @@ -0,0 +1,20 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function researchSkill(): RuntimeSkill { + return { + id: 'agent-builder-research', + name: 'Agent builder research', + description: + 'Use when the user names an external API, service, product, standard, or spec you are unsure about.', + instructions: `\ +Use web search when external facts affect the config you are about to build. + +Search when: +- The user names an API, service, product, standard, or spec you do not fully understand. +- Endpoint shapes, auth methods, parameters, or current product behavior are uncertain. +- The reference might have changed recently. + +Do not search for n8n internals, common JavaScript or TypeScript patterns, or +well-known public APIs you can configure confidently from current context.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/target-skills.skill.ts b/packages/cli/src/modules/agents/builder/skills/target-skills.skill.ts new file mode 100644 index 00000000000..e6586f62867 --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/target-skills.skill.ts @@ -0,0 +1,21 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function targetSkillsSkill(): RuntimeSkill { + return { + id: 'agent-builder-target-skills', + name: 'Agent builder target skills', + description: + 'Use when creating reusable instruction bundles for the target agent to load later.', + instructions: `\ +Use target-agent skills for reusable instructions, playbooks, style guides, +policies, or domain knowledge the target agent should load only when relevant. + +Flow: +- Call \`create_skill\` with \`name\`, \`description\`, and \`body\`. +- The description should say when the runtime should load the skill. +- \`create_skill\` stores the body only; it does not attach the skill. +- After it returns an id, call \`read_config\`. +- Use \`patch_config\` or \`write_config\` to add \`{ "type": "skill", "id": "" }\` to \`skills\`. +- Do not use \`create_skill\` for builder instructions. These skills belong to the target agent.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/tools.skill.ts b/packages/cli/src/modules/agents/builder/skills/tools.skill.ts new file mode 100644 index 00000000000..b2805885f72 --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/tools.skill.ts @@ -0,0 +1,33 @@ +import type { RuntimeSkill } from '@n8n/agents'; + +export function toolsSkill(): RuntimeSkill { + return { + id: 'agent-builder-tools', + name: 'Agent builder tools', + description: 'Use when adding workflow, node, custom, or provider tools to the target agent.', + instructions: `\ +Prefer existing workflow tools and node tools over custom tools for real-world actions. + +Workflow tools: +- Call \`list_workflows\`; reference supported workflows by name with \`{ "type": "workflow", "workflow": "" }\`. + +Node tools: +- Use \`search_nodes\`, then \`get_node_types\`; never guess node type names. +- Use the tool node id from discovery, usually ending in \`Tool\`. +- Put fixed values in \`nodeParameters\`; use \`$fromAI\` for values the agent should decide at runtime. +- Wrap expressions in \`={{ }}\`; do not pipe AI-chosen fields through \`$json\`. +- Do not include \`inputSchema\` or \`toolDescription\` for node tools. +- For each required credential slot, call \`ask_credential\` once before config mutation. If skipped, still add the tool and omit only that credential slot. + +Custom tools: +- Use \`build_custom_tool\` with \`export default new Tool(...)\` and imports only from \`@n8n/agents\` and \`zod\`. +- Custom handlers run in a V8 isolate: no network, filesystem, process, Buffer, fetch, timers, or other host I/O. +- Return JSON-serializable values. Do not call \`.build()\`. +- Register the returned custom tool id in config after \`build_custom_tool\`. + +Provider tools: +- Match provider tools to the configured model provider. +- Anthropic: \`providerTools["anthropic.web_search"]\`. +- OpenAI: \`providerTools["openai.web_search"]\` or \`providerTools["openai.image_generation"]\`, only for compatible OpenAI models.`, + }; +} diff --git a/packages/cli/src/modules/agents/builder/skills/types.ts b/packages/cli/src/modules/agents/builder/skills/types.ts new file mode 100644 index 00000000000..9c5072b0172 --- /dev/null +++ b/packages/cli/src/modules/agents/builder/skills/types.ts @@ -0,0 +1,3 @@ +export interface BuilderRuntimeSkillsOptions { + modelRecommendationsSection: string | null; +}