Source code for klea_utils.nodes.context
#!/usr/bin/env python3
"""
Per-run node context.
A node's intermediate values (prompt, LLM instance, raw output, result,
token usage, ...) are local to a single ``execute`` invocation. They live
in a :class:`NodeContext` built and passed by ``execute``, instead of on
the shared node instance - so two concurrent runs cannot clobber each
other's state.
The context deliberately does **not** carry the graph ``state``: that is
already an explicit argument to the hooks that need it, and keeping it out
avoids duplicating (and diverging from) the checkpointed state.
File: klea_utils/nodes/context.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
from dataclasses import dataclass, field
from typing import Any, Generic, TypeVar
from fastmcp.client.client import CallToolResult
from langchain_core.prompt_values import PromptValue
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import Runnable, RunnableConfig
from pydantic import BaseModel
from klea_utils.graph.schemas import TokenUsage
TOutput = TypeVar("TOutput")
[docs]
@dataclass
class NodeContext:
"""Base per-run node scratch space (no graph state).
Subclasses add the fields a node family computes during execution. The
object is created by ``execute`` for a single run and passed to that
run's hooks; it is never stored on the shared node instance.
"""
[docs]
@dataclass
class LLMNodeContext(NodeContext, Generic[TOutput]):
"""Per-run scratch for an LLM node.
Mirrors the values the LLM template computes and the streaming/inspection
hooks read: the resolved schema, the prompt pipeline, the LLM invocation
and its processed output.
"""
#: Output schema resolved for this run (static or dynamic).
output_schema: type[BaseModel] | None = None
system_prompt: Any = None
human_prompt: str | None = None
template: ChatPromptTemplate | None = None
variables: dict[str, Any] = field(default_factory=dict)
prompt: PromptValue | None = None
llm: Runnable | None = None
config: RunnableConfig | None = None
#: Raw LLM output and its processed/validated result.
output: Any = None
result: TOutput | None = None
#: Validation feedback from the most recent rejected result.
validation_feedback: str = ""
#: State updates produced by ``_update_state`` and the final merged dict.
state_updates: dict[str, Any] | None = None
final_state: dict[str, Any] | None = None
#: Token usage for this run.
token_usage: TokenUsage | None = None
[docs]
@dataclass
class ToolCallerContext(NodeContext):
"""Per-run scratch for the tool-caller node."""
tool_results: list[CallToolResult] = field(default_factory=list)
#: Per-result display flag, aligned with :attr:`tool_results`.
display_flags: list[bool] = field(default_factory=list)
#: Directories the user approved for the session while resolving a
#: permission interrupt (``allow for session``); persisted to
#: ``state.allowed_dirs`` by the node (ADR-0007 update 2026-10-08).
allowed_session_dirs: list[str] = field(default_factory=list)
#: Sensitive files approved for the session (``allow for session`` on a
#: sensitive request); persisted to ``state.allowed_files``.
allowed_session_files: list[str] = field(default_factory=list)