"""NodePool: shared stateless node instances across all sessions. Stateless nodes (InputNode, PANode, ExpertNode, etc.) hold no per-session state — only config (model, system prompt). They can safely serve multiple concurrent sessions. Session-specific HUD routing uses contextvars. Stateful nodes (SensorNode, MemorizerNode, UINode) hold conversational state and must be created per-session. """ import logging from .engine import load_graph, instantiate_nodes log = logging.getLogger("runtime") # Roles that hold per-session state — always created fresh per Runtime STATEFUL_ROLES = frozenset({"sensor", "memorizer", "ui"}) async def _noop_hud(data: dict): """Placeholder HUD — shared nodes use contextvars for session routing.""" pass class NodePool: """Shared node instances for stateless LLM nodes. Usage: pool = NodePool("v4-eras") # Shared nodes (one instance, all sessions): input_node = pool.shared["input"] # Stateful nodes must be created per-session (not in pool) """ def __init__(self, graph_name: str = "v4-eras"): self.graph = load_graph(graph_name) self.graph_name = graph_name # Instantiate all nodes with noop HUD (shared nodes use contextvars) all_nodes = instantiate_nodes(self.graph, send_hud=_noop_hud) # Split: shared (stateless) vs excluded (stateful) self.shared = { role: node for role, node in all_nodes.items() if role not in STATEFUL_ROLES } log.info(f"[pool] created for graph '{graph_name}': " f"{len(self.shared)} shared, {len(STATEFUL_ROLES)} stateful")