Skip to content

feat(cli): ephemeral node-tool executor for agents (AGENT-26 1/4)#28882

Open
DeveloperTheExplorer wants to merge 6 commits inton8n-agentsfrom
agent-26-pr1-backend-foundation
Open

feat(cli): ephemeral node-tool executor for agents (AGENT-26 1/4)#28882
DeveloperTheExplorer wants to merge 6 commits inton8n-agentsfrom
agent-26-pr1-backend-foundation

Conversation

@DeveloperTheExplorer
Copy link
Copy Markdown
Contributor

Summary

Part 1 of 4 of the AGENT-26 "manual tool management UI" scope. Backend-only foundation — no user-visible UI in this PR. Merges into n8n-agents.

  • EphemeralNodeExecutor — runs a single node's execute or supplyData path against an in-memory Workflow + ExecuteContext / SupplyDataContext, with credential resolution scoped to the agent's project.
  • resolveNodeTool in node-tool-factory — converts a persisted node-tool ref into a runnable BuiltTool that routes through the ephemeral executor, including at-registration-time schema introspection for native LangChain tools so the LLM-facing schema matches what tool.invoke(args) zod-parses against.
  • Shared SUPPORTED_WORKFLOW_TOOL_TRIGGERS in @n8n/api-types — one source of truth for the backend compatibility check (workflow-tool-factory.ts) and the PR 4 frontend Available-list pre-filter. A compile-time assertion keeps the map in lockstep.

PR stack

  1. This PR — Backend foundation (~830 additions)
  2. agent-26-pr2-fe-plumbing — FE composables + missing-creds chip
  3. agent-26-pr3-config-modal-sidebar — config modal + sidebar tool list
  4. agent-26-pr4-tools-browse-modal — browse modal + "Add tool" button

Merge order: 1 → 2 → 3 → 4. Each PR retargets to n8n-agents once its parent lands.

Related Linear tickets

https://linear.app/n8n/issue/AGENT-26

Test plan

  • ephemeral-node-executor.test.ts — 22 tests covering credential resolution, verification, validation, direct-execute routing, and supplyData routing (invocation + schema introspection).
  • node-tool-factory.test.ts — 3 tests for tool-name sanitisation.
  • pnpm typecheck clean for files touched by this PR.

Foundation for letting agents invoke node tools without persisting a
workflow or execution. Adds:

- `EphemeralNodeExecutor` — runs a single node's `execute` or
  `supplyData` path against an in-memory Workflow + ExecuteContext /
  SupplyDataContext, with credential resolution scoped to the agent's
  project.
- `resolveNodeTool` in `node-tool-factory` — converts a persisted
  node-tool ref into a runnable `BuiltTool` that routes through the
  ephemeral executor, including at-registration-time schema introspection
  for native LangChain tools so the LLM-facing schema matches the schema
  the tool actually validates against.
- Shared `SUPPORTED_WORKFLOW_TOOL_TRIGGERS` in `@n8n/api-types` — one
  source of truth for the backend compatibility check
  (`workflow-tool-factory.ts`) and the upcoming frontend Available-list
  pre-filter.

Part of AGENT-26 scope breakdown. No user-visible UI in this PR.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

@n8n-assistant n8n-assistant Bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Apr 22, 2026
DeveloperTheExplorer and others added 2 commits April 22, 2026 12:46
… part 1/4)

- `@n8n/api-types/src/index.ts`: the existing `export type * from './agents'`
  re-exported types, and the follow-up `export { SUPPORTED_WORKFLOW_TOOL_TRIGGERS }`
  re-exported the value — but TypeScript treats the const's type as exported twice
  (once via the type-only re-export, once via the value re-export). Collapse to a
  single `export * from './agents'` now that the file carries both types and a
  value export.
- `@n8n/agents/src/runtime/title-generation.ts:144`: `||` → `??`. The downstream
  use is `emoji && { emoji }` (line 200), so collapsing `''` to `undefined` vs
  keeping `''` is indistinguishable, and `??` is preferred by the lint rule.
@DeveloperTheExplorer DeveloperTheExplorer marked this pull request as ready for review April 22, 2026 12:40
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/modules/agents/tools/node-tool-factory.ts">

<violation number="1" location="packages/cli/src/modules/agents/tools/node-tool-factory.ts:89">
P1: Normalize introspected Zod schemas to a top-level object before returning them. Returning raw `zodToJsonSchema()` can produce top-level `oneOf`, which breaks Anthropic tool registration.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Agent as Agent Runtime
    participant Service as AgentsService
    participant Factory as node-tool-factory
    participant Executor as EphemeralNodeExecutor
    participant Registry as NodeTypes Registry
    participant DB as Credentials / Shared DB

    Note over Agent,DB: Tool Resolution & Introspection (Registration Time)

    Agent->>Service: loadTools()
    Service->>Factory: NEW: resolveNodeTool(ref, projectId)
    
    opt NEW: Schema Introspection (for supplyData nodes)
        Factory->>Executor: introspectSupplyDataToolSchema(nodeConfig)
        Executor->>Registry: getByNameAndVersion()
        Registry-->>Executor: nodeType
        Executor->>Executor: buildEphemeralContextParts()
        Note right of Executor: Creates in-memory Workflow<br/>& SupplyDataContext
        Executor->>Executor: nodeType.supplyData()
        Executor-->>Factory: Zod/JSON Schema
    end
    Factory-->>Agent: BuiltTool (with sanitized name)

    Note over Agent,DB: Tool Invocation (Execution Time)

    Agent->>Factory: tool.handler(input)
    Factory->>Executor: NEW: executeInline(request)
    
    Executor->>Executor: isUsableAsAgentTool() validation
    
    alt NEW: Resolve & Verify Credentials
        Executor->>DB: findCredentialsForProject(projectId)
        DB-->>Executor: credentials
        Executor->>Executor: Check access & type compatibility
        Note right of Executor: Throws if missing or unauthorized
    end

    Executor->>Registry: getByNameAndVersion()
    Registry-->>Executor: nodeType

    alt Path A: Native Tool (supplyData)
        Executor->>Executor: invokeSupplyDataTool()
        Executor->>Executor: call supplyData() via SupplyDataContext
        Executor->>Executor: langchainTool.invoke(input)
        Note right of Executor: In-memory execution (no DB records)
    else Path B: Standard Node (execute)
        Executor->>Executor: executeNodeDirectly()
        Executor->>Executor: call execute() via ExecuteContext
    end

    alt Success
        Executor-->>Factory: NodeExecutionResult (success)
        Factory-->>Agent: Data
    else NEW: Important Unhappy Path (Error/Blacklist)
        Executor-->>Factory: NodeExecutionResult (error)
        Note left of Executor: e.g. "sendAndWait" not supported
        Factory-->>Agent: Error Response
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/cli/src/modules/agents/tools/node-tool-factory.ts Outdated
zodToJsonSchema() emits top-level anyOf/allOf or non-object roots for
z.union, z.discriminatedUnion, z.intersection, z.array and primitive
Zod types. Anthropic's tool input_schema requires { type: "object" }
at the root — any other shape breaks tool registration.

Add normalizeToObjectSchema() in node-tool-factory: pass through object
schemas as-is, merge allOf-of-objects losslessly, and fall back to an
empty object for roots that can't be expressed as an object.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/modules/agents/tools/node-tool-factory.ts">

<violation number="1" location="packages/cli/src/modules/agents/tools/node-tool-factory.ts:88">
P2: Union/discriminated-union tool schemas are flattened to an empty object, which loses all input-field guidance for the LLM.

(Based on your team's feedback about Anthropic tool schemas requiring a top-level object without discarding union shape.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/cli/src/modules/agents/tools/node-tool-factory.ts
…GENT-26 part 1/4)

The previous normalizer lifted `type: 'object'` into place for z.intersection
(merged allOf of objects) but collapsed every other non-object root — most
importantly `z.union` / `z.discriminatedUnion` — to `{ type: 'object',
properties: {} }`. For a tool whose runtime schema is an action-keyed
discriminated union (common in MCP tools), the LLM was handed an empty
object and lost every per-branch field hint.

Rework:
- `anyOf` / `oneOf` of objects: hoist `type: 'object'` to the root but keep
  the union. JSON Schema permits both simultaneously and Anthropic accepts
  the shape, so per-branch field lists stay intact.
- Nested non-object members inside a union are normalized recursively —
  primitive branches get wrapped in `{ input: <schema> }` so they survive
  as valid object members rather than being dropped.
- `type: 'array'` or a primitive root: wrap in `{ input: <schema> }` with
  `required: ['input']`, matching the base LangChain `Tool` convention the
  handler already uses.
- Existing allOf-merge path and the last-resort empty-object fallback
  (for allOf-of-primitives and unknown roots) are preserved.

Tests updated: the old assertions pinned the lossy behaviour (anyOf/oneOf
"falls back to empty object"). They now assert union preservation, nested
normalization, and primitive/array wrapping; allOf merging + dedup
coverage is retained.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

Performance Comparison

Comparing currentlatest master14-day baseline

Memory consumption baseline with starter plan resources

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
memory-heap-used-baseline 114.47 MB 114.27 MB 114.47 MB (σ 0.24) +0.2% -0.0%
memory-rss-baseline 344.55 MB 348.40 MB 289.80 MB (σ 40.90) -1.1% +18.9% ⚠️

docker-stats

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
docker-image-size-runners 388.00 MB 386.00 MB 392.13 MB (σ 11.18) +0.5% -1.1%
docker-image-size-n8n 1290.24 MB 1269.76 MB 1273.60 MB (σ 10.49) +1.6% +1.3% ⚠️

Idle baseline with Instance AI module loaded

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
instance-ai-heap-used-baseline 187.33 MB 186.50 MB 186.43 MB (σ 0.25) +0.4% +0.5% 🔴
instance-ai-rss-baseline 348.56 MB 392.12 MB 368.35 MB (σ 22.82) -11.1% -5.4%
How to read this table
  • Current: This PR's value (or latest master if PR perf tests haven't run)
  • Latest Master: Most recent nightly master measurement
  • Baseline: Rolling 14-day average from master
  • vs Master: PR impact (current vs latest master)
  • vs Baseline: Drift from baseline (current vs rolling avg)
  • Status: ✅ within 1σ | ⚠️ 1-2σ | 🔴 >2σ regression

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/modules/agents/tools/node-tool-factory.ts">

<violation number="1" location="packages/cli/src/modules/agents/tools/node-tool-factory.ts:111">
P2: `normalizeToObjectSchema` misses schemas where `type` is an array (`string[]`), causing valid multi-type roots to degrade to an empty object schema.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/cli/src/modules/agents/tools/node-tool-factory.ts Outdated
…ation (AGENT-26 part 1/4)

JSON Schema's `type` can be a single string or an array of strings — e.g.
`{ type: ['string', 'null'] }`, which `zodToJsonSchema` emits for
`z.string().nullable()` / `z.union([z.string(), z.null()])` in its
nullable-as-type-union mode. The previous check (`typeof schema.type ===
'string'`) missed that shape, so nullable primitive roots degraded to an
empty `{ type: 'object', properties: {} }` instead of being wrapped in
`{ input: <schema> }`.

- Accept both `typeof schema.type === 'string'` and `Array.isArray(schema.type)`
  in the primitive/array wrap path.
- Also drop the redundant `schema.type === 'array'` disjunct — `typeof
  'array' === 'string'` already subsumes it.
- New test covers `{ type: ['string', 'null'] }`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant