Add server-side TypeResolver for prompt-graph type resolution

Resolves the concrete io_type of any output/input slot in a prompt by
walking the graph, so API-submitted workflows (no frontend) and the
execution engine agree on resolved types even when MatchType chains are
involved.

* New comfy_execution/type_resolver.py: TypeResolver class with output
  resolution (incl. MatchType template walking, cycle detection, depth
  cap, AnyType fallback + one-shot warning), input resolution (links and
  literals), is_output_list / is_input_list helpers, effective slot
  io_type peeling for dynamic wrappers (Autogrow -> wrapped element
  type, DynamicSlot -> underlying slot type), and bulk
  compute_live_input_types.
* DynamicPrompt now lazily exposes get_type_resolver() and invalidates
  the resolver cache on add_ephemeral_node.
* get_finalized_class_inputs / parse_class_inputs / DYNAMIC_INPUT_LOOKUP
  callable signature accept an optional live_input_types dict. Existing
  Autogrow/DynamicSlot/DynamicCombo expansions accept and ignore it;
  future per-type dynamic inputs use it as their discriminator.
* validate_inputs and get_input_data both build live_input_types via
  the resolver and pass it through; validate_inputs also uses the
  resolver to determine received_type for linked inputs so MatchType
  chains in API workflows validate correctly.
* validate_prompt builds one TypeResolver and shares it across all
  output-node validations to avoid re-walking chains.
* tests-unit/execution_test/test_type_resolver.py covers V1 static
  return types, V1 wildcard warning behavior, MatchType resolution
  including first-wins, cycle termination, chain walking, input
  resolution, Autogrow peeling, list info, and cache invalidation.

Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Jedrzej Kosinski
2026-06-01 16:24:48 -07:00
parent 06b710aa68
commit 19390c112a
5 changed files with 835 additions and 22 deletions

View File

@@ -26,6 +26,9 @@ class DynamicPrompt:
self.ephemeral_prompt = {}
self.ephemeral_parents = {}
self.ephemeral_display = {}
# Lazily-built type resolver, scoped to this DynamicPrompt's lifetime.
# Invalidated whenever the graph mutates via add_ephemeral_node.
self._type_resolver = None
def get_node(self, node_id):
if node_id in self.ephemeral_prompt:
@@ -41,6 +44,18 @@ class DynamicPrompt:
self.ephemeral_prompt[node_id] = node_info
self.ephemeral_parents[node_id] = parent_id
self.ephemeral_display[node_id] = display_id
# Conservatively invalidate the entire resolver cache. Selective
# downstream invalidation would require topological info we don't have
# here cheaply; the resolver's cache is small and easy to rebuild.
if self._type_resolver is not None:
self._type_resolver.invalidate()
def get_type_resolver(self):
"""Lazily build and return the per-prompt TypeResolver."""
if self._type_resolver is None:
from comfy_execution.type_resolver import TypeResolver
self._type_resolver = TypeResolver(self)
return self._type_resolver
def get_real_node_id(self, node_id):
while node_id in self.ephemeral_parents: