From ea9ed60f030af863f660d9b3c2b3ba41afeeb536 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Sat, 8 Aug 2026 13:46:27 -0400 Subject: [PATCH 1/2] fix(agent): treat a nil-content named input as absent An input_transform field whose expression resolves to nil produces an input entry with nil content. The reserved carriers (context, model, agent_id) now read that as "not provided" instead of failing the node, and nil-content inputs render no prompt tag. --- src/node/agent/node.lua | 12 +++++--- src/node/agent/node_context_test.lua | 45 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/node/agent/node.lua b/src/node/agent/node.lua index 919b3da..bf67fda 100644 --- a/src/node/agent/node.lua +++ b/src/node/agent/node.lua @@ -493,8 +493,11 @@ local function build_status_message(iteration, max_iterations, total_tokens, too end local function process_multiple_inputs(inputs) + -- An input entry with nil content is an input that was not provided — an + -- input_transform field whose expression resolved to nil produces exactly + -- this shape. It never counts as a malformed value and never renders a tag. local input_context = nil - if inputs.context then + if inputs.context and inputs.context.content ~= nil then local context_content = inputs.context.content if type(context_content) ~= "table" then return nil, nil, nil, nil, "context must be a table/object" @@ -503,7 +506,7 @@ local function process_multiple_inputs(inputs) end local agent_id_override = nil - if inputs.agent_id then + if inputs.agent_id and inputs.agent_id.content ~= nil then local agent_id_content = inputs.agent_id.content if type(agent_id_content) ~= "string" or agent_id_content == "" then return nil, nil, nil, nil, "agent_id must be a non-empty string" @@ -512,7 +515,7 @@ local function process_multiple_inputs(inputs) end local model_override = nil - if inputs.model then + if inputs.model and inputs.model.content ~= nil then local model_content = inputs.model.content if type(model_content) ~= "string" or model_content == "" then return nil, nil, nil, nil, "model must be a non-empty string" @@ -522,7 +525,7 @@ local function process_multiple_inputs(inputs) local parts = {} for key, input in pairs(inputs) do - if key ~= "context" and key ~= "agent_id" and key ~= "model" then + if key ~= "context" and key ~= "agent_id" and key ~= "model" and input.content ~= nil then local content = input.content if type(content) == "table" then content = json.encode(content) @@ -2372,5 +2375,6 @@ return { run = run, _test = { build_agent_context_config = build_agent_context_config, + process_multiple_inputs = process_multiple_inputs, } } diff --git a/src/node/agent/node_context_test.lua b/src/node/agent/node_context_test.lua index 25f81b6..238f959 100644 --- a/src/node/agent/node_context_test.lua +++ b/src/node/agent/node_context_test.lua @@ -40,6 +40,51 @@ local function define_tests() test.eq(cfg.context.input_only, "visible") end) end) + + describe("agent node reserved inputs", function() + local process = agent_node._test.process_multiple_inputs + + it("treats a nil-content reserved input as absent", function() + -- An input_transform field whose expression resolves to nil produces + -- an entry with nil content; the reserved carriers read it as "not + -- provided", never as a malformed value. + local input_context, agent_id_override, model_override, input_data, err = process({ + context = { content = nil, metadata = {} }, + model = { content = nil, metadata = {} }, + agent_id = { content = nil, metadata = {} }, + lead = { content = { name = "Jane" }, metadata = {} }, + }) + test.is_nil(err) + test.is_nil(input_context) + test.is_nil(agent_id_override) + test.is_nil(model_override) + test.is_true(input_data:find('', 1, true) ~= nil) + end) + + it("renders no input tag for a nil-content input", function() + local _, _, _, input_data, err = process({ + empty = { content = nil, metadata = {} }, + brief = { content = "text", metadata = {} }, + }) + test.is_nil(err) + test.is_true(input_data:find('', 1, true) ~= nil) + test.is_true(input_data:find('', 1, true) == nil) + end) + + it("still merges a table context and applies string overrides", function() + local input_context, agent_id_override, model_override, input_data, err = process({ + context = { content = { kb_ids = { "kb-1" } }, metadata = {} }, + model = { content = "class:fast", metadata = {} }, + agent_id = { content = "ns:researcher", metadata = {} }, + lead = { content = { name = "Jane" }, metadata = {} }, + }) + test.is_nil(err) + test.eq(input_context.kb_ids[1], "kb-1") + test.eq(model_override, "class:fast") + test.eq(agent_id_override, "ns:researcher") + test.is_true(input_data:find('', 1, true) == nil) + end) + end) end return { run_tests = test.run_cases(define_tests) } From acc696dcdeb11072ca9542610d45b13db5d220d2 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Sat, 8 Aug 2026 13:50:35 -0400 Subject: [PATCH 2/2] fix(node): a transform field resolving to nil delivers no input Raw inputs either exist with content or do not exist; input_transform now holds the same contract instead of materializing nil-content entries. --- src/node.lua | 15 ++++++++++----- src/node/agent/node_context_test.lua | 3 ++- src/node_test.lua | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/node.lua b/src/node.lua index 62993a8..bcbb7dc 100644 --- a/src/node.lua +++ b/src/node.lua @@ -307,11 +307,16 @@ function methods:_transform_inputs_with_expr(raw_inputs, transform_config) if err then return nil, "Transform failed for " .. field_name .. ": " .. tostring(err) end - result[field_name] = { - content = content, - metadata = {}, - discriminator = field_name - } + -- A field whose expression resolves to nil is an input that does not + -- exist: consumers see it exactly like an input that was never + -- delivered, matching raw-input presence semantics. + if content ~= nil then + result[field_name] = { + content = content, + metadata = {}, + discriminator = field_name + } + end end return result, nil end diff --git a/src/node/agent/node_context_test.lua b/src/node/agent/node_context_test.lua index 238f959..392f4ca 100644 --- a/src/node/agent/node_context_test.lua +++ b/src/node/agent/node_context_test.lua @@ -79,7 +79,8 @@ local function define_tests() lead = { content = { name = "Jane" }, metadata = {} }, }) test.is_nil(err) - test.eq(input_context.kb_ids[1], "kb-1") + test.not_nil(input_context) + test.eq((input_context :: any).kb_ids[1], "kb-1") test.eq(model_override, "class:fast") test.eq(agent_id_override, "ns:researcher") test.is_true(input_data:find('', 1, true) == nil) diff --git a/src/node_test.lua b/src/node_test.lua index 413932a..241a390 100644 --- a/src/node_test.lua +++ b/src/node_test.lua @@ -428,6 +428,32 @@ local function define_tests() test.eq(inputs["default"].key, "default") end) + it("omits a field whose expression resolves to nil", function() + local args = { + node_id = "test-node-123", + dataflow_id = "test-dataflow-456", + node = { + config = { + input_transform = { + user_name = "inputs.user_data.name", + context = "inputs.user_data.context", + model = "inputs.user_data.model" + } + } + } + } + + local test_node, err = node.new(args, expr_mock_deps) + test.is_nil(err) + test.not_nil(test_node) + + local inputs = test_node:inputs() + test.not_nil(inputs) + test.eq(inputs.user_name.content, "John") + test.is_nil(inputs.context) + test.is_nil(inputs.model) + end) + it("should transform inputs with field mapping", function() local args = { node_id = "test-node-123",