Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
@@ -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())
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user