Skip to content

Commit 893cb76

Browse files
feat(tracing): integrate shared error ownership classifier
Configure Agentex-owned frames in the shared tracing classifier and forward its privacy-safe provenance without duplicating inference logic. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 55930f2 commit 893cb76

6 files changed

Lines changed: 147 additions & 39 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
from agentex.lib.core.tracing.trace import Trace, AsyncTrace
33
from agentex.lib.core.tracing.tracer import Tracer, AsyncTracer
44
from agentex.lib.core.tracing.span_error import (
5+
ERROR_CLASSIFIER_VERSION,
6+
AGENTEX_ERROR_CLASSIFIER_CONFIG,
57
ErrorCategory,
68
PlatformError,
79
ApplicationError,
810
CategorizedError,
11+
ExceptionMapping,
12+
ErrorClassification,
13+
ErrorClassifierConfig,
14+
TracebackOwnershipPolicy,
915
)
1016
from agentex.lib.core.tracing.span_queue import (
1117
AsyncSpanQueue,
@@ -23,6 +29,12 @@
2329
"ApplicationError",
2430
"PlatformError",
2531
"ErrorCategory",
32+
"ExceptionMapping",
33+
"ErrorClassification",
34+
"ErrorClassifierConfig",
35+
"TracebackOwnershipPolicy",
36+
"ERROR_CLASSIFIER_VERSION",
37+
"AGENTEX_ERROR_CLASSIFIER_CONFIG",
2638
"AsyncSpanQueue",
2739
"get_default_span_queue",
2840
"shutdown_default_span_queue",

src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan:
8888
if error is not None:
8989
sgp_span.set_error(error_type=error["type"], error_message=error["message"])
9090
sgp_span.metadata["error_category"] = error.get("category", "unknown")
91+
sgp_span.metadata["error_category_source"] = error.get("category_source", "legacy")
92+
sgp_span.metadata["error_classifier_version"] = error.get("classifier_version", "legacy")
93+
if "category_reason" in error:
94+
sgp_span.metadata["error_category_reason"] = error["category_reason"]
9195
return sgp_span
9296

9397

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

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
from __future__ import annotations
22

3+
import os
34
from typing import Any, cast
45

56
from scale_gp_beta.lib.tracing import (
7+
ERROR_CLASSIFIER_VERSION as ERROR_CLASSIFIER_VERSION,
68
PlatformError as PlatformError,
79
ApplicationError as ApplicationError,
8-
CategorizedError,
10+
CategorizedError as CategorizedError,
11+
ExceptionMapping as ExceptionMapping,
12+
ErrorClassification as ErrorClassification,
13+
ErrorClassifierConfig as ErrorClassifierConfig,
14+
TracebackOwnershipPolicy,
15+
classify_error,
916
)
1017
from scale_gp_beta.lib.tracing.types import ErrorCategory
1118

@@ -20,47 +27,47 @@
2027
# SGP and agentex-native span stores.
2128
SPAN_ERROR_KEY = "__error__"
2229

23-
ERROR_CATEGORY_UNKNOWN: ErrorCategory = "unknown"
24-
_ERROR_CATEGORIES = frozenset({"application", "platform", "unknown"})
25-
26-
27-
def _normalize_error_category(value: object) -> ErrorCategory | None:
28-
if isinstance(value, str):
29-
normalized = value.strip().lower()
30-
if normalized in _ERROR_CATEGORIES:
31-
return cast(ErrorCategory, normalized)
32-
return None
33-
34-
35-
def _error_category(
36-
exc: BaseException,
37-
explicit_category: ErrorCategory | str | None = None,
38-
) -> ErrorCategory:
39-
"""Return an explicit producer classification, defaulting safely to unknown."""
40-
return (
41-
_normalize_error_category(explicit_category)
42-
or (exc.error_category if isinstance(exc, CategorizedError) else None)
43-
or ERROR_CATEGORY_UNKNOWN
30+
_AGENTEX_PACKAGE_ROOT = os.path.normcase(os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
31+
AGENTEX_ERROR_CLASSIFIER_CONFIG = ErrorClassifierConfig(
32+
policy=TracebackOwnershipPolicy(
33+
platform_module_prefixes=("agentex",),
34+
platform_file_roots=(_AGENTEX_PACKAGE_ROOT,),
35+
infer_application_from_unowned_absolute_paths=True,
4436
)
37+
)
4538

4639

4740
def set_span_error(
4841
span: Span,
4942
exc: BaseException,
5043
*,
5144
error_category: ErrorCategory | str | None = None,
45+
boundary_category: ErrorCategory | None = None,
46+
mapping_scope: str | None = None,
47+
classifier_config: ErrorClassifierConfig = AGENTEX_ERROR_CLASSIFIER_CONFIG,
5248
) -> None:
5349
"""Record an exception on ``span`` under ``data[SPAN_ERROR_KEY]``.
5450
55-
An explicit ``error_category`` takes precedence over a ``CategorizedError``
56-
classification. Invalid or absent categories become unknown.
51+
The shared Scale GP classifier inspects the exception's existing traceback
52+
using Agentex's ownership policy. Explicit and typed categories remain
53+
authoritative; boundary hints and scoped mappings are fallback signals.
5754
No-op when ``span.data`` is a list (matching ``_add_source_to_span``, which
5855
only attaches metadata to dict-shaped data).
5956
"""
57+
classification = classify_error(
58+
exc,
59+
explicit_category=cast(ErrorCategory | None, error_category),
60+
boundary_category=boundary_category,
61+
mapping_scope=mapping_scope,
62+
config=classifier_config,
63+
)
6064
error = {
6165
"type": type(exc).__name__,
6266
"message": str(exc),
63-
"category": _error_category(exc, error_category),
67+
"category": classification.category,
68+
"category_source": classification.source,
69+
"classifier_version": classification.classifier_version,
70+
"category_reason": classification.reason,
6471
}
6572
if span.data is None:
6673
span.data = {}

tests/lib/adk/test_tracing_module.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from agentex.types.span import Span
1111
from agentex.lib.core.harness.types import TurnUsage
1212
from agentex.lib.adk._modules.tracing import TurnSpan, TracingModule
13-
from agentex.lib.core.tracing.span_error import get_span_error
13+
from agentex.lib.core.tracing.span_error import ERROR_CLASSIFIER_VERSION, get_span_error
1414
from agentex.lib.core.services.adk.tracing import TracingService
1515

1616

@@ -264,7 +264,10 @@ async def test_span_context_manager_records_and_reraises_body_error(self):
264264
assert get_span_error(started) == {
265265
"type": "RuntimeError",
266266
"message": "boom",
267-
"category": "unknown",
267+
"category": "application",
268+
"category_source": "stack_trace",
269+
"classifier_version": ERROR_CLASSIFIER_VERSION,
270+
"category_reason": "stack_rule:unowned_absolute_source",
268271
}
269272
mock_service.end_span.assert_called_once_with(trace_id="trace-123", span=started)
270273

tests/lib/core/tracing/test_span_error.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
import uuid
45
from typing import Any
56
from datetime import UTC, datetime
@@ -16,6 +17,7 @@
1617
from agentex.lib.core.tracing.trace import Trace, AsyncTrace
1718
from agentex.lib.core.tracing.span_error import (
1819
SPAN_ERROR_KEY,
20+
ERROR_CLASSIFIER_VERSION,
1921
PlatformError,
2022
ApplicationError,
2123
CategorizedError,
@@ -36,6 +38,12 @@ def _make_span(data=None) -> Span:
3638
)
3739

3840

41+
def _synthetic_function(module_name: str, filename: str, body: str, **values: Any) -> Any:
42+
namespace = {"__name__": module_name, **values}
43+
exec(compile(f"def run():\n {body}\n", filename, "exec"), namespace)
44+
return namespace["run"]
45+
46+
3947
# ---------------------------------------------------------------------------
4048
# Helpers: set_span_error / get_span_error
4149
# ---------------------------------------------------------------------------
@@ -54,13 +62,12 @@ def test_set_then_get_on_none_data(self):
5462
"type": "ValueError",
5563
"message": "boom",
5664
"category": "unknown",
65+
"category_source": "fallback",
66+
"classifier_version": ERROR_CLASSIFIER_VERSION,
67+
"category_reason": "stack_no_traceback",
5768
}
5869
assert isinstance(span.data, dict)
59-
assert span.data[SPAN_ERROR_KEY] == {
60-
"type": "ValueError",
61-
"message": "boom",
62-
"category": "unknown",
63-
}
70+
assert span.data[SPAN_ERROR_KEY] == get_span_error(span)
6471

6572
def test_set_uses_explicit_exception_category(self):
6673
span = _make_span(data=None)
@@ -69,12 +76,18 @@ def test_set_uses_explicit_exception_category(self):
6976
"type": "PlatformError",
7077
"message": "unavailable",
7178
"category": "platform",
79+
"category_source": "categorized_error",
80+
"classifier_version": ERROR_CLASSIFIER_VERSION,
81+
"category_reason": "canonical_categorized_error",
7282
}
7383

7484
def test_explicit_category_takes_precedence(self):
7585
span = _make_span(data=None)
7686
set_span_error(span, PlatformError("bad input"), error_category="application")
77-
assert get_span_error(span)["category"] == "application" # type: ignore[index]
87+
error = get_span_error(span)
88+
assert error is not None
89+
assert error["category"] == "application"
90+
assert error["category_source"] == "explicit"
7891

7992
def test_set_uses_application_error_category(self):
8093
span = _make_span(data=None)
@@ -89,6 +102,54 @@ class ImplicitlyCategorizedError(RuntimeError):
89102
set_span_error(span, ImplicitlyCategorizedError("boom"))
90103
assert get_span_error(span)["category"] == "unknown" # type: ignore[index]
91104

105+
def test_agentex_internal_origin_is_platform(self):
106+
platform = _synthetic_function(
107+
"agentex.lib.synthetic_runtime",
108+
"/synthetic/agentex/runtime.py",
109+
"raise RuntimeError('boom')",
110+
)
111+
span = _make_span()
112+
try:
113+
platform()
114+
except Exception as exc:
115+
set_span_error(span, exc)
116+
117+
error = get_span_error(span)
118+
assert error is not None
119+
assert error["category"] == "platform"
120+
assert error["category_source"] == "stack_trace"
121+
assert error["category_reason"] == "stack_rule:platform_module"
122+
123+
def test_stdlib_dependency_under_application_is_application(self):
124+
application = _synthetic_function(
125+
"customer_agent.main",
126+
"/synthetic/application/main.py",
127+
"parse('{')",
128+
parse=json.loads,
129+
)
130+
span = _make_span()
131+
try:
132+
application()
133+
except Exception as exc:
134+
set_span_error(span, exc)
135+
136+
assert get_span_error(span)["category"] == "application" # type: ignore[index]
137+
138+
def test_stdlib_dependency_under_agentex_is_platform(self):
139+
platform = _synthetic_function(
140+
"agentex.lib.synthetic_runtime",
141+
"/synthetic/agentex/runtime.py",
142+
"parse('{')",
143+
parse=json.loads,
144+
)
145+
span = _make_span()
146+
try:
147+
platform()
148+
except Exception as exc:
149+
set_span_error(span, exc)
150+
151+
assert get_span_error(span)["category"] == "platform" # type: ignore[index]
152+
92153
def test_set_preserves_existing_dict_keys(self):
93154
span = _make_span(data={"__span_type__": "LLM"})
94155
set_span_error(span, RuntimeError("nope"))
@@ -127,7 +188,10 @@ def test_sync_span_records_error_and_reraises(self):
127188
assert err == {
128189
"type": "ValueError",
129190
"message": "boom",
130-
"category": "unknown",
191+
"category": "application",
192+
"category_source": "stack_trace",
193+
"classifier_version": ERROR_CLASSIFIER_VERSION,
194+
"category_reason": "stack_rule:unowned_absolute_source",
131195
}
132196

133197
def test_sync_span_success_has_no_error(self):
@@ -148,7 +212,10 @@ async def test_async_span_records_error_and_reraises(self):
148212
assert err == {
149213
"type": "RuntimeError",
150214
"message": "kaboom",
151-
"category": "unknown",
215+
"category": "application",
216+
"category_source": "stack_trace",
217+
"classifier_version": ERROR_CLASSIFIER_VERSION,
218+
"category_reason": "stack_rule:unowned_absolute_source",
152219
}
153220

154221

@@ -193,6 +260,9 @@ def test_error_maps_to_status_error(self):
193260
"type": "ValueError",
194261
"message": "boom",
195262
"category": "application",
263+
"category_source": "stack_trace",
264+
"classifier_version": ERROR_CLASSIFIER_VERSION,
265+
"category_reason": "stack_rule:application_module",
196266
}
197267
}
198268
)
@@ -204,6 +274,9 @@ def test_error_maps_to_status_error(self):
204274
assert sgp_span.metadata["error_type"] == "ValueError"
205275
assert sgp_span.metadata["error_message"] == "boom"
206276
assert sgp_span.metadata["error_category"] == "application"
277+
assert sgp_span.metadata["error_category_source"] == "stack_trace"
278+
assert sgp_span.metadata["error_classifier_version"] == ERROR_CLASSIFIER_VERSION
279+
assert sgp_span.metadata["error_category_reason"] == "stack_rule:application_module"
207280

208281
def test_no_error_leaves_status_success(self):
209282
from agentex.lib.core.tracing.processors.sgp_tracing_processor import _build_sgp_span

tests/test_adk_tracing_span_error.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from agentex.types.span import Span
2525
from agentex.lib.adk._modules.tracing import TracingModule
26-
from agentex.lib.core.tracing.span_error import get_span_error
26+
from agentex.lib.core.tracing.span_error import ERROR_CLASSIFIER_VERSION, get_span_error
2727

2828

2929
def _make_module() -> tuple[TracingModule, Span, AsyncMock]:
@@ -51,7 +51,10 @@ async def test_span_records_error_and_reraises() -> None:
5151
assert error == {
5252
"type": "ValueError",
5353
"message": "boom",
54-
"category": "unknown",
54+
"category": "application",
55+
"category_source": "stack_trace",
56+
"classifier_version": ERROR_CLASSIFIER_VERSION,
57+
"category_reason": "stack_rule:unowned_absolute_source",
5558
}
5659

5760
# end_span still ran (finally) and saw the span with the error already set,
@@ -61,7 +64,10 @@ async def test_span_records_error_and_reraises() -> None:
6164
assert get_span_error(persisted_span) == {
6265
"type": "ValueError",
6366
"message": "boom",
64-
"category": "unknown",
67+
"category": "application",
68+
"category_source": "stack_trace",
69+
"classifier_version": ERROR_CLASSIFIER_VERSION,
70+
"category_reason": "stack_rule:unowned_absolute_source",
6571
}
6672

6773

@@ -115,6 +121,9 @@ async def test_turn_span_records_error_and_reraises() -> None:
115121
assert get_span_error(span) == {
116122
"type": "ValueError",
117123
"message": "boom",
118-
"category": "unknown",
124+
"category": "application",
125+
"category_source": "stack_trace",
126+
"classifier_version": ERROR_CLASSIFIER_VERSION,
127+
"category_reason": "stack_rule:unowned_absolute_source",
119128
}
120129
end_span.assert_awaited_once()

0 commit comments

Comments
 (0)