mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Three independent fixes: 1. Reset activity timestamp on cached agent reuse (#9051) When the gateway reuses a cached AIAgent for a new turn, the _last_activity_ts from the previous turn (possibly hours ago) carried over. The inactivity timeout handler immediately saw the agent as idle for hours and killed it. Fix: reset _last_activity_ts, _last_activity_desc, and _api_call_count when retrieving an agent from the cache. 2. Detect uv-managed virtual environments (#8620 sub-issue 1) The systemd unit generator fell back to sys.executable (uv's standalone Python) when running under 'uv run', because sys.prefix == sys.base_prefix (uv doesn't set up traditional venv activation). The generated ExecStart pointed to a Python binary without site-packages, crashing the service on startup. Fix: check VIRTUAL_ENV env var before falling back to sys.executable. uv sets VIRTUAL_ENV even when sys.prefix doesn't reflect the venv. 3. Nudge model to continue after empty post-tool response (#9400) Weaker models (GLM-5, mimo-v2-pro) sometimes return empty responses after tool calls instead of continuing to the next step. The agent silently abandoned the remaining work with '(empty)' or used prior-turn fallback text. Fix: when the model returns empty after tool calls AND there's no prior-turn content to fall back on, inject a one-time user nudge message telling the model to process the tool results and continue. The flag resets after each successful tool round so it can fire again on later rounds. Test plan: 97 gateway + CLI tests pass, 9 venv detection tests pass
This commit is contained in:
parent
93fe4ead83
commit
50c35dcabe
3 changed files with 65 additions and 1 deletions
|
|
@ -8458,6 +8458,12 @@ class GatewayRunner:
|
|||
cached = _cache.get(session_key)
|
||||
if cached and cached[1] == _sig:
|
||||
agent = cached[0]
|
||||
# Reset activity timestamp so the inactivity timeout
|
||||
# handler doesn't see stale idle time from the previous
|
||||
# turn and immediately kill this agent. (#9051)
|
||||
agent._last_activity_ts = time.time()
|
||||
agent._last_activity_desc = "starting new turn (cached)"
|
||||
agent._api_call_count = 0
|
||||
logger.debug("Reusing cached agent for session %s", session_key)
|
||||
|
||||
if agent is None:
|
||||
|
|
|
|||
|
|
@ -715,7 +715,9 @@ def _detect_venv_dir() -> Path | None:
|
|||
"""Detect the active virtualenv directory.
|
||||
|
||||
Checks ``sys.prefix`` first (works regardless of the directory name),
|
||||
then falls back to probing common directory names under PROJECT_ROOT.
|
||||
then ``VIRTUAL_ENV`` env var (covers uv-managed environments where
|
||||
sys.prefix == sys.base_prefix), then falls back to probing common
|
||||
directory names under PROJECT_ROOT.
|
||||
Returns ``None`` when no virtualenv can be found.
|
||||
"""
|
||||
# If we're running inside a virtualenv, sys.prefix points to it.
|
||||
|
|
@ -724,6 +726,15 @@ def _detect_venv_dir() -> Path | None:
|
|||
if venv.is_dir():
|
||||
return venv
|
||||
|
||||
# uv and some other tools set VIRTUAL_ENV without changing sys.prefix.
|
||||
# This catches `uv run` where sys.prefix == sys.base_prefix but the
|
||||
# environment IS a venv. (#8620)
|
||||
_virtual_env = os.environ.get("VIRTUAL_ENV")
|
||||
if _virtual_env:
|
||||
venv = Path(_virtual_env)
|
||||
if venv.is_dir():
|
||||
return venv
|
||||
|
||||
# Fallback: check common virtualenv directory names under the project root.
|
||||
for candidate in (".venv", "venv"):
|
||||
venv = PROJECT_ROOT / candidate
|
||||
|
|
|
|||
47
run_agent.py
47
run_agent.py
|
|
@ -7858,6 +7858,7 @@ class AIAgent:
|
|||
self._incomplete_scratchpad_retries = 0
|
||||
self._codex_incomplete_retries = 0
|
||||
self._thinking_prefill_retries = 0
|
||||
self._post_tool_empty_retried = False
|
||||
self._last_content_with_tools = None
|
||||
self._mute_post_response = False
|
||||
self._unicode_sanitization_passes = 0
|
||||
|
|
@ -10131,6 +10132,10 @@ class AIAgent:
|
|||
if _had_prefill:
|
||||
self._thinking_prefill_retries = 0
|
||||
self._empty_content_retries = 0
|
||||
# Successful tool execution — reset the post-tool nudge
|
||||
# flag so it can fire again if the model goes empty on
|
||||
# a LATER tool round.
|
||||
self._post_tool_empty_retried = False
|
||||
|
||||
messages.append(assistant_msg)
|
||||
self._emit_interim_assistant_message(assistant_msg)
|
||||
|
|
@ -10299,6 +10304,48 @@ class AIAgent:
|
|||
self._response_was_previewed = True
|
||||
break
|
||||
|
||||
# ── Post-tool-call empty response nudge ───────────
|
||||
# The model returned empty after executing tool calls
|
||||
# but there's no prior-turn content to fall back on.
|
||||
# Instead of giving up, nudge the model to continue by
|
||||
# appending a user-level hint. This is the #9400 case:
|
||||
# weaker models (GLM-5, etc.) sometimes return empty
|
||||
# after tool results instead of continuing to the next
|
||||
# step. One retry with a nudge usually fixes it.
|
||||
_prior_was_tool = any(
|
||||
m.get("role") == "tool"
|
||||
for m in messages[-5:] # check recent messages
|
||||
)
|
||||
if (
|
||||
_prior_was_tool
|
||||
and not getattr(self, "_post_tool_empty_retried", False)
|
||||
):
|
||||
self._post_tool_empty_retried = True
|
||||
logger.info(
|
||||
"Empty response after tool calls — nudging model "
|
||||
"to continue processing"
|
||||
)
|
||||
self._emit_status(
|
||||
"⚠️ Model returned empty after tool calls — "
|
||||
"nudging to continue"
|
||||
)
|
||||
# Append the empty assistant message first so the
|
||||
# message sequence stays valid:
|
||||
# tool(result) → assistant("(empty)") → user(nudge)
|
||||
# Without this, we'd have tool → user which most
|
||||
# APIs reject as an invalid sequence.
|
||||
assistant_msg["content"] = "(empty)"
|
||||
messages.append(assistant_msg)
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
"You just executed tool calls but returned an "
|
||||
"empty response. Please process the tool "
|
||||
"results above and continue with the task."
|
||||
),
|
||||
})
|
||||
continue
|
||||
|
||||
# ── Thinking-only prefill continuation ──────────
|
||||
# The model produced structured reasoning (via API
|
||||
# fields) but no visible text content. Rather than
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue