Code Node AI Authoring — Simplified Plan
Goal: Let a user describe the result they want and get a working, typed Code Node — instead of writing JavaScript or hunting the node catalog for a chain of type-compatible nodes. Describing the result is faster than finding and wiring the chain.
Nothing is removed. The Code Node keeps every capability it has today. AI authoring is an additional way to write the code, not a restricted variant of the node.
Design rule: No new persistence, no new node state, no contract layer.
Generation fills the fields the node already has (code, dynamic_inputs,
dynamic_outputs, dynamic_properties) and then gets out of the way. A
generated node is indistinguishable from a hand-written one.
What exists today
The platform already has almost everything this feature needs. Two recent changes removed most of what the earlier draft of this plan had to build:
- Typed dynamic slots are persisted.
DynamicSlotMeta(packages/protocol/src/graph.ts) carries type, description, default, and required for each dynamic input;dynamic_outputscarries a type per output. Both round-trip throughreactFlowNodeToGraphNode.ts/graphNodeToReactFlowNode.ts. There is nothing left to invent for “stable typed handles” — the generator just writes these fields. - The sandbox grew a real standard library.
js-sandbox.tsnow bridgesformat.*(Intl number/date/relative-time/list),data.parseCsv(papaparse),data.selectHtml(cheerio),crypto.*(WebCrypto),progress(), binary helpers (toBase64/fromBase64/toHex/fromHex,utf8Encode/utf8Decode), extendedworkspace(bytes/stat/mkdir/remove), and per-run limit overrides. The old gap — prompts advertising libraries the sandbox lacked — is now mostly a prompt bug, not a missing capability.
| Existing capability | Where | How this plan uses it |
|---|---|---|
| Universal JS Code Node | packages/code-nodes/src/nodes/code-node.ts (nodetool.code.Code) |
The only runtime node. Unchanged. |
| QuickJS sandbox + bridges | packages/agents/src/js-sandbox.ts |
Export a serializable capability manifest; derive the generation prompt from it. |
| Typed dynamic inputs/outputs | DynamicSlotMeta in protocol, persisted by both graph converters |
Generation output lands here. No new fields. |
| AST handle inference | web/src/utils/codeOutputInference.ts |
Untouched. Handles keep tracking code exactly as today, for generated and hand-written code alike. |
| Monaco editor | web/src/components/node_types/CodeBody.tsx |
Untouched. Ask AI is a button beside it. |
| Snippets | web/src/config/codeSnippets.ts, snippetMetadata.ts |
Untouched. |
| Model preferences | ModelPreferencesStore.ts generic defaults map |
Add a code_model key. No schema change. |
| Planner infrastructure | GraphPlanner + SubmitGraphTool, Tool base class, withAgentSpanGen, eval harness (packages/agents/src) |
Code generation is a mini-planner on this machinery — submit tool, validation-as-tool-result feedback rounds, tracing, evals. |
| Provider abstraction | BaseProvider.generateMessageTraced in packages/runtime |
Reused via the planner loop. |
| Single-node / inline execution | useRunSingleNode.ts, runInlineGraphJob.ts |
Preview runs through the normal execution path. |
| Agent serving path | packages/websocket/src/agent/ (runs GraphPlanner today) |
First choice for serving CodePlanner; a thin tRPC mutation is the fallback. |
What the earlier draft built that this plan drops
The previous version of this plan added an explicit “contract” layer on top of
the node: a versioned CodeTransformationArtifact with schema, authoring-policy
and sandbox-runtime versions, ui_properties.code_contract with port ordering,
ui_properties.code_transform authoring history, contract-precedence rules that
disabled AST inference, a contract editor with edge-migration dialogs, runtime
contract enforcement in the kernel, and a three-table publish/version/draft
store. All of it is dropped:
- No contract. Typed handles are
dynamic_inputs/dynamic_outputs, which already persist. Record insertion order is the display order. If the user edits the code and the returned keys change, handles update through the existing inference path — same as today. No precedence rules, no mismatch dialogs, no “Update Contract / Revert” flow. - No artifact versioning. The generation response is transient. Nothing is saved on the node except the standard fields. No authoring history, no policy versions, no original-intent storage, nothing new in workflow export.
- No authoring policy engine. The prompt is derived from the sandbox manifest, plus a few lines of guidance (below). Static analysis checks syntax and declared outputs — it does not classify code as “transformation” vs “not”.
- No publishing / reusable definitions. Deferred entirely (see Later).
- No runtime type enforcement, no saved test cases, no snippet folding. Deferred.
Product scope
The generator writes code for the shapes users actually ask for: reshape,
merge/join, compute/derive, extract/parse, split, format/render, validate,
seed. Ordinary conditional logic (if/else, switch, ternaries, early
returns) is normal JavaScript and unrestricted. With the extended sandbox,
CSV parsing, HTML extraction, date/number formatting, and hashing are all
in scope without any library discussion.
Prompt guidance (guidance, not enforcement):
- Prefer pure data reshaping; use
fetch,workspace, orgetSecretonly when the instruction asks for it. - Don’t use
stateoryieldunless asked — they exist for streaming and are noise in a one-shot transformation. - Emit every declared output on every return path. Conditional emission is
workflow branching; point the user at
nodetool.control.If/Switchinstead of generating it. - Preserve media/asset reference objects; never invent base64 conversions.
Out of scope (unchanged from before): calling arbitrary NodeTool nodes from
JS, replacing subgraphs/workflow nodes, new iteration models, user-installed
packages, and AI authoring for the other nodetool.code.* executors
(ExecutePython, ExecuteBash, …) — those run real interpreters with a
fixed stdout/stderr shape and need their own generator later.
Design
One generation service, built as a mini-planner
Don’t hand-roll a request/repair function. GraphPlanner
(packages/agents/src/graph-planner.ts + tools/submit-graph-tool.ts)
already implements the exact loop this feature needs: a system prompt, a
forced submit tool, validation run inside the tool, and failures
returned as tool results so the model resubmits over feedback rounds — with
withAgentSpanGen tracing, abort linking, retry caps, and an eval harness
attached. Code generation is the same shape with a smaller payload.
Add a CodePlanner beside graph-planner.ts with one
SubmitCodeTool (the submit_code analog of submit_graph):
- The planner accepts: instruction, input slots (name, type, description — seeded from connected handles), optional expected output (name + type, when launched from a destination handle), optional current code + slots (for edits), optional user-approved sample values, provider, and model.
- The prompt is built from the sandbox capability manifest and the guidance above.
-
The model must call
submit_code, whose arguments are:interface CodeGenSubmission { title: string; // imperative, becomes the node title summary: string; // one sentence code: string; inputs: { name; type; description?; default?; required? }[]; outputs: { name; type; description? }[]; } SubmitCodeToolvalidates the arguments with Zod (valid JS identifiers, NodeTool type metadata shapes, unique names, ≥1 output, bounded sizes), parses the code with acorn, and checks that every declared output is assigned on every statically visible return path. Failures come back as the tool result — same feedback-round mechanism assubmit_graph— with rounds capped at 3.- The planner returns the accepted submission or a discriminated error (provider unavailable, aborted, no valid submission, internal).
Free text and code fences are never scraped; only the accepted tool call’s
arguments count. Prompt building and analysis stay pure functions in
packages/agents/src/code-gen/ so they test without a server, and the
Tool base class, provider handling, and tracing come from the existing
planner infrastructure rather than new code.
The editor chat agent is not the transport: the dialog needs an atomic
validated result with no thread state. But because CodePlanner is an
ordinary planner, the chat agent can later invoke it as a tool without a
second implementation.
Telemetry keeps provider, model, tokens, cost, duration, and failure code; it does not record instructions, samples, or generated code.
Applying a result
The frontend applies an accepted submission as one undoable graph action:
data.code← code, node title ← title.data.dynamic_inputs← input slots (asDynamicSlotMeta),data.dynamic_outputs← output slots, defaults intodynamic_properties.- When launched from a handle, create the edge(s) after
isConnectablepasses. - On cancel or failure, no node or edge is left behind.
That’s the whole persistence story. Reload shows the same typed handles because the protocol already round-trips them.
Samples stay local by default
Preview runs the node through runInlineGraphJob with the latest connected
values (or hand-edited samples) — locally, through the normal execution
path. Sample values are only included in the model prompt when the user
ticks “include sample values”, and the exact payload is shown before sending.
By default the model sees names and types only. This replaces the earlier
consent/redaction machinery with a checkbox.
Model selection
Add code_model to the existing ModelPreferencesStore.defaults map.
Resolution, centralized in useCodeAuthoringModel:
code_modelpreference, if set.- Active global chat model.
- Default
language_modelpreference. - Application default.
Skip any candidate without tool support (the submission is a tool call);
thread the existing requireToolSupport filter through
LanguageModelSelect → LanguageModelMenuDialog, which don’t accept it
yet. Never store the model on the node.
UI
- Ask AI button on the Code Node body (
CodeBody.tsx), gated onisCodeNode(nodeType)—nodetool.code.Codeonly, notisCodeBodyNode, which matches all 19nodetool.code.*executors. - One dialog (
CodeGenDialog.tsx): instruction field, detected typed inputs, expected output when known, effective model (with a link to change the preference), Generate, result review (title, summary, handles, collapsed code view), preview panel, Apply. Monaco lazy-loads only when the code view opens. Keyboard operable;aria-livefor generation and preview status; focus returns to the opener on close. - Entry points:
- A searchable “Write Code with AI” palette entry (aliases: transform,
convert, reshape, merge, join, split, extract, parse, format, compute,
validate — following the
snippetMetadata.tssearch-term pattern). - “Transform this output…” on the output-handle context menu.
- “Create value with AI…” on the input-handle context menu, seeding the expected output name and type.
- A searchable “Write Code with AI” palette entry (aliases: transform,
convert, reshape, merge, join, split, extract, parse, format, compute,
validate — following the
No compact summary body, no contract editor, no mode indicator. A generated node renders and edits exactly like any Code Node.
Tasks
1. Sandbox capability manifest
Files: packages/agents/src/js-sandbox.ts, prompt construction,
useChatIntegration.ts.
- Export one serializable manifest of the sandbox surface — bridges, guest
helpers, limits — built from the module’s own exports, never restating
values. Scope it to
nodetool.code.Code; the other executors don’t share this runtime. - Derive the generation prompt and Code Node help text from the manifest.
- Fix
useChatIntegration.ts, which still advertises_(lodash),dayjs, and a globalcheerio— none exist as guest globals. CSV and HTML work goes throughdata.parseCsv/data.selectHtml; formatting throughformat.*. - Add a test that fails when authoring instructions name an API the manifest doesn’t contain.
2. Transport schemas
Files: packages/protocol/src/api-schemas/code-gen.ts + tests.
- Zod schemas for request,
CodeGenSubmission, and the error union. - Identifier-safe port names, NodeTool type metadata shapes, unique names per direction, ≥1 output, size limits on instruction/ports/samples/code.
- Transport validation only — no JS parser in protocol.
3. Generation core (CodePlanner)
Files: packages/agents/src/code-planner.ts,
packages/agents/src/tools/submit-code-tool.ts,
packages/agents/src/code-gen/{prompt,analyze}.ts + scripted-provider
tests; add acorn to the agents package.
- Follow the
GraphPlanner/SubmitGraphToolpattern: extend theToolbase class, validate inside the tool, return failures as tool results, cap feedback rounds at 3, trace withwithAgentSpanGen, link aborts with the existing helper. - Prompt builder: manifest + guidance + inputs + expected output + optional
samples. Only the accepted
submit_codecall’s arguments count; free text and code fences are ignored. - acorn analysis: syntax, every declared output assigned on every visible
return path. Ternaries,
if/else, partitions into always-emitted outputs all pass; a branch omitting a declared output fails with a pointer to the control nodes. - Typed failure codes on the planner result.
4. Serving path
Files: packages/websocket/src/agent/ or a thin
trpc/routers/code-gen.ts, tests.
- First choice: serve
CodePlannerthrough the existing agent WebSocket path (packages/websocket/src/agent/), which already handles provider resolution, secrets, cancellation, and progress streaming forGraphPlanner. Check whether a one-shot request/response fits that channel cleanly. - Fallback if it doesn’t: a thin protected tRPC mutation that instantiates
CodePlanner— glue only, no generation logic in the router. - Either way: suppress instruction/sample/code bodies from spans and failure logs while keeping usage metadata; per-user in-flight cap with a stable rate-limited error; no chat thread, no message history writes.
5. Model preference
Files: ModelPreferencesStore consumers, DefaultModelsMenu.tsx,
LanguageModelSelect.tsx, LanguageModelMenuDialog.tsx,
useCodeAuthoringModel.ts + tests.
code_modelpreference labeled “Code Generation”, empty state “Use chat model”; fallback chain in the hook;requireToolSupportthreaded through the two select components; blocking selector with a Settings link when no tool-capable model resolves.
6. Dialog and apply
Files: web/src/components/node_types/code_gen/CodeGenDialog.tsx,
web/src/serverState/codeGen.ts, a NodeStore apply action, CodeBody.tsx
integration, component tests.
- The dialog described above; TanStack mutation; single undoable apply that sets code, slots, defaults, title, and edges; accessible states for loading, error, aborted, and offline.
7. Graph entry points
Files: palette config, handle context menus, connection handlers, tests.
- The three entry points above, seeding slot names/types from handle metadata; edges only after validation; collision suffixes for duplicate port names; nothing left behind on cancel.
8. Preview
Files: preview panel in the dialog, sample adapter, tests.
- Latest connected values where available, type-aware sample editors,
“from latest run” vs “manual” labels; run via
runInlineGraphJob; show each named output, logs, duration, errors; warn when an output value doesn’t match its declared type; preview state keyed by job id. - “Include sample values” checkbox with the exact payload shown before the first send.
9. Verification and rollout
- Unit tests: schemas, prompt, analysis, repair, model fallback, apply round-trip.
- E2E: source output → generate → preview → connect → save → reload → run; and the destination-handle variant.
- A
code-genlive-provider eval suite besidegraph-planner, registered withnodetool eval(--provider,--model,--cases,--min-success), reporting first-pass and post-repair acceptance separately. Scripted-provider tests cover mechanics, not model quality. - Feature flag in the web config layer until the E2E journeys and the eval suite pass ≥90% (post-repair) on two configured providers.
npm run typecheck,npm run lint,npm run test;npm run build:packageswhen decorator packages change.
Exit criteria
- A user can go from “I have these values” to a connected, typed, working node without opening Monaco.
- Typed handles survive save/reload (already guaranteed by the protocol — covered by the apply round-trip test).
- No generation path leaves a partially configured node or dangling edge.
- Existing Code Nodes, snippets, inference, and every other
nodetool.code.*node behave exactly as before.
Phase 2 — Edit and repair (after Phase 1 ships)
- Edit: the same planner call with the current code + slots attached; the model returns a full replacement submission. Confirm before applying an edit that removes, renames, or retypes a connected handle (show affected edges: keep, remap, or disconnect). Per-dialog transcript in UI state only.
- Repair: after a preview or run failure, offer one-click Repair — the edit call with the error and the failing (local) sample attached. Preview before replacing working code; keep the last working code until accepted.
- Connection adapter: when a connection fails only on type incompatibility, offer “Create adapter with AI” beside the error, and “Insert transformation” on an existing data edge — both pre-fill exact source and destination types.
Later, deliberately unscheduled
- Reusable published definitions with versions (the dropped Phase 3). If demand shows up, it can be added without a contract layer: publish snapshots a node’s code + slots; nothing in this plan blocks it.
- Saved per-node test cases.
- Runtime output-type checking in the kernel.
- Declared per-node capabilities/libraries.
- AI authoring for
ExecutePythonand friends (needs per-runtime manifests — the manifest seam from Task 1 — and a typed-output convention forstdout-shaped nodes).