Source code for klea_utils.commands.graph
#!/usr/bin/env python3
"""
Graph-side support for the session-command framework (ADR-0047).
Holds the graph-command handler contract and its result type, plus the entry
router that decides whether a query is a server-side command. The
``CommandNode`` itself lives in :mod:`klea_utils.nodes.command`.
See ``devdocs/system/session-commands.md``; ADR-0047.
File: klea_utils/commands/graph.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any
from pydantic import BaseModel
from klea_utils.commands.common import ParsedCommand, parse_command
logger = logging.getLogger(__name__)
[docs]
@dataclass
class GraphCommandResult:
"""The outcome of a server-side command handler.
:param updates: State updates to merge (e.g. a new ``mode``).
:param message: The user-facing reply (written to ``message_for_user``).
:param summary: Inspect-event summary (defaults to :attr:`message`).
:param details: Inspect-event details.
:param route: A logical route to hand off to after the command's state
updates (written to the transient ``command_route``); ``""`` (default)
ends the run. The app maps the route name to a graph node via the
command node's conditional edge (see ``devdocs/system/graph-routing.md``).
"""
updates: dict[str, Any] = field(default_factory=dict)
message: str = ""
summary: str = ""
details: dict[str, Any] = field(default_factory=dict)
route: str = ""
#: A graph command handler: ``(state, parsed) -> GraphCommandResult``, or an
#: awaitable of one. A handler may be ``async`` when it awaits an LLM or tool
#: call (e.g. ``/compact``); the command node awaits the result when needed.
GraphCommandHandler = Callable[
[BaseModel, ParsedCommand],
GraphCommandResult | Awaitable[GraphCommandResult],
]
[docs]
def command_query_router(state: BaseModel) -> str:
"""Return ``"command"`` for a command query, else ``"continue"``.
The router makes a single decision: is the query syntactically a command
(a leading ``/``, with ``//`` escaping a literal slash) or a general
query? The command node does the rest -- validating the name, dispatching
an executable command, or replying with a direct message for an unknown,
client-side, or not-yet-implemented command.
:param state: The graph state (its ``query`` is inspected).
:returns: ``"command"`` or ``"continue"``.
"""
parsed = parse_command(getattr(state, "query", ""))
if parsed is not None:
logger.debug(f"command query routed to the command node: /{parsed.name}")
return "command"
return "continue"
[docs]
def command_route_router(state: BaseModel) -> str:
"""Return a command's handoff route, or ``""`` for a terminal command.
The command node writes the handler's ``GraphCommandResult.route`` to the
transient ``command_route`` state field; the app's conditional edge maps
this label to a graph node (``""`` -> ``END``). Used as the path function
of ``add_conditional_edges`` immediately after the command node, so a
workflow command can hand off to another node (e.g. ``/compact`` -> the
summarise node) before the run ends. See
``devdocs/system/graph-routing.md``.
:param state: The graph state (its ``command_route`` is read).
:returns: The route label (``""`` when the command is terminal).
"""
route = getattr(state, "command_route", "") or ""
logger.debug(f"command route: {route!r}")
return route