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."""
|
||||
|
||||
import contextvars
|
||||
import logging
|
||||
|
||||
from ..llm import estimate_tokens, fit_context
|
||||
|
||||
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:
|
||||
name: str = "node"
|
||||
@ -18,10 +23,11 @@ class Node:
|
||||
self.context_fill_pct = 0
|
||||
|
||||
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:
|
||||
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]:
|
||||
"""Fit messages within this node's token budget."""
|
||||
|
||||
Reference in New Issue
Block a user