mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
fix terminal interactivity
This commit is contained in:
parent
de9c0edc51
commit
faecbddd9b
1 changed files with 83 additions and 36 deletions
|
|
@ -6,6 +6,12 @@ This module provides a single terminal tool using Hecate's VM infrastructure.
|
||||||
It wraps Hecate's functionality to provide a simple interface for executing commands
|
It wraps Hecate's functionality to provide a simple interface for executing commands
|
||||||
on Morph VMs with automatic lifecycle management.
|
on Morph VMs with automatic lifecycle management.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Persistent VM instance: Reused across tool calls within the configured lifetime window
|
||||||
|
- Persistent ExecutionContext: Maintains session state (including interactive/TUI sessions)
|
||||||
|
across multiple tool calls, enabling interactive mode to work correctly
|
||||||
|
- Automatic cleanup: VMs are cleaned up after inactivity
|
||||||
|
|
||||||
Available tool:
|
Available tool:
|
||||||
- terminal_tool: Execute commands with optional interactive session support
|
- terminal_tool: Execute commands with optional interactive session support
|
||||||
|
|
||||||
|
|
@ -21,6 +27,8 @@ Usage:
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
import threading
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
# Detailed description for the terminal tool based on Hermes Terminal system prompt
|
# Detailed description for the terminal tool based on Hermes Terminal system prompt
|
||||||
|
|
@ -70,6 +78,12 @@ When commands enter interactive mode (vim, nano, less, git prompts, package mana
|
||||||
- Test components incrementally with mock inputs
|
- Test components incrementally with mock inputs
|
||||||
- Install whatever tools needed - full system access provided"""
|
- Install whatever tools needed - full system access provided"""
|
||||||
|
|
||||||
|
# Global state for VM lifecycle management
|
||||||
|
# These persist across tool calls to enable session continuity
|
||||||
|
_active_instance = None
|
||||||
|
_active_context = None
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
|
||||||
def terminal_tool(
|
def terminal_tool(
|
||||||
command: Optional[str] = None,
|
command: Optional[str] = None,
|
||||||
input_keys: Optional[str] = None,
|
input_keys: Optional[str] = None,
|
||||||
|
|
@ -111,27 +125,17 @@ def terminal_tool(
|
||||||
# Run a background task
|
# Run a background task
|
||||||
>>> result = terminal_tool(command="sleep 60", background=True)
|
>>> result = terminal_tool(command="sleep 60", background=True)
|
||||||
"""
|
"""
|
||||||
try:
|
global _active_instance, _active_context
|
||||||
# Import hecate and ToolCall lazily so this module can be imported
|
|
||||||
# even when hecate is not installed. If unavailable, gracefully
|
|
||||||
# indicate that the terminal tool is disabled.
|
|
||||||
try:
|
|
||||||
# Primary import path when the hecate package is properly installed
|
|
||||||
try:
|
|
||||||
from hecate import run_tool_with_lifecycle_management # type: ignore
|
|
||||||
except ImportError as primary_import_error:
|
|
||||||
# Fallback for when a local folder named "hecate" shadows the installed package
|
|
||||||
# (common when the repo is cloned inside the project root). In that case,
|
|
||||||
# the actual implementation lives under hecate.hecate.cli.
|
|
||||||
try:
|
|
||||||
from hecate.hecate.cli import run_tool_with_lifecycle_management # type: ignore
|
|
||||||
except Exception as fallback_import_error:
|
|
||||||
raise ImportError(
|
|
||||||
f"Failed to import 'run_tool_with_lifecycle_management' from hecate: "
|
|
||||||
f"{primary_import_error}; fallback failed: {fallback_import_error}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import required modules lazily so this module can be imported
|
||||||
|
# even when hecate is not installed
|
||||||
|
try:
|
||||||
from morphcloud._llm import ToolCall
|
from morphcloud._llm import ToolCall
|
||||||
|
from morphcloud.api import MorphCloudClient
|
||||||
|
from hecate.cli import run_tool, ExecutionContext
|
||||||
|
from rich.console import Console
|
||||||
|
import io
|
||||||
except ImportError as import_error:
|
except ImportError as import_error:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"output": "",
|
"output": "",
|
||||||
|
|
@ -142,6 +146,36 @@ def terminal_tool(
|
||||||
"status": "disabled"
|
"status": "disabled"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Get configuration from environment
|
||||||
|
vm_lifetime_seconds = int(os.getenv("HECATE_VM_LIFETIME_SECONDS", "300"))
|
||||||
|
snapshot_id = os.getenv("HECATE_DEFAULT_SNAPSHOT_ID", "snapshot_p5294qxt")
|
||||||
|
|
||||||
|
# Check API key
|
||||||
|
morph_api_key = os.getenv("MORPH_API_KEY")
|
||||||
|
if not morph_api_key:
|
||||||
|
return json.dumps({
|
||||||
|
"output": "",
|
||||||
|
"screen": "",
|
||||||
|
"session_id": None,
|
||||||
|
"exit_code": -1,
|
||||||
|
"error": "MORPH_API_KEY environment variable not set",
|
||||||
|
"status": "disabled"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get or create VM instance and execution context
|
||||||
|
# This is critical for interactive session support - the context must persist!
|
||||||
|
with _instance_lock:
|
||||||
|
if _active_instance is None:
|
||||||
|
morph_client = MorphCloudClient(api_key=morph_api_key)
|
||||||
|
_active_instance = morph_client.instances.start(snapshot_id=snapshot_id)
|
||||||
|
|
||||||
|
# Get or create persistent execution context
|
||||||
|
if _active_context is None:
|
||||||
|
_active_context = ExecutionContext()
|
||||||
|
|
||||||
|
instance = _active_instance
|
||||||
|
ctx = _active_context
|
||||||
|
|
||||||
# Build tool input based on provided parameters
|
# Build tool input based on provided parameters
|
||||||
tool_input = {}
|
tool_input = {}
|
||||||
|
|
||||||
|
|
@ -163,8 +197,21 @@ def terminal_tool(
|
||||||
input=tool_input
|
input=tool_input
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute with lifecycle management
|
# Create a console for output (redirect to string buffer to avoid printing)
|
||||||
result = run_tool_with_lifecycle_management(tool_call)
|
console_output = io.StringIO()
|
||||||
|
console = Console(file=console_output, force_terminal=False, legacy_windows=False)
|
||||||
|
|
||||||
|
# Generate unique tool block ID
|
||||||
|
tool_block_id = f"tool_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# Execute the tool with hecate
|
||||||
|
result = run_tool(
|
||||||
|
tool_call=tool_call,
|
||||||
|
instance=instance,
|
||||||
|
console=console,
|
||||||
|
tool_block_id=tool_block_id,
|
||||||
|
ctx=ctx
|
||||||
|
)
|
||||||
|
|
||||||
# Format the result with all possible fields
|
# Format the result with all possible fields
|
||||||
# Map hecate's "stdout" to "output" for compatibility
|
# Map hecate's "stdout" to "output" for compatibility
|
||||||
|
|
@ -211,16 +258,16 @@ def check_hecate_requirements() -> bool:
|
||||||
print(f"Warning: Missing optional environment variables: {', '.join(missing_optional)}")
|
print(f"Warning: Missing optional environment variables: {', '.join(missing_optional)}")
|
||||||
print(" (Some Hecate features may be limited)")
|
print(" (Some Hecate features may be limited)")
|
||||||
|
|
||||||
# Check if Hecate entrypoint is importable (handle local-folder shadowing)
|
# Check if Hecate and required modules are importable
|
||||||
try:
|
try:
|
||||||
try:
|
from morphcloud._llm import ToolCall
|
||||||
from hecate import run_tool_with_lifecycle_management # type: ignore
|
from morphcloud.api import MorphCloudClient
|
||||||
except ImportError:
|
from hecate.cli import run_tool, ExecutionContext
|
||||||
from hecate.hecate.cli import run_tool_with_lifecycle_management # type: ignore
|
from rich.console import Console
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Hecate not available: {e}\nIf you cloned the hecate repo into this project, it may shadow the installed package. "
|
print(f"Hecate not available: {e}")
|
||||||
f"Either install it (pip install -e hecate) and/or move/rename the local 'hecate' folder.")
|
print(f"Make sure hecate is installed and MORPH_API_KEY is set.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Module-level initialization check
|
# Module-level initialization check
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue