From 33a5a24ddb2579da871f49c19c071d1f0bf51b55 Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Fri, 24 Apr 2026 17:48:45 -0700 Subject: [PATCH] feat(engine): add workflow.dir, workflow.file, workflow.name template variables Adds three new template variables available in all agent contexts: - {{ workflow.dir }} - absolute path to the workflow YAML's directory - {{ workflow.file }} - absolute path to the workflow YAML file - {{ workflow.name }} - workflow name from the YAML config These enable script agents to resolve co-located scripts relative to the workflow file rather than CWD, which is critical for registry-based workflows where scripts live alongside the YAML in the registry directory. Example: args: - -File - {{ workflow.dir }}/scripts/detect-state.ps1 Available in all context modes (accumulate, last_only, explicit). Empty strings are omitted from context to avoid polluting templates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/conductor/engine/context.py | 25 ++++++++++++++-- src/conductor/engine/workflow.py | 6 +++- tests/test_engine/test_context.py | 47 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/conductor/engine/context.py b/src/conductor/engine/context.py index 82ef4ce..946850f 100644 --- a/src/conductor/engine/context.py +++ b/src/conductor/engine/context.py @@ -78,6 +78,18 @@ class WorkflowContext: workflow_inputs: dict[str, Any] = field(default_factory=dict) """Inputs provided at workflow start.""" + workflow_dir: str = "" + """Directory containing the workflow YAML file (resolved absolute path). + Available in templates as ``{{ workflow.dir }}``.""" + + workflow_file: str = "" + """Absolute path to the workflow YAML file. + Available in templates as ``{{ workflow.file }}``.""" + + workflow_name: str = "" + """Name of the workflow from the YAML config. + Available in templates as ``{{ workflow.name }}``.""" + agent_outputs: dict[str, dict[str, Any]] = field(default_factory=dict) """Outputs from executed agents, keyed by agent name.""" @@ -161,11 +173,20 @@ def build_for_agent( Raises: KeyError: If explicit mode is used and a required (non-optional) input is missing. """ + # Build workflow metadata available in all modes + workflow_meta: dict[str, Any] = {} + if self.workflow_dir: + workflow_meta["dir"] = self.workflow_dir + if self.workflow_file: + workflow_meta["file"] = self.workflow_file + if self.workflow_name: + workflow_meta["name"] = self.workflow_name + # For explicit mode, start with empty workflow inputs # For other modes, include all workflow inputs if mode == "explicit": ctx: dict[str, Any] = { - "workflow": {"input": {}}, + "workflow": {"input": {}, **workflow_meta}, "context": { "iteration": self.current_iteration, "history": self.execution_history.copy(), @@ -176,7 +197,7 @@ def build_for_agent( self._add_explicit_input(ctx, input_ref) else: ctx = { - "workflow": {"input": self.workflow_inputs.copy()}, + "workflow": {"input": self.workflow_inputs.copy(), **workflow_meta}, "context": { "iteration": self.current_iteration, "history": self.execution_history.copy(), diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 484192b..fef2d3c 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -308,7 +308,11 @@ def __init__( self.config = config self.skip_gates = skip_gates self.workflow_path = workflow_path - self.context = WorkflowContext() + self.context = WorkflowContext( + workflow_dir=str(Path(workflow_path).resolve().parent) if workflow_path else "", + workflow_file=str(Path(workflow_path).resolve()) if workflow_path else "", + workflow_name=config.workflow.name, + ) self.renderer = TemplateRenderer() self.router = Router() self.limits = LimitEnforcer( diff --git a/tests/test_engine/test_context.py b/tests/test_engine/test_context.py index 0cbc8d6..0b1b52d 100644 --- a/tests/test_engine/test_context.py +++ b/tests/test_engine/test_context.py @@ -30,6 +30,9 @@ def test_init_default_values(self) -> None: assert ctx.agent_outputs == {} assert ctx.current_iteration == 0 assert ctx.execution_history == [] + assert ctx.workflow_dir == "" + assert ctx.workflow_file == "" + assert ctx.workflow_name == "" def test_set_workflow_inputs(self) -> None: """Test setting workflow inputs.""" @@ -151,6 +154,50 @@ def test_last_only_mode_empty_history(self) -> None: assert "context" in agent_ctx +class TestWorkflowContextMetadata: + """Tests for workflow metadata (dir, file, name) in context.""" + + def test_workflow_dir_file_name_in_accumulate_context(self) -> None: + """Test workflow.dir, workflow.file, workflow.name available in accumulate mode.""" + ctx = WorkflowContext( + workflow_dir="/home/user/workflows", + workflow_file="/home/user/workflows/main.yaml", + workflow_name="my-workflow", + ) + ctx.set_workflow_inputs({"key": "val"}) + + agent_ctx = ctx.build_for_agent("agent", [], mode="accumulate") + + assert agent_ctx["workflow"]["dir"] == "/home/user/workflows" + assert agent_ctx["workflow"]["file"] == "/home/user/workflows/main.yaml" + assert agent_ctx["workflow"]["name"] == "my-workflow" + assert agent_ctx["workflow"]["input"] == {"key": "val"} + + def test_workflow_metadata_in_explicit_mode(self) -> None: + """Test workflow.dir/file/name available in explicit mode (not filtered).""" + ctx = WorkflowContext( + workflow_dir="/registry/twig", + workflow_file="/registry/twig/sdlc.yaml", + workflow_name="twig-sdlc", + ) + + agent_ctx = ctx.build_for_agent("agent", [], mode="explicit") + + assert agent_ctx["workflow"]["dir"] == "/registry/twig" + assert agent_ctx["workflow"]["file"] == "/registry/twig/sdlc.yaml" + assert agent_ctx["workflow"]["name"] == "twig-sdlc" + + def test_empty_metadata_omitted(self) -> None: + """Test that empty workflow metadata fields are not included.""" + ctx = WorkflowContext() + + agent_ctx = ctx.build_for_agent("agent", [], mode="accumulate") + + assert "dir" not in agent_ctx["workflow"] + assert "file" not in agent_ctx["workflow"] + assert "name" not in agent_ctx["workflow"] + + class TestWorkflowContextExplicitMode: """Tests for explicit context mode."""