mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-04 02:21:47 +00:00
chore: merge main
This commit is contained in:
commit
f805323517
107 changed files with 5928 additions and 1195 deletions
|
|
@ -183,6 +183,19 @@ class ProcessRegistry:
|
|||
|
||||
# ----- Spawn -----
|
||||
|
||||
@staticmethod
|
||||
def _env_temp_dir(env: Any) -> str:
|
||||
"""Return the writable sandbox temp dir for env-backed background tasks."""
|
||||
get_temp_dir = getattr(env, "get_temp_dir", None)
|
||||
if callable(get_temp_dir):
|
||||
try:
|
||||
temp_dir = get_temp_dir()
|
||||
if isinstance(temp_dir, str) and temp_dir.startswith("/"):
|
||||
return temp_dir.rstrip("/") or "/"
|
||||
except Exception as exc:
|
||||
logger.debug("Could not resolve environment temp dir: %s", exc)
|
||||
return "/tmp"
|
||||
|
||||
def spawn_local(
|
||||
self,
|
||||
command: str,
|
||||
|
|
@ -327,12 +340,20 @@ class ProcessRegistry:
|
|||
)
|
||||
|
||||
# Run the command in the sandbox with output capture
|
||||
log_path = f"/tmp/hermes_bg_{session.id}.log"
|
||||
pid_path = f"/tmp/hermes_bg_{session.id}.pid"
|
||||
temp_dir = self._env_temp_dir(env)
|
||||
log_path = f"{temp_dir}/hermes_bg_{session.id}.log"
|
||||
pid_path = f"{temp_dir}/hermes_bg_{session.id}.pid"
|
||||
exit_path = f"{temp_dir}/hermes_bg_{session.id}.exit"
|
||||
quoted_command = shlex.quote(command)
|
||||
quoted_temp_dir = shlex.quote(temp_dir)
|
||||
quoted_log_path = shlex.quote(log_path)
|
||||
quoted_pid_path = shlex.quote(pid_path)
|
||||
quoted_exit_path = shlex.quote(exit_path)
|
||||
bg_command = (
|
||||
f"nohup bash -c {quoted_command} > {log_path} 2>&1 & "
|
||||
f"echo $! > {pid_path} && cat {pid_path}"
|
||||
f"mkdir -p {quoted_temp_dir} && "
|
||||
f"( nohup bash -lc {quoted_command} > {quoted_log_path} 2>&1; "
|
||||
f"rc=$?; printf '%s\\n' \"$rc\" > {quoted_exit_path} ) & "
|
||||
f"echo $! > {quoted_pid_path} && cat {quoted_pid_path}"
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -353,7 +374,7 @@ class ProcessRegistry:
|
|||
# Start a poller thread that periodically reads the log file
|
||||
reader = threading.Thread(
|
||||
target=self._env_poller_loop,
|
||||
args=(session, env, log_path, pid_path),
|
||||
args=(session, env, log_path, pid_path, exit_path),
|
||||
daemon=True,
|
||||
name=f"proc-poller-{session.id}",
|
||||
)
|
||||
|
|
@ -397,14 +418,17 @@ class ProcessRegistry:
|
|||
self._move_to_finished(session)
|
||||
|
||||
def _env_poller_loop(
|
||||
self, session: ProcessSession, env: Any, log_path: str, pid_path: str
|
||||
self, session: ProcessSession, env: Any, log_path: str, pid_path: str, exit_path: str
|
||||
):
|
||||
"""Background thread: poll a sandbox log file for non-local backends."""
|
||||
quoted_log_path = shlex.quote(log_path)
|
||||
quoted_pid_path = shlex.quote(pid_path)
|
||||
quoted_exit_path = shlex.quote(exit_path)
|
||||
while not session.exited:
|
||||
time.sleep(2) # Poll every 2 seconds
|
||||
try:
|
||||
# Read new output from the log file
|
||||
result = env.execute(f"cat {log_path} 2>/dev/null", timeout=10)
|
||||
result = env.execute(f"cat {quoted_log_path} 2>/dev/null", timeout=10)
|
||||
new_output = result.get("output", "")
|
||||
if new_output:
|
||||
with session._lock:
|
||||
|
|
@ -414,14 +438,14 @@ class ProcessRegistry:
|
|||
|
||||
# Check if process is still running
|
||||
check = env.execute(
|
||||
f"kill -0 $(cat {pid_path} 2>/dev/null) 2>/dev/null; echo $?",
|
||||
f"kill -0 \"$(cat {quoted_pid_path} 2>/dev/null)\" 2>/dev/null; echo $?",
|
||||
timeout=5,
|
||||
)
|
||||
check_output = check.get("output", "").strip()
|
||||
if check_output and check_output.splitlines()[-1].strip() != "0":
|
||||
# Process has exited -- get exit code
|
||||
# Process has exited -- get exit code captured by the wrapper shell.
|
||||
exit_result = env.execute(
|
||||
f"wait $(cat {pid_path} 2>/dev/null) 2>/dev/null; echo $?",
|
||||
f"cat {quoted_exit_path} 2>/dev/null",
|
||||
timeout=5,
|
||||
)
|
||||
exit_str = exit_result.get("output", "").strip()
|
||||
|
|
@ -711,6 +735,29 @@ class ProcessRegistry:
|
|||
"""Send data + newline to a running process's stdin (like pressing Enter)."""
|
||||
return self.write_stdin(session_id, data + "\n")
|
||||
|
||||
def close_stdin(self, session_id: str) -> dict:
|
||||
"""Close a running process's stdin / send EOF without killing the process."""
|
||||
session = self.get(session_id)
|
||||
if session is None:
|
||||
return {"status": "not_found", "error": f"No process with ID {session_id}"}
|
||||
if session.exited:
|
||||
return {"status": "already_exited", "error": "Process has already finished"}
|
||||
|
||||
if hasattr(session, '_pty') and session._pty:
|
||||
try:
|
||||
session._pty.sendeof()
|
||||
return {"status": "ok", "message": "EOF sent"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
if not session.process or not session.process.stdin:
|
||||
return {"status": "error", "error": "Process stdin not available (non-local backend or stdin closed)"}
|
||||
try:
|
||||
session.process.stdin.close()
|
||||
return {"status": "ok", "message": "stdin closed"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def list_sessions(self, task_id: str = None) -> list:
|
||||
"""List all running and recently-finished processes."""
|
||||
with self._lock:
|
||||
|
|
@ -926,14 +973,14 @@ PROCESS_SCHEMA = {
|
|||
"Actions: 'list' (show all), 'poll' (check status + new output), "
|
||||
"'log' (full output with pagination), 'wait' (block until done or timeout), "
|
||||
"'kill' (terminate), 'write' (send raw stdin data without newline), "
|
||||
"'submit' (send data + Enter, for answering prompts)."
|
||||
"'submit' (send data + Enter, for answering prompts), 'close' (close stdin/send EOF)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["list", "poll", "log", "wait", "kill", "write", "submit"],
|
||||
"enum": ["list", "poll", "log", "wait", "kill", "write", "submit", "close"],
|
||||
"description": "Action to perform on background processes"
|
||||
},
|
||||
"session_id": {
|
||||
|
|
@ -973,7 +1020,7 @@ def _handle_process(args, **kw):
|
|||
|
||||
if action == "list":
|
||||
return _json.dumps({"processes": process_registry.list_sessions(task_id=task_id)}, ensure_ascii=False)
|
||||
elif action in ("poll", "log", "wait", "kill", "write", "submit"):
|
||||
elif action in ("poll", "log", "wait", "kill", "write", "submit", "close"):
|
||||
if not session_id:
|
||||
return tool_error(f"session_id is required for {action}")
|
||||
if action == "poll":
|
||||
|
|
@ -989,7 +1036,9 @@ def _handle_process(args, **kw):
|
|||
return _json.dumps(process_registry.write_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
||||
elif action == "submit":
|
||||
return _json.dumps(process_registry.submit_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
||||
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit")
|
||||
elif action == "close":
|
||||
return _json.dumps(process_registry.close_stdin(session_id), ensure_ascii=False)
|
||||
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit, close")
|
||||
|
||||
|
||||
registry.register(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue