07 - Template Resolver
This document specifies the template variable substitution system used to parameterize task descriptions and expected outputs.
Purpose
Section titled “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
Section titled “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
Section titled “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 workingEdge Case Matrix
Section titled “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
Section titled “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
Section titled “Logging”| Level | What |
|---|---|
| DEBUG | "Resolving template ({length} chars) with {inputCount} variables" |
| DEBUG | "Resolved {variableCount} variables in template" |