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

This commit is contained in:
Brooklyn Nicholson
2026-04-15 16:35:01 -05:00
27 changed files with 1594 additions and 151 deletions

View File

@@ -54,7 +54,15 @@ def get_camofox_url() -> str:
def is_camofox_mode() -> bool:
"""True when Camofox backend is configured."""
"""True when Camofox backend is configured and no CDP override is active.
When the user has explicitly connected to a live Chrome instance via
``/browser connect`` (which sets ``BROWSER_CDP_URL``), the CDP connection
takes priority over Camofox so the browser tools operate on the real
browser instead of being silently routed to the Camofox backend.
"""
if os.getenv("BROWSER_CDP_URL", "").strip():
return False
return bool(get_camofox_url())

View File

@@ -105,6 +105,10 @@ 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
while True:
if is_interrupted():
try:
@@ -128,6 +132,22 @@ class BaseModalExecutionEnvironment(BaseEnvironment):
pass
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
time.sleep(self._poll_interval_seconds)
def _before_execute(self) -> None:

View File

@@ -506,7 +506,7 @@ class SamplingHandler:
"type": "function",
"function": {
"name": tu.name,
"arguments": json.dumps(tu.input) if isinstance(tu.input, dict) else str(tu.input),
"arguments": json.dumps(tu.input, ensure_ascii=False) if isinstance(tu.input, dict) else str(tu.input),
},
})
msg_dict: dict = {"role": msg.role, "tool_calls": tc_list}
@@ -1274,7 +1274,7 @@ def _interrupted_call_result() -> str:
"""Standardized JSON error for a user-interrupted MCP tool call."""
return json.dumps({
"error": "MCP call interrupted: user sent a new message"
})
}, ensure_ascii=False)
# ---------------------------------------------------------------------------
@@ -1361,7 +1361,7 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
if not server or not server.session:
return json.dumps({
"error": f"MCP server '{server_name}' is not connected"
})
}, ensure_ascii=False)
async def _call():
result = await server.session.call_tool(tool_name, arguments=args)
@@ -1375,7 +1375,7 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
"error": _sanitize_error(
error_text or "MCP tool returned an error"
)
})
}, ensure_ascii=False)
# Collect text from content blocks
parts: List[str] = []
@@ -1394,9 +1394,9 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
return json.dumps({
"result": text_result,
"structuredContent": structured,
})
return json.dumps({"result": structured})
return json.dumps({"result": text_result})
}, ensure_ascii=False)
return json.dumps({"result": structured}, ensure_ascii=False)
return json.dumps({"result": text_result}, ensure_ascii=False)
try:
return _run_on_mcp_loop(_call(), timeout=tool_timeout)
@@ -1411,7 +1411,7 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
"error": _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {exc}"
)
})
}, ensure_ascii=False)
return _handler
@@ -1425,7 +1425,7 @@ def _make_list_resources_handler(server_name: str, tool_timeout: float):
if not server or not server.session:
return json.dumps({
"error": f"MCP server '{server_name}' is not connected"
})
}, ensure_ascii=False)
async def _call():
result = await server.session.list_resources()
@@ -1441,7 +1441,7 @@ def _make_list_resources_handler(server_name: str, tool_timeout: float):
if hasattr(r, "mimeType") and r.mimeType:
entry["mimeType"] = r.mimeType
resources.append(entry)
return json.dumps({"resources": resources})
return json.dumps({"resources": resources}, ensure_ascii=False)
try:
return _run_on_mcp_loop(_call(), timeout=tool_timeout)
@@ -1455,7 +1455,7 @@ def _make_list_resources_handler(server_name: str, tool_timeout: float):
"error": _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {exc}"
)
})
}, ensure_ascii=False)
return _handler
@@ -1471,7 +1471,7 @@ def _make_read_resource_handler(server_name: str, tool_timeout: float):
if not server or not server.session:
return json.dumps({
"error": f"MCP server '{server_name}' is not connected"
})
}, ensure_ascii=False)
uri = args.get("uri")
if not uri:
@@ -1487,7 +1487,7 @@ def _make_read_resource_handler(server_name: str, tool_timeout: float):
parts.append(block.text)
elif hasattr(block, "blob"):
parts.append(f"[binary data, {len(block.blob)} bytes]")
return json.dumps({"result": "\n".join(parts) if parts else ""})
return json.dumps({"result": "\n".join(parts) if parts else ""}, ensure_ascii=False)
try:
return _run_on_mcp_loop(_call(), timeout=tool_timeout)
@@ -1501,7 +1501,7 @@ def _make_read_resource_handler(server_name: str, tool_timeout: float):
"error": _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {exc}"
)
})
}, ensure_ascii=False)
return _handler
@@ -1515,7 +1515,7 @@ def _make_list_prompts_handler(server_name: str, tool_timeout: float):
if not server or not server.session:
return json.dumps({
"error": f"MCP server '{server_name}' is not connected"
})
}, ensure_ascii=False)
async def _call():
result = await server.session.list_prompts()
@@ -1536,7 +1536,7 @@ def _make_list_prompts_handler(server_name: str, tool_timeout: float):
for a in p.arguments
]
prompts.append(entry)
return json.dumps({"prompts": prompts})
return json.dumps({"prompts": prompts}, ensure_ascii=False)
try:
return _run_on_mcp_loop(_call(), timeout=tool_timeout)
@@ -1550,7 +1550,7 @@ def _make_list_prompts_handler(server_name: str, tool_timeout: float):
"error": _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {exc}"
)
})
}, ensure_ascii=False)
return _handler
@@ -1566,7 +1566,7 @@ def _make_get_prompt_handler(server_name: str, tool_timeout: float):
if not server or not server.session:
return json.dumps({
"error": f"MCP server '{server_name}' is not connected"
})
}, ensure_ascii=False)
name = args.get("name")
if not name:
@@ -1593,7 +1593,7 @@ def _make_get_prompt_handler(server_name: str, tool_timeout: float):
resp = {"messages": messages}
if hasattr(result, "description") and result.description:
resp["description"] = result.description
return json.dumps(resp)
return json.dumps(resp, ensure_ascii=False)
try:
return _run_on_mcp_loop(_call(), timeout=tool_timeout)
@@ -1607,7 +1607,7 @@ def _make_get_prompt_handler(server_name: str, tool_timeout: float):
"error": _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {exc}"
)
})
}, ensure_ascii=False)
return _handler

View File

@@ -310,7 +310,15 @@ def session_search(
if db is None:
return tool_error("Session database not available.", success=False)
limit = min(limit, 5) # Cap at 5 sessions to avoid excessive LLM calls
# Defensive: models (especially open-source) may send non-int limit values
# (None when JSON null, string "int", or even a type object). Coerce to a
# safe integer before any arithmetic/comparison to prevent TypeError.
if not isinstance(limit, int):
try:
limit = int(limit)
except (TypeError, ValueError):
limit = 3
limit = max(1, min(limit, 5)) # Clamp to [1, 5]
# Recent sessions mode: when query is empty, return metadata for recent sessions.
# No LLM calls — just DB queries for titles, previews, timestamps.