tool/actor_runtime: ToolCallingRuntime._execute_tool_call router path lacks error handling — exceptions not recorded in ToolCallRecord #10386

Open
opened 2026-04-18 09:22:18 +00:00 by HAL9000 · 0 comments
Owner

Bug Report

Metadata

  • Module: src/cleveragents/tool/actor_runtime.py
  • Class: ToolCallingRuntime
  • Method: _execute_tool_call
  • Severity: Critical
  • TDD Testing Issue: #10383

Background and Context

ToolCallingRuntime._execute_tool_call() has asymmetric error handling between the router path and the runner path. When self._router is not None, any exception from self._router.route(payload) propagates uncaught, leaving the tool call unrecorded in the actor context and the ACTOR_ERRORED event unemitted. This is a production-path bug since ToolCallRouter is used for provider format handling.

Expected Behavior

When self._router.route(payload) raises any exception:

  • The exception is caught and handled gracefully
  • A failed ToolCallRecord is recorded in the actor context
  • The ACTOR_ERRORED event is emitted
  • An error result is returned to the LLM (not a crash)

Acceptance Criteria

  • Router path in _execute_tool_call has try/except matching the runner path
  • Exceptions from self._router.route() are caught and recorded as failed ToolCallRecord
  • ACTOR_ERRORED event is emitted when router raises
  • TDD test from #10383 passes
  • All existing tests continue to pass
  • Coverage >= 97%

Subtasks

  • Add try/except around router path in _execute_tool_call
  • Ensure elapsed_ms is set in the except clause
  • Ensure ACTOR_ERRORED event is emitted on router failure
  • Verify TDD test from #10383 now passes
  • Run nox and verify all tests pass

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off
  • A Git commit is created with a descriptive message
  • The commit is pushed to a branch and submitted as a pull request to master
  • The TDD testing issue #10383 is resolved (its @tdd_expected_fail scenario now passes)

Summary

ToolCallingRuntime._execute_tool_call() has asymmetric error handling between the router path and the runner path. When self._router is not None, any exception from self._router.route(payload) propagates uncaught, leaving the tool call unrecorded in the actor context and the ACTOR_ERRORED event unemitted.

Code Evidence

In src/cleveragents/tool/actor_runtime.py (lines ~350-383):

if self._router is not None:
    # Route through ToolCallRouter for provider format handling
    payload: dict[str, Any] = {
        "name": tool_call.name,
        "args": enriched_inputs,
        "type": "tool_call",
    }
    routed_result = self._router.route(payload)  # ← NO try/except!
    elapsed_ms = (time.monotonic() - start) * 1000.0
    success = routed_result.result.success
    output = routed_result.result.output
    error = routed_result.result.error
else:
    # Execute directly via runner, catching ToolError for not-found
    try:
        result = self._runner.execute(...)
        ...
    except ToolError as exc:
        elapsed_ms = (time.monotonic() - start) * 1000.0
        success = False
        output = {}
        error = str(exc)
    except Exception as exc:
        elapsed_ms = (time.monotonic() - start) * 1000.0
        success = False
        output = {}
        error = f"{type(exc).__name__}: {exc}"

The runner path (else branch) has proper try/except for both ToolError and Exception. The router path (if branch) has none.

Impact

When using ToolCallRouter (the production path for provider format handling):

  1. Any exception from self._router.route(payload) propagates to run_tool_loop
  2. No ToolCallRecord is created — the tool call history is incomplete
  3. ACTOR_ERRORED event is not emitted — observability is broken
  4. The tool-call loop crashes instead of gracefully handling the error
  5. The LLM does not receive an error result to reason about

Reproduction

from unittest.mock import MagicMock
from cleveragents.tool.actor_runtime import ToolCallingRuntime, LLMToolCall
from cleveragents.tool.actor_context import ToolActorContext

router = MagicMock()
router.route.side_effect = RuntimeError("Router failure")

runtime = ToolCallingRuntime(
    registry=MagicMock(),
    runner=MagicMock(),
    llm_caller=MagicMock(),
    router=router,
)
context = ToolActorContext(plan_id="test-plan", phase="execute")
tool_call = LLMToolCall(name="my_tool", arguments={})

# This raises RuntimeError instead of returning an error result
runtime._execute_tool_call(tool_call, context, iteration=1)

Fix

Wrap the router path in try/except similar to the runner path:

if self._router is not None:
    payload = {"name": tool_call.name, "args": enriched_inputs, "type": "tool_call"}
    try:
        routed_result = self._router.route(payload)
        elapsed_ms = (time.monotonic() - start) * 1000.0
        success = routed_result.result.success
        output = routed_result.result.output
        error = routed_result.result.error
    except Exception as exc:
        elapsed_ms = (time.monotonic() - start) * 1000.0
        success = False
        output = {}
        error = f"{type(exc).__name__}: {exc}"

Automated by CleverAgents Bot
Agent: new-issue-creator

## Bug Report ### Metadata - **Module**: `src/cleveragents/tool/actor_runtime.py` - **Class**: `ToolCallingRuntime` - **Method**: `_execute_tool_call` - **Severity**: Critical - **TDD Testing Issue**: #10383 ## Background and Context `ToolCallingRuntime._execute_tool_call()` has asymmetric error handling between the router path and the runner path. When `self._router is not None`, any exception from `self._router.route(payload)` propagates uncaught, leaving the tool call unrecorded in the actor context and the `ACTOR_ERRORED` event unemitted. This is a production-path bug since `ToolCallRouter` is used for provider format handling. ## Expected Behavior When `self._router.route(payload)` raises any exception: - The exception is caught and handled gracefully - A failed `ToolCallRecord` is recorded in the actor context - The `ACTOR_ERRORED` event is emitted - An error result is returned to the LLM (not a crash) ## Acceptance Criteria - [ ] Router path in `_execute_tool_call` has try/except matching the runner path - [ ] Exceptions from `self._router.route()` are caught and recorded as failed `ToolCallRecord` - [ ] `ACTOR_ERRORED` event is emitted when router raises - [ ] TDD test from #10383 passes - [ ] All existing tests continue to pass - [ ] Coverage >= 97% ## Subtasks - [ ] Add try/except around router path in `_execute_tool_call` - [ ] Ensure `elapsed_ms` is set in the except clause - [ ] Ensure `ACTOR_ERRORED` event is emitted on router failure - [ ] Verify TDD test from #10383 now passes - [ ] Run `nox` and verify all tests pass ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off - A Git commit is created with a descriptive message - The commit is pushed to a branch and submitted as a pull request to `master` - The TDD testing issue #10383 is resolved (its `@tdd_expected_fail` scenario now passes) --- ### Summary `ToolCallingRuntime._execute_tool_call()` has asymmetric error handling between the router path and the runner path. When `self._router is not None`, any exception from `self._router.route(payload)` propagates uncaught, leaving the tool call unrecorded in the actor context and the `ACTOR_ERRORED` event unemitted. ### Code Evidence In `src/cleveragents/tool/actor_runtime.py` (lines ~350-383): ```python if self._router is not None: # Route through ToolCallRouter for provider format handling payload: dict[str, Any] = { "name": tool_call.name, "args": enriched_inputs, "type": "tool_call", } routed_result = self._router.route(payload) # ← NO try/except! elapsed_ms = (time.monotonic() - start) * 1000.0 success = routed_result.result.success output = routed_result.result.output error = routed_result.result.error else: # Execute directly via runner, catching ToolError for not-found try: result = self._runner.execute(...) ... except ToolError as exc: elapsed_ms = (time.monotonic() - start) * 1000.0 success = False output = {} error = str(exc) except Exception as exc: elapsed_ms = (time.monotonic() - start) * 1000.0 success = False output = {} error = f"{type(exc).__name__}: {exc}" ``` The runner path (else branch) has proper try/except for both `ToolError` and `Exception`. The router path (if branch) has none. ### Impact When using `ToolCallRouter` (the production path for provider format handling): 1. Any exception from `self._router.route(payload)` propagates to `run_tool_loop` 2. No `ToolCallRecord` is created — the tool call history is incomplete 3. `ACTOR_ERRORED` event is not emitted — observability is broken 4. The tool-call loop crashes instead of gracefully handling the error 5. The LLM does not receive an error result to reason about ### Reproduction ```python from unittest.mock import MagicMock from cleveragents.tool.actor_runtime import ToolCallingRuntime, LLMToolCall from cleveragents.tool.actor_context import ToolActorContext router = MagicMock() router.route.side_effect = RuntimeError("Router failure") runtime = ToolCallingRuntime( registry=MagicMock(), runner=MagicMock(), llm_caller=MagicMock(), router=router, ) context = ToolActorContext(plan_id="test-plan", phase="execute") tool_call = LLMToolCall(name="my_tool", arguments={}) # This raises RuntimeError instead of returning an error result runtime._execute_tool_call(tool_call, context, iteration=1) ``` ### Fix Wrap the router path in try/except similar to the runner path: ```python if self._router is not None: payload = {"name": tool_call.name, "args": enriched_inputs, "type": "tool_call"} try: routed_result = self._router.route(payload) elapsed_ms = (time.monotonic() - start) * 1000.0 success = routed_result.result.success output = routed_result.result.output error = routed_result.result.error except Exception as exc: elapsed_ms = (time.monotonic() - start) * 1000.0 success = False output = {} error = f"{type(exc).__name__}: {exc}" ``` --- **Automated by CleverAgents Bot** Agent: new-issue-creator
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#10386
No description provided.