Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 8 additions & 16 deletions backend/app/services/agent_runtime/node_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,24 +1051,16 @@ async def _tool(
lifecycle.pop("step_tool_context", None)
lifecycle.update(
{
"status": "waiting_user",
"next_route": "wait",
"status": "failed",
"next_route": "terminal",
"reason": repair_pause_reason,
"pending_tool_calls": [],
"waiting_request": {
"waiting_type": "user",
"correlation_id": _runtime_message_id(
context,
f"tool-repair:{repair_pause_reason}:{repair_pause_tool}",
),
"reason": repair_pause_reason,
"question": (
f"Tool {repair_pause_tool or 'unknown'} reached its "
"repair safety limit. Provide corrected requirements "
"or arguments to continue."
),
},
"error": None,
"waiting_request": None,
"error": _error(
repair_pause_reason,
f"Tool {repair_pause_tool or 'unknown'} reached its "
"repair safety limit.",
),
}
)
else:
Expand Down
28 changes: 9 additions & 19 deletions backend/tests/test_agent_runtime_node_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ async def no_sleep(_seconds: float) -> None:


@pytest.mark.asyncio
async def test_tenth_same_tool_failure_pauses_before_next_model_call() -> None:
async def test_tenth_same_tool_failure_fails_run_before_next_model_call() -> None:
run_id = uuid.uuid4()
proposals = tuple(
ModelStepResult(
Expand Down Expand Up @@ -822,34 +822,24 @@ async def test_tenth_same_tool_failure_pauses_before_next_model_call() -> None:
result = await _invoke(run_id, executor)

lifecycle = result["lifecycle"]
assert lifecycle["status"] == "waiting_user"
assert lifecycle["status"] == "failed"
assert lifecycle["next_route"] == "terminal"
assert lifecycle["reason"] == (
"tool_repair_same_fingerprint_limit_reached"
)
assert lifecycle["error"] == {
"code": "tool_repair_same_fingerprint_limit_reached",
"message": "Tool read_file reached its repair safety limit.",
}
assert lifecycle["pending_tool_calls"] == []
assert lifecycle.get("waiting_request") is None
assert lifecycle["model_step_count"] == 10
repair_episode = lifecycle["tool_repair_episodes"]["by_tool"]["read_file"]
assert repair_episode["total_failures"] == 10
assert repair_episode["same_fingerprint_failures"] == 10
assert model.calls == 10
assert len(tools.calls) == 10

resumed = await executor.execute(
"wait",
cast(RuntimeGraphState, result),
_context(run_id, executor, "command-user-correction"),
resume_value={
"resume_type": "user_input",
"payload": {"content": "Use README.md as the path."},
},
)
assert resumed["lifecycle"]["tool_repair_episodes"] == {
"version": 1,
"by_tool": {},
}
assert resumed["lifecycle"]["tool_repair_reset"]["reason"] == (
"explicit_user_correction"
)


@pytest.mark.asyncio
async def test_duplicate_tool_call_ids_fail_before_any_provider_execution() -> None:
Expand Down