Spaces:
Running
Running
| """Per-run dependencies handed to graph nodes through LangGraph's runtime context. | |
| Nodes stay pure functions of ``(state, config)``; everything with a lifecycle β | |
| the MCP session, the model engine, the event bus, the budget guard β travels in | |
| :class:`RunContext` so a node never reaches for a global. | |
| Delivery uses LangGraph's first-class ``context`` channel | |
| (``StateGraph(..., context_schema=RunContext)`` plus ``ainvoke(..., context=ctx)``). | |
| This matters: values placed in ``config["configurable"]`` are checkpoint state and | |
| are filtered when a checkpointer is attached, whereas the runtime context is | |
| explicitly *not* persisted β which is exactly right for live handles like an open | |
| MCP session. | |
| A ``config``-based fallback is retained so a node can be unit-tested in isolation | |
| by invoking it directly with ``ctx.to_config()``. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Iterator | |
| from contextlib import contextmanager | |
| from contextvars import ContextVar | |
| from dataclasses import dataclass, field | |
| from typing import Any | |
| from langchain_core.runnables import RunnableConfig | |
| from langgraph.runtime import get_runtime | |
| from app.core.budget import BudgetGuard | |
| from app.core.claude import LLMEngine | |
| from app.core.events import EventBus, EventKind | |
| from app.core.settings import Settings | |
| from app.core.tracing import Tracer | |
| from app.mcp_server.client import McpToolClient | |
| CONTEXT_KEY = "alphabrief_context" | |
| class RunContext: | |
| """Everything a node needs that is not state.""" | |
| run_id: str | |
| settings: Settings | |
| engine: LLMEngine | |
| mcp: McpToolClient | |
| bus: EventBus | |
| tracer: Tracer | |
| budget: BudgetGuard | |
| extras: dict[str, Any] = field(default_factory=dict) | |
| async def emit( | |
| self, | |
| kind: EventKind, | |
| message: str, | |
| payload: dict[str, Any] | None = None, | |
| ) -> None: | |
| """Publish one telemetry row for this run.""" | |
| await self.bus.publish(self.run_id, kind, message, payload) | |
| def to_config(self, **extra: Any) -> RunnableConfig: | |
| """Thread config for the graph. Identity only β deps travel in `context`.""" | |
| configurable: dict[str, Any] = {"thread_id": self.run_id, CONTEXT_KEY: self} | |
| configurable.update(extra) | |
| return RunnableConfig(configurable=configurable) | |
| #: Test-only override, so a node can be invoked directly without a live graph. | |
| _override: ContextVar[RunContext | None] = ContextVar("alphabrief_run_context", default=None) | |
| def use_context(ctx: RunContext) -> Iterator[RunContext]: | |
| """Bind a :class:`RunContext` for direct node invocation (tests, eval).""" | |
| token = _override.set(ctx) | |
| try: | |
| yield ctx | |
| finally: | |
| _override.reset(token) | |
| def current_context() -> RunContext: | |
| """The :class:`RunContext` the current node is executing under. | |
| Nodes take no ``config`` parameter: LangGraph's runtime context is the | |
| supported channel for per-run dependencies, and a ``config`` parameter | |
| annotated under ``from __future__ import annotations`` is a string at | |
| inspection time, which LangGraph cannot match β it would be silently ignored. | |
| """ | |
| override = _override.get() | |
| if override is not None: | |
| return override | |
| try: | |
| runtime = get_runtime(RunContext) | |
| except Exception: # noqa: BLE001 - not running inside a graph | |
| runtime = None | |
| if runtime is not None and isinstance(runtime.context, RunContext): | |
| return runtime.context | |
| raise RuntimeError( | |
| "RunContext unavailable β invoke the graph with `context=ctx`, or wrap a " | |
| "direct node call in `with use_context(ctx): ...`." | |
| ) | |