-
Notifications
You must be signed in to change notification settings - Fork 51k
feat(ai-builder): Implement Core Subgraph Infrastructure #22325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
…ectionChangingParameters Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
96764fd to
59e64d4
Compare
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Signed-off-by: Oleg Ivaniv <[email protected]>
Signed-off-by: Oleg Ivaniv <[email protected]>
mike12345567
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great and from my experience playing with it, it seems to be helping with generation, I think the discovery agent is doing a better job!
I feel like this really unlocks some great future avenues to test, like using Opus for the discovery agent - or integrating templates in different ways throughout the subgraphs. I can't wait to get this in/get some experiments running on it, great improvement and great work @OlegIvaniv!
| }), | ||
|
|
||
| // Output: Categorization result | ||
| categorization: Annotation<PromptCategorization | undefined>({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is used, looking at the formatOutput it doesn't ever put anything in the categorization field.
| // No tool calls = agent is done (or failed to call tool) | ||
| // In this pattern, we expect a tool call. If none, we might want to force it or just end. | ||
| // For now, let's treat it as an end, but ideally we'd reprompt. | ||
| console.warn('[Discovery Subgraph] Agent stopped without submitting results'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use the Logger rather than console logging directly - theres a few places I've noticed console logs which might need updated.
| TChildState extends StateRecord = StateRecord, | ||
| TParentState extends StateRecord = StateRecord, | ||
| > { | ||
| name: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice - great to define this early!
| - Document Loader defaults to 'json' but MUST be 'binary' when processing files | ||
| - HTTP Request defaults to GET but APIs often need POST | ||
| - Vector Store mode affects available connections - set explicitly (retrieve-as-tool when using with AI Agent) | ||
| </avoid_default_traps> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This closing XML doesn't seem to have a section where it opens.
|
|
||
| // 1. User request (primary) | ||
| if (userRequest) { | ||
| contextParts.push('=== USER REQUEST ==='); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of interest, do you think for the transform input/output it would be easier to read if we did something like:
const context = `=== USER REQUEST ===
${userRequest}
${parentState.discoveryContext?.bestPractices ?? ''}
=== WORKFLOW TO CONFIGURE ===
${buildWorkflowJsonBlock(parentState.workflowJSON)}
`.split('\n');And so on - I feel like this might be a little easier to comprehend/adjust at a glance.
| return 'No nodes in workflow'; | ||
| } | ||
|
|
||
| const nodeList = workflow.nodes.map((n) => `- ${n.name} (${n.type})`).join('\n'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've written some functions in my templates PoC which converts a workflow to mermaid, that might be useful here, a very lightweight format to describe the whole workflow? Just commenting for the future!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was also thinking about having some simplified WF representation, especially for builder subgraph that doesnt really need to see all params. If you already have something implemented that'd be very helpful!
| function createMessageChunk(text: string): AgentMessageChunk { | ||
| return { | ||
| export function cleanContextTags(text: string): string { | ||
| return text.replace(/\n*<current_workflow_json>[\s\S]*?<\/current_execution_nodes_schemas>/, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first part is replacing an open tag, second is replacing a closing tag, is this correct?
| /** Handle delete_messages node update */ | ||
| function processDeleteMessages(update: unknown): StreamOutput | null { | ||
| const typed = update as { messages?: MessageContent[] }; | ||
| if (!typed?.messages?.length) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typed is typed as never being null/undefined, but there is a typed? check here - if it can be undefined we should probably type it as { messages?: MessageContent[] } | undefined
| /** Process a parent graph event */ | ||
| function processParentEvent(event: ParentEvent): StreamOutput | null { | ||
| const [streamMode, chunk] = event; | ||
| if (typeof streamMode !== 'string' || streamMode.length <= 1) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why checking if the the stream mode is a single character then return?
Summary
This PR refactors the AI workflow builder from a single monolithic agent into a multi-agent system with specialized subgraphs. Each subgraph handles one phase of workflow building, coordinated by a supervisor.
Problem
The single-agent approach has limitations:
Solution
Split into four specialized agents, each with focused responsibilities:
Architecture
Supervisor Agent
Routes user requests to the appropriate subgraph based on intent:
The supervisor only runs once at the start. After that, routing is deterministic: discovery → builder → configurator → responder.
Discovery Subgraph
Purpose: Find n8n nodes relevant to the user's request.
Tools:
search_nodes,get_node_details,get_best_practices,submit_discovery_resultsProcess:
Output: List of nodes with
{nodeName, version, reasoning, connectionChangingParameters}The connection-changing parameters are critical—they tell the Builder which parameters affect what inputs/outputs a node has (e.g., Vector Store's
modeparameter changes whether it accepts documents or queries).Builder Subgraph
Purpose: Create workflow structure using discovery results.
Tools:
add_nodes,connect_nodes,remove_node,remove_connection,validate_structureProcess:
Key prompt sections:
The Builder must call
validate_structurebefore finishing to catch missing triggers or invalid connections.Configurator Subgraph
Purpose: Set parameters on all nodes after structure exists.
Tools:
update_node_parameters,get_node_parameter,validate_configurationProcess:
Special handling:
$fromAI('key', 'description', 'type')expressions for AI tool nodesThe Configurator must call
validate_configurationbefore finishing.Responder Agent
Purpose: Synthesize user-facing response from coordination log.
No tools—just a prompt that receives context about what was discovered, built, and configured, then generates a coherent response.
Output format:
State Management
Parent Graph State
Shared state that coordinates between subgraphs:
Coordination Log
Tracks subgraph completion for deterministic routing:
After each subgraph completes,
getNextPhaseFromLog()determines the next step without LLM involvement.Subgraph Isolation
Each subgraph has its own internal state:
DiscoverySubgraphState: userRequest, nodesFound, bestPracticesBuilderSubgraphState: workflowJSON, discoveryContext, workflowOperationsConfiguratorSubgraphState: workflowJSON, instanceUrl, workflowOperationsState transforms at boundaries:
transformInput(): Extract what subgraph needs from parent statetransformOutput(): Return results to update parent stateStreaming
Event Formats
Parent graph events (legacy):
Subgraph events (new):
Filtering
Internal subgraph messages are filtered—users only see:
Messages from discovery/builder/configurator agents are suppressed.
Feature Flag
When
false(default), uses legacy single-agent for backward compatibility.Validation Tools
Two new validation tools enforce correctness:
validate_structure (Builder):
validate_configuration (Configurator):
Subgraphs must call their validation tool before finishing.
Related Linear tickets, Github issues, and Community forum posts
Review / Merge checklist
release/backport(if the PR is an urgent fix that needs to be backported)