You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_name2prompt.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
Configurable mustache delimiters — Add configurable mustache delimiters pydantic-handlebars#11. open_delim / close_delim on HandlebarsEnvironment, render(), compile(), and extract_dependencies(). Lets the SDK run a Handlebars pass that consumes @{...}@ without disturbing {{...}} runtime placeholders. Full Handlebars syntax (block helpers, dotted paths, subexpressions, …) now works under @{...}@ because we're running the real parser instead of a regex pretending to.
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:
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:
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.
Where to configure — per-template_var() arg, LocalVariablesOptions, or both. (Possibly a broader dev-mode-vs-production-mode toggle.)
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).
Context
logfire.template_var()resolves a value, expands@{ref}@composition references, and renders Handlebars{{placeholder}}templates against typedinputs. Right now the render step silently substitutes empty strings when a template references a field the inputs don't have:Meanwhile the composition path does warn when a referenced variable is missing:
The asymmetry is bad. Surfaced by Alex on #1731 (
templates-and-composition.md:36).Upstream changes shipped in
pydantic-handlebars0.2.0strict=TrueonHandlebarsEnvironment,render(), andcompile()— Add strict mode and extract_dependencies() pydantic-handlebars#10. RaisesHandlebarsRuntimeErroron missing context references; explicitNonevalues still pass.extract_dependencies(source, ...)— Add strict mode and extract_dependencies() pydantic-handlebars#10. Statically returns the set of top-level context field names a template depends on. Scope-aware (handleseach/with, block params,../,@root.x).open_delim/close_delimonHandlebarsEnvironment,render(),compile(), andextract_dependencies(). Lets the SDK run a Handlebars pass that consumes@{...}@without disturbing{{...}}runtime placeholders. Full Handlebars syntax (block helpers, dotted paths, subexpressions, …) now works under@{...}@because we're running the real parser instead of a regex pretending to.SDK follow-up work now unblocked
A. Replace
logfire/variables/reference_syntax.pywith native@{...}@renderingrender_oncecurrently does: protect{{...}}with sentinels → regex-translate@{...}@→{{...}}→ render → restore. With configurable delimiters that becomes:The sentinel + regex dance goes away. This also closes the
@{...}@syntax-parity issue Alex flagged ontemplates-and-composition.md:186of #1731 —@{#if user.active}@, helper subexpressions, etc. start working because the actual Handlebars parser handles them.reference_syntax.pyeither becomes a thin wrapper or gets deleted.B. Swap
composition.find_references()forextract_dependencies(..., open_delim='@{', close_delim='}@')Current
find_referencesincomposition.pyis 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:RuntimeWarningand fall back to the code-default template (also strict), and only on second failure render lenient. Two warnings, no exceptions thrown.template_var()arg,LocalVariablesOptions, or both. (Possibly a broader dev-mode-vs-production-mode toggle.){{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_compositionintovariables_push/variables_validatelogfire/variables/template_validation.py::validate_template_compositionexists and usespydantic_handlebars.check_template_compatibilityto 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_typeand the server-storedtemplate_inputs_schemadiverge.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_failuredouble-call edge caseCallable defaults can still run twice when
_lookup_serializedfalls back to the code-default tier and composition/render then fails. Needs_lookup_serializedto thread the unserialized default through alongside the serialized form. Noted in #1951 (Cubic thread); not a release blocker.H.
VariableCompositionErrorsubclass propagation throughComposedReferenceexpand_referencesraises specific subclasses (VariableCompositionCycleError, etc.) but they're flattened tostr(e)insideComposedReference.errorand re-wrapped as the base class, so subclass-isinstancechecks against the nested path never match. Fix is to propagate the original exception viaComposedReferencerather than just the message. Touches the composition module's public surface. Noted in #1731 (logfire/variables/variable.py:364thread).