From 6957d4626cf0d7807a3b3db0b0c113fc1c4523f3 Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Fri, 24 Apr 2026 17:26:55 -0700 Subject: [PATCH] fix(engine): ensure workflow.input is available for script and sub-workflow template rendering in explicit mode In explicit context mode, build_for_agent() starts with workflow.input: {} and only populates entries declared in the agent's input: list. Script agents and sub-workflow input_mapping templates that reference workflow.input.X without declaring it in their input: list get an empty dict, causing TemplateError at runtime. Script args and input_mapping are rendered locally (no LLM cost), so workflow inputs must always be available for template resolution regardless of context mode. This fix injects the full workflow_inputs into the template context after build_for_agent() for script and workflow agent types. Fixes: script agents in explicit mode failing with TemplateError: 'dict object' has no attribute '' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/conductor/engine/workflow.py | 13 +++++++++++ tests/test_engine/test_workflow.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 484192b..bc66d3a 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -1399,6 +1399,13 @@ async def _execute_loop(self, current_agent_name: str) -> dict[str, Any]: agent.input, mode=self.config.workflow.context.mode, ) + # Script args are rendered locally (no LLM cost), so + # workflow inputs must always be available for template + # resolution — even in explicit mode where they'd + # otherwise be filtered out. + agent_context.setdefault("workflow", {})["input"] = ( + self.context.workflow_inputs.copy() + ) _script_start = _time.time() # Count how many times this specific script has been executed @@ -1491,6 +1498,12 @@ async def _execute_loop(self, current_agent_name: str) -> dict[str, Any]: agent.input, mode=self.config.workflow.context.mode, ) + # input_mapping templates are rendered locally (no LLM + # cost), so workflow inputs must always be available — + # even in explicit mode. + agent_context.setdefault("workflow", {})["input"] = ( + self.context.workflow_inputs.copy() + ) _sub_start = _time.time() sub_execution_count = ( diff --git a/tests/test_engine/test_workflow.py b/tests/test_engine/test_workflow.py index 14843a4..d3bc5df 100644 --- a/tests/test_engine/test_workflow.py +++ b/tests/test_engine/test_workflow.py @@ -298,6 +298,43 @@ def mock_handler(agent, prompt, context): # Workflow.input.goal should not be in agent2's context since it's not in input list assert "other" not in agent2_context.get("workflow", {}).get("input", {}) + @pytest.mark.asyncio + async def test_explicit_mode_script_gets_workflow_inputs(self) -> None: + """Regression: script agents in explicit mode must see workflow.input. + + In explicit mode, workflow.input was empty {} for script agents that + didn't declare inputs. Script args are rendered locally (no LLM cost), + so workflow inputs must always be available for template resolution. + """ + config = WorkflowConfig( + workflow=WorkflowDef( + name="explicit-script", + entry_point="detector", + context=ContextConfig(mode="explicit"), + ), + agents=[ + AgentDef( + name="detector", + type="script", + command="pwsh", + args=[ + "-Command", + "Write-Output '{{ workflow.input.work_item_id }}'; exit 0", + ], + # No input: list — should still see workflow.input + routes=[RouteDef(to="$end")], + ), + ], + ) + + provider = CopilotProvider(mock_handler=lambda a, p, c: {}) + engine = WorkflowEngine(config, provider) + + await engine.run({"work_item_id": 42}) + + # Script should have rendered the template successfully + assert engine.context.agent_outputs["detector"]["stdout"].strip() == "42" + class TestWorkflowEngineRouting: """Tests for workflow routing."""