Skip to content

Consolidate duplicate template expansion implementations #437

@andreasronge

Description

@andreasronge

Context

PR #436 extracted shared placeholder extraction helpers to the Template module. However, there are still two identical expand_template/2 private functions that were not consolidated:

  • lib/ptc_runner/sub_agent.ex:439-454
  • lib/ptc_runner/sub_agent/loop.ex:409-424

Both are textually identical and perform the same "lenient" expansion (returning {{key}} unchanged on missing keys).

Problem

Code duplication between SubAgent.expand_template/2 and Loop.expand_template/2. Both functions:

  • Use the same regex: ~r/\{\{\s*(\w+)\s*\}\}/
  • Have identical logic for atom/string key fallback
  • Return {{key}} unchanged when a key is missing

The existing Template.expand/2 returns {:error, {:missing_keys, [...]}} on missing keys (strict behavior), while these private functions need lenient behavior.

Solution

Add an on_missing option to Template.expand/2:

@spec expand(String.t(), map(), keyword()) :: {:ok, String.t()} | {:error, {:missing_keys, [String.t()]}}

# Default: strict - error on missing (current behavior)
Template.expand("{{name}}", %{})
# => {:error, {:missing_keys, ["name"]}}

# Opt-in: lenient - keep placeholders unchanged
Template.expand("{{name}}", %{}, on_missing: :keep)
# => {:ok, "{{name}}"}

Then replace the duplicate private functions with:

defp expand_template(prompt, context) do
  {:ok, result} = Template.expand(prompt, context, on_missing: :keep)
  result
end

Implementation Steps

  1. Add on_missing option to Template.expand/2 (default :error for backward compatibility)
  2. Add tests for the new :keep behavior
  3. Replace expand_template/2 in SubAgent (line 439) with call to Template.expand/3
  4. Replace expand_template/2 in Loop (line 409) with call to Template.expand/3
  5. Verify all existing tests pass

Acceptance Criteria

  • Template.expand/2 continues to error on missing keys (backward compatible)
  • Template.expand("{{x}}", %{}, on_missing: :keep) returns {:ok, "{{x}}"}
  • No duplicate expand_template/2 functions remain in SubAgent or Loop
  • All existing tests pass
  • Test at prompt_test.exs:57-65 continues to pass (handles missing variables gracefully)

Files to Modify

  • lib/ptc_runner/sub_agent/template.ex - Add on_missing option
  • lib/ptc_runner/sub_agent.ex - Replace private function
  • lib/ptc_runner/sub_agent/loop.ex - Replace private function
  • test/ptc_runner/sub_agent/template_test.exs - Add tests for :keep behavior

Notes

  • The existing Prompt.expand_mission/2 already demonstrates the pattern for handling Template.expand/2 with lenient fallback - this consolidation will make that pattern reusable
  • The private implementations don't support nested paths ({{user.name}}), but this is acceptable since those callers don't currently use nested paths

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfrom-pr-reviewIssue created from PR review feedbackneeds-reviewIssue needs review before implementationready-for-implementationIssue is approved and ready to implementtech-debtTechnical debt or refactoring

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions