mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
commit
9c71f3a6ea
55 changed files with 1413 additions and 3197 deletions
|
|
@ -871,7 +871,18 @@ def _execute_remote(
|
|||
}
|
||||
|
||||
if status == "timeout":
|
||||
result["error"] = f"Script timed out after {timeout}s and was killed."
|
||||
timeout_msg = f"Script timed out after {timeout}s and was killed."
|
||||
result["error"] = timeout_msg
|
||||
# Include timeout message in output so the LLM always surfaces it
|
||||
# to the user (see local path comment — same reasoning, #10807).
|
||||
if stdout_text:
|
||||
result["output"] = stdout_text + f"\n\n⏰ {timeout_msg}"
|
||||
else:
|
||||
result["output"] = f"⏰ {timeout_msg}"
|
||||
logger.warning(
|
||||
"execute_code (remote) timed out after %ss (limit %ss) with %d tool calls",
|
||||
duration, timeout, tool_call_counter[0],
|
||||
)
|
||||
elif status == "interrupted":
|
||||
result["output"] = (
|
||||
stdout_text + "\n[execution interrupted — user sent a new message]"
|
||||
|
|
@ -1117,6 +1128,10 @@ def execute_code(
|
|||
stderr_reader.start()
|
||||
|
||||
status = "success"
|
||||
_activity_state = {
|
||||
"last_touch": time.monotonic(),
|
||||
"start": exec_start,
|
||||
}
|
||||
while proc.poll() is None:
|
||||
if _is_interrupted():
|
||||
_kill_process_group(proc)
|
||||
|
|
@ -1126,6 +1141,13 @@ def execute_code(
|
|||
_kill_process_group(proc, escalate=True)
|
||||
status = "timeout"
|
||||
break
|
||||
# Periodic activity touch so the gateway's inactivity timeout
|
||||
# doesn't kill the agent during long code execution (#10807).
|
||||
try:
|
||||
from tools.environments.base import touch_activity_if_due
|
||||
touch_activity_if_due(_activity_state, "execute_code running")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
|
||||
# Wait for readers to finish draining
|
||||
|
|
@ -1179,7 +1201,20 @@ def execute_code(
|
|||
}
|
||||
|
||||
if status == "timeout":
|
||||
result["error"] = f"Script timed out after {timeout}s and was killed."
|
||||
timeout_msg = f"Script timed out after {timeout}s and was killed."
|
||||
result["error"] = timeout_msg
|
||||
# Include timeout message in output so the LLM always surfaces it
|
||||
# to the user. When output is empty, models often treat the result
|
||||
# as "nothing happened" and produce an empty response, which the
|
||||
# gateway stream consumer silently drops (#10807).
|
||||
if stdout_text:
|
||||
result["output"] = stdout_text + f"\n\n⏰ {timeout_msg}"
|
||||
else:
|
||||
result["output"] = f"⏰ {timeout_msg}"
|
||||
logger.warning(
|
||||
"execute_code timed out after %ss (limit %ss) with %d tool calls",
|
||||
duration, timeout, tool_call_counter[0],
|
||||
)
|
||||
elif status == "interrupted":
|
||||
result["output"] = stdout_text + "\n[execution interrupted — user sent a new message]"
|
||||
elif exit_code != 0:
|
||||
|
|
|
|||
|
|
@ -863,28 +863,28 @@ def delegate_task(
|
|||
results.append(entry)
|
||||
completed_count += 1
|
||||
|
||||
# Print per-task completion line above the spinner
|
||||
idx = entry["task_index"]
|
||||
label = task_labels[idx] if idx < len(task_labels) else f"Task {idx}"
|
||||
dur = entry.get("duration_seconds", 0)
|
||||
status = entry.get("status", "?")
|
||||
icon = "✓" if status == "completed" else "✗"
|
||||
remaining = n_tasks - completed_count
|
||||
completion_line = f"{icon} [{idx+1}/{n_tasks}] {label} ({dur}s)"
|
||||
if spinner_ref:
|
||||
try:
|
||||
spinner_ref.print_above(completion_line)
|
||||
except Exception:
|
||||
# Print per-task completion line above the spinner
|
||||
idx = entry["task_index"]
|
||||
label = task_labels[idx] if idx < len(task_labels) else f"Task {idx}"
|
||||
dur = entry.get("duration_seconds", 0)
|
||||
status = entry.get("status", "?")
|
||||
icon = "✓" if status == "completed" else "✗"
|
||||
remaining = n_tasks - completed_count
|
||||
completion_line = f"{icon} [{idx+1}/{n_tasks}] {label} ({dur}s)"
|
||||
if spinner_ref:
|
||||
try:
|
||||
spinner_ref.print_above(completion_line)
|
||||
except Exception:
|
||||
print(f" {completion_line}")
|
||||
else:
|
||||
print(f" {completion_line}")
|
||||
else:
|
||||
print(f" {completion_line}")
|
||||
|
||||
# Update spinner text to show remaining count
|
||||
if spinner_ref and remaining > 0:
|
||||
try:
|
||||
spinner_ref.update_text(f"🔀 {remaining} task{'s' if remaining != 1 else ''} remaining")
|
||||
except Exception as e:
|
||||
logger.debug("Spinner update_text failed: %s", e)
|
||||
# Update spinner text to show remaining count
|
||||
if spinner_ref and remaining > 0:
|
||||
try:
|
||||
spinner_ref.update_text(f"🔀 {remaining} task{'s' if remaining != 1 else ''} remaining")
|
||||
except Exception as e:
|
||||
logger.debug("Spinner update_text failed: %s", e)
|
||||
|
||||
# Sort by task_index so results match input order
|
||||
results.sort(key=lambda r: r["task_index"])
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ def _get_activity_callback() -> Callable[[str], None] | None:
|
|||
return getattr(_activity_callback_local, "callback", None)
|
||||
|
||||
|
||||
def touch_activity_if_due(
|
||||
state: dict,
|
||||
label: str,
|
||||
) -> None:
|
||||
"""Fire the activity callback at most once every ``state['interval']`` seconds.
|
||||
|
||||
*state* must contain ``last_touch`` (monotonic timestamp) and ``start``
|
||||
(monotonic timestamp of the operation start). An optional ``interval``
|
||||
key overrides the default 10 s cadence.
|
||||
|
||||
Swallows all exceptions so callers don't need their own try/except.
|
||||
"""
|
||||
now = time.monotonic()
|
||||
interval = state.get("interval", 10.0)
|
||||
if now - state["last_touch"] < interval:
|
||||
return
|
||||
state["last_touch"] = now
|
||||
try:
|
||||
cb = _get_activity_callback()
|
||||
if cb:
|
||||
elapsed = int(now - state["start"])
|
||||
cb(f"{label} ({elapsed}s elapsed)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_sandbox_dir() -> Path:
|
||||
"""Return the host-side root for all sandbox storage (Docker workspaces,
|
||||
Singularity overlays/SIF cache, etc.).
|
||||
|
|
@ -405,8 +431,11 @@ class BaseEnvironment(ABC):
|
|||
drain_thread = threading.Thread(target=_drain, daemon=True)
|
||||
drain_thread.start()
|
||||
deadline = time.monotonic() + timeout
|
||||
_last_activity_touch = time.monotonic()
|
||||
_ACTIVITY_INTERVAL = 10.0 # seconds between activity touches
|
||||
_now = time.monotonic()
|
||||
_activity_state = {
|
||||
"last_touch": _now,
|
||||
"start": _now,
|
||||
}
|
||||
|
||||
while proc.poll() is None:
|
||||
if is_interrupted():
|
||||
|
|
@ -428,16 +457,7 @@ class BaseEnvironment(ABC):
|
|||
"returncode": 124,
|
||||
}
|
||||
# Periodic activity touch so the gateway knows we're alive
|
||||
_now = time.monotonic()
|
||||
if _now - _last_activity_touch >= _ACTIVITY_INTERVAL:
|
||||
_last_activity_touch = _now
|
||||
_cb = _get_activity_callback()
|
||||
if _cb:
|
||||
try:
|
||||
_elapsed = int(_now - (deadline - timeout))
|
||||
_cb(f"terminal command running ({_elapsed}s elapsed)")
|
||||
except Exception:
|
||||
pass
|
||||
touch_activity_if_due(_activity_state, "terminal command running")
|
||||
time.sleep(0.2)
|
||||
|
||||
drain_thread.join(timeout=5)
|
||||
|
|
|
|||
|
|
@ -105,9 +105,11 @@ class BaseModalExecutionEnvironment(BaseEnvironment):
|
|||
if self._client_timeout_grace_seconds is not None:
|
||||
deadline = time.monotonic() + prepared.timeout + self._client_timeout_grace_seconds
|
||||
|
||||
_last_activity_touch = time.monotonic()
|
||||
_modal_exec_start = time.monotonic()
|
||||
_ACTIVITY_INTERVAL = 10.0 # match _wait_for_process cadence
|
||||
_now = time.monotonic()
|
||||
_activity_state = {
|
||||
"last_touch": _now,
|
||||
"start": _now,
|
||||
}
|
||||
|
||||
while True:
|
||||
if is_interrupted():
|
||||
|
|
@ -133,20 +135,11 @@ class BaseModalExecutionEnvironment(BaseEnvironment):
|
|||
return self._timeout_result_for_modal(prepared.timeout)
|
||||
|
||||
# Periodic activity touch so the gateway knows we're alive
|
||||
_now = time.monotonic()
|
||||
if _now - _last_activity_touch >= _ACTIVITY_INTERVAL:
|
||||
_last_activity_touch = _now
|
||||
try:
|
||||
from tools.environments.base import _get_activity_callback
|
||||
_cb = _get_activity_callback()
|
||||
except Exception:
|
||||
_cb = None
|
||||
if _cb:
|
||||
try:
|
||||
_elapsed = int(_now - _modal_exec_start)
|
||||
_cb(f"modal command running ({_elapsed}s elapsed)")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from tools.environments.base import touch_activity_if_due
|
||||
touch_activity_if_due(_activity_state, "modal command running")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
time.sleep(self._poll_interval_seconds)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue