Skip to content

07 - Template Resolver

This document specifies the template variable substitution system used to parameterize task descriptions and expected outputs.

Purpose

Users can include {variable} placeholders in task descriptions and expected outputs. These are resolved at ensemble.run(inputs) time, allowing the same ensemble definition to be reused with different inputs.

var task = Task.builder()
    .description("Research the latest developments in {topic}")
    .expectedOutput("A detailed report on {topic} covering the last {period}")
    .agent(researcher)
    .build();

// At run time:
ensemble.run(Map.of("topic", "AI agents", "period", "6 months"));
// Resolves to:
// description: "Research the latest developments in AI agents"
// expectedOutput: "A detailed report on AI agents covering the last 6 months"

TemplateResolver

public final class TemplateResolver {

    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(\\w+)}");
    private static final Pattern ESCAPED_PATTERN = Pattern.compile("\\{\\{(\\w+)}}");

    private TemplateResolver() {} // Utility class, not instantiable

    /**
     * Resolve template variables in the given string.
     *
     * Variables are denoted by {name} where name contains only word characters
     * (letters, digits, underscores). Escaped variables {{name}} are converted
     * to literal {name} without substitution.
     *
     * @param template String with {variable} placeholders. May be null.
     * @param inputs Map of variable names to replacement values. May be null (treated as empty).
     * @return Resolved string, or null if template was null
     * @throws PromptTemplateException if any unescaped variables are not found in inputs
     */
    public static String resolve(String template, Map<String, String> inputs) { ... }
}

Algorithm

resolve(template, inputs):

1. IF template is null:
     RETURN null

2. IF template is blank (empty or whitespace only):
     RETURN template

3. effectiveInputs = (inputs != null) ? inputs : Map.of()

4. // Step 1: Protect escaped variables
   // Replace {{var}} with a sentinel that won't collide with real content
   working = ESCAPED_PATTERN.replaceAll(template, "__AGENTENSEMBLE_ESCAPED_$1__")

5. // Step 2: Find all unescaped variables
   matcher = VARIABLE_PATTERN.matcher(working)
   foundVariables = new LinkedHashSet<String>()
   WHILE matcher.find():
     foundVariables.add(matcher.group(1))

6. // Step 3: Check for missing variables
   missingVariables = foundVariables.stream()
     .filter(name -> !effectiveInputs.containsKey(name))
     .toList()

   IF missingVariables is not empty:
     throw new PromptTemplateException(
       "Missing template variables: " + missingVariables
         + ". Provide them in ensemble.run(inputs). Template: '"
         + truncate(template, 100) + "'",
       missingVariables,
       template)

7. // Step 4: Replace variables with values
   FOR EACH variableName IN foundVariables:
     value = effectiveInputs.get(variableName)
     IF value is null:
       value = ""
     working = working.replace("{" + variableName + "}", value)

8. // Step 5: Restore escaped variables as literals
   working = working.replaceAll("__AGENTENSEMBLE_ESCAPED_(\\w+)__", "{$1}")

9. RETURN working

Edge Case Matrix

Input Expected Output Notes
template=null, inputs=any null Null-safe pass-through
template="", inputs=any "" Empty pass-through
template=" ", inputs=any " " Whitespace-only pass-through
template="no variables", inputs={} "no variables" No variables, returned unchanged
template="no variables", inputs={"a":"b"} "no variables" Extra inputs ignored
template="{topic}", inputs={"topic":"AI"} "AI" Simple substitution
template="{a} and {b}", inputs={"a":"X","b":"Y"} "X and Y" Multiple variables
template="{topic}", inputs={} Throws PromptTemplateException Missing: [topic]
template="{a} and {b}", inputs={"a":"X"} Throws PromptTemplateException Missing: [b] (reports ALL missing)
template="{a} and {b}", inputs={} Throws PromptTemplateException Missing: [a, b]
template="{{topic}}", inputs={} "{topic}" Escaped braces, no substitution
template="{{topic}}", inputs={"topic":"AI"} "{topic}" Escaped even when input exists
template="{topic}", inputs={"topic":""} "" Empty value is valid
template="{topic}", inputs={"topic":null} "" Null value treated as empty
template="{a}{a}", inputs={"a":"X"} "XX" Same variable used multiple times
template="Hello {name}!", inputs=null Throws PromptTemplateException Null inputs treated as empty, variable missing
template="{under_score}", inputs={"under_score":"ok"} "ok" Underscores allowed in names

Where Templates Are Resolved

Templates are resolved in Ensemble.run(inputs) BEFORE tasks are passed to the WorkflowExecutor:

// In Ensemble.run(inputs):
List<Task> resolvedTasks = tasks.stream()
    .map(task -> task.toBuilder()
        .description(TemplateResolver.resolve(task.description(), inputs))
        .expectedOutput(TemplateResolver.resolve(task.expectedOutput(), inputs))
        .build())
    .toList();

This creates new Task instances with resolved text. The original Task objects are immutable and unchanged.

Logging

Level What
DEBUG "Resolving template ({length} chars) with {inputCount} variables"
DEBUG "Resolved {variableCount} variables in template"