BUG-HUNT: [error-handling] Silent failure suppression in agent invocation hides critical errors #7230

Open
opened 2026-04-10 09:47:42 +00:00 by HAL9000 · 4 comments
Owner

Metadata

  • Branch: fix/error-handling-silent-failure-agent-invocation
  • Commit Message: fix(reactive): propagate agent invocation errors instead of silently suppressing failures
  • Milestone: (none — backlog, see note below)
  • Parent Epic: (orphan — see note below)

Backlog note: This issue was discovered during autonomous operation
on milestone v3.5.0. It does not block milestone completion and has been
placed in the backlog for human review and future milestone assignment.


Background and Context

The GraphExecutor._invoke_agent() static method in src/cleveragents/reactive/graph_executor.py is responsible for dispatching messages to registered agents during graph route execution. The method dispatches to one of three code paths depending on the agent type, then returns the result as a string.

The project's error handling standards (CONTRIBUTING.md — Error and Exception Handling) require:

  • No silent failures — raise exceptions or return explicit error types.
  • Fail-fast principles — check preconditions at function entry; let exceptions propagate.
  • Exception propagation — do not suppress errors; let exceptions propagate to top-level execution.

The specification's Exception propagation requirement is violated by the current implementation.

Current Behavior (Bug)

@staticmethod
def _invoke_agent(agent: Any, message: str, context: dict[str, Any]) -> str:
    """Execute an agent and return the string result."""
    if isinstance(agent, (SimpleToolAgent | SimpleLLMAgent)):
        result = agent.process(message, context=context)
    elif hasattr(agent, "process"):
        result = agent.process(message)
    elif callable(agent):
        result = agent(message)
    else:
        result = message
    return str(result) if result else ""

Location: src/cleveragents/reactive/graph_executor.py, GraphExecutor._invoke_agent(), lines 139–152

The final line return str(result) if result else "" silently converts any falsy result — including None, 0, False, and empty collections — to an empty string "". This means:

  1. An agent that returns None due to an internal error is indistinguishable from an agent that legitimately returns an empty response.
  2. An agent that returns False (a valid boolean result from a tool agent) is silently coerced to "".
  3. The calling code in execute() treats an empty string as a signal to return "" immediately (if not current_message: return ""), causing the entire graph execution to terminate silently.
  4. Similarly, _follow_chained_edges() falls back to the previous message (current_message = result if result else current_message) when the result is falsy, masking the failure entirely.

Severity: High — critical agent failures are hidden, preventing proper error diagnosis and violating the project's error handling standards.

Likelihood: Medium — occurs whenever an agent returns a falsy value (including None from an exception-swallowing agent, or a legitimate False/0 result).

Expected Behavior

Per the project's fail-fast principles and exception propagation requirements:

  1. _invoke_agent() should not silently convert falsy results to empty string. A None result should be treated as an error condition and either raise an exception or return an explicit sentinel that callers can distinguish from a legitimate empty response.
  2. If an agent raises an exception, it should propagate to the caller (or be caught with meaningful recovery logic, not suppressed).
  3. Argument validation should be added at method entry per the project's "validate all arguments in public/protected methods" requirement.

Evidence

# In execute() — silent termination on falsy result:
current_message = self._invoke_agent(agent, extracted_message, context)
if not current_message:
    return ""  # ← silent exit; no log, no error

# In _follow_chained_edges() — silent fallback on falsy result:
result = GraphExecutor._invoke_agent(next_agent, current_message, context)
current_message = result if result else current_message  # ← silent fallback

Suggested Fix

@staticmethod
def _invoke_agent(agent: Any, message: str, context: dict[str, Any]) -> str:
    """Execute an agent and return the string result.

    Raises:
        TypeError: If agent is not a recognised callable type.
        ValueError: If message is empty.
    """
    if not message:
        raise ValueError("message must not be empty")
    if isinstance(agent, (SimpleToolAgent | SimpleLLMAgent)):
        result = agent.process(message, context=context)
    elif hasattr(agent, "process"):
        result = agent.process(message)
    elif callable(agent):
        result = agent(message)
    else:
        raise TypeError(
            f"Agent of type {type(agent)!r} is not callable and has no 'process' method"
        )
    if result is None:
        raise RuntimeError(
            f"Agent {agent!r} returned None for message {message!r}; "
            "agent must return a non-None string result"
        )
    return str(result)

Callers (execute() and _follow_chained_edges()) should be updated to handle the propagated exceptions appropriately.


Subtasks

  • Audit _invoke_agent() and all call sites in graph_executor.py for silent failure patterns
  • Add argument validation (message, agent) at method entry per fail-fast principles
  • Replace return str(result) if result else "" with explicit None-check and exception raise
  • Update execute() to handle propagated exceptions (log + re-raise or structured error return)
  • Update _follow_chained_edges() to handle propagated exceptions consistently
  • Write BDD Gherkin scenarios covering: agent returns None, agent raises exception, agent returns empty string legitimately
  • Ensure all nox stages pass
  • Verify coverage ≥ 97%

Definition of Done

  • _invoke_agent() raises RuntimeError (or appropriate exception) when agent returns None
  • _invoke_agent() raises ValueError when message is empty
  • _invoke_agent() raises TypeError when agent is not callable and has no process method
  • Call sites in execute() and _follow_chained_edges() handle propagated exceptions with proper logging
  • No silent return "" on agent failure without at minimum a logger.error(...) call
  • BDD scenarios added covering all new error paths
  • All nox stages pass
  • Coverage >= 97%

Automated by CleverAgents Bot
Supervisor: Acting on behalf of: Bug Hunter | Agent: new-issue-creator

## Metadata - **Branch**: `fix/error-handling-silent-failure-agent-invocation` - **Commit Message**: `fix(reactive): propagate agent invocation errors instead of silently suppressing failures` - **Milestone**: *(none — backlog, see note below)* - **Parent Epic**: *(orphan — see note below)* > **Backlog note:** This issue was discovered during autonomous operation > on milestone v3.5.0. It does not block milestone completion and has been > placed in the backlog for human review and future milestone assignment. --- ## Background and Context The `GraphExecutor._invoke_agent()` static method in `src/cleveragents/reactive/graph_executor.py` is responsible for dispatching messages to registered agents during graph route execution. The method dispatches to one of three code paths depending on the agent type, then returns the result as a string. The project's error handling standards (CONTRIBUTING.md — *Error and Exception Handling*) require: - **No silent failures** — raise exceptions or return explicit error types. - **Fail-fast principles** — check preconditions at function entry; let exceptions propagate. - **Exception propagation** — do not suppress errors; let exceptions propagate to top-level execution. The specification's *Exception propagation* requirement is violated by the current implementation. ## Current Behavior (Bug) ```python @staticmethod def _invoke_agent(agent: Any, message: str, context: dict[str, Any]) -> str: """Execute an agent and return the string result.""" if isinstance(agent, (SimpleToolAgent | SimpleLLMAgent)): result = agent.process(message, context=context) elif hasattr(agent, "process"): result = agent.process(message) elif callable(agent): result = agent(message) else: result = message return str(result) if result else "" ``` **Location**: `src/cleveragents/reactive/graph_executor.py`, `GraphExecutor._invoke_agent()`, lines 139–152 The final line `return str(result) if result else ""` silently converts **any falsy result** — including `None`, `0`, `False`, and empty collections — to an empty string `""`. This means: 1. An agent that returns `None` due to an internal error is indistinguishable from an agent that legitimately returns an empty response. 2. An agent that returns `False` (a valid boolean result from a tool agent) is silently coerced to `""`. 3. The calling code in `execute()` treats an empty string as a signal to `return ""` immediately (`if not current_message: return ""`), causing the entire graph execution to terminate silently. 4. Similarly, `_follow_chained_edges()` falls back to the previous message (`current_message = result if result else current_message`) when the result is falsy, masking the failure entirely. **Severity**: High — critical agent failures are hidden, preventing proper error diagnosis and violating the project's error handling standards. **Likelihood**: Medium — occurs whenever an agent returns a falsy value (including `None` from an exception-swallowing agent, or a legitimate `False`/`0` result). ## Expected Behavior Per the project's fail-fast principles and exception propagation requirements: 1. `_invoke_agent()` should **not** silently convert falsy results to empty string. A `None` result should be treated as an error condition and either raise an exception or return an explicit sentinel that callers can distinguish from a legitimate empty response. 2. If an agent raises an exception, it should propagate to the caller (or be caught with meaningful recovery logic, not suppressed). 3. Argument validation should be added at method entry per the project's "validate all arguments in public/protected methods" requirement. ## Evidence ```python # In execute() — silent termination on falsy result: current_message = self._invoke_agent(agent, extracted_message, context) if not current_message: return "" # ← silent exit; no log, no error # In _follow_chained_edges() — silent fallback on falsy result: result = GraphExecutor._invoke_agent(next_agent, current_message, context) current_message = result if result else current_message # ← silent fallback ``` ## Suggested Fix ```python @staticmethod def _invoke_agent(agent: Any, message: str, context: dict[str, Any]) -> str: """Execute an agent and return the string result. Raises: TypeError: If agent is not a recognised callable type. ValueError: If message is empty. """ if not message: raise ValueError("message must not be empty") if isinstance(agent, (SimpleToolAgent | SimpleLLMAgent)): result = agent.process(message, context=context) elif hasattr(agent, "process"): result = agent.process(message) elif callable(agent): result = agent(message) else: raise TypeError( f"Agent of type {type(agent)!r} is not callable and has no 'process' method" ) if result is None: raise RuntimeError( f"Agent {agent!r} returned None for message {message!r}; " "agent must return a non-None string result" ) return str(result) ``` Callers (`execute()` and `_follow_chained_edges()`) should be updated to handle the propagated exceptions appropriately. --- ## Subtasks - [ ] Audit `_invoke_agent()` and all call sites in `graph_executor.py` for silent failure patterns - [ ] Add argument validation (`message`, `agent`) at method entry per fail-fast principles - [ ] Replace `return str(result) if result else ""` with explicit `None`-check and exception raise - [ ] Update `execute()` to handle propagated exceptions (log + re-raise or structured error return) - [ ] Update `_follow_chained_edges()` to handle propagated exceptions consistently - [ ] Write BDD Gherkin scenarios covering: agent returns `None`, agent raises exception, agent returns empty string legitimately - [ ] Ensure all nox stages pass - [ ] Verify coverage ≥ 97% ## Definition of Done - [ ] `_invoke_agent()` raises `RuntimeError` (or appropriate exception) when agent returns `None` - [ ] `_invoke_agent()` raises `ValueError` when `message` is empty - [ ] `_invoke_agent()` raises `TypeError` when agent is not callable and has no `process` method - [ ] Call sites in `execute()` and `_follow_chained_edges()` handle propagated exceptions with proper logging - [ ] No silent `return ""` on agent failure without at minimum a `logger.error(...)` call - [ ] BDD scenarios added covering all new error paths - [ ] All nox stages pass - [ ] Coverage >= 97% --- **Automated by CleverAgents Bot** Supervisor: Acting on behalf of: Bug Hunter | Agent: new-issue-creator
Author
Owner

⚠️ Orphan Issue — Needs Manual Parent Epic Linking

This issue was created during automated bug hunting of the src/cleveragents/reactive/ module and could not be automatically linked to a parent Epic.

Why no parent was found: An exhaustive search of all open issues with Type/Epic label did not find a specific Epic covering reactive module error handling or GraphExecutor correctness. The closest candidates are:

  • #5337 — Refactor reactive module to unify stream and graph execution models (Type/Refactor, not an Epic)
  • #5407 — EPIC: Testing Infrastructure Improvements (covers test infra, not reactive module bugs)

Action required: A project maintainer should link this issue to the appropriate parent Epic (or create one if none exists) using Forgejo's dependency system:

  • This issue should block the parent Epic (child blocks parent direction)

Automated by CleverAgents Bot
Supervisor: Acting on behalf of: Bug Hunter | Agent: new-issue-creator

## ⚠️ Orphan Issue — Needs Manual Parent Epic Linking This issue was created during automated bug hunting of the `src/cleveragents/reactive/` module and could not be automatically linked to a parent Epic. **Why no parent was found**: An exhaustive search of all open issues with `Type/Epic` label did not find a specific Epic covering reactive module error handling or `GraphExecutor` correctness. The closest candidates are: - **#5337** — Refactor reactive module to unify stream and graph execution models (`Type/Refactor`, not an Epic) - **#5407** — EPIC: Testing Infrastructure Improvements (covers test infra, not reactive module bugs) **Action required**: A project maintainer should link this issue to the appropriate parent Epic (or create one if none exists) using Forgejo's dependency system: - This issue should **block** the parent Epic (child blocks parent direction) --- **Automated by CleverAgents Bot** Supervisor: Acting on behalf of: Bug Hunter | Agent: new-issue-creator
Author
Owner

Verified — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: silent failure suppression in agent invocation hides critical errors. MoSCoW: Must-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
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#7230
No description provided.