Files
ttulttul-ComfyUI-Modal/remote_plan_types.py
T
Ken SimpsonandClaude Opus 5.5 dba3a6abd9 Add Remote Progressive Preview node and run every mapped item (#9)
* Expect resolved STRING type for ModalMapInput boundaries

69a33e3 made _remote_output_io_type resolve wildcard "*" outputs through
their linked input, so a ModalMapInput boundary fed by a STRING list now
reports STRING, matching the mapped_input type the test already expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add Remote Progressive Preview node

A local PreviewImage-style node that shows one empty slot per mapped item
when remote work starts and fills each slot as soon as that item's images
stream back, instead of waiting for ComfyUI to deliver the whole result.

- Planning records direct consumers as progressive_preview_target_node_ids
  and exempts them from local bridge materializers.
- The remote shim streams boundary outputs for those targets; mapped item
  payloads carry map_item_count and dispatch emits a start event.
- The host saves streamed images to temp, records them per prompt/node, and
  sends modal_progressive_preview websocket events.
- The node reuses streamed slots on final execution and only materializes
  bridge refs that were never streamed.
- web/progressive_preview.js renders the slot grid and hands off to the
  native preview on the final executed event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Execute every mapped item instead of reusing shared-session outputs

Mapped items share one remote session. The restored-session short-circuit,
meant for identical per-lane seed phases, looked outputs up by node id
only, so after the first item on a container every later item skipped
sampling and returned that earlier item's image under its own bridge key.
Payloads carrying map_item_index now always execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep finished progressive previews handed off to the native preview

Hide the panel with an inline display style so the frontend's DOM-widget
wrapper cannot override the hidden attribute, and ignore replayed events
for a prompt whose final images were already delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:09:02 -07:00

228 lines
7.7 KiB
Python

"""Shared value types for remote graph analysis and execution planning."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
if __package__:
from .execution_environments import ExecutionAssignment
from .remote_configurations import RemoteConfiguration, RemoteConfigurationSet
from .remote_hosts import SshHostConfig
from .sync_engine import SyncedAsset
from .vast_service import VastService
else: # pragma: no cover - flat import inside the Modal container.
from execution_environments import ExecutionAssignment
from remote_configurations import RemoteConfiguration, RemoteConfigurationSet
from remote_hosts import SshHostConfig
from sync_engine import SyncedAsset
from vast_service import VastService
@dataclass(frozen=True)
class ComponentMemoryEstimate:
"""Describe conservative scheduler memory floors inferred from model assets."""
minimum_vram_bytes: int
minimum_ram_bytes: int
model_asset_count: int = 0
largest_model_bytes: int = 0
@dataclass(frozen=True)
class LinkedOutputRef:
"""Reference a node output slot within a prompt graph."""
node_id: str
output_index: int
@dataclass(frozen=True)
class InputTarget:
"""Describe a target input inside a remote component."""
node_id: str
input_name: str
@dataclass
class BoundaryInputSpec:
"""Describe one local-to-remote boundary value for a component."""
proxy_input_name: str
source: LinkedOutputRef
io_type: str
targets: list[InputTarget] = field(default_factory=list)
@dataclass
class BoundaryOutputSpec:
"""Describe one value exported across a remote component boundary."""
proxy_output_name: str
source: LinkedOutputRef
io_type: str
is_list: bool
preview_target_node_ids: list[str] = field(default_factory=list)
progressive_preview_target_node_ids: list[str] = field(default_factory=list)
session_output: bool = False
session_consumer_node_ids: list[str] = field(default_factory=list)
local_materializer_node_id: str | None = None
local_materializer_consumer_node_ids: list[str] = field(default_factory=list)
@dataclass
class StaticToMappedBoundarySpec:
"""Describe one static-phase output injected into each mapped item run."""
proxy_name: str
source: LinkedOutputRef
io_type: str
is_list: bool
targets: list[InputTarget] = field(default_factory=list)
@dataclass(frozen=True)
class ProducedPhaseOutputSpec:
"""Describe one output published by an earlier split-proxy phase."""
proxy_output_name: str
source: LinkedOutputRef
io_type: str
is_list: bool
session_output: bool = False
@dataclass
class RemoteComponentPlan:
"""Execution and rewrite plan for one connected remote component."""
node_ids: list[str]
representative_node_id: str
boundary_inputs: list[BoundaryInputSpec]
boundary_outputs: list[BoundaryOutputSpec]
execute_node_ids: list[str]
contains_output_node: bool
mapped_boundary_input_name: str | None = None
mapped_boundary_input_io_type: str | None = None
mapped_boundary_source_node_id: str | None = None
static_node_ids: list[str] = field(default_factory=list)
mapped_node_ids: list[str] = field(default_factory=list)
mapped_execute_node_ids: list[str] = field(default_factory=list)
static_execute_node_ids: list[str] = field(default_factory=list)
static_to_mapped_boundaries: list[StaticToMappedBoundarySpec] = field(
default_factory=list
)
local_tap_node_ids: list[str] = field(default_factory=list)
local_tap_terminal_node_ids: list[str] = field(default_factory=list)
@dataclass
class RewriteSummary:
"""Summary of the prompt rewrite performed for a queue request."""
remote_node_ids: list[str] = field(default_factory=list)
remote_component_ids: list[str] = field(default_factory=list)
component_node_ids_by_representative: dict[str, list[str]] = field(
default_factory=dict
)
component_dependency_ids_by_representative: dict[str, list[str]] = field(
default_factory=dict
)
component_execution_stages: list[list[str]] = field(default_factory=list)
mapped_component_ids: list[str] = field(default_factory=list)
estimated_max_parallel_requests: int = 0
max_parallel_requests_upper_bound: int | None = None
requires_volume_reload: bool = False
volume_reload_marker: str | None = None
uploaded_volume_paths: list[str] = field(default_factory=list)
rewritten_node_id_map: dict[str, str] = field(default_factory=dict)
sandwiched_local_node_ids: list[str] = field(default_factory=list)
parallel_local_branch_node_ids: list[str] = field(default_factory=list)
synced_assets: list[SyncedAsset] = field(default_factory=list)
custom_nodes_bundle: SyncedAsset | None = None
artifact_finalizer_node_id: str | None = None
execution_assignments_by_representative: dict[str, ExecutionAssignment] = field(
default_factory=dict
)
execution_worker_indices_by_representative: dict[str, int] = field(
default_factory=dict
)
custom_nodes_bundles_by_environment: dict[str, SyncedAsset | None] = field(
default_factory=dict
)
remote_configurations: list[dict[str, Any]] = field(default_factory=list)
execution_locations_by_environment: dict[str, str] = field(
default_factory=dict
)
@dataclass
class ComponentExecutionPlan:
"""Hold assignments and prepared provider state for one prompt rewrite."""
assignments: dict[str, ExecutionAssignment]
configuration_set: RemoteConfigurationSet | None = None
configurations_by_id: dict[str, RemoteConfiguration] = field(
default_factory=dict
)
safe_configurations: list[dict[str, Any]] = field(default_factory=list)
ssh_hosts_by_id: dict[str, SshHostConfig] = field(default_factory=dict)
vast_service: VastService | None = None
vast_leases_by_environment: dict[str, Any] = field(default_factory=dict)
resolved_llm_profiles: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class _EnvironmentAssetPreparationResult:
"""Collect custom-node and prompt-asset results for one environment."""
environment_id: str
custom_nodes_bundle: SyncedAsset | None
component_prompts: dict[str, dict[str, Any]]
assets_by_component_id: dict[str, list[SyncedAsset]]
@dataclass(frozen=True)
class RemoteExpansionReason:
"""Describe why one upstream node had to join a remote component."""
node_id: str
class_type: str
required_by_node_id: str
required_by_class_type: str
output_index: int
io_type: str
@dataclass
class RemoteNodeAnalysis:
"""Structured dry-run result for context-menu remote expansion."""
requested_node_ids: list[str] = field(default_factory=list)
requested_workflow_node_paths: list[str] = field(default_factory=list)
current_remote_node_ids: list[str] = field(default_factory=list)
current_remote_workflow_node_paths: list[str] = field(default_factory=list)
resolved_remote_node_ids: list[str] = field(default_factory=list)
resolved_workflow_node_paths: list[str] = field(default_factory=list)
added_node_ids: list[str] = field(default_factory=list)
added_workflow_node_paths: list[str] = field(default_factory=list)
sandwiched_local_node_ids: list[str] = field(default_factory=list)
reasons: list[RemoteExpansionReason] = field(default_factory=list)
@dataclass(frozen=True)
class PromptGraphLink:
"""Describe one direct prompt dependency edge."""
source_node_id: str
source_output_index: int
target_node_id: str
target_input_name: str
source_class_type: str
target_class_type: str
class ModalPromptValidationError(ValueError):
"""Raised when a prompt cannot be executed with the current Modal transport."""