Orchestrator framework¶
- class klea_utils.graph.base.BaseLangGraph(logging_level: int = 20, checkpoint: str = 'inmemory', log_file: bool = True)[source]¶
Bases:
ABCAbstract base class for LangGraph-based orchestrators.
Provides common infrastructure for: - Configuration loading from env files - MCP client creation from JSON config - LLM model setup (delegated to subclasses) - LangGraph compilation and execution - Session checkpointing - Dual-stream logging
Subclasses must implement:
_setup_models: createself.llm_models; the env schema is generated from its roles._create_graph: build and compile the LangGraph.config_class: the Pydantic class for the JSON configuration.
- config_class: type[BaseModel]¶
Pydantic BaseModel class for configuration loading. Subclasses must set this to their AppConfig class.
- config_file_default: str = ''¶
Default JSON config file name, used as the
app_config_fileenv field default when none is set in the env file or process env.
- context_snapshot(state: dict[str, Any]) dict[str, Any] | None[source]¶
Return the session context to surface as a graph-level event.
Called by
run_graph_astream_events()on every per-superstepvaluesstate snapshot. Returning a dict publishes acontextstream event (change-deduped); returningNone(the default) publishes nothing.Session context is a projection of state, not a node-authored message: the app defines what the snapshot is by overriding this hook (e.g. the agent’s operating mode), while the emission itself is structural – nodes cannot write a
contextevent through thecustomchannel.- Parameters:
state – The current graph state snapshot (a dict).
- Returns:
A JSON-serializable dict for the
contextevent, orNoneif there is no session context to surface.
- env_class¶
Pydantic BaseSettings class for env loading. Subclasses need not set this: it is generated at load time from
self.llm_models(see_build_env_class()).alias of
BaseModel
- env_file_default: str = 'config.env'¶
Default config file name if the environment variable is not set.
- env_prefix: str = ''¶
Prefix prepended to generated env var names (e.g.
"KLEA_AGENT_"). Each model rolerbecomes the env var<env_prefix>R_MODEL.
- get_allowed_msgpack_modules() list[type | tuple[str, ...]][source]¶
Return types allowed for checkpoint msgpack deserialization.
Subclasses should override to add their state schemas (e.g.
EvaluateAnswerSchema,RetrievalQueryOutput). The base list covers shared utils types checkpointed by all graphs.
- graph_name: str = 'BaseLangGraph'¶
Logger name for this orchestrator, also used as the app name for
platformdirsdata/cache directories.
- async graph_stream(query: str | None, thread_id: str = 'default_thread', *, extra_state: dict[str, Any] | None = None, context: KleaRunContext | dict[str, Any] | None = None) Any[source]¶
Run the graph and return the raw astream result.
- Parameters:
query – User query string, or
Noneto resume the thread from its last checkpoint (seerun_graph_invoke()).thread_id – Session/thread identifier for checkpointing
extra_state – Optional initial state fields merged into the invocation alongside
query(e.g. an app-specificmoderequest). Ignored on resume.context – Per-run runtime context (ADR-0033), forwarded verbatim to the graph run (see
run_graph_invoke()).
- Returns:
Raw async generator from
graph.astream()
- mcp_session_pool: Any | None¶
Optional per-session MCP client pool (set by the API app before
setup()so the tool-caller node can use it).Nonekeeps the sharedmcp_clientfor non-server callers and tests.
- new_mcp_client(client_context: ClientContext | None = None) Client | None[source]¶
Return a new MCP client over the configured servers.
The returned client owns its own connections and server subprocesses; it is used to give each chat session an isolated MCP session. When client_context is given, only the
bundledserver entry is rebuilt with that cwd/env/interpreter; external MCP servers keep their configured invocation. ReturnsNonewhen no MCP server is configured, matching_create_mcp_client().- Parameters:
client_context – Optional client-supplied environment for the bundled tools server.
- Returns:
A new MCP client, or
Nonewhen nothing is configured.
- async run_graph_astream_events(query: str | Command | None, thread_id: str = 'default_thread', *, extra_state: dict[str, Any] | None = None, context: KleaRunContext | dict[str, Any] | None = None)[source]¶
Run the graph and yield structured streaming events.
Yields dicts with:
{"type": "progress", "node": "<label>", "data": {...}}When the graph enters a new node, or when the node’s progress heading changes (e.g. an LLM invoke retry).
data["heading"]is the line shown while the node runs.{"type": "inspect", "node": "<label>", "data": {...}}Inspection data from a node after execution (summary + details)
{"type": "token", "content": "<chunk>", "node": "<label>"}LLM token chunk from the current node. Emitted only for nodes in
token_stream_nodes(free-text answer nodes), not for structured-output nodes.{"type": "usage", "node": "<label>", "data": {...}}Per-node token usage (input / output / total tokens)
{"type": "context", "data": {...}}Session-level context (app-defined, e.g. an operating mode and its assurance), emitted by a node via a
contextcustom event{"type": "interrupt", "node": "<label>", "data": {...}}The run paused at a node’s
interrupt().datacarries the node-authored payload (kind,question) plus the framework’sinterrupt_id; when a pause is emitted nocompleteevent follows.{"type": "complete", "message_for_user": "<answer>"}Final answer from the completed graph
Uses LangGraph’s
astream_eventsv3 protocol. Progress events from all nodes (LLM and non-LLM) arrive via thecustomchannel. LLM token output is read from themessageschannel. AStreamTransformerenables the custom channel so those events flow through.Reference: https://docs.langchain.com/oss/python/langgraph/event-streaming Interrupts: https://docs.langchain.com/oss/python/langgraph/interrupts
- Parameters:
query – User query string,
Noneto resume the thread from its last checkpoint (see the fault-tolerance note), or aCommand(e.g.Command(resume=...)) to continue a paused thread. On aNoneresume the graph re-enters the failed node rather than the entry node, so per-turn state is preserved; aCommandis passed through verbatim.thread_id – Session/thread identifier for checkpointing
extra_state – Optional initial state fields merged into the invocation alongside
query(e.g. an app-specificmoderequest). Ignored when query isNoneor aCommand.context – Per-run runtime context (ADR-0033), forwarded verbatim to the graph run (see
run_graph_invoke()).
- Yields:
Structured event dicts
- async run_graph_invoke(query: str | Command | None, thread_id: str = 'default_thread', *, extra_state: dict[str, Any] | None = None, context: KleaRunContext | dict[str, Any] | None = None) str[source]¶
Run the graph with a simple string query.
- Parameters:
query – User query string,
Noneto resume the thread from its last checkpoint (the failed node is re-run; the entry node is not), or aCommand(e.g.Command(resume=...)) to continue a paused thread. On aNoneresumeextra_stateis ignored; aCommandis passed through verbatim.thread_id – Session/thread identifier for checkpointing
extra_state – Optional initial state fields merged into the invocation (e.g. an app-specific
moderequest). These are passed tograph.ainvokealongsidequeryand validated against the graph’s state schema.context – Per-run runtime context (ADR-0033), forwarded verbatim to the graph run. Accepts a
KleaRunContext(or app subclass) instance or a plain dict – dicts are coerced/validated against the graph’scontext_schemaat the run boundary. The base contract is generic:model_overridesis a framework-provided convention (apps may diverge at the node layer) and apps add their own keys.
- Returns:
The
message_for_userfield from the final state- Note:
This is a bare
ainvokewith novalues-event loop, socontext_snapshotis never invoked here and the session context projection is not produced on this path. Callers that need it should userun_graph_astream_events()(contextstream events) or the hydration endpointGET /chat/{user_id}/{chat_id}/context(ADR-0032).
- async run_graph_invoke_state(state: dict, thread_id: str = 'default_thread') dict[source]¶
Run the graph, accepting and returning full state dicts.
- Parameters:
state – Initial graph state (must contain
querykey)thread_id – Session/thread identifier for checkpointing
- Returns:
Final graph state
- async run_graph_stream(query: str | None, thread_id: str = 'default_thread', *, extra_state: dict[str, Any] | None = None, context: KleaRunContext | dict[str, Any] | None = None)[source]¶
Run the graph and yield intermediate
message_for_uservalues.- Parameters:
query – User query string, or
Noneto resume the thread from its last checkpoint (seerun_graph_invoke()).thread_id – Session/thread identifier for checkpointing
extra_state – Optional initial state fields merged into the invocation alongside
query(e.g. an app-specificmoderequest). Ignored on resume.context – Per-run runtime context (ADR-0033), forwarded verbatim to the graph run (see
run_graph_invoke()).
- Yields:
message_for_userstrings from each node
- final async setup() None[source]¶
Set up the orchestrator.
Calls hooks and template methods in this order:
_pre_setup()_setup_checkpointer()_setup_models(): buildself.llm_models(roles and required flags) that the env schema is generated from._load_env(): parse the env intoself.app_envusing the schema generated fromllm_models, then load the JSON config._configure_resources()_check_required_models()_create_mcp_client()_pre_graph()_create_graph()_post_setup()
- storage_root¶
Storage root for the checkpoint/session databases, lockfile, and logs. Defaults to this graph’s per-app data directory; a graph may opt into an environment-variable override via
storage_root_env.
- storage_root_env: str | None = None¶
Environment variable that relocates this graph’s storage root (data and runtime artifacts).
Nonedisables relocation; the agent sets this toKLEA_AGENT_HOME.
- token_stream_nodes: set[str]¶
Labels of nodes whose LLM token deltas should be forwarded as
tokenstream events. A node opts in withstream_tokens = True; the app adds its label here at graph build time. Empty means no token events are emitted, so structured-output nodes never flood the stream with JSON fragments.
- user_agent¶
Klea identity sent to OpenAI-compatible endpoints (User-Agent).
Shared state schemas¶
Schemas shared by LangGraph orchestrators.
File: klea_utils/graph/schemas.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.graph.schemas.TokenUsage(*, input_tokens: int = 0, output_tokens: int = 0, total_tokens: int = 0, reasoning_tokens: int = 0, cached_tokens: int = 0, role: str = '')[source]¶
Bases:
BaseModelToken usage for one node, or accumulated across a graph run.
role,reasoning_tokensandcached_tokensare populated on the per-nodeusagestream event so a run can be benchmarked per node and per role. When this schema is accumulated into the graph state’susage_metrics(seeadd_token_usage()) only the numeric fields are summed;roleis identity, not a quantity, so it is dropped from the aggregate.- cached_tokens: int¶
Prompt tokens served from the provider’s prompt cache (cache reads only; cache-creation/write tokens remain part of
input_tokens).
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Reducers¶
Reducers shared by LangGraph orchestrators.
File: klea_utils/graph/reducers.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.graph.reducers.add_token_usage(left: TokenUsage | dict[str, Any], right: TokenUsage | dict[str, Any]) TokenUsage[source]¶
Add token usage updates from sequential or concurrent graph nodes.
Only the numeric counters are summed.
roleidentifies the node that produced a single update, so it is not meaningful for the aggregate and is dropped (left empty) rather than concatenated.
Per-run runtime context¶
Per-run runtime context for Klea graphs.
Carried by LangGraph’s Runtime context mechanism (ADR-0033): the run
methods accept a context= value forwarded to ainvoke/astream/
astream_events, and nodes read it inside execution via ambient
get_runtime(). This is the framework-native replacement for the
hand-rolled model_overrides_ctx contextvar removed in ADR-0033; the
LLM’s per-invocation RunnableConfig merge is unchanged (ADR-0014).
See devdocs/system/streams.md (context event) and
devdocs/system/api-sse-sequence.md.
File: klea_utils/graph/context.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.graph.context.KleaRunContext(*, model_overrides: dict[str, dict[str, ~typing.Any]]=<factory>, client_context: ClientContext | None = None, **extra_data: Any)[source]¶
Bases:
BaseModelPer-run runtime context for one graph invocation.
model_overridesis the conventional key the shared nodes inklea_utils.nodesconsume (chat_corepopulates it from the sessions database).client_contextcarries the runtime environment a client (currently the TUI) wants its per-session tool server to use; it is accepted from loopback peers only and never persisted.extra="allow"keeps the schema generic (ADR-0031): an app that wants its own keys validated at the run boundary subclasses this model and registers the subclass as itscontext_schema(LangGraph coerces thecontextdict viacontext_schema(**context)); an app that prefers to validate custom keys itself reads them frommodel_extra.- client_context: ClientContext | None¶
Client-supplied environment for the per-session bundled MCP server (cwd, interpreter, env). Runtime-only and never checkpointed.
- model_config = {'extra': 'allow'}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.graph.context.model_overrides_from_context(context: KleaRunContext | dict[str, Any] | None) dict[str, dict[str, Any]][source]¶
Return the
model_overridesslice from a run context, or{}.Tolerates a
KleaRunContext(attribute access), a plain dict (context["model_overrides"]), orNone–Runtime.contextisNonewhen nocontext=was passed (ADR-0033, probe-verified), so the shared nodes must never seeNone.- Parameters:
context – The run context, e.g.
get_runtime().context.- Returns:
The overrides dict (never
None).