Skip to content

Managed variables: template render-time warnings and missing-input handling #1950

@dmontagu

Description

@dmontagu

Status (2026-05-22) — Both upstream blockers in pydantic-handlebars are resolved and released in v0.2.0. The SDK-side work below is now unblocked.

Context

logfire.template_var() resolves a value, expands @{ref}@ composition references, and renders Handlebars {{placeholder}} templates against typed inputs. Right now the render step silently substitutes empty strings when a template references a field the inputs don't have:

prompt = logfire.template_var(
    'system_prompt',
    type=str,
    default='Hello {{user_name}}! Welcome to our service.',
    inputs_type=PromptInputs,
)
# inputs has user_name, but the template references user_name2
prompt.get(PromptInputs(user_name='Alice'))
# → "Hello ! Welcome to our service."  (no warning)

Meanwhile the composition path does warn when a referenced variable is missing:

RuntimeWarning: Variable 'agent_prompt' composition failed; falling back to code default:
Referenced variable 'safetyd_rules' could not be resolved.

The asymmetry is bad. Surfaced by Alex on #1731 (templates-and-composition.md:36).

Upstream changes shipped in pydantic-handlebars 0.2.0

SDK follow-up work now unblocked

A. Replace logfire/variables/reference_syntax.py with native @{...}@ rendering

render_once currently does: protect {{...}} with sentinels → regex-translate @{...}@{{...}} → render → restore. With configurable delimiters that becomes:

HandlebarsEnvironment(open_delim='@{', close_delim='}@').render(template, context)

The sentinel + regex dance goes away. This also closes the @{...}@ syntax-parity issue Alex flagged on templates-and-composition.md:186 of #1731@{#if user.active}@, helper subexpressions, etc. start working because the actual Handlebars parser handles them. reference_syntax.py either becomes a thin wrapper or gets deleted.

B. Swap composition.find_references() for extract_dependencies(..., open_delim='@{', close_delim='}@')

Current find_references in composition.py is regex-based. The upstream primitive is AST-aware and scope-correct — needed for accurate cycle/dependency analysis once @{...}@ block helpers are real.

C. Render-time strict mode in TemplateVariable.get()

pydantic_handlebars.render(..., strict=True) exists. The SDK can call it. Open SDK-side design questions remain:

  1. Default policy — warn vs raise vs configurable. Options from the 2026-05-21 sync:
    • Warn by default, allow opt-in raise. (Alex's leaning.)
    • Server-template-first strict mode: try server template strict, on failure emit a RuntimeWarning and fall back to the code-default template (also strict), and only on second failure render lenient. Two warnings, no exceptions thrown.
    • Mark the resolve-variable span as error regardless.
  2. Where to configure — per-template_var() arg, LocalVariablesOptions, or both. (Possibly a broader dev-mode-vs-production-mode toggle.)
  3. What to render in the error case — current behavior matches Handlebars' default (empty string). One direction: retain the literal {{ref}} so the misrender is visible, optionally configurable. Composition does this already for unresolved @{ref}@ references; render-path consistency is an open question.

D. Wire validate_template_composition into variables_push / variables_validate

logfire/variables/template_validation.py::validate_template_composition exists and uses pydantic_handlebars.check_template_compatibility to detect schema-incompatible templates across a composition graph; it's currently only exercised by its own tests. Wire it into the push/validate path so users get a loud error at sync time.

E. Schema-mismatch warning between server template and local schema

Distinct from D — fires when the local inputs_type and the server-stored template_inputs_schema diverge.

F. Misrender retention parity between composition and rendering

Unresolved @{ref}@ currently preserves the literal reference; render-side {{}} currently substitutes empty. Worth deciding whether both should behave the same way (and configurable).

Additional follow-ups surfaced after the issue was filed

G. _composition_failure double-call edge case

Callable defaults can still run twice when _lookup_serialized falls back to the code-default tier and composition/render then fails. Needs _lookup_serialized to thread the unserialized default through alongside the serialized form. Noted in #1951 (Cubic thread); not a release blocker.

H. VariableCompositionError subclass propagation through ComposedReference

expand_references raises specific subclasses (VariableCompositionCycleError, etc.) but they're flattened to str(e) inside ComposedReference.error and re-wrapped as the base class, so subclass-isinstance checks against the nested path never match. Fix is to propagate the original exception via ComposedReference rather than just the message. Touches the composition module's public surface. Noted in #1731 (logfire/variables/variable.py:364 thread).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions