mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 03:24:59 +02:00
wip
This commit is contained in:
parent
33c6d6fcba
commit
baf50839d3
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {}): 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']);
|
||||
});
|
||||
});
|
||||
|
|
@ -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<DelegateOutput> {
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 — \`<nodeType>\` (+ 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.`;
|
||||
|
|
@ -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<typeof discoverWorkflowContextInputSchema>;
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -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<string>(Object.values(ORCHESTRAT
|
|||
export const ALWAYS_LOADED_TOOL_NAMES = new Set<string>([
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user