Schema.validate() now walks DynamicCombo.Input / DynamicSlot.Input options
recursively when building the selector-target map, so DynamicOutputs.ByKey /
BySlot can address a dynamic input nested inside another dynamic option via
its dotted path (e.g. selector="outer.inner_slot"). The runtime dispatch /
type resolver already handled dotted-path nested inputs; this just removes
the schema-level rejection.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
ByKey already covered literal-driven dispatch (Combo/DynamicCombo/String);
add BySlot as the symmetric resolved-type-driven form (mirrors DynamicSlot).
Inputs no longer carry output declarations. DynamicCombo.Option / DynamicSlot.Option
go back to {key|when, inputs} only — outputs always live on the corresponding
DynamicOutputs entry in Schema.outputs. Validation enforces that ByKey option keys
align with the referenced DynamicCombo's keys and BySlot option 'when' types are a
subset of the referenced DynamicSlot's accepted types (including when=None).
Removes FromInput/_select_from_input_outputs/_from_input_as_dict and the
option-level output serialization helpers.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Lets dynamic-input nodes co-declare per-option outputs so the option list is a
single source of truth for both inputs and outputs.
* DynamicCombo.Option / DynamicSlot.Option gain an optional outputs=[...] list.
* DynamicOutputs.FromInput(input_id) is a positional placeholder in outputs[]
that resolves to the referenced input's active-option outputs at finalize
time. DynamicCombo selects by literal value; DynamicSlot selects by the
upstream slot's resolved type (or when=None when unlinked).
* get_finalized_class_outputs gains schema_inputs / live_input_types and the
TypeResolver / execute() compute live_input_types only when at least one
FromInput → DynamicSlot is present.
* Schema.validate(): FromInput must reference an existing DynamicCombo /
DynamicSlot input, each input may be referenced at most once, and option
output ids stay globally unique.
* V1 info synthesizes the dynamic_outputs entry as kind='by_key' for combos
and kind='by_slot' for slots, inlining the option outputs.
Tests: combo + slot FromInput finalization, FromInput validation (missing /
duplicate / id collision), and TypeResolver end-to-end picks for both kinds.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Address code-review feedback:
* Fix pre-existing bug surfaced by this work: a V3 node returning a bare
ExecutionBlocker (normalized to NodeOutput(block_execution=msg) with no
positional/named result) was silently dropped in get_output_from_returns.
block_execution now takes precedence and shapes the blocker tuple to the
active output count first.
* Reject malformed linked-input source node ids (non-string) up front in
validate_inputs as 'bad_linked_input' instead of letting them fall into
the type resolver.
* Schema.validate(): reject duplicate DynamicOutputs group ids.
* Drop a couple of over-defensive / restating comments.
New tests: blocker sizing through both the NodeOutput and bare-blocker paths
for dynamic nodes, and duplicate-group-id schema rejection.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Introduce a per-prompt finalized output list for V3 nodes whose active
output shape depends on a literal input. First slice:
* Schema:
- io.DynamicOutputs.ByKey(id, selector, options=[io.DynamicOutputs.Option(...)])
- Validates: selector input exists; output ids unique within and across
static + dynamic branches; no overlap between branches.
- get_v1_info() advertises dynamic groups separately via NodeInfoV1.dynamic_outputs;
static RETURN_TYPES / OUTPUT_IS_LIST stay as the always-present prefix.
* NodeOutput:
- New NodeOutput.from_named({id: value}) for dynamic nodes; engine reorders
against the finalized active output list. Mixing positional+named raises.
* Execution:
- execute() computes FinalizedOutputs once per attempt and threads it
through initial / pending-async / pending-subgraph output paths.
- merge_result_data / get_output_data / get_output_from_returns size
blocker tuples and concat behaviour against the finalized list.
- validate_inputs rejects positional links whose slot index is outside
the upstream node's active output count as 'bad_linked_output'.
* TypeResolver:
- resolve_output_type and is_output_list consult the finalized output
list when present; finalized_output_count() exposes the active count
for validation.
Covered by new unit tests under tests-unit/comfy_api_test and
tests-unit/execution_test.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
- Add `optional` kwarg to DynamicSlot.Input (default True). When False,
declaring a when=None Option is rejected because the unconnected branch
is unreachable.
- Always publish `forceInput=True` on the slot itself. slotType may
include widget-capable types (INT/STRING/etc.) but a DynamicSlot is
meant to look like a connection point, never a widget.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Grouping AnyType with concrete types conflates the known-type case with
the unresolvable-wildcard case under one branch and muddies slotType.
AnyType must stand alone as when=io.AnyType so the two states stay
distinct in both dispatch and the serialized schema.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Bug fixes:
- execution.py validate_inputs() now calls _select_option for DynamicSlot
links instead of validating against slotType. Old code accepted any
concrete upstream type whenever AnyType was enumerated (slotType
contained '*', which validate_node_input treats as accept-anything).
Verified end-to-end: LATENT into an IMAGE+AnyType slot is now rejected.
- Thread live_input_types through get_input_data so custom V3
validate_inputs() sees the same DynamicSlot branch that finalization
picked, instead of re-finalizing without resolver context.
- DynamicSlot.Option._when_types is now an ordered tuple (preserves
author declaration order); _slot_io_type/slotType ordering was
previously nondeterministic via frozenset iteration.
Design:
- DynamicSlot.Input.get_all() now returns [self] + children, matching
Autogrow / DynamicCombo so consumers like PriceBadge work uniformly.
- Enforce per-option type uniqueness in DynamicSlot.Input: each io_type
may appear in at most one option's 'when', and at most one option may
declare when=None. Removes the ambiguous first-match-on-overlap case
for single concrete types; ordering still matters when upstream is a
multi-type union.
- Reject non-Option entries in options=[...] explicitly.
Polish:
- Trim verbose DynamicSlot docstrings and inline comments.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
Replaces the old connected/unconnected fixed-child DynamicSlot with a
type-keyed option list. Each Option declares a 'when' condition (None,
io.AnyType, a single ComfyType, a list, or a MultiType.Input) and the
child inputs revealed when that condition matches the slot's resolved
upstream type.
Selection happens at schema-finalization time using live_input_types
computed by TypeResolver, so API-only workflows (no frontend) get the
same expansion the UI would.
- _io.py: redesign DynamicSlot.Input / Option; auto-derive slotType as
the union of all non-None when sets; expose it via as_dict so the
frontend knows what types are accepted; the class io_type stays
COMFY_DYNAMICSLOT_V3 as the parse-time dispatch tag.
- type_resolver.py: return the auto-derived _slot_io_type for
DynamicSlot.Input; document the AnyType (*) limitation.
- execution.py: validate links into a DynamicSlot against slotType,
not the dispatch tag COMFY_DYNAMICSLOT_V3.
- tests: new test_dynamic_slot.py + regression coverage in
test_type_resolver.py.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
register_dynamic_input_func is a private helper (underscore module, not
in __all__, not re-exported). Its only callers are the three core
setup_dynamic_input_funcs() registrations, all of which were updated
together. No third party can be relying on the old 5-arg signature, so
the inspect.signature shim and accompanying backward-compat tests are
over-engineered.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>
* Add _parse_link helper validating both node_id (str) and slot_idx (int,
rejecting bool) so malformed API JSON (e.g. ['n1', '0']) degrades to
AnyType instead of crashing with TypeError.
* Add slot_idx type guards in resolve_output_type and is_output_list.
* Extract _get_class_def_for_node helper to dedupe node/class lookup
across resolve_output_type, is_output_list, get_declared_slot_io_type.
* register_dynamic_input_func now detects 5-argument legacy callables
via inspect.signature and silently wraps them; preserves backward
compatibility for any custom node that registered its own dynamic
input expansion against the pre-live_input_types signature.
* Tests: malformed link (str slot idx, wrong arity), bad slot type
directly to resolve_output_type, non-string class_type. Tests for
the legacy 5-arg shim and the modern 6-arg passthrough, including
callables with uninspectable signatures.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501
Co-authored-by: Amp <amp@ampcode.com>