Skip to content

Fail on unresolved template references instead of writing literal {{ ... }} and exiting 0 #153

Description

@jeremymanning

Systematic Template Rendering Issues Across Pipeline Tools

Problem Statement

Multiple pipelines are experiencing consistent issues with template placeholders not being properly rendered in outputs. This affects:

  • research_basic.yaml - Final reports contain {{variable}} placeholders
  • research_advanced_tools.yaml - Outputs are entirely placeholder text
  • filesystem operations - Template variables not resolved at runtime
  • Model prompts - Step results not accessible in templates

Current commit: a62433e

Current Approach Analysis

1. Template Rendering Lifecycle

The current system uses Jinja2 templating at multiple stages:

Pipeline YAML → Compiler (compile-time) → Runtime Execution → Output

Key Issues:

  • Compile-time vs Runtime: Templates in YAML are rendered at compile time, but step results only exist at runtime
  • Context Propagation: Step results are stored but not consistently made available to subsequent steps
  • Tool-specific Handling: Each tool handles templates differently, leading to inconsistencies

2. Current Implementation Points

a) Compiler Level (src/orchestrator/compiler/)

# Templates are rendered during YAML compilation
compiled_pipeline = compiler.compile(pipeline_yaml)
  • Templates like {{topic}} are resolved here
  • But {{step_id.result}} references don't exist yet

b) Execution Level (src/orchestrator/executor/)

# Step results stored in state
state.set_step_result(step_id, result)
  • Results are stored but not automatically injected into template contexts

c) Tool Level (src/orchestrator/tools/)

  • Each tool independently handles parameter templating
  • No standardized approach for accessing previous step results

3. Specific Problem Areas

  1. Filesystem Write Operations

    • Content parameter contains unrendered templates
    • Example: "{{compile_report.result}}" written literally to files
  2. Model Prompts

    • Templates reference future steps at compile time
    • Example: {{extract_key_points.result}} undefined during compilation
  3. Report Generation

    • Complex nested templates fail to render
    • Example: {{ search_topic.results[0].title }} when results is empty

Proposed Solution: Infrastructure-Level Template Management

Design Principles

  1. Lazy Evaluation: Defer template rendering until execution time when all context is available
  2. Unified Context: Maintain a global template context accessible to all tools
  3. Graceful Defaults: Handle missing values without breaking execution
  4. Type Safety: Preserve object structures for complex template expressions

Implementation Plan

Phase 1: Core Infrastructure Changes

  1. Create Template Manager (src/orchestrator/core/template_manager.py)
class TemplateManager:
    def __init__(self):
        self.env = Environment(undefined=ChainableUndefined)
        self.context = {}
    
    def register_context(self, key: str, value: Any):
        """Register values for template resolution"""
        self.context[key] = value
    
    def render(self, template: str, additional_context: dict = None):
        """Render template with full context"""
        context = {**self.context, **(additional_context or {})}
        return self.env.from_string(template).render(context)
    
    def defer_render(self, template: str) -> DeferredTemplate:
        """Return a deferred template for lazy evaluation"""
        return DeferredTemplate(template, self)
  1. Modify Executor (src/orchestrator/executor/step_executor.py)
# Before executing each step
template_manager.register_context('previous_results', state.get_all_results())
template_manager.register_context(step_id, result)

# Render parameters just-in-time
rendered_params = template_manager.render_dict(step.parameters)
  1. Update Tool Base Class (src/orchestrator/tools/base.py)
class Tool:
    def execute(self, template_manager: TemplateManager, **kwargs):
        # Automatically render all string parameters
        rendered_kwargs = self._render_parameters(kwargs, template_manager)
        return self._execute_impl(**rendered_kwargs)

Phase 2: Tool Integration

  1. Standardize Parameter Rendering

    • All tools inherit automatic template rendering
    • Special handling for nested structures (lists, dicts)
  2. Context Injection Points

    • Pipeline parameters
    • Step results
    • Execution metadata
    • Environment variables
  3. Error Handling

    • Undefined variable warnings
    • Fallback to original template if rendering fails
    • Debug mode to show template resolution steps

Phase 3: Testing and Validation

  1. Unit Tests

    • Test template rendering with missing context
    • Test nested template expressions
    • Test deferred evaluation
  2. Integration Tests

    • Run all existing pytests
    • Verify no regression in functionality
  3. Pipeline Validation

    • Re-run all verified pipelines:
      • research_minimal.yaml
      • research_basic.yaml
      • research_advanced_tools.yaml
      • auto_tags_demo.yaml
      • control_flow/*.yaml

Success Criteria

  1. No Placeholder Text in Outputs

    • All {{variable}} expressions resolved
    • No template syntax in final outputs
  2. Backward Compatibility

    • All existing tests pass
    • All verified pipelines produce same/better outputs
  3. Developer Experience

    • Clear error messages for undefined variables
    • Debug mode showing template resolution
    • Documentation for template best practices
  4. Performance

    • Minimal overhead from deferred rendering
    • Efficient context propagation

Migration Strategy

  1. Phase 1: Implement core infrastructure (non-breaking)
  2. Phase 2: Update tools to use new system with fallback
  3. Phase 3: Migrate pipelines to leverage new features
  4. Phase 4: Deprecate old template handling

Example Usage After Implementation

# Templates "just work" without special handling
steps:
  - id: search
    tool: web-search
    parameters:
      query: "{{topic}}"  # Pipeline parameter
      
  - id: analyze
    action: generate_text
    parameters:
      # Step results automatically available
      prompt: "Analyze: {{search.results}}"
      
  - id: save
    tool: filesystem
    parameters:
      # Complex expressions work
      content: "{{analyze.result | markdown_format}}"
      path: "output/{{topic | slugify}}.md"

Risks and Mitigations

  1. Risk: Breaking existing pipelines

    • Mitigation: Comprehensive testing, gradual rollout
  2. Risk: Performance degradation

    • Mitigation: Benchmark before/after, optimize hot paths
  3. Risk: Complex debugging

    • Mitigation: Enhanced logging, template preview tools

Timeline Estimate

  • Week 1: Core infrastructure implementation
  • Week 2: Tool integration and testing
  • Week 3: Pipeline validation and documentation
  • Week 4: Bug fixes and optimization

This systematic approach will eliminate the recurring template issues and make the system more robust for future development.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requesthigh-priorityCritical issue that needs to be addressed ASAP

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions