Skip to content

fix(templates): strict prompt rendering + steps.X.output registry - #49

Open
addadi wants to merge 1 commit into
nullclaw:mainfrom
addadi:fix/template-strict-steps-registry
Open

fix(templates): strict prompt rendering + steps.X.output registry#49
addadi wants to merge 1 commit into
nullclaw:mainfrom
addadi:fix/template-strict-steps-registry

Conversation

@addadi

@addadi addadi commented Aug 22, 2026

Copy link
Copy Markdown

Fixes #40

resolveNewExpression silently returned an empty string for any unknown expression or missing key, and had no steps. prefix handler — the path used by executeTaskNode prompt rendering. So {{steps.X.output}} rendered as an empty prompt which the worker then answered from nothing (or hallucinated from residual session context). No error, no failed step: the failure is invisible at the orchestration layer.

Changes:

  • Add renderPromptTemplateStrict which propagates UnresolvedReference, UnknownExpression, and StepNotFound instead of swallowing them. executeTaskNode now uses the strict variant — a missing variable fails the node visibly.
  • Parametrize resolveNewExpression with a strict flag; the lenient path is preserved for {% if %} conditional evaluation where empty-as-falsy is legitimate.
  • Add a {{steps.X.Y}} prefix handler resolving against a state.steps.X.Y registry. The registry is populated from each step's raw output (buildStepRegistryUpdate) and deep-merged via state.applyDeepUpdates so multi-step chains coexist without last_value clobbering. {{steps.X.output}} works with no per-step output_key configured.
  • workflow_validation.checkStateRefs skips the state-schema check for {{steps.*}} refs since the registry is runtime-populated.

Integration test: 2-step sequential — step a emits SENTINEL_XYZ, step b renders "Echo: {{steps.a.output}}" verbatim; a and b coexist in the registry after deep-merge. Full suite green (zig build test, 366/366).

resolveNewExpression silently returned an empty string for any unknown
expression or missing key, and had no steps. prefix handler — the path
used by executeTaskNode prompt rendering. So {{steps.X.output}}
rendered as an empty prompt which the worker then answered from
nothing (or hallucinated from residual session context). No error, no
failed step: the orchestration layer cannot see the failure.

Changes:

- Add renderPromptTemplateStrict which propagates UnresolvedReference,
  UnknownExpression, and StepNotFound instead of swallowing them.
  executeTaskNode now uses the strict variant — a missing variable
  fails the node visibly.
- Parametrize resolveNewExpression with a strict flag; the lenient
  path is preserved for {% if %} conditional evaluation where
  empty-as-falsy is legitimate.
- Add a {{steps.X.Y}} prefix handler resolving against a
  state.steps.X.Y registry. The registry is populated from each step's
  raw output (buildStepRegistryUpdate) and deep-merged via
  state.applyDeepUpdates so multi-step chains coexist without
  last_value clobbering. {{steps.X.output}} works with no per-step
  output_key configured.
- workflow_validation.checkStateRefs skips the state-schema check for
  {{steps.*}} refs since the registry is runtime-populated.

Integration test: 2-step sequential run, step a emits SENTINEL_XYZ,
step b renders "Echo: {{steps.a.output}}" verbatim; a and b coexist in
the registry after deep-merge.

Fixes nullclaw#40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: {{steps.X.output}} renders as empty string silently — worker receives blank prompt

1 participant