New process registry and tool for managing long-running background processes across all terminal backends (local, Docker, Singularity, Modal, SSH). Process Registry (tools/process_registry.py): - ProcessSession tracking with rolling 200KB output buffer - spawn_local() with optional PTY via ptyprocess for interactive CLIs - spawn_via_env() for non-local backends (runs inside sandbox, never on host) - Background reader threads per process (Popen stdout or PTY) - wait() with timeout clamping, interrupt support, and transparent limit reporting - JSON checkpoint to ~/.hermes/processes.json for gateway crash recovery - Module-level singleton shared across agent loop, gateway, and RL Process Tool (model_tools.py): - 7 actions: list, poll, log, wait, kill, write, submit - Paired with terminal in all toolsets (CLI, messaging, RL) - Timeout clamping with transparent notes in response Terminal Tool Updates (tools/terminal_tool.py): - Replaced nohup background mode with registry spawn (returns session_id) - Added workdir parameter for per-command working directory - Added check_interval parameter for gateway auto-check watchers - Added pty parameter for interactive CLI tools (Codex, Claude Code) - Updated TERMINAL_TOOL_DESCRIPTION with full background workflow docs - Cleanup thread now respects active background processes (won't reap sandbox) Gateway Integration (gateway/run.py, session.py, config.py): - Session reset protection: sessions with active processes exempt from reset - Default idle timeout increased from 2 hours to 24 hours - from_dict fallback aligned to match (was 120, now 1440) - session_key env var propagated to process registry for session mapping - Crash recovery on gateway startup via checkpoint probe - check_interval watcher: asyncio task polls process, delivers updates to platform RL Safety (environments/): - tool_context.py cleanup() kills background processes on episode end - hermes_base_env.py warns when enabled_toolsets is None (loads all tools) - Process tool safe in RL via wait() blocking the agent loop Also: - Added ptyprocess as optional dependency (in pyproject.toml [pty] extra + [all]) - Fixed pre-existing bug: rl_test_inference missing from TOOL_TO_TOOLSET_MAP - Updated AGENTS.md with process management docs and project structure - Updated README.md terminal section with process management overview
537 lines
17 KiB
Python
537 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Toolsets Module
|
||
|
||
This module provides a flexible system for defining and managing tool aliases/toolsets.
|
||
Toolsets allow you to group tools together for specific scenarios and can be composed
|
||
from individual tools or other toolsets.
|
||
|
||
Features:
|
||
- Define custom toolsets with specific tools
|
||
- Compose toolsets from other toolsets
|
||
- Built-in common toolsets for typical use cases
|
||
- Easy extension for new toolsets
|
||
- Support for dynamic toolset resolution
|
||
|
||
Usage:
|
||
from toolsets import get_toolset, resolve_toolset, get_all_toolsets
|
||
|
||
# Get tools for a specific toolset
|
||
tools = get_toolset("research")
|
||
|
||
# Resolve a toolset to get all tool names (including from composed toolsets)
|
||
all_tools = resolve_toolset("full_stack")
|
||
"""
|
||
|
||
from typing import List, Dict, Any, Set, Optional
|
||
import json
|
||
|
||
|
||
# Core toolset definitions
|
||
# These can include individual tools or reference other toolsets
|
||
TOOLSETS = {
|
||
# Basic toolsets - individual tool categories
|
||
"web": {
|
||
"description": "Web research and content extraction tools",
|
||
"tools": ["web_search", "web_extract"],
|
||
"includes": [] # No other toolsets included
|
||
},
|
||
|
||
"search": {
|
||
"description": "Web search only (no content extraction/scraping)",
|
||
"tools": ["web_search"],
|
||
"includes": []
|
||
},
|
||
|
||
"vision": {
|
||
"description": "Image analysis and vision tools",
|
||
"tools": ["vision_analyze"],
|
||
"includes": []
|
||
},
|
||
|
||
"image_gen": {
|
||
"description": "Creative generation tools (images)",
|
||
"tools": ["image_generate"],
|
||
"includes": []
|
||
},
|
||
|
||
"terminal": {
|
||
"description": "Terminal/command execution and process management tools",
|
||
"tools": ["terminal", "process"],
|
||
"includes": []
|
||
},
|
||
|
||
"moa": {
|
||
"description": "Advanced reasoning and problem-solving tools",
|
||
"tools": ["mixture_of_agents"],
|
||
"includes": []
|
||
},
|
||
|
||
"skills": {
|
||
"description": "Access skill documents with specialized instructions and knowledge",
|
||
"tools": ["skills_list", "skill_view"],
|
||
"includes": []
|
||
},
|
||
|
||
"browser": {
|
||
"description": "Browser automation for web interaction (navigate, click, type, scroll, iframes, hold-click) with web search for finding URLs",
|
||
"tools": [
|
||
"browser_navigate", "browser_snapshot", "browser_click",
|
||
"browser_type", "browser_scroll", "browser_back",
|
||
"browser_press", "browser_close", "browser_get_images",
|
||
"browser_vision", "web_search"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"cronjob": {
|
||
"description": "Cronjob management tools - schedule, list, and remove automated tasks (CLI-only)",
|
||
"tools": ["schedule_cronjob", "list_cronjobs", "remove_cronjob"],
|
||
"includes": []
|
||
},
|
||
|
||
"rl": {
|
||
"description": "RL training tools for running reinforcement learning on Tinker-Atropos",
|
||
"tools": [
|
||
"rl_list_environments", "rl_select_environment",
|
||
"rl_get_current_config", "rl_edit_config",
|
||
"rl_start_training", "rl_check_status",
|
||
"rl_stop_training", "rl_get_results",
|
||
"rl_list_runs", "rl_test_inference"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"file": {
|
||
"description": "File manipulation tools: read, write, patch (with fuzzy matching), and search (content + files)",
|
||
"tools": ["read_file", "write_file", "patch", "search"],
|
||
"includes": []
|
||
},
|
||
|
||
"tts": {
|
||
"description": "Text-to-speech: convert text to audio with Edge TTS (free), ElevenLabs, or OpenAI",
|
||
"tools": ["text_to_speech"],
|
||
"includes": []
|
||
},
|
||
|
||
# Scenario-specific toolsets
|
||
|
||
"debugging": {
|
||
"description": "Debugging and troubleshooting toolkit",
|
||
"tools": ["terminal", "process"],
|
||
"includes": ["web", "file"] # For searching error messages and solutions, and file operations
|
||
},
|
||
|
||
"safe": {
|
||
"description": "Safe toolkit without terminal access",
|
||
"tools": ["mixture_of_agents"],
|
||
"includes": ["web", "vision", "creative"]
|
||
},
|
||
|
||
# ==========================================================================
|
||
# CLI-specific toolsets (only available when running via cli.py)
|
||
# ==========================================================================
|
||
|
||
"hermes-cli": {
|
||
"description": "Full interactive CLI toolset - all default tools plus cronjob management",
|
||
"tools": [
|
||
# Web tools
|
||
"web_search", "web_extract",
|
||
# Terminal + process management
|
||
"terminal", "process",
|
||
# File manipulation
|
||
"read_file", "write_file", "patch", "search",
|
||
# Vision
|
||
"vision_analyze",
|
||
# Image generation
|
||
"image_generate",
|
||
# MoA
|
||
"mixture_of_agents",
|
||
# Skills
|
||
"skills_list", "skill_view",
|
||
# Browser
|
||
"browser_navigate", "browser_snapshot", "browser_click",
|
||
"browser_type", "browser_scroll", "browser_back",
|
||
"browser_press", "browser_close", "browser_get_images",
|
||
"browser_vision",
|
||
# Text-to-speech
|
||
"text_to_speech",
|
||
# Cronjob management (CLI-only)
|
||
"schedule_cronjob", "list_cronjobs", "remove_cronjob"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
# ==========================================================================
|
||
# Messaging Platform-Specific Toolsets
|
||
# ==========================================================================
|
||
|
||
"hermes-telegram": {
|
||
"description": "Telegram bot toolset - full access for personal use (terminal has safety checks)",
|
||
"tools": [
|
||
# Terminal + process management
|
||
"terminal", "process",
|
||
# File manipulation
|
||
"read_file", "write_file", "patch", "search",
|
||
# Web tools
|
||
"web_search", "web_extract",
|
||
# Vision - analyze images sent by users
|
||
"vision_analyze",
|
||
# Image generation
|
||
"image_generate",
|
||
# Text-to-speech
|
||
"text_to_speech",
|
||
# Skills - access knowledge base
|
||
"skills_list", "skill_view",
|
||
# Cronjob management - let users schedule tasks
|
||
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
||
# Cross-channel messaging
|
||
"send_message"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"hermes-discord": {
|
||
"description": "Discord bot toolset - full access (terminal has safety checks via dangerous command approval)",
|
||
"tools": [
|
||
# Terminal + process management
|
||
"terminal", "process",
|
||
# File manipulation
|
||
"read_file", "write_file", "patch", "search",
|
||
# Web tools
|
||
"web_search", "web_extract",
|
||
# Vision - analyze images sent by users
|
||
"vision_analyze",
|
||
# Image generation
|
||
"image_generate",
|
||
# Text-to-speech
|
||
"text_to_speech",
|
||
# Skills - access knowledge base
|
||
"skills_list", "skill_view",
|
||
# Cronjob management - let users schedule tasks
|
||
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
||
# Cross-channel messaging
|
||
"send_message"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"hermes-whatsapp": {
|
||
"description": "WhatsApp bot toolset - similar to Telegram (personal messaging, more trusted)",
|
||
"tools": [
|
||
# Web tools
|
||
"web_search", "web_extract",
|
||
# Terminal + process management
|
||
"terminal", "process",
|
||
# File manipulation
|
||
"read_file", "write_file", "patch", "search",
|
||
# Vision
|
||
"vision_analyze",
|
||
# Image generation
|
||
"image_generate",
|
||
# Text-to-speech
|
||
"text_to_speech",
|
||
# Skills
|
||
"skills_list", "skill_view",
|
||
# Cronjob management
|
||
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
||
# Cross-channel messaging
|
||
"send_message"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"hermes-slack": {
|
||
"description": "Slack bot toolset - full access for workspace use (terminal has safety checks)",
|
||
"tools": [
|
||
# Terminal + process management
|
||
"terminal", "process",
|
||
# File manipulation
|
||
"read_file", "write_file", "patch", "search",
|
||
# Web tools
|
||
"web_search", "web_extract",
|
||
# Vision - analyze images sent by users
|
||
"vision_analyze",
|
||
# Image generation
|
||
"image_generate",
|
||
# Text-to-speech
|
||
"text_to_speech",
|
||
# Skills - access knowledge base
|
||
"skills_list", "skill_view",
|
||
# Cronjob management - let users schedule tasks
|
||
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
||
# Cross-channel messaging
|
||
"send_message"
|
||
],
|
||
"includes": []
|
||
},
|
||
|
||
"hermes-gateway": {
|
||
"description": "Gateway toolset - union of all messaging platform tools",
|
||
"tools": [],
|
||
"includes": ["hermes-telegram", "hermes-discord", "hermes-whatsapp", "hermes-slack"]
|
||
}
|
||
}
|
||
|
||
|
||
|
||
def get_toolset(name: str) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Get a toolset definition by name.
|
||
|
||
Args:
|
||
name (str): Name of the toolset
|
||
|
||
Returns:
|
||
Dict: Toolset definition with description, tools, and includes
|
||
None: If toolset not found
|
||
"""
|
||
# Return toolset definition
|
||
return TOOLSETS.get(name)
|
||
|
||
|
||
def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]:
|
||
"""
|
||
Recursively resolve a toolset to get all tool names.
|
||
|
||
This function handles toolset composition by recursively resolving
|
||
included toolsets and combining all tools.
|
||
|
||
Args:
|
||
name (str): Name of the toolset to resolve
|
||
visited (Set[str]): Set of already visited toolsets (for cycle detection)
|
||
|
||
Returns:
|
||
List[str]: List of all tool names in the toolset
|
||
"""
|
||
if visited is None:
|
||
visited = set()
|
||
|
||
# Special aliases that represent all tools across every toolset
|
||
# This ensures future toolsets are automatically included without changes.
|
||
if name in {"all", "*"}:
|
||
all_tools: Set[str] = set()
|
||
for toolset_name in get_toolset_names():
|
||
# Use a fresh visited set per branch to avoid cross-branch contamination
|
||
resolved = resolve_toolset(toolset_name, visited.copy())
|
||
all_tools.update(resolved)
|
||
return list(all_tools)
|
||
|
||
# Check for cycles
|
||
if name in visited:
|
||
print(f"⚠️ Circular dependency detected in toolset '{name}'")
|
||
return []
|
||
|
||
visited.add(name)
|
||
|
||
# Get toolset definition
|
||
toolset = TOOLSETS.get(name)
|
||
if not toolset:
|
||
return []
|
||
|
||
# Collect direct tools
|
||
tools = set(toolset.get("tools", []))
|
||
|
||
# Recursively resolve included toolsets
|
||
for included_name in toolset.get("includes", []):
|
||
included_tools = resolve_toolset(included_name, visited.copy())
|
||
tools.update(included_tools)
|
||
|
||
return list(tools)
|
||
|
||
|
||
def resolve_multiple_toolsets(toolset_names: List[str]) -> List[str]:
|
||
"""
|
||
Resolve multiple toolsets and combine their tools.
|
||
|
||
Args:
|
||
toolset_names (List[str]): List of toolset names to resolve
|
||
|
||
Returns:
|
||
List[str]: Combined list of all tool names (deduplicated)
|
||
"""
|
||
all_tools = set()
|
||
|
||
for name in toolset_names:
|
||
tools = resolve_toolset(name)
|
||
all_tools.update(tools)
|
||
|
||
return list(all_tools)
|
||
|
||
|
||
def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
|
||
"""
|
||
Get all available toolsets with their definitions.
|
||
|
||
Returns:
|
||
Dict: All toolset definitions
|
||
"""
|
||
return TOOLSETS.copy()
|
||
|
||
|
||
def get_toolset_names() -> List[str]:
|
||
"""
|
||
Get names of all available toolsets (excluding aliases).
|
||
|
||
Returns:
|
||
List[str]: List of toolset names
|
||
"""
|
||
return list(TOOLSETS.keys())
|
||
|
||
|
||
|
||
|
||
def validate_toolset(name: str) -> bool:
|
||
"""
|
||
Check if a toolset name is valid.
|
||
|
||
Args:
|
||
name (str): Toolset name to validate
|
||
|
||
Returns:
|
||
bool: True if valid, False otherwise
|
||
"""
|
||
# Accept special alias names for convenience
|
||
if name in {"all", "*"}:
|
||
return True
|
||
return name in TOOLSETS
|
||
|
||
|
||
def create_custom_toolset(
|
||
name: str,
|
||
description: str,
|
||
tools: List[str] = None,
|
||
includes: List[str] = None
|
||
) -> None:
|
||
"""
|
||
Create a custom toolset at runtime.
|
||
|
||
Args:
|
||
name (str): Name for the new toolset
|
||
description (str): Description of the toolset
|
||
tools (List[str]): Direct tools to include
|
||
includes (List[str]): Other toolsets to include
|
||
"""
|
||
TOOLSETS[name] = {
|
||
"description": description,
|
||
"tools": tools or [],
|
||
"includes": includes or []
|
||
}
|
||
|
||
|
||
|
||
|
||
def get_toolset_info(name: str) -> Dict[str, Any]:
|
||
"""
|
||
Get detailed information about a toolset including resolved tools.
|
||
|
||
Args:
|
||
name (str): Toolset name
|
||
|
||
Returns:
|
||
Dict: Detailed toolset information
|
||
"""
|
||
toolset = get_toolset(name)
|
||
if not toolset:
|
||
return None
|
||
|
||
resolved_tools = resolve_toolset(name)
|
||
|
||
return {
|
||
"name": name,
|
||
"description": toolset["description"],
|
||
"direct_tools": toolset["tools"],
|
||
"includes": toolset["includes"],
|
||
"resolved_tools": resolved_tools,
|
||
"tool_count": len(resolved_tools),
|
||
"is_composite": len(toolset["includes"]) > 0
|
||
}
|
||
|
||
|
||
def print_toolset_tree(name: str, indent: int = 0) -> None:
|
||
"""
|
||
Print a tree view of a toolset and its composition.
|
||
|
||
Args:
|
||
name (str): Toolset name
|
||
indent (int): Current indentation level
|
||
"""
|
||
prefix = " " * indent
|
||
toolset = get_toolset(name)
|
||
|
||
if not toolset:
|
||
print(f"{prefix}❌ Unknown toolset: {name}")
|
||
return
|
||
|
||
# Print toolset name and description
|
||
print(f"{prefix}📦 {name}: {toolset['description']}")
|
||
|
||
# Print direct tools
|
||
if toolset["tools"]:
|
||
print(f"{prefix} 🔧 Tools: {', '.join(toolset['tools'])}")
|
||
|
||
# Print included toolsets
|
||
if toolset["includes"]:
|
||
print(f"{prefix} 📂 Includes:")
|
||
for included in toolset["includes"]:
|
||
print_toolset_tree(included, indent + 2)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
"""
|
||
Demo and testing of the toolsets system
|
||
"""
|
||
print("🎯 Toolsets System Demo")
|
||
print("=" * 60)
|
||
|
||
# Show all available toolsets
|
||
print("\n📦 Available 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")
|
||
|
||
|
||
# Demo toolset resolution
|
||
print("\n🔍 Toolset Resolution Examples:")
|
||
print("-" * 40)
|
||
|
||
examples = ["research", "development", "full_stack", "minimal", "safe"]
|
||
for name in examples:
|
||
tools = resolve_toolset(name)
|
||
print(f"\n{name}:")
|
||
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
|
||
|
||
# Show toolset composition tree
|
||
print("\n🌳 Toolset Composition Tree:")
|
||
print("-" * 40)
|
||
print("\nExample: 'content_creation' toolset:")
|
||
print_toolset_tree("content_creation")
|
||
|
||
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("-" * 40)
|
||
create_custom_toolset(
|
||
name="my_custom",
|
||
description="My custom toolset for specific tasks",
|
||
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'])}")
|