Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions src/node/agent/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -2372,5 +2375,6 @@ return {
run = run,
_test = {
build_agent_context_config = build_agent_context_config,
process_multiple_inputs = process_multiple_inputs,
}
}
46 changes: 46 additions & 0 deletions src/node/agent/node_context_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input key="lead">', 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('<input key="brief">', 1, true) ~= nil)
test.is_true(input_data:find('<input key="empty">', 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('<input key="context">', 1, true) == nil)
end)
end)
end

return { run_tests = test.run_cases(define_tests) }
26 changes: 26 additions & 0 deletions src/node_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down