From baf50839d38e230873dcafe508c6f2cc5a2f2271 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 29 Jun 2026 23:58:09 +0200 Subject: [PATCH] wip --- packages/@n8n/instance-ai/docs/tools.md | 25 ++ .../skills/workflow-builder/SKILL.md | 29 +- .../instance-ai/src/agent/system-prompt.ts | 14 +- packages/@n8n/instance-ai/src/tools/index.ts | 8 + .../discover-workflow-context.tool.test.ts | 111 ++++++ .../src/tools/orchestration/delegate.tool.ts | 322 ++++++++++-------- .../discover-workflow-context.prompt.ts | 31 ++ .../discover-workflow-context.tool.ts | 111 ++++++ .../@n8n/instance-ai/src/tools/tool-ids.ts | 2 + 9 files changed, 490 insertions(+), 163 deletions(-) create mode 100644 packages/@n8n/instance-ai/src/tools/orchestration/__tests__/discover-workflow-context.tool.test.ts create mode 100644 packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.prompt.ts create mode 100644 packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.tool.ts diff --git a/packages/@n8n/instance-ai/docs/tools.md b/packages/@n8n/instance-ai/docs/tools.md index b4c0381ae3d..fd207eeaa4a 100644 --- a/packages/@n8n/instance-ai/docs/tools.md +++ b/packages/@n8n/instance-ai/docs/tools.md @@ -87,6 +87,31 @@ fixed taxonomy of sub-agent types. - Sub-agent has no memory — receives context only via the briefing - Past failed attempts from `iterationLog` are appended to the briefing (if available) +### `discover-workflow-context` + +Preconfigured pre-build discovery. Spawns a focused, **synchronous** sub-agent +(fixed role, prompt, and tool subset: `nodes`, `credentials`, `research`) that +inventories the nodes and credential types a build needs and returns the relevant +node **type definitions verbatim** — it selects which types are relevant rather +than summarizing them. Unlike free-form `delegate`, the scout is fixed so +discovery is consistent and testable. Call before loading `workflow-builder` for +any build touching external services or unfamiliar nodes; act on the result in +the same turn. Does not build, patch, or run workflows. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `services` | string[] | yes | External services as short node-search terms (e.g. `["Gmail", "OpenAI"]`) | +| `categories` | string[] | no | Workflow technique categories to anchor node suggestions | +| `conversationContext` | string | no | Build goal and user constraints to scope discovery | + +**Returns**: `{ result: string }` — the scout's debrief: brief Nodes and +Credentials bullets, the relevant node type definitions verbatim, and Gaps. + +**Behavior**: Resolves the fixed tool subset from the orchestrator's domain +tools, runs the sub-agent via the shared synchronous runner (same tracing and +`agent-spawned`/`agent-completed` events as `delegate`), and returns the +synthesized debrief. Returns an error string if the `nodes` tool is unavailable. + ### `update-tasks` Update a visible task checklist for the user. Used for lightweight progress diff --git a/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md b/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md index b53f6caffcf..0af904d58d6 100644 --- a/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md +++ b/packages/@n8n/instance-ai/skills/workflow-builder/SKILL.md @@ -118,24 +118,27 @@ When mapping downstream fields from an OpenAI node, read ## Pre-build discovery -Before writing code, the orchestrator may have already run parallel `delegate` -sub-agents — **KB scout**, **credential scout**, and **node researcher** (see -orchestrator delegation rules). Their debriefings are compressed summaries, not -full tool payloads: technique bullets, credential availability, and node IDs with -discriminators. +Before writing code, the orchestrator may have already run pre-build discovery — +a `discover-workflow-context` call (nodes, credentials, and type definitions) and +optionally a `delegate` **KB scout** (technique bullets). The discovery debrief +gives node IDs with discriminators and credential availability as brief bullets, +plus the relevant node **type definitions verbatim** (selected, not summarized); +the KB scout returns technique bullets. -When debriefings are in context, use them as authoritative discovery output — do +When debriefings are in context, use them as authoritative discovery output — the +verbatim type definitions are the real schema, so build from them directly and do not repeat the same `nodes`, `credentials`, or workspace reads unless a debriefing is missing a fact you still need. -When no delegate debriefings exist, run steps 1–5 of Mandatory Process inline in +When no discovery debriefings exist, run steps 1–5 of Mandatory Process inline in this turn — but only for simple single-service builds or edits that reuse an -existing `.workflow.ts` file. For a multi-system build (two or more external -services, or work needing 2+ knowledge-base guides/templates) with no debriefings -in context, the orchestrator skipped required discovery delegation: stop and let -it run the Pre-build discovery delegation scouts first, then resume here with -their debriefings. Do not call `delegate` from this skill — discovery delegation -is the orchestrator's job before loading `workflow-builder`. +existing `.workflow.ts` file. For a build touching external services or unfamiliar +nodes (especially two or more external services, or work needing 2+ knowledge-base +guides/templates) with no debriefings in context, the orchestrator skipped +required discovery: stop and let it run `discover-workflow-context` (and any KB +`delegate` scout) first, then resume here with their debriefings. Do not call +`discover-workflow-context` or `delegate` from this skill — pre-build discovery is +the orchestrator's job before loading `workflow-builder`. ## Workflow-Level Error Workflows diff --git a/packages/@n8n/instance-ai/src/agent/system-prompt.ts b/packages/@n8n/instance-ai/src/agent/system-prompt.ts index 6f27cb2be03..768e37e9d62 100644 --- a/packages/@n8n/instance-ai/src/agent/system-prompt.ts +++ b/packages/@n8n/instance-ai/src/agent/system-prompt.ts @@ -127,7 +127,7 @@ ${getProjectScopeSection(projectId)} Match the user's request against skill descriptions in the catalog. Call \`load_skill\` before acting on a matched skill's guidance — never call \`data-tables\` or \`parse-file\` without loading \`data-table-manager\` first, and never call \`build-workflow\` without loading \`workflow-builder\` first. A single turn may need more than one skill when routing requires it (e.g. \`data-table-manager\` then \`workflow-builder\`). - **Single workflow build or edit** (new workflow, add/remove/rewire nodes, expression/credential/schedule/Code fixes, including workflows that create or write to Data Tables): - - **Multi-system build first turn** (two or more external services, e.g. Form + OpenAI + Google Sheets, or work needing 2+ knowledge-base guides/templates) → you MUST run **Pre-build discovery delegation** (see below) before \`workflow-builder\`: spawn parallel \`delegate\` scouts, wait for their inline results, then \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`. + - **Build that touches external services or unfamiliar nodes** (one or more external integrations, e.g. Form + OpenAI + Google Sheets, or any node whose exact type/params you are unsure of) → you MUST run **Pre-build discovery** (see below) before \`workflow-builder\`: call \`discover-workflow-context\` for the nodes/credentials/types, optionally spawn parallel \`delegate\` KB scouts, wait for their inline results, then \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`. - **Simple single-service builds and existing-workflow edits** → skip delegation; go straight to \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`. - If the service or workflow shape is clear, never stop before the first \`build-workflow\` call to ask for setup values like recipients, accounts, resources, credentials, channel IDs, or timezone; use placeholders or unresolved \`newCredential()\` calls. - After every successful direct \`build-workflow\` result, if the tool output contains \`postBuildFlow.required: true\`, load \`post-build-flow\` exactly once and follow it before verification, setup, error-workflow follow-up, publishing, testing, or any final user-visible summary. Do not create a plan just for verification. @@ -141,15 +141,15 @@ Match the user's request against skill descriptions in the catalog. Call \`load_ Use \`task-control(action="update-checklist")\` only for lightweight visible checklists that do not need scheduler-driven execution. -## Pre-build discovery delegation +## Pre-build discovery -\`delegate\` runs **synchronously**: each call spawns a focused sub-agent, runs it, and returns its result text to you in the same turn (it is NOT a background task and shows no approval card). Spawn several in one step to run them in parallel, then read their results and continue building. +Discovery sub-agents run **synchronously**: each call spawns a focused sub-agent, runs it, and returns its result text to you in the same turn (it is NOT a background task and shows no approval card). Run them, read their results, then continue building. -Before loading \`workflow-builder\` or \`planning\`, delegate discovery when any of these apply: multiple external systems; reading two or more knowledge-base best-practice guides or templates; credential inventory plus \`nodes(action="explore-resources")\` across services. For multi-system builds, spawn parallel \`delegate\` calls with these roles: -- **KB & skills scout** — use sandbox workspace tools (read, list, grep/rg via execute). They attach automatically to delegated sub-agents when a sandbox exists — pass \`tools: []\` for KB-only scouts. Do not pass skill-catalog aliases like \`read_file\`/\`write_file\`; those are not delegate tools. Return technique bullets only, not full file contents. -- **Node researcher & Credential scout** — \`tools: ["nodes", "credentials"]\`: \`suggested\`, \`search\`, \`type-definition\`, and \`explore-resources\` when credentials exist; return node IDs, discriminators, credential types, and key parameter notes. list available types and names for required services; note gaps. +Before loading \`workflow-builder\` or \`planning\` for any build touching external services or unfamiliar nodes: +- **Nodes, credentials, and types → \`discover-workflow-context\`.** This is the primary, preferred discovery call: pass \`services\` (short node-search terms like \`["Gmail", "Google Sheets", "OpenAI"]\`), optional \`categories\`, and \`conversationContext\`. It spawns a fixed scout that returns node IDs with discriminators, required credential types and whether they exist, and the relevant node type definitions **verbatim** (it selects the relevant types rather than summarizing them). Prefer one \`discover-workflow-context\` call over hand-composing \`delegate\` node/credential scouts. +- **Knowledge-base guides/templates → \`delegate\` KB scout** (only when the build needs 2+ KB best-practice guides or templates) — use sandbox workspace tools (read, list, grep/rg via execute). They attach automatically to delegated sub-agents when a sandbox exists — pass \`tools: []\` for KB-only scouts. Do not pass skill-catalog aliases like \`read_file\`/\`write_file\`; those are not delegate tools. Return technique bullets only, not full file contents. -Pass complete briefings and \`conversationContext\`; synthesize compressed debriefings before loading \`workflow-builder\`. For simple single-service builds with one credential type, or edits that reuse an existing workspace \`.workflow.ts\` file, skip delegation and discover inline. Never use \`delegate\` to build, patch, fix, or update workflows — workflow building runs in the orchestrator with \`workflow-builder\`, workspace file tools, and \`build-workflow\`. +Run \`discover-workflow-context\` and any KB \`delegate\` scout in parallel in one step. Pass complete briefings and \`conversationContext\`; synthesize the compressed debriefings before loading \`workflow-builder\`. For simple single-service builds with one well-known node and credential type, or edits that reuse an existing workspace \`.workflow.ts\` file, you may skip discovery and proceed inline. Never use \`delegate\` or \`discover-workflow-context\` to build, patch, fix, or update workflows — workflow building runs in the orchestrator with \`workflow-builder\`, workspace file tools, and \`build-workflow\`. ## System follow-ups diff --git a/packages/@n8n/instance-ai/src/tools/index.ts b/packages/@n8n/instance-ai/src/tools/index.ts index 5db55977c2f..8e1a1752625 100644 --- a/packages/@n8n/instance-ai/src/tools/index.ts +++ b/packages/@n8n/instance-ai/src/tools/index.ts @@ -37,6 +37,10 @@ const loadCompleteCheckpointTool = lazyMod( const loadDelegateTool = lazyMod( () => require('./orchestration/delegate.tool') as typeof import('./orchestration/delegate.tool'), ); +const loadDiscoverWorkflowContextTool = lazyMod( + () => + require('./orchestration/discover-workflow-context.tool') as typeof import('./orchestration/discover-workflow-context.tool'), +); const loadEvalDataAgentTool = lazyMod( () => require('./orchestration/eval-data-agent.tool') as typeof import('./orchestration/eval-data-agent.tool'), @@ -142,6 +146,10 @@ export function createOrchestrationTools(context: OrchestrationContext): Instanc [ORCHESTRATION_TOOL_IDS.CREATE_TASKS, loadPlanTool().createPlanTool(context)], [ORCHESTRATION_TOOL_IDS.TASK_CONTROL, loadTaskControlTool().createTaskControlTool(context)], [ORCHESTRATION_TOOL_IDS.DELEGATE, loadDelegateTool().createDelegateTool(context)], + [ + ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT, + loadDiscoverWorkflowContextTool().createDiscoverWorkflowContextTool(context), + ], [ ORCHESTRATION_TOOL_IDS.COMPLETE_CHECKPOINT, loadCompleteCheckpointTool().createCompleteCheckpointTool(context), diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/discover-workflow-context.tool.test.ts b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/discover-workflow-context.tool.test.ts new file mode 100644 index 00000000000..834453d6727 --- /dev/null +++ b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/discover-workflow-context.tool.test.ts @@ -0,0 +1,111 @@ +import { executeTool } from '../../../__tests__/tool-test-utils'; +import { createToolRegistry } from '../../../tool-registry'; +import type { OrchestrationContext } from '../../../types'; +import { runSyncSubAgent } from '../delegate.tool'; +import { + createDiscoverWorkflowContextTool, + discoverWorkflowContextInputSchema, +} from '../discover-workflow-context.tool'; + +vi.mock('../delegate.tool', () => ({ runSyncSubAgent: vi.fn() })); + +const runSyncSubAgentMock = vi.mocked(runSyncSubAgent); + +function createMockContext(domainTools: Record = {}): OrchestrationContext { + return { + threadId: 'test-thread', + runId: 'test-run', + userId: 'test-user', + orchestratorAgentId: 'test-agent', + modelId: 'test-model', + subAgentMaxSteps: 5, + eventBus: { + publish: vi.fn(), + subscribe: vi.fn(), + getEventsAfter: vi.fn(), + getNextEventId: vi.fn(), + getEventsForRun: vi.fn().mockReturnValue([]), + getEventsForRuns: vi.fn().mockReturnValue([]), + }, + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + domainTools: createToolRegistry( + Object.entries(domainTools).map(([name, tool]) => [ + name, + { name, description: name, ...(tool as object) }, + ]), + ), + abortSignal: new AbortController().signal, + taskStorage: { get: vi.fn(), save: vi.fn() }, + } as unknown as OrchestrationContext; +} + +beforeEach(() => { + runSyncSubAgentMock.mockReset(); +}); + +describe('discoverWorkflowContextInputSchema', () => { + it('accepts valid input', () => { + const result = discoverWorkflowContextInputSchema.safeParse({ + services: ['Gmail', 'Google Sheets'], + categories: ['data_persistence'], + conversationContext: 'Build a lead capture workflow', + }); + expect(result.success).toBe(true); + }); + + it('rejects an empty services array', () => { + const result = discoverWorkflowContextInputSchema.safeParse({ services: [] }); + expect(result.success).toBe(false); + }); + + it('rejects missing services', () => { + const result = discoverWorkflowContextInputSchema.safeParse({ categories: ['chatbot'] }); + expect(result.success).toBe(false); + }); +}); + +describe('createDiscoverWorkflowContextTool', () => { + it('errors when the nodes tool is unavailable', async () => { + const context = createMockContext({ credentials: {} }); + const tool = createDiscoverWorkflowContextTool(context); + + const output = await executeTool<{ result: string }>( + tool, + { services: ['Gmail'] }, + {} as never, + ); + + expect(output.result).toContain('nodes'); + expect(runSyncSubAgentMock).not.toHaveBeenCalled(); + }); + + it('runs the scout with the available discovery tools and returns its debrief', async () => { + runSyncSubAgentMock.mockResolvedValue({ result: 'debrief text' }); + const context = createMockContext({ nodes: {}, credentials: {}, research: {} }); + const tool = createDiscoverWorkflowContextTool(context); + + const output = await executeTool<{ result: string }>( + tool, + { services: ['Gmail', 'OpenAI'], conversationContext: 'ctx' }, + {} as never, + ); + + expect(output.result).toBe('debrief text'); + expect(runSyncSubAgentMock).toHaveBeenCalledTimes(1); + const callArg = runSyncSubAgentMock.mock.calls[0][1]; + expect(callArg.toolNames).toEqual(['nodes', 'credentials', 'research']); + expect(callArg.conversationContext).toBe('ctx'); + expect(callArg.briefing).toContain('Gmail, OpenAI'); + }); + + it('only resolves the discovery tools that are registered', async () => { + runSyncSubAgentMock.mockResolvedValue({ result: 'ok' }); + const context = createMockContext({ nodes: {} }); + const tool = createDiscoverWorkflowContextTool(context); + + await executeTool(tool, { services: ['Slack'] }, {} as never); + + const callArg = runSyncSubAgentMock.mock.calls[0][1]; + expect(callArg.toolNames).toEqual(['nodes']); + }); +}); diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/delegate.tool.ts b/packages/@n8n/instance-ai/src/tools/orchestration/delegate.tool.ts index a13098eae31..28f1263a1e6 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/delegate.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/delegate.tool.ts @@ -2,7 +2,12 @@ import { Tool } from '@n8n/agents'; import { nanoid } from 'nanoid'; import { createSubAgentPersistence } from './agent-persistence'; -import { delegateInputSchema, delegateOutputSchema, type DelegateInput } from './delegate.schemas'; +import { + delegateInputSchema, + delegateOutputSchema, + type DelegateInput, + type DelegateOutput, +} from './delegate.schemas'; import { truncateLabel } from './display-utils'; import { createDetachedSubAgentTraceFactory, @@ -289,6 +294,172 @@ export async function startDetachedDelegateTask( }; } +/** + * Input for {@link runSyncSubAgent} — tools are already resolved and validated by + * the caller so this helper stays focused on spawning, streaming, and debriefing. + */ +export interface RunSyncSubAgentInput { + role: string; + instructions: string; + briefing: string; + /** Pre-resolved, validated tool registry the sub-agent may use. */ + validTools: InstanceAiToolRegistry; + /** Tool names for the agent-spawned UI event (display only). */ + toolNames: string[]; + artifacts?: unknown; + conversationContext?: string; +} + +/** + * Run a focused sub-agent synchronously: spawn it, stream with HITL support, and + * return a synthesized debrief in the same turn. Shared by `delegate` and the + * preconfigured discovery tool so both follow identical tracing/event semantics. + */ +export async function runSyncSubAgent( + context: OrchestrationContext, + input: RunSyncSubAgentInput, +): Promise { + const subAgentId = generateAgentId(); + const startTime = Date.now(); + + context.eventBus.publish(context.threadId, { + type: 'agent-spawned', + runId: context.runId, + agentId: subAgentId, + payload: { + parentId: context.orchestratorAgentId, + role: input.role, + tools: input.toolNames, + kind: 'delegate', + subtitle: truncateLabel(input.briefing), + goal: input.briefing, + }, + }); + const traceRun = await startSubAgentTrace(context, { + agentId: subAgentId, + role: input.role, + kind: 'delegate', + inputs: { + briefing: input.briefing, + instructions: input.instructions, + tools: input.toolNames, + conversationContext: input.conversationContext, + }, + }); + const tracedTools = traceSubAgentTools(context, input.validTools, input.role); + + try { + const subAgent = createSubAgent({ + agentId: subAgentId, + role: input.role, + instructions: input.instructions, + tools: tracedTools, + modelId: context.subAgentModelId ?? context.modelId, + traceRun, + tracing: context.tracing, + ...buildSubAgentWorkspaceOptions(context), + runtimeSkills: context.runtimeSkills, + timeZone: context.timeZone, + checkpointStore: context.checkpointStore, + }); + + const briefingMessage = await buildDelegateBriefing( + context, + input.role, + input.briefing, + input.artifacts, + input.conversationContext, + ); + + const consumeResult = await withTraceRun(context, traceRun, async () => { + const maxIterations = context.subAgentMaxSteps ?? MAX_STEPS.DELEGATE_FALLBACK; + const persistence = await createSubAgentPersistence(context, { + agentKind: input.role, + }); + const stream = await subAgent.stream(briefingMessage, { + maxIterations, + abortSignal: context.abortSignal, + persistence, + providerOptions: { + anthropic: { cacheControl: { type: 'ephemeral' } }, + }, + }); + + return await consumeStreamWithHitl({ + agent: subAgent, + stream, + runId: context.runId, + agentId: subAgentId, + eventBus: context.eventBus, + logger: context.logger, + threadId: context.threadId, + outputRedaction: context.outputRedaction, + abortSignal: context.abortSignal, + waitForConfirmation: context.waitForConfirmation, + maxIterations, + persistence, + }); + }); + + const resultText = await requireCompletedHitlText(consumeResult, 'Delegate sub-agent'); + const debriefing = buildDebriefing({ + agentId: subAgentId, + role: input.role, + result: resultText, + workSummary: consumeResult.workSummary, + startTime, + }); + + await finishTraceRun(context, traceRun, { + outputs: { + result: resultText, + agentId: subAgentId, + role: input.role, + toolCallCount: debriefing.toolCallCount, + toolErrorCount: debriefing.toolErrorCount, + durationMs: debriefing.durationMs, + }, + }); + + context.eventBus.publish(context.threadId, { + type: 'agent-completed', + runId: context.runId, + agentId: subAgentId, + payload: { + role: input.role, + result: resultText, + }, + }); + + return { + result: resultText, + toolCallCount: debriefing.toolCallCount, + toolErrorCount: debriefing.toolErrorCount, + durationMs: debriefing.durationMs, + blockers: debriefing.blockers, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await failTraceRun(context, traceRun, error, { + agent_id: subAgentId, + agent_role: input.role, + }); + + context.eventBus.publish(context.threadId, { + type: 'agent-completed', + runId: context.runId, + agentId: subAgentId, + payload: { + role: input.role, + result: '', + error: errorMessage, + }, + }); + + return { result: `Sub-agent error: ${errorMessage}` }; + } +} + export function createDelegateTool(context: OrchestrationContext) { return new Tool('delegate') .description( @@ -317,150 +488,15 @@ export function createDelegateTool(context: OrchestrationContext) { }; } - const subAgentId = generateAgentId(); - const startTime = Date.now(); - - // 2. Publish agent-spawned - context.eventBus.publish(context.threadId, { - type: 'agent-spawned', - runId: context.runId, - agentId: subAgentId, - payload: { - parentId: context.orchestratorAgentId, - role: input.role, - tools: input.tools, - kind: 'delegate', - subtitle: truncateLabel(input.briefing), - goal: input.briefing, - }, - }); - const traceRun = await startSubAgentTrace(context, { - agentId: subAgentId, + return await runSyncSubAgent(context, { role: input.role, - kind: 'delegate', - inputs: { - briefing: input.briefing, - instructions: input.instructions, - tools: input.tools, - conversationContext: input.conversationContext, - }, + instructions: input.instructions, + briefing: input.briefing, + validTools, + toolNames: input.tools, + artifacts: input.artifacts, + conversationContext: input.conversationContext, }); - const tracedTools = traceSubAgentTools(context, validTools, input.role); - - try { - // 3. Create sub-agent - const subAgent = createSubAgent({ - agentId: subAgentId, - role: input.role, - instructions: input.instructions, - tools: tracedTools, - modelId: context.subAgentModelId ?? context.modelId, - traceRun, - tracing: context.tracing, - ...buildSubAgentWorkspaceOptions(context), - runtimeSkills: context.runtimeSkills, - timeZone: context.timeZone, - checkpointStore: context.checkpointStore, - }); - - const briefingMessage = await buildDelegateBriefing( - context, - input.role, - input.briefing, - input.artifacts, - input.conversationContext, - ); - - // 4. Stream sub-agent with HITL support - const consumeResult = await withTraceRun(context, traceRun, async () => { - const maxIterations = context.subAgentMaxSteps ?? MAX_STEPS.DELEGATE_FALLBACK; - const persistence = await createSubAgentPersistence(context, { - agentKind: input.role, - }); - const stream = await subAgent.stream(briefingMessage, { - maxIterations, - abortSignal: context.abortSignal, - persistence, - providerOptions: { - anthropic: { cacheControl: { type: 'ephemeral' } }, - }, - }); - - return await consumeStreamWithHitl({ - agent: subAgent, - stream, - runId: context.runId, - agentId: subAgentId, - eventBus: context.eventBus, - logger: context.logger, - threadId: context.threadId, - outputRedaction: context.outputRedaction, - abortSignal: context.abortSignal, - waitForConfirmation: context.waitForConfirmation, - maxIterations, - persistence, - }); - }); - - const resultText = await requireCompletedHitlText(consumeResult, 'Delegate sub-agent'); - const debriefing = buildDebriefing({ - agentId: subAgentId, - role: input.role, - result: resultText, - workSummary: consumeResult.workSummary, - startTime, - }); - - await finishTraceRun(context, traceRun, { - outputs: { - result: resultText, - agentId: subAgentId, - role: input.role, - toolCallCount: debriefing.toolCallCount, - toolErrorCount: debriefing.toolErrorCount, - durationMs: debriefing.durationMs, - }, - }); - - // 7. Publish agent-completed - context.eventBus.publish(context.threadId, { - type: 'agent-completed', - runId: context.runId, - agentId: subAgentId, - payload: { - role: input.role, - result: resultText, - }, - }); - - return { - result: resultText, - toolCallCount: debriefing.toolCallCount, - toolErrorCount: debriefing.toolErrorCount, - durationMs: debriefing.durationMs, - blockers: debriefing.blockers, - }; - } catch (error) { - // 8. Publish agent-completed with error - const errorMessage = error instanceof Error ? error.message : String(error); - await failTraceRun(context, traceRun, error, { - agent_id: subAgentId, - agent_role: input.role, - }); - - context.eventBus.publish(context.threadId, { - type: 'agent-completed', - runId: context.runId, - agentId: subAgentId, - payload: { - role: input.role, - result: '', - error: errorMessage, - }, - }); - - return { result: `Sub-agent error: ${errorMessage}` }; - } }) .build(); } diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.prompt.ts b/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.prompt.ts new file mode 100644 index 00000000000..9d866df4900 --- /dev/null +++ b/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.prompt.ts @@ -0,0 +1,31 @@ +/** + * System prompt for the discovery sub-agent spawned by `discover-workflow-context`. + * + * The sub-agent inventories the nodes and credentials a build needs and returns + * the relevant node type definitions VERBATIM (selection only, no summarizing) so + * the builder agent works from the real schema. It must NOT build, patch, or run + * anything — discovery only. + */ +export const DISCOVER_WORKFLOW_CONTEXT_PROMPT = `You are a workflow discovery specialist for n8n. Before a workflow is built, you gather the exact nodes, credentials, and type definitions the build needs, so the builder agent never guesses. You report to a parent agent, not a human. + +## Scope +- Discovery ONLY. Never build, patch, update, or run workflows. You have no build tools. +- Cover three things for the services in the briefing: which NODES to use, which CREDENTIALS exist or are missing, and the relevant TYPE DEFINITIONS. +- Your job with types is **selection, not summarization**: decide which node types are relevant, then return their type definitions exactly as the tool gave them. + +## Mandatory Process +1. If the briefing names workflow categories, call \`nodes(action="suggested")\` for them first to anchor on canonical nodes. +2. For each external service, call \`nodes(action="search")\` with a short service name (e.g. "Gmail", "Slack") — not full task phrases. Note the resource/operation/mode discriminators in results. +3. Call \`nodes(action="type-definition")\` with the exact node IDs you will recommend (include discriminators; up to 5 per call). Do not fetch definitions for nodes you will not recommend. +4. Call \`credentials(action="list")\` (filter by \`type\` when you know it) to record which required credential types already exist (by name) and which are missing. Use \`credentials(action="search-types")\` to confirm the canonical credential type for a service when unsure. NEVER expose secrets — metadata only. +5. Use \`research\` only when a node's type definition is insufficient to understand a provider's requirements (e.g. an HTTP Request integration with no dedicated node). Keep it to targeted lookups. + +## Output Contract +Output these sections in order: + +- **Nodes**: one bullet per node you recommend — \`\` (+ version) with the required \`resource\`/\`operation\`/\`mode\` discriminators and the credential type it needs. This is a selection list, keep it brief. +- **Credentials**: per required type, state whether a usable credential EXISTS (give its name) or is MISSING. Metadata only — never secret values. +- **Type definitions**: for every node listed under **Nodes**, paste the \`nodes(action="type-definition")\` output for that node **VERBATIM**. Do NOT summarize, condense, paraphrase, reformat, or drop any fields — include every parameter, enum value, \`@builderHint\`, \`@default\`, and display condition exactly as returned. The only filtering allowed is at the node level: omit definitions for nodes you are not recommending. Label each block with its node type. +- **Gaps**: any requested service with no suitable node/credential, or anything you could not verify — state it plainly. + +Keep the **Nodes**, **Credentials**, and **Gaps** sections tight (bullets, not prose), but leave **Type definitions** untouched and complete. Do NOT narrate your steps; output only the final debrief.`; diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.tool.ts b/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.tool.ts new file mode 100644 index 00000000000..4df0a2555a2 --- /dev/null +++ b/packages/@n8n/instance-ai/src/tools/orchestration/discover-workflow-context.tool.ts @@ -0,0 +1,111 @@ +/** + * Preconfigured Workflow Discovery Tool + * + * Spawns a focused, synchronous sub-agent that inventories the nodes and + * credentials a build needs and returns the relevant node type definitions + * verbatim (selection only, no summarizing). Unlike free-form `delegate`, the + * role, system prompt, and tool subset are fixed here so pre-build discovery is + * consistent and testable. + * + * Runs synchronously: the result is returned to the orchestrator in the same + * turn, which then loads `workflow-builder` and builds with that context. + */ +import { Tool } from '@n8n/agents'; +import { z } from 'zod'; + +import { runSyncSubAgent } from './delegate.tool'; +import { DISCOVER_WORKFLOW_CONTEXT_PROMPT } from './discover-workflow-context.prompt'; +import { createToolRegistry, toolRegistryKeys } from '../../tool-registry'; +import type { OrchestrationContext } from '../../types'; +import { DOMAIN_TOOL_IDS, ORCHESTRATION_TOOL_IDS } from '../tool-ids'; + +const DISCOVERY_ROLE = 'workflow-context-scout'; + +/** Native domain tools the discovery sub-agent may use, in priority order. */ +const DISCOVERY_TOOL_NAMES = [ + DOMAIN_TOOL_IDS.NODES, + DOMAIN_TOOL_IDS.CREDENTIALS, + DOMAIN_TOOL_IDS.RESEARCH, +] as const; + +export const discoverWorkflowContextInputSchema = z.object({ + services: z + .array(z.string().min(1)) + .min(1) + .describe( + 'External services / integrations the workflow will touch, as short node-search terms (e.g. ["Gmail", "Google Sheets", "OpenAI"]). The scout finds the right nodes and credential types for each.', + ), + categories: z + .array(z.string().min(1)) + .optional() + .describe( + 'Optional workflow technique categories to anchor node suggestions (e.g. ["form_input", "content_generation", "data_persistence"]).', + ), + conversationContext: z + .string() + .optional() + .describe( + 'Brief summary of the build goal and any user constraints (named accounts, channels, tables, required node families). The scout uses this to scope discovery.', + ), +}); + +export type DiscoverWorkflowContextInput = z.infer; + +function buildDiscoveryBriefing(input: DiscoverWorkflowContextInput): string { + const lines = [ + 'Discover the nodes, credentials, and type definitions needed to build a workflow.', + '', + `Services to cover: ${input.services.join(', ')}.`, + ]; + if (input.categories?.length) { + lines.push(`Workflow categories: ${input.categories.join(', ')}.`); + } + lines.push( + '', + 'For each service: identify the exact node type(s) with their resource/operation/mode discriminators, record the credential type(s) plus whether a usable credential already exists, and return the relevant node type definitions verbatim. Follow the output contract in your instructions — do not summarize the type definitions.', + ); + return lines.join('\n'); +} + +export function createDiscoverWorkflowContextTool(context: OrchestrationContext) { + return new Tool(ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT) + .description( + 'Run pre-build discovery: spawn a focused sub-agent that inventories the ' + + 'nodes and credentials for the given services and returns the relevant ' + + 'node type definitions verbatim (it selects the relevant types, it does ' + + 'not summarize them). Call this BEFORE loading `workflow-builder` for any ' + + 'build touching external services or unfamiliar nodes, then build with the ' + + 'returned context. Runs synchronously — act on the result in the same ' + + 'turn. Does not build, patch, or run workflows.', + ) + .input(discoverWorkflowContextInputSchema) + .output(z.object({ result: z.string() })) + .handler(async (input: DiscoverWorkflowContextInput) => { + const validTools = createToolRegistry(); + for (const name of DISCOVERY_TOOL_NAMES) { + const tool = context.domainTools.get(name); + if (tool) { + validTools.set(name, tool); + } + } + + if (!validTools.has(DOMAIN_TOOL_IDS.NODES)) { + return { + result: + 'Discovery failed: the `nodes` tool is not available, so node/type discovery cannot run.', + }; + } + + const { result } = await runSyncSubAgent(context, { + role: DISCOVERY_ROLE, + instructions: DISCOVER_WORKFLOW_CONTEXT_PROMPT, + briefing: buildDiscoveryBriefing(input), + validTools, + toolNames: toolRegistryKeys(validTools), + conversationContext: input.conversationContext, + }); + + return { result }; + }) + .build(); +} diff --git a/packages/@n8n/instance-ai/src/tools/tool-ids.ts b/packages/@n8n/instance-ai/src/tools/tool-ids.ts index a3dc02455df..2e1a1299673 100644 --- a/packages/@n8n/instance-ai/src/tools/tool-ids.ts +++ b/packages/@n8n/instance-ai/src/tools/tool-ids.ts @@ -17,6 +17,7 @@ export const ORCHESTRATION_TOOL_IDS = { CREATE_TASKS: 'create-tasks', TASK_CONTROL: 'task-control', DELEGATE: 'delegate', + DISCOVER_WORKFLOW_CONTEXT: 'discover-workflow-context', EVAL_SETUP_WITH_AGENT: 'eval-setup-with-agent', EVAL_DATA: 'eval-data', COMPLETE_CHECKPOINT: 'complete-checkpoint', @@ -39,6 +40,7 @@ export const ORCHESTRATION_TOOL_NAMES = new Set(Object.values(ORCHESTRAT export const ALWAYS_LOADED_TOOL_NAMES = new Set([ ORCHESTRATION_TOOL_IDS.CREATE_TASKS, ORCHESTRATION_TOOL_IDS.DELEGATE, + ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT, DOMAIN_TOOL_IDS.ASK_USER, DOMAIN_TOOL_IDS.CREDENTIALS, DOMAIN_TOOL_IDS.WORKFLOWS,