- 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>
41 lines
1.5 KiB
Python
41 lines
1.5 KiB
Python
"""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"
|
|
model: str | None = None
|
|
max_context_tokens: int = 4000
|
|
|
|
def __init__(self, send_hud):
|
|
self.send_hud = send_hud
|
|
self.last_context_tokens = 0
|
|
self.context_fill_pct = 0
|
|
|
|
async def hud(self, event: str, **data):
|
|
# 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 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."""
|
|
before = len(messages)
|
|
result = fit_context(messages, self.max_context_tokens)
|
|
self.last_context_tokens = sum(estimate_tokens(m["content"]) for m in result)
|
|
self.context_fill_pct = int(100 * self.last_context_tokens / self.max_context_tokens)
|
|
if before != len(result):
|
|
log.info(f"[{self.name}] context trimmed: {before} -> {len(result)} msgs, {self.context_fill_pct}% fill")
|
|
return result
|