Skip to content

Commit f24aef5

Browse files
fix: make error ownership classification explicit
Provide typed application and platform error classes with documented ownership boundaries instead of relying on arbitrary exception attributes. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d62ef7d commit f24aef5

3 files changed

Lines changed: 50 additions & 15 deletions

File tree

src/agentex/lib/core/tracing/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from agentex.types.span import Span
22
from agentex.lib.core.tracing.trace import Trace, AsyncTrace
33
from agentex.lib.core.tracing.tracer import Tracer, AsyncTracer
4+
from agentex.lib.core.tracing.span_error import (
5+
PlatformError,
6+
ApplicationError,
7+
CategorizedError,
8+
)
49
from agentex.lib.core.tracing.span_queue import (
510
AsyncSpanQueue,
611
get_default_span_queue,
@@ -13,6 +18,9 @@
1318
"Span",
1419
"Tracer",
1520
"AsyncTracer",
21+
"CategorizedError",
22+
"ApplicationError",
23+
"PlatformError",
1624
"AsyncSpanQueue",
1725
"get_default_span_queue",
1826
"shutdown_default_span_queue",

src/agentex/lib/core/tracing/span_error.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,31 @@
1818
_ERROR_CATEGORIES = frozenset({"application", "platform", "unknown"})
1919

2020

21+
class CategorizedError(Exception):
22+
"""Base class for failures with known operational ownership.
23+
24+
Use ``ApplicationError`` for failures owned by agent or caller code, such
25+
as business logic, user input, tools, or application configuration. Use
26+
``PlatformError`` only at a known Agentex/SGP-owned boundary, such as
27+
managed runtime, tracing, persistence, or platform networking. Leave
28+
unclassified failures as ordinary exceptions so they remain ``unknown``.
29+
"""
30+
31+
error_category: ErrorCategory = ERROR_CATEGORY_UNKNOWN
32+
33+
34+
class ApplicationError(CategorizedError):
35+
"""Failure owned by the agent application or its caller."""
36+
37+
error_category: ErrorCategory = "application"
38+
39+
40+
class PlatformError(CategorizedError):
41+
"""Failure owned by Agentex/SGP or a platform-managed dependency."""
42+
43+
error_category: ErrorCategory = "platform"
44+
45+
2146
def _normalize_error_category(value: object) -> ErrorCategory | None:
2247
if isinstance(value, str):
2348
normalized = value.strip().lower()
@@ -33,7 +58,7 @@ def _error_category(
3358
"""Return an explicit producer classification, defaulting safely to unknown."""
3459
return (
3560
_normalize_error_category(explicit_category)
36-
or _normalize_error_category(getattr(exc, "error_category", None))
61+
or (exc.error_category if isinstance(exc, CategorizedError) else None)
3762
or ERROR_CATEGORY_UNKNOWN
3863
)
3964

@@ -46,8 +71,8 @@ def set_span_error(
4671
) -> None:
4772
"""Record an exception on ``span`` under ``data[SPAN_ERROR_KEY]``.
4873
49-
An explicit ``error_category`` takes precedence over an exception's
50-
``error_category`` attribute. Invalid or absent categories become unknown.
74+
An explicit ``error_category`` takes precedence over a ``CategorizedError``
75+
classification. Invalid or absent categories become unknown.
5176
No-op when ``span.data`` is a list (matching ``_add_source_to_span``, which
5277
only attaches metadata to dict-shaped data).
5378
"""

tests/lib/core/tracing/test_span_error.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from agentex.lib.core.tracing.trace import Trace, AsyncTrace
1212
from agentex.lib.core.tracing.span_error import (
1313
SPAN_ERROR_KEY,
14+
PlatformError,
15+
ApplicationError,
1416
get_span_error,
1517
set_span_error,
1618
)
@@ -50,30 +52,30 @@ def test_set_then_get_on_none_data(self):
5052
}
5153

5254
def test_set_uses_explicit_exception_category(self):
53-
class PlatformFailure(RuntimeError):
54-
error_category = " PLATFORM "
55-
5655
span = _make_span(data=None)
57-
set_span_error(span, PlatformFailure("unavailable"))
56+
set_span_error(span, PlatformError("unavailable"))
5857
assert get_span_error(span) == {
59-
"type": "PlatformFailure",
58+
"type": "PlatformError",
6059
"message": "unavailable",
6160
"category": "platform",
6261
}
6362

6463
def test_explicit_category_takes_precedence(self):
65-
class PlatformFailure(RuntimeError):
66-
error_category = "platform"
64+
span = _make_span(data=None)
65+
set_span_error(span, PlatformError("bad input"), error_category="application")
66+
assert get_span_error(span)["category"] == "application" # type: ignore[index]
6767

68+
def test_set_uses_application_error_category(self):
6869
span = _make_span(data=None)
69-
set_span_error(span, PlatformFailure("bad input"), error_category="application")
70+
set_span_error(span, ApplicationError("bad input"))
7071
assert get_span_error(span)["category"] == "application" # type: ignore[index]
7172

72-
def test_set_rejects_invalid_exception_category(self):
73-
exc = RuntimeError("boom")
74-
exc.error_category = "infrastructure" # type: ignore[attr-defined]
73+
def test_bare_exception_attribute_does_not_opt_in(self):
74+
class ImplicitlyCategorizedError(RuntimeError):
75+
error_category = "platform"
76+
7577
span = _make_span(data=None)
76-
set_span_error(span, exc)
78+
set_span_error(span, ImplicitlyCategorizedError("boom"))
7779
assert get_span_error(span)["category"] == "unknown" # type: ignore[index]
7880

7981
def test_set_preserves_existing_dict_keys(self):

0 commit comments

Comments
 (0)