Files
DumiFlex-ComfyUI-Wildcard-P…/engine/context.py
T
DumiFlex 108d5f49e3 fix(engine): internal vars propagate across nodes, filter at prompt-render only
User-reported bug: a variable marked internal in an upstream Context
node could not be read by downstream Combine / Derivation modules —
they saw it as missing. The previous design treated the
PIPELINE_CONTEXT socket as the public boundary and stripped both
engine `__` keys AND user-flagged internal vars at socket emit.

Correct semantic: internal = "don't render as standalone $var in the
final prompt." It does NOT mean "don't propagate through context."
Downstream nodes still need to read internal vars to compose them
into other vars (e.g. a Combine builds `$subject` from an internal
`$style` + a public `$pose`). Only the final PromptAssembler must
skip internal vars at template substitution time.

Implementation:

- engine/context.py: split the filter into two helpers.
  `strip_engine_internals` drops only `__`-prefixed engine keys —
  used at the socket boundary. `strip_internals` keeps the original
  dual-layer behavior (engine + user-flagged) — used at the prompt-
  render boundary.

- wp_nodes/types.py: socket emit now uses `strip_engine_internals`,
  so user-flagged internal vars cross node boundaries as regular
  context keys. `__wp_internal_flags__` is added to
  `_CROSS_NODE_INTERNAL_KEYS` so downstream PromptAssemblers can
  still know which vars to filter at render.

- wp_nodes/assembler_node.py: before resolving variables, merges
  the cross-node `__wp_internal_flags__` back into a copy of the
  socket context and calls `strip_internals` — drops both engine
  keys and user-flagged internal vars. Net effect on the rendered
  prompt: unchanged from the user's perspective (internal vars
  still don't appear as standalone $vars).

- tests/test_context.py: regression tests for the new helper +
  propagation behavior.
2026-05-16 13:59:45 +03:00

67 lines
2.6 KiB
Python

"""Context type definitions and internals helpers.
The engine uses a flat ``dict[str, Any]`` for context. Keys starting with ``__``
are reserved for engine internals (seed, trace, internal flags). User-defined
variables use plain identifier names.
"""
from __future__ import annotations
from typing import Any, TypedDict
class ContextInternals(TypedDict, total=False):
"""Documents the reserved internal keys. Not enforced at runtime."""
__wp_node_seed__: int
__wp_internal_flags__: dict[str, bool]
__wp_trace__: list[dict[str, Any]]
Context = dict[str, Any]
def strip_internals(ctx: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of ``ctx`` with engine-only + user-flagged-internal keys removed.
Use at the *prompt render* boundary (PromptAssembler) — both engine
bookkeeping AND user-marked-internal vars are filtered:
1. ``__``-prefixed keys: engine bookkeeping (``__wp_node_seed__``,
``__wp_trace__``, ``__wp_internal_flags__``, …).
2. Keys whose name appears with ``True`` in the
``__wp_internal_flags__`` map: user-marked-internal bindings
from modules with ``instance.internal == True``. These vars
drive composition (other modules can read them) but must not
appear as standalone ``$var`` substitutions in the rendered
prompt — that's the "internal" UX promise.
For the *socket* boundary (PIPELINE_CONTEXT carrying state between
Context nodes), use ``strip_engine_internals`` instead — it drops
only the ``__``-prefixed engine keys so user-flagged internal vars
continue to propagate through downstream Context / Combine / etc.
until they reach a PromptAssembler that filters them at render
time.
"""
flags = ctx.get("__wp_internal_flags__")
internal_names: set[str] = set()
if isinstance(flags, dict):
internal_names = {k for k, v in flags.items() if v}
return {
k: v for k, v in ctx.items()
if not k.startswith("__") and k not in internal_names
}
def strip_engine_internals(ctx: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of ``ctx`` with only the ``__``-prefixed engine keys removed.
User-flagged internal vars stay in the result — they need to
propagate across Context-node boundaries so downstream Combine /
Derivation / Constraint modules can read them. Only the final
PromptAssembler filters them out at render time (via
``strip_internals``) so they don't appear as standalone ``$var``
substitutions in the prompt text.
"""
return {k: v for k, v in ctx.items() if not k.startswith("__")}