Skip to content

Commit 6122e82

Browse files
NiteshDhanpalclaude
andcommitted
feat(tracing): auto-apply sgp-obs @business_trace to ACP handlers
Every agent's message/send and task/create handler is now wrapped with sgp-obs `business_trace(business_trace_id=lambda p: p.task.id)` at registration, so an agent gets the full business<->obs correlation edge — the turn anchor plus the process-local correlation context the openai-agents bridge and auto-egress read for their reverse tag — with NO per-agent code. Proven manually on analyst-agent (trace 9ad202d0b0bbdc0a78d8e5719c830811); this bakes it into the base server so no agent has to decorate. The decorator preserves the handler's shape (sync / async / async-generator), so streaming message/send handlers keep streaming. Fail-open + version-tolerant: `_with_business_trace` no-ops when the installed sgp-obs predates `business_trace` (defensive import, same stance as init_tracing), so this is inert until the sgp-obs pin is bumped to the release that ships it (0.5.0) — no code change needed here at that point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0f72af4 commit 6122e82

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

src/agentex/lib/sdk/fastacp/base/base_acp_server.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,33 @@ async def _process_request(
360360
- on_message_send
361361
"""
362362

363+
@staticmethod
364+
def _with_business_trace(fn: Callable[..., Any], id_getter: Callable[[Any], Any]) -> Callable[..., Any]:
365+
"""Auto-apply sgp-obs ``@business_trace`` so every agent handler gets the
366+
business<->obs correlation edge with NO per-agent code: it opens the turn
367+
anchor and binds the process-local correlation context that the
368+
openai-agents bridge and auto-egress read for their reverse tag. The
369+
decorator preserves the handler's shape (sync / async / async-generator),
370+
so streaming ``message/send`` handlers keep streaming.
371+
372+
Fail-open + version-tolerant: a no-op (returns ``fn`` unchanged) when the
373+
installed sgp-obs predates ``business_trace`` — the same defensive stance
374+
as the ``init_tracing`` wiring above, so bumping sgp-obs turns it on with
375+
no code change here.
376+
"""
377+
try:
378+
from sgp_obs.traces import business_trace
379+
except Exception:
380+
return fn
381+
try:
382+
return business_trace(business_trace_id=id_getter)(fn)
383+
except Exception: # pragma: no cover - correlation must never break registration
384+
return fn
385+
363386
# Type: Async
364387
def on_task_create(self, fn: Callable[[CreateTaskParams], Awaitable[Any]]):
365388
"""Handle task/init method"""
389+
fn = self._with_business_trace(fn, lambda p: p.task.id)
366390
wrapped = self._wrap_handler(fn)
367391
self._handlers[RPCMethod.TASK_CREATE] = wrapped
368392
return fn
@@ -417,6 +441,9 @@ def on_message_send(
417441
For non-streaming: return a single TaskMessage
418442
For streaming: return an AsyncGenerator that yields TaskMessageUpdate objects
419443
"""
444+
# One annotation, applied for the agent: correlate this turn end-to-end.
445+
# Preserves the handler's async-generator shape so streaming still streams.
446+
fn = self._with_business_trace(fn, lambda p: p.task.id)
420447

421448
async def message_send_wrapper(params: SendMessageParams):
422449
"""Special wrapper for message_send that handles both regular async functions and async generators"""
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""BaseACPServer auto-applies sgp-obs @business_trace to handlers, fail-open and
2+
version-tolerant (no-op when the installed sgp-obs predates business_trace)."""
3+
4+
import sys
5+
import types
6+
7+
from agentex.lib.sdk.fastacp.base.base_acp_server import BaseACPServer
8+
9+
10+
def test_noop_when_sgp_obs_lacks_business_trace(monkeypatch):
11+
# sgp-obs too old (no business_trace) -> import fails -> handler returned unchanged
12+
monkeypatch.setitem(sys.modules, "sgp_obs.traces", None)
13+
14+
def handler(params):
15+
return "ok"
16+
17+
assert BaseACPServer._with_business_trace(handler, lambda p: p.task.id) is handler
18+
19+
20+
def test_applies_business_trace_and_passes_id_getter(monkeypatch):
21+
calls: dict[str, object] = {}
22+
23+
fake = types.ModuleType("sgp_obs.traces")
24+
25+
def business_trace(*, business_trace_id):
26+
calls["getter"] = business_trace_id
27+
28+
def deco(fn):
29+
def wrapped(*a, **k):
30+
return ("wrapped", fn(*a, **k))
31+
32+
return wrapped
33+
34+
return deco
35+
36+
fake.business_trace = business_trace # type: ignore[attr-defined]
37+
monkeypatch.setitem(sys.modules, "sgp_obs.traces", fake)
38+
39+
def handler(params):
40+
return "ok"
41+
42+
wrapped = BaseACPServer._with_business_trace(handler, lambda p: p.task.id)
43+
assert wrapped is not handler
44+
assert wrapped(None) == ("wrapped", "ok")
45+
# the id resolver threads through to business_trace
46+
params = types.SimpleNamespace(task=types.SimpleNamespace(id="T-9"))
47+
assert calls["getter"](params) == "T-9"
48+
49+
50+
def test_fail_open_when_decorator_raises(monkeypatch):
51+
fake = types.ModuleType("sgp_obs.traces")
52+
53+
def business_trace(*, business_trace_id):
54+
raise RuntimeError("boom")
55+
56+
fake.business_trace = business_trace # type: ignore[attr-defined]
57+
monkeypatch.setitem(sys.modules, "sgp_obs.traces", fake)
58+
59+
def handler(params):
60+
return "ok"
61+
62+
# a decorator that blows up must not break handler registration
63+
assert BaseACPServer._with_business_trace(handler, lambda p: p.task.id) is handler

0 commit comments

Comments
 (0)