diff --git a/tests/tools/test_managed_modal_environment.py b/tests/tools/test_managed_modal_environment.py index ded9cd3d4ba..9464959e812 100644 --- a/tests/tools/test_managed_modal_environment.py +++ b/tests/tools/test_managed_modal_environment.py @@ -75,10 +75,27 @@ def _install_fake_tools_package(*, credential_mounts=None): self.cwd = cwd self.timeout = timeout self.env = env or {} + self._snapshot_path = None + self._cwdfile_path = None + self._snapshot_ready = False + self._session_id = "" def _prepare_command(self, command: str): return command, None + def _wrap_command(self, command: str, cwd: str) -> str: + """Simplified wrapper for tests — just returns the command.""" + return command + + def _update_cwd_from_file(self): + pass + + def stop(self): + self.cleanup() + + def cleanup(self): + pass + sys.modules["tools.environments.base"] = types.SimpleNamespace(BaseEnvironment=_DummyBaseEnvironment) sys.modules["tools.managed_tool_gateway"] = types.SimpleNamespace( resolve_managed_tool_gateway=lambda vendor: types.SimpleNamespace( @@ -110,7 +127,8 @@ class _FakeResponse: def test_managed_modal_execute_polls_until_completed(monkeypatch): _install_fake_tools_package() managed_modal = _load_tool_module("tools.environments.managed_modal", "environments/managed_modal.py") - modal_common = sys.modules["tools.environments.modal_common"] + # time.sleep / time.monotonic now live in managed_modal (not modal_common) + modal_common = managed_modal calls = [] poll_count = {"value": 0} @@ -173,7 +191,8 @@ def test_managed_modal_create_sends_a_stable_idempotency_key(monkeypatch): def test_managed_modal_execute_cancels_on_interrupt(monkeypatch): interrupt_event = _install_fake_tools_package() managed_modal = _load_tool_module("tools.environments.managed_modal", "environments/managed_modal.py") - modal_common = sys.modules["tools.environments.modal_common"] + # time.sleep / time.monotonic now live in managed_modal (not modal_common) + modal_common = managed_modal calls = [] @@ -215,7 +234,8 @@ def test_managed_modal_execute_cancels_on_interrupt(monkeypatch): def test_managed_modal_execute_returns_descriptive_error_on_missing_exec(monkeypatch): _install_fake_tools_package() managed_modal = _load_tool_module("tools.environments.managed_modal", "environments/managed_modal.py") - modal_common = sys.modules["tools.environments.modal_common"] + # time.sleep / time.monotonic now live in managed_modal (not modal_common) + modal_common = managed_modal def fake_request(method, url, headers=None, json=None, timeout=None): if method == "POST" and url.endswith("/v1/sandboxes"): @@ -293,7 +313,8 @@ def test_managed_modal_rejects_host_credential_passthrough(): def test_managed_modal_execute_times_out_and_cancels(monkeypatch): _install_fake_tools_package() managed_modal = _load_tool_module("tools.environments.managed_modal", "environments/managed_modal.py") - modal_common = sys.modules["tools.environments.modal_common"] + # time.sleep / time.monotonic now live in managed_modal (not modal_common) + modal_common = managed_modal calls = [] monotonic_values = iter([0.0, 12.5]) diff --git a/tools/environments/local.py b/tools/environments/local.py index 197f7c24902..ec3b6ac3c0e 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -204,58 +204,6 @@ def _find_bash() -> str: _find_shell = _find_bash -# Noise lines emitted by interactive shells when stdin is not a terminal. -# Used as a fallback when output fence markers are missing. -_SHELL_NOISE_SUBSTRINGS = ( - # bash - "bash: cannot set terminal process group", - "bash: no job control in this shell", - "no job control in this shell", - "cannot set terminal process group", - "tcsetattr: Inappropriate ioctl for device", - # zsh / oh-my-zsh / macOS terminal session - "Restored session:", - "Saving session...", - "Last login:", - "command not found:", - "Oh My Zsh", - "compinit:", -) - - -def _clean_shell_noise(output: str) -> str: - """Strip shell startup/exit warnings that leak when using -i without a TTY. - - Removes lines matching known noise patterns from both the beginning - and end of the output. Lines in the middle are left untouched. - """ - - def _is_noise(line: str) -> bool: - return any(noise in line for noise in _SHELL_NOISE_SUBSTRINGS) - - lines = output.split("\n") - - # Strip leading noise - while lines and _is_noise(lines[0]): - lines.pop(0) - - # Strip trailing noise (walk backwards, skip empty lines from split) - end = len(lines) - 1 - while end >= 0 and (not lines[end] or _is_noise(lines[end])): - end -= 1 - - if end < 0: - return "" - - cleaned = lines[: end + 1] - result = "\n".join(cleaned) - - # Preserve trailing newline if original had one - if output.endswith("\n") and result and not result.endswith("\n"): - result += "\n" - return result - - # Standard PATH entries for environments with minimal PATH (e.g. systemd services). # Includes macOS Homebrew paths (/opt/homebrew/* for Apple Silicon). _SANE_PATH = ( @@ -285,30 +233,6 @@ def _make_run_env(env: dict) -> dict: return run_env -def _extract_fenced_output(raw: str) -> str: - """Extract real command output from between fence markers. - - The execute() method wraps each command with printf(FENCE) markers. - This function finds the first and last fence and returns only the - content between them, which is the actual command output free of - any shell init/exit noise. - - Falls back to pattern-based _clean_shell_noise if fences are missing. - """ - first = raw.find(_OUTPUT_FENCE) - if first == -1: - return _clean_shell_noise(raw) - - start = first + len(_OUTPUT_FENCE) - last = raw.rfind(_OUTPUT_FENCE) - - if last <= first: - # Only start fence found (e.g. user command called `exit`) - return _clean_shell_noise(raw[start:]) - - return raw[start:last] - - class LocalEnvironment(BaseEnvironment): """Run commands directly on the host machine. diff --git a/tools/environments/managed_modal.py b/tools/environments/managed_modal.py index a8197bccf28..121de1113d2 100644 --- a/tools/environments/managed_modal.py +++ b/tools/environments/managed_modal.py @@ -1,20 +1,24 @@ -"""Managed Modal environment backed by tool-gateway.""" +"""Managed Modal environment backed by tool-gateway. + +Uses ``BaseEnvironment`` for command shaping (``_wrap_command()``) but keeps +its own ``execute()`` override because the HTTP gateway cannot return a +ProcessHandle — all execution is request/response. +""" from __future__ import annotations import json import logging import os +import shlex import requests +import time import uuid from dataclasses import dataclass from typing import Any, Dict, Optional -from tools.environments.modal_common import ( - BaseModalExecutionEnvironment, - ModalExecStart, - PreparedModalExec, -) +from tools.environments.base import BaseEnvironment +from tools.interrupt import is_interrupted from tools.managed_tool_gateway import resolve_managed_tool_gateway logger = logging.getLogger(__name__) @@ -33,15 +37,18 @@ class _ManagedModalExecHandle: exec_id: str -class ManagedModalEnvironment(BaseModalExecutionEnvironment): - """Gateway-owned Modal sandbox with Hermes-compatible execute/cleanup.""" +class ManagedModalEnvironment(BaseEnvironment): + """Gateway-owned Modal sandbox with Hermes-compatible execute/cleanup. + + Inherits from BaseEnvironment for _wrap_command() (CWD tracking, + snapshot sourcing) but keeps its own execute() since the HTTP gateway + cannot return a ProcessHandle. + """ _CONNECT_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_CONNECT_TIMEOUT_SECONDS", 1.0) _POLL_READ_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_POLL_READ_TIMEOUT_SECONDS", 5.0) _CANCEL_READ_TIMEOUT_SECONDS = _request_timeout_env("TERMINAL_MANAGED_MODAL_CANCEL_READ_TIMEOUT_SECONDS", 5.0) _client_timeout_grace_seconds = 10.0 - _interrupt_output = "[Command interrupted - Modal sandbox exec cancelled]" - _unexpected_error_prefix = "Managed Modal exec failed" def __init__( self, @@ -69,16 +76,124 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): self._create_idempotency_key = str(uuid.uuid4()) self._sandbox_id = self._create_sandbox() - def _start_modal_exec(self, prepared: PreparedModalExec) -> ModalExecStart: + # ------------------------------------------------------------------ + # _run_bash stub — ManagedModal cannot return a ProcessHandle + # ------------------------------------------------------------------ + + def _run_bash(self, cmd_string: str, *, stdin_data: str | None = None): + raise NotImplementedError( + "ManagedModalEnvironment is HTTP-based and cannot return a " + "ProcessHandle. Use execute() directly." + ) + + # ------------------------------------------------------------------ + # execute() override — HTTP request/response model + # ------------------------------------------------------------------ + + def execute( + self, + command: str, + cwd: str = "", + *, + timeout: int | None = None, + stdin_data: str | None = None, + ) -> dict: + effective_timeout = timeout or self.timeout + + # Handle stdin via heredoc embedding (gateway has payload support too) + exec_stdin = stdin_data + if stdin_data is not None: + marker = f"HERMES_EOF_{uuid.uuid4().hex[:8]}" + while marker in stdin_data: + marker = f"HERMES_EOF_{uuid.uuid4().hex[:8]}" + command = f"{command} << '{marker}'\n{stdin_data}\n{marker}" + exec_stdin = None # embedded in command now + + exec_command, sudo_stdin = self._prepare_command(command) + if sudo_stdin is not None: + exec_command = ( + f"printf '%s\\n' {shlex.quote(sudo_stdin.rstrip())} | {exec_command}" + ) + + # Use _wrap_command for consistent CWD tracking and snapshot sourcing + wrapped = self._wrap_command(exec_command, cwd) + effective_cwd = cwd or self.cwd + + # Start the exec via the gateway + start_result = self._start_exec(wrapped, effective_cwd, effective_timeout, exec_stdin) + + if start_result.get("_immediate"): + result = {k: v for k, v in start_result.items() if k != "_immediate"} + self._update_cwd_from_gateway_output(result) + return result + + handle = start_result.get("_handle") + if handle is None: + return self._error_result( + "Managed Modal exec start did not return an exec handle" + ) + + # Poll loop + deadline = None + if self._client_timeout_grace_seconds is not None: + deadline = time.monotonic() + effective_timeout + self._client_timeout_grace_seconds + + while True: + if is_interrupted(): + try: + self._cancel_exec(handle.exec_id) + except Exception: + pass + return self._result( + "[Command interrupted - Modal sandbox exec cancelled]", 130, + ) + + try: + result = self._poll_exec(handle) + except Exception as exc: + return self._error_result(f"Managed Modal exec poll failed: {exc}") + + if result is not None: + self._update_cwd_from_gateway_output(result) + return result + + if deadline is not None and time.monotonic() >= deadline: + try: + self._cancel_exec(handle.exec_id) + except Exception: + pass + return self._result( + f"Managed Modal exec timed out after {effective_timeout}s", 124, + ) + + time.sleep(0.25) + + def _update_cwd_from_gateway_output(self, result: dict) -> None: + """Best-effort CWD update from the cwdfile written by _wrap_command. + + Since we can't read files from the gateway sandbox directly, we + only update if there's a subsequent call that reads it. The + _wrap_command template writes CWD to the cwdfile which will be + read on the next execute() call if we had file access. For now, + CWD tracking in managed modal relies on the cwd parameter. + """ + pass + + # ------------------------------------------------------------------ + # Gateway transport + # ------------------------------------------------------------------ + + def _start_exec(self, command: str, cwd: str, timeout: int, + stdin_data: str | None) -> dict: exec_id = str(uuid.uuid4()) payload: Dict[str, Any] = { "execId": exec_id, - "command": prepared.command, - "cwd": prepared.cwd, - "timeoutMs": int(prepared.timeout * 1000), + "command": command, + "cwd": cwd, + "timeoutMs": int(timeout * 1000), } - if prepared.stdin_data is not None: - payload["stdinData"] = prepared.stdin_data + if stdin_data is not None: + payload["stdinData"] = stdin_data try: response = self._request( @@ -88,37 +203,38 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): timeout=10, ) except Exception as exc: - return ModalExecStart( - immediate_result=self._error_result(f"Managed Modal exec failed: {exc}") - ) + return { + **self._error_result(f"Managed Modal exec failed: {exc}"), + "_immediate": True, + } if response.status_code >= 400: - return ModalExecStart( - immediate_result=self._error_result( + return { + **self._error_result( self._format_error("Managed Modal exec failed", response) - ) - ) + ), + "_immediate": True, + } body = response.json() status = body.get("status") if status in {"completed", "failed", "cancelled", "timeout"}: - return ModalExecStart( - immediate_result=self._result( - body.get("output", ""), - body.get("returncode", 1), - ) - ) + return { + **self._result(body.get("output", ""), body.get("returncode", 1)), + "_immediate": True, + } if body.get("execId") != exec_id: - return ModalExecStart( - immediate_result=self._error_result( + return { + **self._error_result( "Managed Modal exec start did not return the expected exec id" - ) - ) + ), + "_immediate": True, + } - return ModalExecStart(handle=_ManagedModalExecHandle(exec_id=exec_id)) + return {"_handle": _ManagedModalExecHandle(exec_id=exec_id)} - def _poll_modal_exec(self, handle: _ManagedModalExecHandle) -> dict | None: + def _poll_exec(self, handle: _ManagedModalExecHandle) -> dict | None: try: status_response = self._request( "GET", @@ -145,11 +261,9 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): ) return None - def _cancel_modal_exec(self, handle: _ManagedModalExecHandle) -> None: - self._cancel_exec(handle.exec_id) - - def _timeout_result_for_modal(self, timeout: int) -> dict: - return self._result(f"Managed Modal exec timed out after {timeout}s", 124) + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ def cleanup(self): if not getattr(self, "_sandbox_id", None): @@ -169,6 +283,10 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): finally: self._sandbox_id = None + # ------------------------------------------------------------------ + # Sandbox creation + # ------------------------------------------------------------------ + def _create_sandbox(self) -> str: cpu = self._coerce_number(self._sandbox_kwargs.get("cpu"), 1) memory = self._coerce_number( @@ -211,6 +329,10 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): raise RuntimeError("Managed Modal create did not return a sandbox id") return sandbox_id + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + def _guard_unsupported_credential_passthrough(self) -> None: """Managed Modal does not sync or mount host credential files.""" try: @@ -226,6 +348,12 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment): "credential files inside the sandbox." ) + def _result(self, output: str, returncode: int) -> dict: + return {"output": output, "returncode": returncode} + + def _error_result(self, output: str) -> dict: + return self._result(output, 1) + def _request(self, method: str, path: str, *, json: Dict[str, Any] | None = None, timeout: int = 30, diff --git a/tools/environments/persistent_shell.py b/tools/environments/persistent_shell.py deleted file mode 100644 index c4344ff5a12..00000000000 --- a/tools/environments/persistent_shell.py +++ /dev/null @@ -1,290 +0,0 @@ -"""Persistent shell mixin: file-based IPC protocol for long-lived bash shells.""" - -import logging -import shlex -import subprocess -import threading -import time -import uuid -from abc import abstractmethod - -from tools.interrupt import is_interrupted - -logger = logging.getLogger(__name__) - - -class PersistentShellMixin: - """Mixin that adds persistent shell capability to any BaseEnvironment. - - Subclasses must implement ``_spawn_shell_process()``, ``_read_temp_files()``, - ``_kill_shell_children()``, ``_execute_oneshot()``, and ``_cleanup_temp_files()``. - """ - - persistent: bool - - @abstractmethod - def _spawn_shell_process(self) -> subprocess.Popen: ... - - @abstractmethod - def _read_temp_files(self, *paths: str) -> list[str]: ... - - @abstractmethod - def _kill_shell_children(self): ... - - @abstractmethod - def _execute_oneshot(self, command: str, cwd: str, *, - timeout: int | None = None, - stdin_data: str | None = None) -> dict: ... - - @abstractmethod - def _cleanup_temp_files(self): ... - - _session_id: str = "" - _poll_interval_start: float = 0.01 # initial poll interval (10ms) - _poll_interval_max: float = 0.25 # max poll interval (250ms) — reduces I/O for long commands - - @property - def _temp_prefix(self) -> str: - return f"/tmp/hermes-persistent-{self._session_id}" - - # ------------------------------------------------------------------ - # Lifecycle - # ------------------------------------------------------------------ - - def _init_persistent_shell(self): - self._shell_lock = threading.Lock() - self._shell_proc: subprocess.Popen | None = None - self._shell_alive: bool = False - self._shell_pid: int | None = None - - self._session_id = uuid.uuid4().hex[:12] - p = self._temp_prefix - self._pshell_stdout = f"{p}-stdout" - self._pshell_stderr = f"{p}-stderr" - self._pshell_status = f"{p}-status" - self._pshell_cwd = f"{p}-cwd" - self._pshell_pid_file = f"{p}-pid" - - self._shell_proc = self._spawn_shell_process() - self._shell_alive = True - - self._drain_thread = threading.Thread( - target=self._drain_shell_output, daemon=True, - ) - self._drain_thread.start() - - init_script = ( - f"export TERM=${{TERM:-dumb}}\n" - f"touch {self._pshell_stdout} {self._pshell_stderr} " - f"{self._pshell_status} {self._pshell_cwd} {self._pshell_pid_file}\n" - f"echo $$ > {self._pshell_pid_file}\n" - f"pwd > {self._pshell_cwd}\n" - ) - self._send_to_shell(init_script) - - deadline = time.monotonic() + 3.0 - while time.monotonic() < deadline: - pid_str = self._read_temp_files(self._pshell_pid_file)[0].strip() - if pid_str.isdigit(): - self._shell_pid = int(pid_str) - break - time.sleep(0.05) - else: - logger.warning("Could not read persistent shell PID") - self._shell_pid = None - - if self._shell_pid: - logger.info( - "Persistent shell started (session=%s, pid=%d)", - self._session_id, self._shell_pid, - ) - - reported_cwd = self._read_temp_files(self._pshell_cwd)[0].strip() - if reported_cwd: - self.cwd = reported_cwd - - def _cleanup_persistent_shell(self): - if self._shell_proc is None: - return - - if self._session_id: - self._cleanup_temp_files() - - try: - self._shell_proc.stdin.close() - except Exception: - pass - try: - self._shell_proc.terminate() - self._shell_proc.wait(timeout=3) - except subprocess.TimeoutExpired: - self._shell_proc.kill() - - self._shell_alive = False - self._shell_proc = None - - if hasattr(self, "_drain_thread") and self._drain_thread.is_alive(): - self._drain_thread.join(timeout=1.0) - - # ------------------------------------------------------------------ - # execute() / cleanup() — shared dispatcher, subclasses inherit - # ------------------------------------------------------------------ - - def execute(self, command: str, cwd: str = "", *, - timeout: int | None = None, - stdin_data: str | None = None) -> dict: - if self.persistent: - return self._execute_persistent( - command, cwd, timeout=timeout, stdin_data=stdin_data, - ) - return self._execute_oneshot( - command, cwd, timeout=timeout, stdin_data=stdin_data, - ) - - def execute_oneshot(self, command: str, cwd: str = "", *, - timeout: int | None = None, - stdin_data: str | None = None) -> dict: - """Always use the oneshot (non-persistent) execution path. - - This bypasses _shell_lock so it can run concurrently with a - long-running command in the persistent shell — used by - execute_code's file-based RPC polling thread. - """ - return self._execute_oneshot( - command, cwd, timeout=timeout, stdin_data=stdin_data, - ) - - def cleanup(self): - if self.persistent: - self._cleanup_persistent_shell() - - # ------------------------------------------------------------------ - # Shell I/O - # ------------------------------------------------------------------ - - def _drain_shell_output(self): - try: - for _ in self._shell_proc.stdout: - pass - except Exception: - pass - self._shell_alive = False - - def _send_to_shell(self, text: str): - if not self._shell_alive or self._shell_proc is None: - return - try: - self._shell_proc.stdin.write(text) - self._shell_proc.stdin.flush() - except (BrokenPipeError, OSError): - self._shell_alive = False - - def _read_persistent_output(self) -> tuple[str, int, str]: - stdout, stderr, status_raw, cwd = self._read_temp_files( - self._pshell_stdout, self._pshell_stderr, - self._pshell_status, self._pshell_cwd, - ) - output = self._merge_output(stdout, stderr) - status = status_raw.strip() - if ":" in status: - status = status.split(":", 1)[1] - try: - exit_code = int(status.strip()) - except ValueError: - exit_code = 1 - return output, exit_code, cwd.strip() - - # ------------------------------------------------------------------ - # Execution - # ------------------------------------------------------------------ - - def _execute_persistent(self, command: str, cwd: str, *, - timeout: int | None = None, - stdin_data: str | None = None) -> dict: - if not self._shell_alive: - logger.info("Persistent shell died, restarting...") - self._init_persistent_shell() - - exec_command, sudo_stdin = self._prepare_command(command) - effective_timeout = timeout or self.timeout - if stdin_data or sudo_stdin: - return self._execute_oneshot( - command, cwd, timeout=timeout, stdin_data=stdin_data, - ) - - with self._shell_lock: - return self._execute_persistent_locked( - exec_command, cwd, effective_timeout, - ) - - def _execute_persistent_locked(self, command: str, cwd: str, - timeout: int) -> dict: - work_dir = cwd or self.cwd - cmd_id = uuid.uuid4().hex[:8] - truncate = ( - f": > {self._pshell_stdout}\n" - f": > {self._pshell_stderr}\n" - f": > {self._pshell_status}\n" - ) - self._send_to_shell(truncate) - escaped = command.replace("'", "'\\''") - - ipc_script = ( - f"cd {shlex.quote(work_dir)}\n" - f"eval '{escaped}' < /dev/null > {self._pshell_stdout} 2> {self._pshell_stderr}\n" - f"__EC=$?\n" - f"pwd > {self._pshell_cwd}\n" - f"echo {cmd_id}:$__EC > {self._pshell_status}\n" - ) - self._send_to_shell(ipc_script) - deadline = time.monotonic() + timeout - poll_interval = self._poll_interval_start # starts at 10ms, backs off to 250ms - - while True: - if is_interrupted(): - self._kill_shell_children() - output, _, _ = self._read_persistent_output() - return { - "output": output + "\n[Command interrupted]", - "returncode": 130, - } - - if time.monotonic() > deadline: - self._kill_shell_children() - output, _, _ = self._read_persistent_output() - if output: - return { - "output": output + f"\n[Command timed out after {timeout}s]", - "returncode": 124, - } - return self._timeout_result(timeout) - - if not self._shell_alive: - return { - "output": "Persistent shell died during execution", - "returncode": 1, - } - - status_content = self._read_temp_files(self._pshell_status)[0].strip() - if status_content.startswith(cmd_id + ":"): - break - - time.sleep(poll_interval) - # Exponential backoff: fast start (10ms) for quick commands, - # ramps up to 250ms for long-running commands — reduces I/O by 10-25x - # on WSL2 where polling keeps the VM hot and memory pressure high. - poll_interval = min(poll_interval * 1.5, self._poll_interval_max) - - output, exit_code, new_cwd = self._read_persistent_output() - if new_cwd: - self.cwd = new_cwd - return {"output": output, "returncode": exit_code} - - @staticmethod - def _merge_output(stdout: str, stderr: str) -> str: - parts = [] - if stdout.strip(): - parts.append(stdout.rstrip("\n")) - if stderr.strip(): - parts.append(stderr.rstrip("\n")) - return "\n".join(parts) diff --git a/uv.lock b/uv.lock index 8a5db543671..0c19c2d2c4b 100644 --- a/uv.lock +++ b/uv.lock @@ -10,14 +10,14 @@ resolution-markers = [ [[package]] name = "agent-client-protocol" -version = "0.9.0" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/13/3b893421369767e7043cc115d6ef0df417c298b84563be3a12df0416158d/agent_client_protocol-0.9.0.tar.gz", hash = "sha256:f744c48ab9af0f0b4452e5ab5498d61bcab97c26dbe7d6feec5fd36de49be30b", size = 71853, upload-time = "2026-03-26T01:21:00.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7b/7cdac86db388809d9e3bc58cac88cc7dfa49b7615b98fab304a828cd7f8a/agent_client_protocol-0.8.1.tar.gz", hash = "sha256:1bbf15663bf51f64942597f638e32a6284c5da918055d9672d3510e965143dbd", size = 68866, upload-time = "2026-02-13T15:34:54.567Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ed/c284543c08aa443a4ef2c8bd120be51da8433dd174c01749b5d87c333f22/agent_client_protocol-0.9.0-py3-none-any.whl", hash = "sha256:06911500b51d8cb69112544e2be01fc5e7db39ef88fecbc3848c5c6f194798ee", size = 56850, upload-time = "2026-03-26T01:20:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/219eeca0ad4a20843d4b9eaac5532f87018b9d25730a62a16f54f6c52d1a/agent_client_protocol-0.8.1-py3-none-any.whl", hash = "sha256:9421a11fd435b4831660272d169c3812d553bb7247049c138c3ca127e4b8af8e", size = 54529, upload-time = "2026-02-13T15:34:53.344Z" }, ] [[package]] @@ -1643,7 +1643,7 @@ wheels = [ [[package]] name = "hermes-agent" -version = "0.7.0" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "anthropic" }, @@ -1682,6 +1682,7 @@ all = [ { name = "faster-whisper" }, { name = "honcho-ai" }, { name = "lark-oapi" }, + { name = "matrix-nio", extra = ["e2e"] }, { name = "mcp" }, { name = "modal" }, { name = "numpy" }, @@ -1689,7 +1690,7 @@ all = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-xdist" }, - { name = "python-telegram-bot", extra = ["webhooks"] }, + { name = "python-telegram-bot" }, { name = "pywinpty", marker = "sys_platform == 'win32'" }, { name = "simple-term-menu" }, { name = "slack-bolt" }, @@ -1725,7 +1726,6 @@ honcho = [ { name = "honcho-ai" }, ] matrix = [ - { name = "markdown" }, { name = "matrix-nio", extra = ["e2e"] }, ] mcp = [ @@ -1734,7 +1734,7 @@ mcp = [ messaging = [ { name = "aiohttp" }, { name = "discord-py", extra = ["voice"] }, - { name = "python-telegram-bot", extra = ["webhooks"] }, + { name = "python-telegram-bot" }, { name = "slack-bolt" }, { name = "slack-sdk" }, ] @@ -1773,7 +1773,7 @@ yc-bench = [ [package.metadata] requires-dist = [ - { name = "agent-client-protocol", marker = "extra == 'acp'", specifier = ">=0.9.0,<1.0" }, + { name = "agent-client-protocol", marker = "extra == 'acp'", specifier = ">=0.8.1,<0.9" }, { name = "aiohttp", marker = "extra == 'homeassistant'", specifier = ">=3.9.0,<4" }, { name = "aiohttp", marker = "extra == 'messaging'", specifier = ">=3.13.3,<4" }, { name = "aiohttp", marker = "extra == 'sms'", specifier = ">=3.9.0,<4" }, @@ -1801,6 +1801,7 @@ requires-dist = [ { name = "hermes-agent", extras = ["feishu"], marker = "extra == 'all'" }, { name = "hermes-agent", extras = ["homeassistant"], marker = "extra == 'all'" }, { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'all'" }, + { name = "hermes-agent", extras = ["matrix"], marker = "extra == 'all'" }, { name = "hermes-agent", extras = ["mcp"], marker = "extra == 'all'" }, { name = "hermes-agent", extras = ["messaging"], marker = "extra == 'all'" }, { name = "hermes-agent", extras = ["modal"], marker = "extra == 'all'" }, @@ -1813,7 +1814,6 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1,<1" }, { name = "jinja2", specifier = ">=3.1.5,<4" }, { name = "lark-oapi", marker = "extra == 'feishu'", specifier = ">=1.5.3,<2" }, - { name = "markdown", marker = "extra == 'matrix'", specifier = ">=3.6,<4" }, { name = "matrix-nio", extras = ["e2e"], marker = "extra == 'matrix'", specifier = ">=0.24.0,<1" }, { name = "mcp", marker = "extra == 'dev'", specifier = ">=1.2.0,<2" }, { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.2.0,<2" }, @@ -1829,7 +1829,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0,<2" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.0,<4" }, { name = "python-dotenv", specifier = ">=1.2.1,<2" }, - { name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'messaging'", specifier = ">=22.6,<23" }, + { name = "python-telegram-bot", marker = "extra == 'messaging'", specifier = ">=22.6,<23" }, { name = "pywinpty", marker = "sys_platform == 'win32' and extra == 'pty'", specifier = ">=2.0.0,<3" }, { name = "pyyaml", specifier = ">=6.0.2,<7" }, { name = "requests", specifier = ">=2.33.0,<3" }, @@ -3966,11 +3966,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" }, ] -[package.optional-dependencies] -webhooks = [ - { name = "tornado" }, -] - [[package]] name = "pytz" version = "2025.2"