mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix: return JSON parse error to model instead of dispatching with empty args (#2342)
When the model produces malformed JSON in tool call arguments, the agent
loop was setting args={} and dispatching the tool anyway, wasting an
iteration and producing a confusing downstream error. Now the error is
returned directly as the tool result so the model can retry with valid JSON.
Co-authored-by: alireza78a <alireza78.crypto@gmail.com>
This commit is contained in:
@@ -346,78 +346,89 @@ class HermesAgentLoop:
|
|||||||
tool_name, turn + 1,
|
tool_name, turn + 1,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Parse arguments and dispatch
|
# Parse arguments
|
||||||
try:
|
try:
|
||||||
args = json.loads(tool_args_raw)
|
args = json.loads(tool_args_raw)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
args = {}
|
args = None
|
||||||
|
tool_result = json.dumps(
|
||||||
|
{"error": f"Invalid JSON in tool arguments: {e}. Please retry with valid JSON."}
|
||||||
|
)
|
||||||
|
tool_errors.append(ToolError(
|
||||||
|
turn=turn + 1, tool_name=tool_name,
|
||||||
|
arguments=tool_args_raw[:200],
|
||||||
|
error=f"Invalid JSON: {e}",
|
||||||
|
tool_result=tool_result,
|
||||||
|
))
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Invalid JSON in tool call arguments for '%s': %s",
|
"Invalid JSON in tool call arguments for '%s': %s",
|
||||||
tool_name, tool_args_raw[:200],
|
tool_name, tool_args_raw[:200],
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
# Dispatch tool only if arguments parsed successfully
|
||||||
if tool_name == "terminal":
|
if args is not None:
|
||||||
backend = os.getenv("TERMINAL_ENV", "local")
|
try:
|
||||||
cmd_preview = args.get("command", "")[:80]
|
if tool_name == "terminal":
|
||||||
logger.info(
|
backend = os.getenv("TERMINAL_ENV", "local")
|
||||||
"[%s] $ %s", self.task_id[:8], cmd_preview,
|
cmd_preview = args.get("command", "")[:80]
|
||||||
)
|
logger.info(
|
||||||
|
"[%s] $ %s", self.task_id[:8], cmd_preview,
|
||||||
|
)
|
||||||
|
|
||||||
tool_submit_time = _time.monotonic()
|
tool_submit_time = _time.monotonic()
|
||||||
|
|
||||||
# Todo tool -- handle locally (needs per-loop TodoStore)
|
# Todo tool -- handle locally (needs per-loop TodoStore)
|
||||||
if tool_name == "todo":
|
if tool_name == "todo":
|
||||||
tool_result = _todo_tool(
|
tool_result = _todo_tool(
|
||||||
todos=args.get("todos"),
|
todos=args.get("todos"),
|
||||||
merge=args.get("merge", False),
|
merge=args.get("merge", False),
|
||||||
store=_todo_store,
|
store=_todo_store,
|
||||||
)
|
)
|
||||||
tool_elapsed = _time.monotonic() - tool_submit_time
|
tool_elapsed = _time.monotonic() - tool_submit_time
|
||||||
elif tool_name == "memory":
|
elif tool_name == "memory":
|
||||||
tool_result = json.dumps({"error": "Memory is not available in RL environments."})
|
tool_result = json.dumps({"error": "Memory is not available in RL environments."})
|
||||||
tool_elapsed = _time.monotonic() - tool_submit_time
|
tool_elapsed = _time.monotonic() - tool_submit_time
|
||||||
elif tool_name == "session_search":
|
elif tool_name == "session_search":
|
||||||
tool_result = json.dumps({"error": "Session search is not available in RL environments."})
|
tool_result = json.dumps({"error": "Session search is not available in RL environments."})
|
||||||
tool_elapsed = _time.monotonic() - tool_submit_time
|
tool_elapsed = _time.monotonic() - tool_submit_time
|
||||||
else:
|
else:
|
||||||
# Run tool calls in a thread pool so backends that
|
# Run tool calls in a thread pool so backends that
|
||||||
# use asyncio.run() internally (modal, docker, daytona) get
|
# use asyncio.run() internally (modal, docker, daytona) get
|
||||||
# a clean event loop instead of deadlocking.
|
# a clean event loop instead of deadlocking.
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
# Capture current tool_name/args for the lambda
|
# Capture current tool_name/args for the lambda
|
||||||
_tn, _ta, _tid = tool_name, args, self.task_id
|
_tn, _ta, _tid = tool_name, args, self.task_id
|
||||||
tool_result = await loop.run_in_executor(
|
tool_result = await loop.run_in_executor(
|
||||||
_tool_executor,
|
_tool_executor,
|
||||||
lambda: handle_function_call(
|
lambda: handle_function_call(
|
||||||
_tn, _ta, task_id=_tid,
|
_tn, _ta, task_id=_tid,
|
||||||
user_task=_user_task,
|
user_task=_user_task,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
tool_elapsed = _time.monotonic() - tool_submit_time
|
tool_elapsed = _time.monotonic() - tool_submit_time
|
||||||
|
|
||||||
# Log slow tools and thread pool stats for debugging
|
# Log slow tools and thread pool stats for debugging
|
||||||
pool_active = _tool_executor._work_queue.qsize()
|
pool_active = _tool_executor._work_queue.qsize()
|
||||||
if tool_elapsed > 30:
|
if tool_elapsed > 30:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[%s] turn %d: %s took %.1fs (pool queue=%d)",
|
"[%s] turn %d: %s took %.1fs (pool queue=%d)",
|
||||||
self.task_id[:8], turn + 1, tool_name,
|
self.task_id[:8], turn + 1, tool_name,
|
||||||
tool_elapsed, pool_active,
|
tool_elapsed, pool_active,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
tool_result = json.dumps(
|
||||||
|
{"error": f"Tool execution failed: {type(e).__name__}: {str(e)}"}
|
||||||
|
)
|
||||||
|
tool_errors.append(ToolError(
|
||||||
|
turn=turn + 1, tool_name=tool_name,
|
||||||
|
arguments=tool_args_raw[:200],
|
||||||
|
error=f"{type(e).__name__}: {str(e)}",
|
||||||
|
tool_result=tool_result,
|
||||||
|
))
|
||||||
|
logger.error(
|
||||||
|
"Tool '%s' execution failed on turn %d: %s",
|
||||||
|
tool_name, turn + 1, e,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
tool_result = json.dumps(
|
|
||||||
{"error": f"Tool execution failed: {type(e).__name__}: {str(e)}"}
|
|
||||||
)
|
|
||||||
tool_errors.append(ToolError(
|
|
||||||
turn=turn + 1, tool_name=tool_name,
|
|
||||||
arguments=tool_args_raw[:200],
|
|
||||||
error=f"{type(e).__name__}: {str(e)}",
|
|
||||||
tool_result=tool_result,
|
|
||||||
))
|
|
||||||
logger.error(
|
|
||||||
"Tool '%s' execution failed on turn %d: %s",
|
|
||||||
tool_name, turn + 1, e,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also check if the tool returned an error in its JSON result
|
# Also check if the tool returned an error in its JSON result
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user