fix(agent): add retry hint to tool exceptions

This commit is contained in:
Oxygen56
2026-08-30 17:03:51 +08:00
committed by Xubin Ren
parent 2c55934198
commit 919e3d341e
2 changed files with 39 additions and 7 deletions
+15 -7
View File
@@ -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(
tools: ToolRegistry,
tool_calls: list[ToolCallRequest],
@@ -105,7 +112,7 @@ async def _execute_tool_call(
"status": "error",
"detail": "repeated external lookup blocked",
}
return lookup_error + _RETRY_HINT, event
return _with_retry_hint(lookup_error), event
prepare_call = cast(
Callable[[str, Any], object] | None,
@@ -119,6 +126,7 @@ async def _execute_tool_call(
if len(prepared_tuple) == 3:
tool, params, prep_error = cast(tuple[Any, Any, str | None], prepared_tuple)
if prep_error:
payload = _with_retry_hint(prep_error)
event = {
"name": tool_call.name,
"status": "error",
@@ -126,14 +134,14 @@ async def _execute_tool_call(
}
handled = _classify_violation(
raw_text=prep_error,
soft_payload=prep_error + _RETRY_HINT,
soft_payload=payload,
event=event,
tool_call=tool_call,
workspace_violation_counts=workspace_violation_counts,
)
if handled is not None:
return handled
return prep_error + _RETRY_HINT, event
return payload, event
await hook.before_execute_tool(context, tool_call, tool, params)
try:
@@ -150,10 +158,9 @@ async def _execute_tool_call(
"status": "error",
"detail": str(exc),
}
payload = f"Error: {type(exc).__name__}: {exc}"
payload = _with_retry_hint(f"Error: {type(exc).__name__}: {exc}")
handled = _classify_violation(
raw_text=str(exc),
# Preserve legacy exception payloads without the retry hint.
soft_payload=payload,
event=event,
tool_call=tool_call,
@@ -165,6 +172,7 @@ async def _execute_tool_call(
if is_tool_error_result(result):
await hook.on_execute_tool_error(context, tool_call, tool, params, result)
payload = _with_retry_hint(result)
event = {
"name": tool_call.name,
"status": "error",
@@ -172,14 +180,14 @@ async def _execute_tool_call(
}
handled = _classify_violation(
raw_text=result,
soft_payload=result + _RETRY_HINT,
soft_payload=payload,
event=event,
tool_call=tool_call,
workspace_violation_counts=workspace_violation_counts,
)
if handled is not None:
return handled
return result + _RETRY_HINT, event
return payload, event
await hook.after_execute_tool(context, tool_call, tool, params, result)
+24
View File
@@ -52,7 +52,31 @@ async def test_runner_returns_tool_exception_to_model_for_recovery():
{"name": "list_dir", "status": "error", "detail": "boom"}
]
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 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