mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
fix(agent): add retry hint to tool exceptions
This commit is contained in:
@@ -43,6 +43,13 @@ _WORKSPACE_VIOLATION_MARKERS: tuple[str, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _with_retry_hint(payload: str) -> str:
|
||||||
|
"""Append the recovery hint exactly once."""
|
||||||
|
if payload.endswith(_RETRY_HINT):
|
||||||
|
return payload
|
||||||
|
return payload + _RETRY_HINT
|
||||||
|
|
||||||
|
|
||||||
async def execute_tool_calls(
|
async def execute_tool_calls(
|
||||||
tools: ToolRegistry,
|
tools: ToolRegistry,
|
||||||
tool_calls: list[ToolCallRequest],
|
tool_calls: list[ToolCallRequest],
|
||||||
@@ -105,7 +112,7 @@ async def _execute_tool_call(
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"detail": "repeated external lookup blocked",
|
"detail": "repeated external lookup blocked",
|
||||||
}
|
}
|
||||||
return lookup_error + _RETRY_HINT, event
|
return _with_retry_hint(lookup_error), event
|
||||||
|
|
||||||
prepare_call = cast(
|
prepare_call = cast(
|
||||||
Callable[[str, Any], object] | None,
|
Callable[[str, Any], object] | None,
|
||||||
@@ -119,6 +126,7 @@ async def _execute_tool_call(
|
|||||||
if len(prepared_tuple) == 3:
|
if len(prepared_tuple) == 3:
|
||||||
tool, params, prep_error = cast(tuple[Any, Any, str | None], prepared_tuple)
|
tool, params, prep_error = cast(tuple[Any, Any, str | None], prepared_tuple)
|
||||||
if prep_error:
|
if prep_error:
|
||||||
|
payload = _with_retry_hint(prep_error)
|
||||||
event = {
|
event = {
|
||||||
"name": tool_call.name,
|
"name": tool_call.name,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@@ -126,14 +134,14 @@ async def _execute_tool_call(
|
|||||||
}
|
}
|
||||||
handled = _classify_violation(
|
handled = _classify_violation(
|
||||||
raw_text=prep_error,
|
raw_text=prep_error,
|
||||||
soft_payload=prep_error + _RETRY_HINT,
|
soft_payload=payload,
|
||||||
event=event,
|
event=event,
|
||||||
tool_call=tool_call,
|
tool_call=tool_call,
|
||||||
workspace_violation_counts=workspace_violation_counts,
|
workspace_violation_counts=workspace_violation_counts,
|
||||||
)
|
)
|
||||||
if handled is not None:
|
if handled is not None:
|
||||||
return handled
|
return handled
|
||||||
return prep_error + _RETRY_HINT, event
|
return payload, event
|
||||||
|
|
||||||
await hook.before_execute_tool(context, tool_call, tool, params)
|
await hook.before_execute_tool(context, tool_call, tool, params)
|
||||||
try:
|
try:
|
||||||
@@ -150,10 +158,9 @@ async def _execute_tool_call(
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"detail": str(exc),
|
"detail": str(exc),
|
||||||
}
|
}
|
||||||
payload = f"Error: {type(exc).__name__}: {exc}"
|
payload = _with_retry_hint(f"Error: {type(exc).__name__}: {exc}")
|
||||||
handled = _classify_violation(
|
handled = _classify_violation(
|
||||||
raw_text=str(exc),
|
raw_text=str(exc),
|
||||||
# Preserve legacy exception payloads without the retry hint.
|
|
||||||
soft_payload=payload,
|
soft_payload=payload,
|
||||||
event=event,
|
event=event,
|
||||||
tool_call=tool_call,
|
tool_call=tool_call,
|
||||||
@@ -165,6 +172,7 @@ async def _execute_tool_call(
|
|||||||
|
|
||||||
if is_tool_error_result(result):
|
if is_tool_error_result(result):
|
||||||
await hook.on_execute_tool_error(context, tool_call, tool, params, result)
|
await hook.on_execute_tool_error(context, tool_call, tool, params, result)
|
||||||
|
payload = _with_retry_hint(result)
|
||||||
event = {
|
event = {
|
||||||
"name": tool_call.name,
|
"name": tool_call.name,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@@ -172,14 +180,14 @@ async def _execute_tool_call(
|
|||||||
}
|
}
|
||||||
handled = _classify_violation(
|
handled = _classify_violation(
|
||||||
raw_text=result,
|
raw_text=result,
|
||||||
soft_payload=result + _RETRY_HINT,
|
soft_payload=payload,
|
||||||
event=event,
|
event=event,
|
||||||
tool_call=tool_call,
|
tool_call=tool_call,
|
||||||
workspace_violation_counts=workspace_violation_counts,
|
workspace_violation_counts=workspace_violation_counts,
|
||||||
)
|
)
|
||||||
if handled is not None:
|
if handled is not None:
|
||||||
return handled
|
return handled
|
||||||
return result + _RETRY_HINT, event
|
return payload, event
|
||||||
|
|
||||||
await hook.after_execute_tool(context, tool_call, tool, params, result)
|
await hook.after_execute_tool(context, tool_call, tool, params, result)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,31 @@ async def test_runner_returns_tool_exception_to_model_for_recovery():
|
|||||||
{"name": "list_dir", "status": "error", "detail": "boom"}
|
{"name": "list_dir", "status": "error", "detail": "boom"}
|
||||||
]
|
]
|
||||||
tool_message = next(message for message in result.messages if message.get("role") == "tool")
|
tool_message = next(message for message in result.messages if message.get("role") == "tool")
|
||||||
|
retry_hint = "[Analyze the error above and try a different approach.]"
|
||||||
assert "Error: RuntimeError: boom" in tool_message["content"]
|
assert "Error: RuntimeError: boom" in tool_message["content"]
|
||||||
|
assert tool_message["content"].count(retry_hint) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_execution_does_not_duplicate_existing_retry_hint():
|
||||||
|
retry_hint = "\n\n[Analyze the error above and try a different approach.]"
|
||||||
|
tools = SimpleNamespace(
|
||||||
|
execute=AsyncMock(return_value=ToolResult.error("Error: boom" + retry_hint)),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, events = await execute_tool_calls(
|
||||||
|
tools,
|
||||||
|
[ToolCallRequest(id="call_1", name="list_dir", arguments={})],
|
||||||
|
concurrent=False,
|
||||||
|
external_lookup_counts={},
|
||||||
|
workspace_violation_counts={},
|
||||||
|
hook=AgentHook(),
|
||||||
|
context=AgentHookContext(iteration=0, messages=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert results == ["Error: boom" + retry_hint]
|
||||||
|
assert results[0].count(retry_hint) == 1
|
||||||
|
assert events[0]["status"] == "error"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user