Invert default exception handling: stop swallowing by default; catch only recoverable exceptions and propagate the rest #78
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cleveragents/cleveractors-core#78
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Description
The library's default exception-handling posture is inverted. Across the runtime, the prevailing pattern is a broad
except Exceptionthat catches everything and neutralizes it — logging and continuing, converting the error into a string that is injected into graph state as if it were a valid response, or returning an{"error": ...}dict that a downstream caller silently discards. Swallowing is the default; propagation is the exception.This is backwards. The correct posture is the opposite:
Catching should be an explicit, whitelisted decision per recovery path — not a blanket catch-all that masks bugs, network failures, budget breaches, and programming errors alike.
Current behavior (evidence)
There are ~101 broad
except Exception/ bare-exceptsites insrc/. The problematic ones don't just log-and-reraise — they catch-all and neutralize. Representative swallow-and-continue sites:langgraph/nodes.py:409(Node._execute_agent) — catches all exceptions and doesagent_response = f"Error processing message: {str(e)}", injecting the error text into graph state as a legitimate agent response.langgraph/nodes.py:231(Node.execute) — catch-all returns{"error": str(e), "failed_node": self.name}, which_execute_from_nodethen silently drops (falls back to the original input message).langgraph/pure_graph.py:988, and further broad catches at531,1265,1273,1608,1899,2058,2374,2380.langgraph/bridge.py—235,256,389,410,516,648.agents/tool.py—244,493,537,577,896,907;agents/llm.py—714,1308,1466,1587,1748;agents/base.py:88,agents/factory.py:294,agents/llm_client.py:102.The codebase already acknowledges this as an anti-pattern — the module docstring of
_safe_node_token_intinlanggraph/nodes.pyexplicitly notes that an escaping error would "be caught byNode.execute()'s broadexcept Exception, and silently discard the LLM response." The defensive helper exists precisely to route around the catch-all, which is a symptom of the inverted default rather than a fix for it.Root cause
The default is "catch everything, keep going." A genuine error (a bug, an unreachable provider, a budget breach, a malformed config) is indistinguishable at the catch site from a recoverable, expected condition, so all of them are absorbed. Failures become invisible: the graph proceeds with the original input or an error-string masquerading as output.
Proposed fix (policy)
Invert the default to an allowlist model:
core/exceptions.py(CleverAgentsException→ConfigurationError,ExecutionError(kind, reason),RoutingError,TemplateError,ApplicationError,MissingUsageMetadataError,StreamRoutingError, …), so narrowing is feasible today with no new infrastructure.# pylint: disable=broad-exception-caughtat many sites; flip the posture by keepingbroad-exception-caughtenabled and requiring per-site justification, so new catch-alls are caught in review.Definition of Done
except Exception/ bare-exceptinsrc/is either (a) narrowed to a specific recoverable type, or (b) retained at a justified boundary that logs and re-raises/surfaces a typed error — none convert an exception into success-shaped output.{"error": ...}dict into graph state / response content in place of propagating a typed error.broad-exception-caughtis enforced by lint; remaining broad catches carry an inline justification.Relationship to other issues
nodes.py:409→nodes.py:231→pure_graph.py:988) is exactly the pattern this ticket generalizes. #71's fix should conform to the policy defined here.fix(runtime): … proper error propagation) — fixed specificruntime.py/_execute_llmsites (wrong error type, no cleanup). This ticket extends the same principle across the whole runtime as a default posture rather than one-off fixes.Non-conflict with specs / ADRs
This does not contradict any spec or ADR:
ExecutionError(kind="timeout")to propagate once retries are exhausted — the current swallow behavior actively violates its intent.core/exceptions.pytaxonomy is the intended vehicle for typed propagation; this ticket makes the runtime use it as designed.Proposed Branch
fix/error-handling-allowlist-no-default-swallowProposed Commit Title