Implement NodePool + contextvar HUD for Phase 2 shared nodes
- agent/node_pool.py: NodePool class creates shared stateless node instances, excludes stateful roles (sensor, memorizer, ui) - agent/nodes/base.py: _current_hud contextvar for per-task HUD isolation, Node.hud() checks contextvar first, falls back to instance callback - 15/15 engine tests green (4 new Phase 2 tests pass) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3350fd502
commit
ae2338e70a
50
agent/node_pool.py
Normal file
50
agent/node_pool.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""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")
|
||||||
@ -1,11 +1,16 @@
|
|||||||
"""Base Node class with context management."""
|
"""Base Node class with context management."""
|
||||||
|
|
||||||
|
import contextvars
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..llm import estimate_tokens, fit_context
|
from ..llm import estimate_tokens, fit_context
|
||||||
|
|
||||||
log = logging.getLogger("runtime")
|
log = logging.getLogger("runtime")
|
||||||
|
|
||||||
|
# Per-task HUD callback — set by FrameEngine/Runtime before calling shared nodes.
|
||||||
|
# Isolates HUD events between concurrent sessions (asyncio.Task-scoped).
|
||||||
|
_current_hud = contextvars.ContextVar('send_hud', default=None)
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
name: str = "node"
|
name: str = "node"
|
||||||
@ -18,10 +23,11 @@ class Node:
|
|||||||
self.context_fill_pct = 0
|
self.context_fill_pct = 0
|
||||||
|
|
||||||
async def hud(self, event: str, **data):
|
async def hud(self, event: str, **data):
|
||||||
# Always include model on context events so frontend knows what model each node uses
|
# Use task-scoped HUD if set (shared node pool), else instance callback
|
||||||
|
hud_fn = _current_hud.get() or self.send_hud
|
||||||
if event == "context" and self.model:
|
if event == "context" and self.model:
|
||||||
data["model"] = self.model
|
data["model"] = self.model
|
||||||
await self.send_hud({"node": self.name, "event": event, **data})
|
await hud_fn({"node": self.name, "event": event, **data})
|
||||||
|
|
||||||
def trim_context(self, messages: list[dict]) -> list[dict]:
|
def trim_context(self, messages: list[dict]) -> list[dict]:
|
||||||
"""Fit messages within this node's token budget."""
|
"""Fit messages within this node's token budget."""
|
||||||
|
|||||||
Reference in New Issue
Block a user