mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c75fa88d60 |
53
run_agent.py
53
run_agent.py
@@ -7281,6 +7281,7 @@ class AIAgent:
|
|||||||
length_continue_retries = 0
|
length_continue_retries = 0
|
||||||
truncated_response_prefix = ""
|
truncated_response_prefix = ""
|
||||||
compression_attempts = 0
|
compression_attempts = 0
|
||||||
|
_turn_exit_reason = "unknown" # Diagnostic: why the loop ended
|
||||||
|
|
||||||
# Clear any stale interrupt state at start
|
# Clear any stale interrupt state at start
|
||||||
self.clear_interrupt()
|
self.clear_interrupt()
|
||||||
@@ -7305,6 +7306,7 @@ class AIAgent:
|
|||||||
# Check for interrupt request (e.g., user sent new message)
|
# Check for interrupt request (e.g., user sent new message)
|
||||||
if self._interrupt_requested:
|
if self._interrupt_requested:
|
||||||
interrupted = True
|
interrupted = True
|
||||||
|
_turn_exit_reason = "interrupted_by_user"
|
||||||
if not self.quiet_mode:
|
if not self.quiet_mode:
|
||||||
self._safe_print("\n⚡ Breaking out of tool loop due to interrupt...")
|
self._safe_print("\n⚡ Breaking out of tool loop due to interrupt...")
|
||||||
break
|
break
|
||||||
@@ -7313,6 +7315,7 @@ class AIAgent:
|
|||||||
self._api_call_count = api_call_count
|
self._api_call_count = api_call_count
|
||||||
self._touch_activity(f"starting API call #{api_call_count}")
|
self._touch_activity(f"starting API call #{api_call_count}")
|
||||||
if not self.iteration_budget.consume():
|
if not self.iteration_budget.consume():
|
||||||
|
_turn_exit_reason = "budget_exhausted"
|
||||||
if not self.quiet_mode:
|
if not self.quiet_mode:
|
||||||
self._safe_print(f"\n⚠️ Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)")
|
self._safe_print(f"\n⚠️ Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)")
|
||||||
break
|
break
|
||||||
@@ -8614,6 +8617,7 @@ class AIAgent:
|
|||||||
|
|
||||||
# If the API call was interrupted, skip response processing
|
# If the API call was interrupted, skip response processing
|
||||||
if interrupted:
|
if interrupted:
|
||||||
|
_turn_exit_reason = "interrupted_during_api_call"
|
||||||
break
|
break
|
||||||
|
|
||||||
if restart_with_compressed_messages:
|
if restart_with_compressed_messages:
|
||||||
@@ -8633,6 +8637,7 @@ class AIAgent:
|
|||||||
# (e.g. repeated context-length errors that exhausted retry_count),
|
# (e.g. repeated context-length errors that exhausted retry_count),
|
||||||
# the `response` variable is still None. Break out cleanly.
|
# the `response` variable is still None. Break out cleanly.
|
||||||
if response is None:
|
if response is None:
|
||||||
|
_turn_exit_reason = "all_retries_exhausted_no_response"
|
||||||
print(f"{self.log_prefix}❌ All API retries exhausted with no successful response.")
|
print(f"{self.log_prefix}❌ All API retries exhausted with no successful response.")
|
||||||
self._persist_session(messages, conversation_history)
|
self._persist_session(messages, conversation_history)
|
||||||
break
|
break
|
||||||
@@ -9096,6 +9101,7 @@ class AIAgent:
|
|||||||
# instead of wasting API calls on retries that won't help.
|
# instead of wasting API calls on retries that won't help.
|
||||||
fallback = getattr(self, '_last_content_with_tools', None)
|
fallback = getattr(self, '_last_content_with_tools', None)
|
||||||
if fallback:
|
if fallback:
|
||||||
|
_turn_exit_reason = "fallback_prior_turn_content"
|
||||||
logger.debug("Empty follow-up after tool calls — using prior turn content as final response")
|
logger.debug("Empty follow-up after tool calls — using prior turn content as final response")
|
||||||
self._last_content_with_tools = None
|
self._last_content_with_tools = None
|
||||||
self._empty_content_retries = 0
|
self._empty_content_retries = 0
|
||||||
@@ -9162,6 +9168,7 @@ class AIAgent:
|
|||||||
# Exhausted prefill attempts, empty retries, or
|
# Exhausted prefill attempts, empty retries, or
|
||||||
# structured reasoning with no content —
|
# structured reasoning with no content —
|
||||||
# fall through to "(empty)" terminal.
|
# fall through to "(empty)" terminal.
|
||||||
|
_turn_exit_reason = "empty_response_exhausted"
|
||||||
reasoning_text = self._extract_reasoning(assistant_message)
|
reasoning_text = self._extract_reasoning(assistant_message)
|
||||||
assistant_msg = self._build_assistant_message(assistant_message, finish_reason)
|
assistant_msg = self._build_assistant_message(assistant_message, finish_reason)
|
||||||
assistant_msg["content"] = "(empty)"
|
assistant_msg["content"] = "(empty)"
|
||||||
@@ -9233,6 +9240,7 @@ class AIAgent:
|
|||||||
|
|
||||||
messages.append(final_msg)
|
messages.append(final_msg)
|
||||||
|
|
||||||
|
_turn_exit_reason = f"text_response(finish_reason={finish_reason})"
|
||||||
if not self.quiet_mode:
|
if not self.quiet_mode:
|
||||||
self._safe_print(f"🎉 Conversation completed after {api_call_count} OpenAI-compatible API call(s)")
|
self._safe_print(f"🎉 Conversation completed after {api_call_count} OpenAI-compatible API call(s)")
|
||||||
break
|
break
|
||||||
@@ -9282,6 +9290,7 @@ class AIAgent:
|
|||||||
|
|
||||||
# If we're near the limit, break to avoid infinite loops
|
# If we're near the limit, break to avoid infinite loops
|
||||||
if api_call_count >= self.max_iterations - 1:
|
if api_call_count >= self.max_iterations - 1:
|
||||||
|
_turn_exit_reason = f"error_near_max_iterations({error_msg[:80]})"
|
||||||
final_response = f"I apologize, but I encountered repeated errors: {error_msg}"
|
final_response = f"I apologize, but I encountered repeated errors: {error_msg}"
|
||||||
# Append as assistant so the history stays valid for
|
# Append as assistant so the history stays valid for
|
||||||
# session resume (avoids consecutive user messages).
|
# session resume (avoids consecutive user messages).
|
||||||
@@ -9292,6 +9301,7 @@ class AIAgent:
|
|||||||
api_call_count >= self.max_iterations
|
api_call_count >= self.max_iterations
|
||||||
or self.iteration_budget.remaining <= 0
|
or self.iteration_budget.remaining <= 0
|
||||||
):
|
):
|
||||||
|
_turn_exit_reason = f"max_iterations_reached({api_call_count}/{self.max_iterations})"
|
||||||
if self.iteration_budget.remaining <= 0 and not self.quiet_mode:
|
if self.iteration_budget.remaining <= 0 and not self.quiet_mode:
|
||||||
print(f"\n⚠️ Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)")
|
print(f"\n⚠️ Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)")
|
||||||
final_response = self._handle_max_iterations(messages, api_call_count)
|
final_response = self._handle_max_iterations(messages, api_call_count)
|
||||||
@@ -9308,6 +9318,49 @@ class AIAgent:
|
|||||||
# Persist session to both JSON log and SQLite
|
# Persist session to both JSON log and SQLite
|
||||||
self._persist_session(messages, conversation_history)
|
self._persist_session(messages, conversation_history)
|
||||||
|
|
||||||
|
# ── Turn-exit diagnostic log ─────────────────────────────────────
|
||||||
|
# Always logged at INFO so agent.log captures WHY every turn ended.
|
||||||
|
# When the last message is a tool result (agent was mid-work), log
|
||||||
|
# at WARNING — this is the "just stops" scenario users report.
|
||||||
|
_last_msg_role = messages[-1].get("role") if messages else None
|
||||||
|
_last_tool_name = None
|
||||||
|
if _last_msg_role == "tool":
|
||||||
|
# Walk back to find the assistant message with the tool call
|
||||||
|
for _m in reversed(messages):
|
||||||
|
if _m.get("role") == "assistant" and _m.get("tool_calls"):
|
||||||
|
_tcs = _m["tool_calls"]
|
||||||
|
if _tcs and isinstance(_tcs[0], dict):
|
||||||
|
_last_tool_name = _tcs[-1].get("function", {}).get("name")
|
||||||
|
break
|
||||||
|
|
||||||
|
_turn_tool_count = sum(
|
||||||
|
1 for m in messages
|
||||||
|
if isinstance(m, dict) and m.get("role") == "assistant" and m.get("tool_calls")
|
||||||
|
)
|
||||||
|
_resp_len = len(final_response) if final_response else 0
|
||||||
|
_budget_used = self.iteration_budget.used if self.iteration_budget else 0
|
||||||
|
_budget_max = self.iteration_budget.max_total if self.iteration_budget else 0
|
||||||
|
|
||||||
|
_diag_msg = (
|
||||||
|
"Turn ended: reason=%s model=%s api_calls=%d/%d budget=%d/%d "
|
||||||
|
"tool_turns=%d last_msg_role=%s response_len=%d session=%s"
|
||||||
|
)
|
||||||
|
_diag_args = (
|
||||||
|
_turn_exit_reason, self.model, api_call_count, self.max_iterations,
|
||||||
|
_budget_used, _budget_max,
|
||||||
|
_turn_tool_count, _last_msg_role, _resp_len,
|
||||||
|
self.session_id or "none",
|
||||||
|
)
|
||||||
|
|
||||||
|
if _last_msg_role == "tool" and not interrupted:
|
||||||
|
# Agent was mid-work — this is the "just stops" case.
|
||||||
|
logger.warning(
|
||||||
|
"Turn ended with pending tool result (agent may appear stuck). "
|
||||||
|
+ _diag_msg + " last_tool=%s",
|
||||||
|
*_diag_args, _last_tool_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(_diag_msg, *_diag_args)
|
||||||
|
|
||||||
# Plugin hook: post_llm_call
|
# Plugin hook: post_llm_call
|
||||||
# Fired once per turn after the tool-calling loop completes.
|
# Fired once per turn after the tool-calling loop completes.
|
||||||
|
|||||||
Reference in New Issue
Block a user