Cleanup time!

This commit is contained in:
teknium1
2026-02-20 23:23:32 -08:00
parent 9a19fe1f50
commit 70dd3a16dc
38 changed files with 150 additions and 351 deletions

View File

@@ -27,7 +27,7 @@ import time
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
from multiprocessing import Pool, Manager, Lock
from multiprocessing import Pool, Lock
import traceback
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn, MofNCompleteColumn
@@ -36,7 +36,6 @@ import fire
from run_agent import AIAgent
from toolset_distributions import (
get_distribution,
list_distributions,
sample_toolsets_from_distribution,
validate_distribution
@@ -173,7 +172,7 @@ def _extract_tool_stats(messages: List[Dict[str, Any]]) -> Dict[str, Dict[str, i
if content_json.get("success") is False:
is_success = False
except:
except (json.JSONDecodeError, ValueError, TypeError):
# If not JSON, check if content is empty or explicitly states an error
# Note: We avoid simple substring matching to prevent false positives
if not content:

15
cli.py
View File

@@ -39,16 +39,16 @@ from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.widgets import TextArea
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.keys import Keys
from prompt_toolkit import print_formatted_text as _pt_print
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI
import threading
import queue
import tempfile
# Load environment variables first
from dotenv import load_dotenv
from hermes_constants import OPENROUTER_BASE_URL
env_path = Path(__file__).parent / '.env'
if env_path.exists():
load_dotenv(dotenv_path=env_path)
@@ -88,7 +88,7 @@ def load_cli_config() -> Dict[str, Any]:
defaults = {
"model": {
"default": "anthropic/claude-opus-4.6",
"base_url": "https://openrouter.ai/api/v1",
"base_url": OPENROUTER_BASE_URL,
"provider": "auto",
},
"terminal": {
@@ -262,20 +262,15 @@ def load_cli_config() -> Dict[str, Any]:
# Load configuration at module startup
CLI_CONFIG = load_cli_config()
from rich.console import Console, Group
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.table import Table
from rich.markdown import Markdown
from rich.columns import Columns
from rich.align import Align
from rich import box
import fire
# Import the agent and tool systems
from run_agent import AIAgent
from model_tools import get_tool_definitions, get_all_tool_names, get_toolset_for_tool, get_available_toolsets
from model_tools import get_tool_definitions, get_toolset_for_tool
from toolsets import get_all_toolsets, get_toolset_info, resolve_toolset, validate_toolset
# Cron job system for scheduled tasks

View File

@@ -8,14 +8,13 @@ Routes messages to the appropriate destination based on:
- Local (always saved to files)
"""
import json
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass
from typing import Dict, List, Optional, Any, Union
from enum import Enum
from .config import Platform, GatewayConfig, HomeChannel
from .config import Platform, GatewayConfig
from .session import SessionSource

View File

@@ -1081,7 +1081,7 @@ class GatewayRunner:
try:
msg = progress_queue.get_nowait()
await adapter.send(chat_id=source.chat_id, content=msg)
except:
except Exception:
break
return
except Exception as e:

View File

@@ -30,6 +30,7 @@ import httpx
import yaml
from hermes_cli.config import get_hermes_home, get_config_path
from hermes_constants import OPENROUTER_BASE_URL
try:
import fcntl
@@ -865,7 +866,7 @@ def _reset_config_provider() -> Path:
if isinstance(model, dict):
model["provider"] = "auto"
if "base_url" in model:
model["base_url"] = "https://openrouter.ai/api/v1"
model["base_url"] = OPENROUTER_BASE_URL
config_path.write_text(yaml.safe_dump(config, sort_keys=False))
return config_path

22
hermes_cli/colors.py Normal file
View File

@@ -0,0 +1,22 @@
"""Shared ANSI color utilities for Hermes CLI modules."""
import sys
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
"""Apply color codes to text (only when output is a TTY)."""
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET

View File

@@ -20,22 +20,7 @@ from typing import Dict, Any, Optional, List, Tuple
import yaml
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
# =============================================================================

View File

@@ -4,29 +4,14 @@ Cron subcommand for hermes CLI.
Handles: hermes cron [list|daemon|tick]
"""
import json
import sys
import time
from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
sys.path.insert(0, str(PROJECT_ROOT))
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
def cron_list(show_all: bool = False):

View File

@@ -23,20 +23,8 @@ if _env_path.exists():
# Also try project .env as fallback
load_dotenv(PROJECT_ROOT / ".env", override=False)
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
from hermes_constants import OPENROUTER_MODELS_URL
def check_ok(text: str, detail: str = ""):
print(f" {color('', Colors.GREEN)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else ""))
@@ -314,7 +302,7 @@ def run_doctor(args):
try:
import httpx
response = httpx.get(
"https://openrouter.ai/api/v1/models",
OPENROUTER_MODELS_URL,
headers={"Authorization": f"Bearer {openrouter_key}"},
timeout=10
)

View File

@@ -40,6 +40,7 @@ if env_path.exists():
load_dotenv(dotenv_path=env_path)
from hermes_cli import __version__
from hermes_constants import OPENROUTER_BASE_URL
def cmd_chat(args):
@@ -241,7 +242,7 @@ def _model_flow_openrouter(config, current_model=""):
model = cfg.get("model")
if isinstance(model, dict):
model["provider"] = "openrouter"
model["base_url"] = "https://openrouter.ai/api/v1"
model["base_url"] = OPENROUTER_BASE_URL
save_config(cfg)
deactivate_provider()
print(f"Default model set to: {selected} (via OpenRouter)")

View File

@@ -8,9 +8,6 @@ Usage:
hermes pairing clear-pending # Clear all expired/pending codes
"""
import sys
def pairing_command(args):
"""Handle hermes pairing subcommands."""
from gateway.pairing import PairingStore

View File

@@ -26,23 +26,7 @@ from hermes_cli.config import (
ensure_hermes_home, DEFAULT_CONFIG
)
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
"""Apply color codes to text."""
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
def print_header(title: str):
"""Print a section header."""

View File

@@ -12,7 +12,6 @@ handler are thin wrappers that parse args and delegate.
import json
import shutil
import sys
from pathlib import Path
from typing import Optional

View File

@@ -11,20 +11,8 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
from hermes_constants import OPENROUTER_MODELS_URL
def check_mark(ok: bool) -> str:
if ok:
@@ -232,7 +220,7 @@ def show_status(args):
jobs = data.get("jobs", [])
enabled_jobs = [j for j in jobs if j.get("enabled", True)]
print(f" Jobs: {len(enabled_jobs)} active, {len(jobs)} total")
except:
except Exception:
print(f" Jobs: (error reading jobs file)")
else:
print(f" Jobs: 0")
@@ -250,7 +238,7 @@ def show_status(args):
with open(sessions_file) as f:
data = json.load(f)
print(f" Active: {len(data)} session(s)")
except:
except Exception:
print(f" Active: (error reading sessions file)")
else:
print(f" Active: 0")
@@ -268,7 +256,7 @@ def show_status(args):
try:
import httpx
response = httpx.get(
"https://openrouter.ai/api/v1/models",
OPENROUTER_MODELS_URL,
headers={"Authorization": f"Bearer {openrouter_key}"},
timeout=10
)
@@ -288,7 +276,7 @@ def show_status(args):
port_in_use = result == 0
# This is informational, not necessarily bad
print(f" Port 18789: {'in use' if port_in_use else 'available'}")
except:
except OSError:
pass
print()

View File

@@ -13,23 +13,7 @@ import subprocess
from pathlib import Path
from typing import Optional
# ANSI colors
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
def color(text: str, *codes) -> str:
"""Apply color codes to text (only in TTY)."""
if not sys.stdout.isatty():
return text
return "".join(codes) + text + Colors.RESET
from hermes_cli.colors import Colors, color
def log_info(msg: str):
print(f"{color('', Colors.CYAN)} {msg}")

9
hermes_constants.py Normal file
View File

@@ -0,0 +1,9 @@
"""Shared constants for Hermes Agent.
Import-safe module with no dependencies — can be imported from anywhere
without risk of circular imports.
"""
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
OPENROUTER_MODELS_URL = f"{OPENROUTER_BASE_URL}/models"
OPENROUTER_CHAT_URL = f"{OPENROUTER_BASE_URL}/chat/completions"

View File

@@ -31,19 +31,17 @@ import asyncio
import os
from typing import Dict, Any, List, Optional, Tuple
from tools.web_tools import web_search_tool, web_extract_tool, web_crawl_tool, check_firecrawl_api_key
from tools.terminal_tool import terminal_tool, check_terminal_requirements, TERMINAL_TOOL_DESCRIPTION, cleanup_vm
from tools.web_tools import web_search_tool, web_extract_tool, check_firecrawl_api_key
from tools.terminal_tool import terminal_tool, check_terminal_requirements, TERMINAL_TOOL_DESCRIPTION
# File manipulation tools (read, write, patch, search)
from tools.file_tools import read_file_tool, write_file_tool, patch_tool, search_tool
from tools import check_file_requirements
# Hecate/MorphCloud terminal tool (cloud VMs) - available as alternative backend
from tools.terminal_hecate import terminal_hecate_tool, check_hecate_requirements, TERMINAL_HECATE_DESCRIPTION
from tools.vision_tools import vision_analyze_tool, check_vision_requirements
from tools.mixture_of_agents_tool import mixture_of_agents_tool, check_moa_requirements
from tools.image_generation_tool import image_generate_tool, check_image_generation_requirements
from tools.skills_tool import skills_list, skill_view, check_skills_requirements, SKILLS_TOOL_DESCRIPTION
from tools.skills_tool import skills_list, skill_view, check_skills_requirements
# Agent-managed skill creation/editing
from tools.skill_manager_tool import skill_manage, check_skill_manage_requirements, SKILL_MANAGE_SCHEMA
from tools.skill_manager_tool import skill_manage, SKILL_MANAGE_SCHEMA
# RL Training tools (Tinker-Atropos)
from tools.rl_training_tool import (
rl_list_environments,
@@ -64,7 +62,6 @@ from tools.cronjob_tools import (
list_cronjobs,
remove_cronjob,
check_cronjob_requirements,
get_cronjob_tool_definitions,
SCHEDULE_CRONJOB_SCHEMA,
LIST_CRONJOBS_SCHEMA,
REMOVE_CRONJOB_SCHEMA
@@ -99,11 +96,7 @@ from tools.clarify_tool import clarify_tool, check_clarify_requirements, CLARIFY
from tools.code_execution_tool import execute_code, check_sandbox_requirements, EXECUTE_CODE_SCHEMA
# Subagent delegation
from tools.delegate_tool import delegate_task, check_delegate_requirements, DELEGATE_TASK_SCHEMA
from toolsets import (
get_toolset, resolve_toolset, resolve_multiple_toolsets,
get_all_toolsets, get_toolset_names, validate_toolset,
get_toolset_info, print_toolset_tree
)
from toolsets import resolve_toolset, validate_toolset
# =============================================================================
@@ -262,43 +255,6 @@ def check_tool_availability(quiet: bool = False) -> Tuple[List[str], List[Dict[s
return available, unavailable
def print_tool_availability_warnings(unavailable: List[Dict[str, Any]], prefix: str = ""):
"""Print warnings about unavailable tools."""
if not unavailable:
return
# Filter to only those missing API keys (not system dependencies)
api_key_missing = [u for u in unavailable if u["missing_vars"]]
if api_key_missing:
print(f"{prefix}⚠️ Some tools are disabled due to missing API keys:")
for item in api_key_missing:
vars_str = ", ".join(item["missing_vars"])
print(f"{prefix}{item['name']}: missing {vars_str}")
if item["setup_url"]:
print(f"{prefix} Get key at: {item['setup_url']}")
print(f"{prefix} Run 'hermes setup' to configure API keys")
print()
def get_tool_availability_summary() -> Dict[str, Any]:
"""
Get a summary of tool availability for display in status/doctor commands.
Returns:
Dict with 'available' and 'unavailable' lists of tool info
"""
available, unavailable = check_tool_availability()
return {
"available": [
{"id": tid, "name": TOOLSET_REQUIREMENTS[tid]["name"], "tools": TOOLSET_REQUIREMENTS[tid]["tools"]}
for tid in available
],
"unavailable": unavailable,
}
def get_web_tool_definitions() -> List[Dict[str, Any]]:
"""
Get tool definitions for web tools in OpenAI's expected format.

View File

@@ -62,7 +62,7 @@ hermes = "hermes_cli.main:main"
hermes-agent = "run_agent:main"
[tool.setuptools]
py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli"]
py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_constants"]
[tool.setuptools.packages.find]
include = ["tools", "hermes_cli", "gateway", "cron"]

View File

@@ -1,3 +1,7 @@
# NOTE: This file is maintained for convenience only.
# The canonical dependency list is in pyproject.toml.
# Preferred install: pip install -e ".[all]"
# Core dependencies
openai
python-dotenv
@@ -10,6 +14,7 @@ pyyaml
requests
jinja2
pydantic>=2.0
PyJWT[crypto]
# Web tools
firecrawl-py
@@ -23,27 +28,13 @@ litellm>=1.75.5
typer
platformdirs
# Optional: For Docker backend (recommended)
# Requires Docker installed and user in 'docker' group
# Optional: For Modal backend (cloud execution)
# swe-rex[modal]>=1.4.0 # Includes modal + boto3 + swe-rex runtime
# Text-to-speech (Edge TTS is free, no API key needed)
edge-tts
# Optional: Premium TTS providers
# elevenlabs # Uncomment if using ElevenLabs TTS (needs ELEVENLABS_API_KEY)
# Optional: For cron expression parsing (cronjob scheduling)
croniter
# Optional: For messaging platform integrations (gateway)
# Telegram
python-telegram-bot>=20.0
# Discord
discord.py>=2.0
# WhatsApp bridge communication + general async HTTP (used by gateway)
aiohttp>=3.9.0

View File

@@ -64,8 +64,10 @@ from tools.rl_training_tool import check_rl_api_keys, get_missing_keys
# Config Loading
# ============================================================================
from hermes_constants import OPENROUTER_BASE_URL
DEFAULT_MODEL = "anthropic/claude-opus-4.5"
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
DEFAULT_BASE_URL = OPENROUTER_BASE_URL
def load_hermes_config() -> dict:

View File

@@ -25,6 +25,7 @@ import json
import logging
import os
import random
import re
import sys
import time
import threading
@@ -54,6 +55,8 @@ from tools.browser_tool import cleanup_browser
import requests
from hermes_constants import OPENROUTER_BASE_URL, OPENROUTER_MODELS_URL
# =============================================================================
# Default Agent Identity & Platform Hints
# =============================================================================
@@ -133,7 +136,7 @@ def fetch_model_metadata(force_refresh: bool = False) -> Dict[str, Dict[str, Any
try:
response = requests.get(
"https://openrouter.ai/api/v1/models",
OPENROUTER_MODELS_URL,
timeout=10
)
response.raise_for_status()
@@ -282,7 +285,7 @@ class ContextCompressor:
api_key = os.getenv("OPENROUTER_API_KEY", "")
self.client = OpenAI(
api_key=api_key,
base_url="https://openrouter.ai/api/v1"
base_url=OPENROUTER_BASE_URL
) if api_key else None
def update_from_response(self, usage: Dict[str, Any]):
@@ -600,7 +603,6 @@ def build_skills_system_prompt() -> str:
str: The skills system prompt section, or empty string if no skills found.
"""
import os
import re
from pathlib import Path
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
@@ -1093,8 +1095,8 @@ class AIAgent:
Args:
base_url (str): Base URL for the model API (optional)
api_key (str): API key for authentication (optional, uses env var if not provided)
model (str): Model name to use (default: "gpt-4")
max_iterations (int): Maximum number of tool calling iterations (default: 10)
model (str): Model name to use (default: "anthropic/claude-opus-4.6")
max_iterations (int): Maximum number of tool calling iterations (default: 60)
tool_delay (float): Delay between tool calls in seconds (default: 1.0)
enabled_toolsets (List[str]): Only enable tools from these toolsets (optional)
disabled_toolsets (List[str]): Disable tools from these toolsets (optional)
@@ -1102,7 +1104,7 @@ class AIAgent:
verbose_logging (bool): Enable verbose logging for debugging (default: False)
quiet_mode (bool): Suppress progress output for clean CLI experience (default: False)
ephemeral_system_prompt (str): System prompt used during agent execution but NOT saved to trajectories (optional)
log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses (default: 20)
log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses (default: 100)
log_prefix (str): Prefix to add to all log messages for identification in parallel processing (default: "")
providers_allowed (List[str]): OpenRouter providers to allow (optional)
providers_ignored (List[str]): OpenRouter providers to ignore (optional)
@@ -1137,7 +1139,7 @@ class AIAgent:
self.log_prefix = f"{log_prefix} " if log_prefix else ""
# Store effective base URL for feature detection (prompt caching, reasoning, etc.)
# When no base_url is provided, the client defaults to OpenRouter, so reflect that here.
self.base_url = base_url or "https://openrouter.ai/api/v1"
self.base_url = base_url or OPENROUTER_BASE_URL
self.tool_progress_callback = tool_progress_callback
self.clarify_callback = clarify_callback
self._last_reported_tool = None # Track for "new tool" mode
@@ -1215,7 +1217,7 @@ class AIAgent:
if base_url:
client_kwargs["base_url"] = base_url
else:
client_kwargs["base_url"] = "https://openrouter.ai/api/v1"
client_kwargs["base_url"] = OPENROUTER_BASE_URL
# Handle API key - OpenRouter is the primary provider
if api_key:
@@ -1636,7 +1638,6 @@ class AIAgent:
if not content:
return False
import re
# Remove all <think>...</think> blocks (including nested ones, non-greedy)
cleaned = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
@@ -1686,6 +1687,19 @@ class AIAgent:
return None
def _cleanup_task_resources(self, task_id: str) -> None:
"""Clean up VM and browser resources for a given task."""
try:
cleanup_vm(task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup VM for task {task_id}: {e}")
try:
cleanup_browser(task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup browser for task {task_id}: {e}")
def _get_messages_up_to_last_assistant(self, messages: List[Dict]) -> List[Dict]:
"""
Get messages up to (but not including) the last assistant turn.
@@ -2331,7 +2345,6 @@ class AIAgent:
Dict: Complete conversation result with final response and message history
"""
# Generate unique task_id if not provided to isolate VMs between concurrent tasks
import uuid
effective_task_id = task_id or str(uuid.uuid4())
# Reset retry counters at the start of each conversation to prevent state leakage
@@ -2628,17 +2641,7 @@ class AIAgent:
print(f"{self.log_prefix} ⏪ Rolling back to last complete assistant turn")
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
# Clean up VM and browser
try:
cleanup_vm(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
try:
cleanup_browser(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
self._cleanup_task_resources(effective_task_id)
return {
"final_response": None,
@@ -2846,15 +2849,7 @@ class AIAgent:
self._incomplete_scratchpad_retries = 0
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
try:
cleanup_vm(effective_task_id)
except Exception:
pass
try:
cleanup_browser(effective_task_id)
except Exception:
pass
self._cleanup_task_resources(effective_task_id)
return {
"final_response": None,
@@ -3247,18 +3242,7 @@ class AIAgent:
self._empty_content_retries = 0 # Reset for next conversation
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
# Clean up VM and browser
try:
cleanup_vm(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
try:
cleanup_browser(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
self._cleanup_task_resources(effective_task_id)
return {
"final_response": None,
@@ -3365,7 +3349,6 @@ class AIAgent:
final_response = summary_response.choices[0].message.content
# Strip think blocks from final response
if "<think>" in final_response:
import re
final_response = re.sub(r'<think>.*?</think>\s*', '', final_response, flags=re.DOTALL).strip()
# Add to messages for session continuity
@@ -3384,17 +3367,7 @@ class AIAgent:
self._save_trajectory(messages, user_message, completed)
# Clean up VM and browser for this task after conversation completes
try:
cleanup_vm(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
try:
cleanup_browser(effective_task_id)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
self._cleanup_task_resources(effective_task_id)
# Update session messages and save session log
self._session_messages = messages
@@ -3644,7 +3617,6 @@ def main(
# Save sample trajectory to UUID-named file if requested
if save_sample:
import uuid
sample_id = str(uuid.uuid4())[:8]
sample_filename = f"sample_{sample_id}.json"

View File

@@ -108,7 +108,7 @@ def _count_tokens_for_entry(entry: Dict) -> Tuple[Dict, int]:
if value:
try:
total += len(_TOKENIZER.encode(value))
except:
except Exception:
# Fallback to character estimate
total += len(value) // 4

View File

@@ -23,7 +23,6 @@ import os
import shutil
import sys
import time
import signal
from pathlib import Path
from typing import List, Dict, Any
import traceback

View File

@@ -12,7 +12,6 @@ Run with: python -m pytest tests/test_delegate.py -v
import json
import os
import sys
import time
import unittest
from unittest.mock import MagicMock, patch

View File

@@ -84,7 +84,7 @@ Create a markdown summary that captures all key information in a well-organized,
max_tokens=4000
)
print(f"✅ SUCCESS")
except:
except Exception:
print(f"❌ FAILED")
await asyncio.sleep(0.5)
@@ -101,7 +101,7 @@ Create a markdown summary that captures all key information in a well-organized,
max_tokens=4000
)
print(f"✅ SUCCESS")
except:
except Exception:
print(f"❌ FAILED")
if __name__ == "__main__":

View File

@@ -21,7 +21,7 @@ import sys
import os
import argparse
from datetime import datetime
from typing import List, Dict, Any
from typing import List
# Import the web tools to test (updated path after moving tools/)
from tools.web_tools import (

View File

@@ -57,6 +57,7 @@ import time
import requests
from typing import Dict, Any, Optional, List
from pathlib import Path
from hermes_constants import OPENROUTER_CHAT_URL
# Try to import httpx for async LLM calls
try:
@@ -821,7 +822,7 @@ Provide a concise summary focused on interactive elements and key content."""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
OPENROUTER_CHAT_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
@@ -1324,7 +1325,7 @@ Focus on answering the user's specific question."""
async def analyze_screenshot():
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
OPENROUTER_CHAT_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
@@ -1374,7 +1375,7 @@ Focus on answering the user's specific question."""
else:
# Fallback: use synchronous requests
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
OPENROUTER_CHAT_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"

View File

@@ -345,7 +345,7 @@ def execute_code(
# --- Set up temp directory with hermes_tools.py and script.py ---
tmpdir = tempfile.mkdtemp(prefix="hermes_sandbox_")
sock_path = f"/tmp/hermes_rpc_{uuid.uuid4().hex}.sock"
sock_path = os.path.join(tempfile.gettempdir(), f"hermes_rpc_{uuid.uuid4().hex}.sock")
tool_call_log: list = []
tool_call_counter = [0] # mutable so the RPC thread can increment

View File

@@ -446,33 +446,3 @@ def _map_normalized_positions(original: str, normalized: str,
original_matches.append((orig_start, min(orig_end, len(original))))
return original_matches
# =============================================================================
# Utility Functions
# =============================================================================
def find_best_match(content: str, pattern: str) -> Optional[Tuple[int, int, str]]:
"""
Find the best match for a pattern and return the strategy name.
Returns:
Tuple of (start, end, strategy_name) or None if no match
"""
strategies = [
("exact", _strategy_exact),
("line_trimmed", _strategy_line_trimmed),
("whitespace_normalized", _strategy_whitespace_normalized),
("indentation_flexible", _strategy_indentation_flexible),
("escape_normalized", _strategy_escape_normalized),
("trimmed_boundary", _strategy_trimmed_boundary),
("block_anchor", _strategy_block_anchor),
("context_aware", _strategy_context_aware),
]
for strategy_name, strategy_fn in strategies:
matches = strategy_fn(content, pattern)
if matches:
return (matches[0][0], matches[0][1], strategy_name)
return None

View File

@@ -53,8 +53,8 @@ import datetime
from pathlib import Path
from typing import Dict, Any, List, Optional
from openai import AsyncOpenAI
from hermes_constants import OPENROUTER_BASE_URL
# Initialize OpenRouter API client lazily (only when needed)
_openrouter_client = None
def _get_openrouter_client():
@@ -66,7 +66,7 @@ def _get_openrouter_client():
raise ValueError("OPENROUTER_API_KEY environment variable not set")
_openrouter_client = AsyncOpenAI(
api_key=api_key,
base_url="https://openrouter.ai/api/v1"
base_url=OPENROUTER_BASE_URL
)
return _openrouter_client

View File

@@ -23,6 +23,7 @@ import logging
from typing import Dict, Any, List, Optional
from openai import AsyncOpenAI
from hermes_constants import OPENROUTER_BASE_URL
SUMMARIZER_MODEL = "google/gemini-3-flash-preview"
MAX_SESSION_CHARS = 100_000
@@ -40,7 +41,7 @@ def _get_client() -> AsyncOpenAI:
raise ValueError("OPENROUTER_API_KEY not set")
_summarizer_client = AsyncOpenAI(
api_key=api_key,
base_url="https://openrouter.ai/api/v1",
base_url=OPENROUTER_BASE_URL,
)
return _summarizer_client

View File

@@ -29,6 +29,8 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import List, Tuple
from hermes_constants import OPENROUTER_BASE_URL
# ---------------------------------------------------------------------------
# Hardcoded trust configuration
@@ -941,7 +943,7 @@ def llm_audit_skill(skill_path: Path, static_result: ScanResult,
return static_result
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
base_url=OPENROUTER_BASE_URL,
api_key=api_key,
)
response = client.chat.completions.create(
@@ -1037,11 +1039,6 @@ def _get_configured_model() -> str:
return ""
def check_guard_requirements() -> Tuple[bool, str]:
"""Check if the guard module can operate. Always returns True (no external deps)."""
return True, "Skills Guard ready"
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

View File

@@ -212,7 +212,7 @@ def _check_disk_usage_warning():
if f.is_file():
try:
total_bytes += f.stat().st_size
except:
except OSError:
pass
total_gb = total_bytes / (1024 ** 3)
@@ -341,7 +341,7 @@ def _prompt_dangerous_approval(command: str, description: str, timeout_seconds:
def get_input():
try:
result["choice"] = input(" Choice [o/s/a/D]: ").strip().lower()
except:
except (EOFError, OSError):
result["choice"] = ""
thread = threading.Thread(target=get_input, daemon=True)
@@ -894,7 +894,7 @@ class _SingularityEnvironment:
"""Cleanup on destruction."""
try:
self.cleanup()
except:
except Exception:
pass
@@ -1022,13 +1022,13 @@ class _SSHEnvironment:
cmd = ["ssh", "-o", f"ControlPath={self.control_socket}", "-O", "exit",
f"{self.user}@{self.host}"]
subprocess.run(cmd, capture_output=True, timeout=5)
except:
except (OSError, subprocess.SubprocessError):
pass
# Remove socket file
try:
self.control_socket.unlink()
except:
except OSError:
pass
def stop(self):
@@ -1039,7 +1039,7 @@ class _SSHEnvironment:
"""Cleanup on destruction."""
try:
self.cleanup()
except:
except Exception:
pass
@@ -1112,7 +1112,7 @@ class _DockerEnvironment:
"""Cleanup on destruction."""
try:
self.cleanup()
except:
except Exception:
pass
@@ -1189,7 +1189,7 @@ class _ModalEnvironment:
"""Cleanup on destruction."""
try:
self.cleanup()
except:
except Exception:
pass
@@ -1504,7 +1504,7 @@ def get_active_environments_info() -> Dict[str, Any]:
try:
size = sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
total_size += size
except:
except OSError:
pass
info["total_disk_usage_mb"] = round(total_size / (1024 * 1024), 2)
@@ -1532,7 +1532,7 @@ def cleanup_all_environments():
try:
shutil.rmtree(path, ignore_errors=True)
print(f"[Terminal Cleanup] Removed orphaned: {path}")
except:
except OSError:
pass
if not os.getenv("HERMES_QUIET") and cleaned > 0:
@@ -1669,7 +1669,6 @@ def terminal_tool(
# This prevents parallel tasks from overwriting each other's files
# In CLI mode (HERMES_QUIET), use the cwd directly without subdirectories
if env_type == "local" and not os.getenv("HERMES_QUIET"):
import uuid
with _env_lock:
if effective_task_id not in _task_workdirs:
task_workdir = Path(cwd) / f"hermes-{effective_task_id}-{uuid.uuid4().hex[:8]}"
@@ -1944,7 +1943,7 @@ def check_terminal_requirements() -> bool:
if __name__ == "__main__":
"""Simple test when run directly."""
# Simple test when run directly
print("Terminal Tool Module (mini-swe-agent backend)")
print("=" * 50)

View File

@@ -99,8 +99,3 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> dict:
"transcript": "",
"error": str(e),
}
def check_stt_requirements() -> bool:
"""Check if OpenAI API key is available for speech-to-text."""
return bool(os.getenv("HERMES_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY"))

View File

@@ -36,9 +36,9 @@ import base64
from pathlib import Path
from typing import Dict, Any, Optional
from openai import AsyncOpenAI
import httpx # Use httpx for async HTTP requests
import httpx
from hermes_constants import OPENROUTER_BASE_URL
# Initialize OpenRouter API client lazily (only when needed)
_openrouter_client = None
def _get_openrouter_client():
@@ -50,7 +50,7 @@ def _get_openrouter_client():
raise ValueError("OPENROUTER_API_KEY environment variable not set")
_openrouter_client = AsyncOpenAI(
api_key=api_key,
base_url="https://openrouter.ai/api/v1"
base_url=OPENROUTER_BASE_URL
)
return _openrouter_client

View File

@@ -50,6 +50,7 @@ from pathlib import Path
from typing import List, Dict, Any, Optional
from firecrawl import Firecrawl
from openai import AsyncOpenAI
from hermes_constants import OPENROUTER_BASE_URL
# Initialize Firecrawl client lazily (only when needed)
# This prevents import errors when FIRECRAWL_API_KEY is not set
@@ -77,7 +78,7 @@ def _get_summarizer_client():
raise ValueError("OPENROUTER_API_KEY environment variable not set")
_summarizer_client = AsyncOpenAI(
api_key=api_key,
base_url="https://openrouter.ai/api/v1"
base_url=OPENROUTER_BASE_URL
)
return _summarizer_client

View File

@@ -24,7 +24,6 @@ Usage:
"""
from typing import List, Dict, Any, Set, Optional
import json
# Core toolset definitions
@@ -162,7 +161,7 @@ TOOLSETS = {
"safe": {
"description": "Safe toolkit without terminal access",
"tools": ["mixture_of_agents"],
"includes": ["web", "vision", "creative"]
"includes": ["web", "vision", "image_gen"]
},
# ==========================================================================
@@ -587,50 +586,31 @@ def print_toolset_tree(name: str, indent: int = 0) -> None:
if __name__ == "__main__":
"""
Demo and testing of the toolsets system
"""
print("🎯 Toolsets System Demo")
print("Toolsets System Demo")
print("=" * 60)
# Show all available toolsets
print("\n📦 Available Toolsets:")
print("\nAvailable Toolsets:")
print("-" * 40)
for name, toolset in get_all_toolsets().items():
info = get_toolset_info(name)
composite = "📂" if info["is_composite"] else "🔧"
print(f"{composite} {name:20} - {toolset['description']}")
print(f" Tools: {len(info['resolved_tools'])} total")
composite = "[composite]" if info["is_composite"] else "[leaf]"
print(f" {composite} {name:20} - {toolset['description']}")
print(f" Tools: {len(info['resolved_tools'])} total")
# Demo toolset resolution
print("\n🔍 Toolset Resolution Examples:")
print("\nToolset Resolution Examples:")
print("-" * 40)
examples = ["research", "development", "full_stack", "minimal", "safe"]
for name in examples:
for name in ["web", "terminal", "safe", "debugging"]:
tools = resolve_toolset(name)
print(f"\n{name}:")
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
print(f"\n {name}:")
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
# Show toolset composition tree
print("\n🌳 Toolset Composition Tree:")
print("\nMultiple Toolset Resolution:")
print("-" * 40)
print("\nExample: 'content_creation' toolset:")
print_toolset_tree("content_creation")
combined = resolve_multiple_toolsets(["web", "vision", "terminal"])
print(f" Combining ['web', 'vision', 'terminal']:")
print(f" Result: {', '.join(sorted(combined))}")
print("\nExample: 'full_stack' toolset:")
print_toolset_tree("full_stack")
# Demo multiple toolset resolution
print("\n🔗 Multiple Toolset Resolution:")
print("-" * 40)
combined = resolve_multiple_toolsets(["minimal", "vision", "reasoning"])
print(f"Combining ['minimal', 'vision', 'reasoning']:")
print(f" Result: {', '.join(sorted(combined))}")
# Demo custom toolset creation
print("\n Custom Toolset Creation:")
print("\nCustom Toolset Creation:")
print("-" * 40)
create_custom_toolset(
name="my_custom",
@@ -638,8 +618,7 @@ if __name__ == "__main__":
tools=["web_search"],
includes=["terminal", "vision"]
)
custom_info = get_toolset_info("my_custom")
print(f"Created 'my_custom' toolset:")
print(f" Description: {custom_info['description']}")
print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")
print(f" Created 'my_custom' toolset:")
print(f" Description: {custom_info['description']}")
print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")

View File

@@ -44,6 +44,7 @@ from datetime import datetime
import fire
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
from rich.console import Console
from hermes_constants import OPENROUTER_BASE_URL
# Load environment variables
from dotenv import load_dotenv
@@ -70,7 +71,7 @@ class CompressionConfig:
# Summarization (OpenRouter)
summarization_model: str = "google/gemini-3-flash-preview"
base_url: str = "https://openrouter.ai/api/v1"
base_url: str = OPENROUTER_BASE_URL
api_key_env: str = "OPENROUTER_API_KEY"
temperature: float = 0.3
max_retries: int = 3