Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor

This commit is contained in:
Brooklyn Nicholson
2026-04-15 10:21:00 -05:00
35 changed files with 2388 additions and 122 deletions

View File

@@ -1268,6 +1268,19 @@ class AIAgent:
try:
_config_context_length = int(_config_context_length)
except (TypeError, ValueError):
logger.warning(
"Invalid model.context_length in config.yaml: %r"
"must be a plain integer (e.g. 256000, not '256K'). "
"Falling back to auto-detection.",
_config_context_length,
)
import sys
print(
f"\n⚠ Invalid model.context_length in config.yaml: {_config_context_length!r}\n"
f" Must be a plain integer (e.g. 256000, not '256K').\n"
f" Falling back to auto-detected context window.\n",
file=sys.stderr,
)
_config_context_length = None
# Store for reuse in switch_model (so config override persists across model switches)
@@ -1296,7 +1309,20 @@ class AIAgent:
try:
_config_context_length = int(_cp_ctx)
except (TypeError, ValueError):
pass
logger.warning(
"Invalid context_length for model %r in "
"custom_providers: %r — must be a plain "
"integer (e.g. 256000, not '256K'). "
"Falling back to auto-detection.",
self.model, _cp_ctx,
)
import sys
print(
f"\n⚠ Invalid context_length for model {self.model!r} in custom_providers: {_cp_ctx!r}\n"
f" Must be a plain integer (e.g. 256000, not '256K').\n"
f" Falling back to auto-detected context window.\n",
file=sys.stderr,
)
break
# Select context engine: config-driven (like memory providers).
@@ -3563,7 +3589,12 @@ class AIAgent:
item_id = ri.get("id")
if item_id and item_id in seen_item_ids:
continue
items.append(ri)
# Strip the "id" field — with store=False the
# Responses API cannot look up items by ID and
# returns 404. The encrypted_content blob is
# self-contained for reasoning chain continuity.
replay_item = {k: v for k, v in ri.items() if k != "id"}
items.append(replay_item)
if item_id:
seen_item_ids.add(item_id)
has_codex_reasoning = True
@@ -3704,8 +3735,10 @@ class AIAgent:
continue
seen_ids.add(item_id)
reasoning_item = {"type": "reasoning", "encrypted_content": encrypted}
if isinstance(item_id, str) and item_id:
reasoning_item["id"] = item_id
# Do NOT include the "id" in the outgoing item — with
# store=False (our default) the API tries to resolve the
# id server-side and returns 404. The id is still used
# above for local deduplication via seen_ids.
summary = item.get("summary")
if isinstance(summary, list):
reasoning_item["summary"] = summary
@@ -7833,6 +7866,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
@@ -9011,6 +9045,11 @@ class AIAgent:
self.api_key = _clean_key
if isinstance(getattr(self, "_client_kwargs", None), dict):
self._client_kwargs["api_key"] = _clean_key
# Also update the live client — it holds its
# own copy of api_key which auth_headers reads
# dynamically on every request.
if getattr(self, "client", None) is not None and hasattr(self.client, "api_key"):
self.client.api_key = _clean_key
_credential_sanitized = True
self._vprint(
f"{self.log_prefix}⚠️ API key contained non-ASCII characters "
@@ -10106,6 +10145,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)
@@ -10274,6 +10317,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