Node settings arrive as keyword arguments, not a params dict

A python node's settings are constants of its own function, so they are passed
the way its ports are: by name. The controller binds them to the compiled
function, the `params` field is gone from the worker and remote protocols, and
a setting sharing a port's name is reported as a node error rather than
shadowing it. The panel's scaffold follows suit and keeps the header in step
with both ports and settings.

The demo's `pace` moves from a flow input to a setting of the training node,
which is what it always was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
This commit is contained in:
2026-08-20 17:47:45 +02:00
co-authored by Claude Opus 5
parent 2552c92a45
commit 3508713e85
21 changed files with 202 additions and 106 deletions
+27 -11
View File
@@ -73,6 +73,9 @@ const SECTION = PANEL_SECTION
/** Settings the engine reads itself, so they are not the author's to name. */
const RESERVED_PARAMS = new Set(["synchronous"])
/** A setting reaches the function by name, so the name has to be one. */
const IDENTIFIER = /^[A-Za-z_]\w*$/
/**
* A text field that offers what is already in use elsewhere.
*
@@ -421,7 +424,9 @@ function FreeParamsForm({
const entries = Object.entries(params).filter(([key]) => !reserved.has(key))
const rename = (from: string, to: string) => {
if (to === from) return
// A setting is an argument of `process`, so a name it cannot take is not
// a rename — it is a typo on its way to a node that will not load.
if (to === from || !IDENTIFIER.test(to)) return
// Rebuilt rather than patched, so the settings keep the order they were
// typed in instead of jumping around as one is renamed.
const next: Record<string, unknown> = {}
@@ -453,7 +458,8 @@ function FreeParamsForm({
{entries.length === 0 ? (
<p className="text-sm text-muted-foreground">
Values your code reads from <code>params</code>.
Constants of this node, passed to <code>process</code> by name like
its inputs.
</p>
) : null}
@@ -855,7 +861,7 @@ const SCAFFOLD_SHAPE = (() => {
const value = Object.values(PLACEHOLDER).map(quote).join("|")
const entry = `"[^"]+": (?:${value})`
return new RegExp(
`^${quote(SCAFFOLD_DOC)}\\n\\n\\ndef process\\(\\w+(?:, \\w+)*\\):\\n return \\{(?:${entry}(?:, ${entry})*)?\\}\\n$`,
`^${quote(SCAFFOLD_DOC)}\\n\\n\\ndef process\\((?:\\w+(?:, \\w+)*)?\\):\\n return \\{(?:${entry}(?:, ${entry})*)?\\}\\n$`,
)
})()
@@ -863,20 +869,30 @@ const SCAFFOLD_SHAPE = (() => {
const portName = (spec: MessageSpec) =>
spec.port || (spec.name ?? "").split(".").pop() || ""
/** A `process` that takes this node's inputs and returns its outputs. */
/**
* A `process` that takes this node's inputs and settings, and returns its
* outputs.
*
* Both arrive by name, so both are arguments — the ports first, in the order
* they are declared, then the settings.
*/
function scaffoldFor(node: NodeDef_Input): string {
const args = [
...new Set(
(node.requires ?? [])
.map(portName)
// Anything else cannot be a keyword argument, so it cannot be a port.
.filter((port) => /^[A-Za-z_]\w*$/.test(port) && port !== "params"),
[
...(node.requires ?? []).map(portName),
...Object.keys(node.params ?? {}).filter(
(name) => !RESERVED_PARAMS.has(name),
),
]
// Anything else cannot be a keyword argument, so it cannot be one.
.filter((name) => IDENTIFIER.test(name)),
),
]
const returns = (node.provides ?? [])
.filter((spec) => portName(spec))
.map((spec) => `"${portName(spec)}": ${PLACEHOLDER[spec.dtype ?? "float"]}`)
return `${SCAFFOLD_DOC}\n\n\ndef process(${[...args, "params"].join(
return `${SCAFFOLD_DOC}\n\n\ndef process(${args.join(
", ",
)}):\n return {${returns.join(", ")}}\n`
}
@@ -998,13 +1014,13 @@ function PanelBody({
type={node.type}
schema={nodeType?.params_schema}
params={node.params ?? {}}
onChange={(params) => onChange({ ...node, params })}
onChange={(params) => editNode({ ...node, params })}
/>
{nodeType?.free_params ? (
<FreeParamsForm
params={node.params ?? {}}
reserved={RESERVED_PARAMS}
onChange={(params) => onChange({ ...node, params })}
onChange={(params) => editNode({ ...node, params })}
/>
) : null}
{hasSource ? (
+2 -2
View File
@@ -101,12 +101,12 @@ test("running a flow puts values on its edges", async ({ page }) => {
await setNodeSource(
page,
"python",
'def process(params):\n return {"reading": 42.0}\n',
'def process():\n return {"reading": 42.0}\n',
)
await setNodeSource(
page,
"python_2",
"def process(reading, params):\n return {}\n",
"def process(reading):\n return {}\n",
)
await page.reload()
+2 -2
View File
@@ -12,11 +12,11 @@ test.use({ storageState: "playwright/.auth/user.json" })
test.describe.configure({ mode: "serial" })
const PRINTING_NODE = `def process(params):
const PRINTING_NODE = `def process():
print("sensor read 21.5 degrees")
return {"reading": 21.5}
`
const BROKEN_NODE = `def process(reading, params):
const BROKEN_NODE = `def process(reading):
raise RuntimeError("downstream blew up")
`