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.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..392f4ca 100644
--- a/src/node/agent/node_context_test.lua
+++ b/src/node/agent/node_context_test.lua
@@ -40,6 +40,52 @@ 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.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)
+ end)
+ end)
end
return { run_tests = test.run_cases(define_tests) }
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",