Files
EvoScientist-Multi/EvoScientist/commands/base.py
T
dinos bd307f3a11 refactor: LangGraph gateway layer for UI-agnostic graph and thread access (#295)
* feat(gateway): graph gateway protocol

* refactor(cli): wire gateway in cli/tui

* refactor(gateway): centralize runtime gateway init

* chore(gateway): restrict RunRequest message type

* feat(gateway): add langgraph server gateway

* chore(cli): tighten serve runtime state typing

* refactor(cli): route async task state reads through graph gateway

* refactor(gateway): support graph targets in server gateway

* refactor(cli): route session commands through graph gateway

* refactor(cli): fold thread store under graph gateway

* refactor(gateway): route graph state access through gateway

* refactor(channels): wire graph gateway

* refactor(memory): preserve graph threads for cloning

* feat(gateway): add thread cloning

* fix(tui): pass effective workspace for thread creation

* chore(memory): add workspare dir to memory worker metadata

* fix(sessions): filter preloaded UUID registy entries by the current scope

* test(fakes): use https

* refactor(consumer): consolidate imports

* fix(stream): optional summarization event

* fix(gateway): resolve abbreviated thread IDs by search

* fix(gateway): page server thread listings

* fix(gateway): emit pending interrupt events

* style: fmt

* feat(gateway): persist workspace_dir & model in thread metadata

* fix(gateway): page server thread prefix resolution

* fix(gateway): expose server thread list metadata

* refactor: add back type def

* refactor: tighten types

* revert: add back worker thread deletion

The worker thread forking changes are out of scope for now, so to
maintain parity with the existing behavior we'll leave this intact.

* fix(gateway): apply compaction to server thread history

* refactor(stream): restore direct summary replay suppression

* fix(gateway): preserve compaction state and server stream output

* fix(gateway): close local stream generator on cancellation

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
2026-06-22 13:54:14 +00:00

127 lines
3.8 KiB
Python

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable
if TYPE_CHECKING:
from ..gateway import GraphGateway
@dataclass
class Argument:
"""Definition of a command argument."""
name: str
type: type
description: str
required: bool = True
@dataclass
class SubCommand:
"""A subcommand of a parent slash command."""
name: str
description: str
arguments: list[Argument] = field(default_factory=list)
@runtime_checkable
class CommandUI(Protocol):
"""Protocol for UI operations that commands can perform."""
@property
def supports_interactive(self) -> bool: ...
def append_system(self, text: str, style: str = "dim") -> None: ...
def mount_renderable(self, renderable: Any) -> None: ...
# Optional interactive operations
async def wait_for_thread_pick(
self, threads: list[dict], current_thread: str, title: str
) -> str | None: ...
async def wait_for_skill_browse(
self, index: list[dict], installed_names: set[str], pre_filter_tag: str
) -> list[str] | None: ...
async def wait_for_mcp_browse(
self, servers: list, installed_names: set[str], pre_filter_tag: str
) -> list | None: ...
async def wait_for_model_pick(
self,
entries: list[tuple[str, str, str]],
current_model: str | None,
current_provider: str | None,
) -> tuple[str, str] | None: ...
def clear_chat(self) -> None: ...
def request_quit(self) -> None: ...
def force_quit(self) -> None: ...
async def start_new_session(self) -> None: ...
async def handle_session_resume(
self, thread_id: str, workspace_dir: str | None = None
) -> None: ...
async def flush(self) -> None: ...
@dataclass
class ChannelRuntime:
"""Mutable handle to the agent + thread bound to running channels."""
agent: Any = None
thread_id: str | None = None
def bind(self, agent: Any, thread_id: str) -> None:
self.agent = agent
self.thread_id = thread_id
def clear(self) -> None:
self.agent = None
self.thread_id = None
@dataclass
class CommandContext:
"""Context passed to commands during execution."""
agent: Any
thread_id: str
ui: CommandUI
workspace_dir: str | None = None
checkpointer: Any = None
config: Any = None
channel_runtime: ChannelRuntime | None = None
graph_gateway: GraphGateway | None = None
command_error: str | None = None
# Real LLM input token count from last usage_metadata (includes system
# prompt + tool schemas). Used by /compact for accurate display.
input_tokens_hint: int | None = None
class Command(ABC):
"""Base class for all EvoScientist slash commands."""
name: str
alias: ClassVar[list[str]] = []
description: str
arguments: ClassVar[list[Argument]] = []
subcommands: ClassVar[list[SubCommand]] = []
# When False, callers may dispatch this command without waiting for
# the background agent load to finish — important so recovery
# commands like ``/mcp add`` can run even when the MCP load is
# failing and ``_await_agent_ready`` would hang.
requires_agent: ClassVar[bool] = False
def needs_agent(self, args: list[str]) -> bool:
"""Whether this specific invocation needs the agent.
Default returns :attr:`requires_agent`. Override when a command
has a mix of agent-using and agent-free subcommands (e.g.
``/channel start`` vs ``/channel status``).
"""
return self.requires_agent
@abstractmethod
async def execute(self, ctx: CommandContext, args: list[str]) -> None:
"""Execute the command with given context and arguments."""
pass