Skip to content

Commit 61cd788

Browse files
address comments
1 parent c3241c1 commit 61cd788

4 files changed

Lines changed: 96 additions & 9 deletions

File tree

‎sdks/python/src/agent_control/evaluation.py‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ._state import state
2121
from .client import AgentControlClient
2222
from .observability import add_event, get_logger, is_observability_enabled
23+
from .tracing import get_trace_and_span_ids
2324
from .validation import ensure_agent_name
2425

2526
_logger = get_logger(__name__)
@@ -291,6 +292,13 @@ async def check_evaluation_with_local(
291292
httpx.HTTPError: If server request fails
292293
"""
293294
normalized_name = ensure_agent_name(agent_name)
295+
resolved_trace_id = trace_id
296+
resolved_span_id = span_id
297+
if trace_id is None or span_id is None:
298+
current_trace_id, current_span_id = get_trace_and_span_ids()
299+
resolved_trace_id = trace_id or current_trace_id
300+
resolved_span_id = span_id or current_span_id
301+
294302
# Partition controls by local flag
295303
local_controls: list[_ControlAdapter] = []
296304
parse_errors: list[ControlMatch] = []
@@ -389,8 +397,8 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult:
389397
local_result,
390398
request,
391399
applicable_local_controls,
392-
trace_id,
393-
span_id,
400+
resolved_trace_id,
401+
resolved_span_id,
394402
agent_name=event_agent_name,
395403
)
396404

@@ -409,10 +417,10 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult:
409417
if _has_applicable_prefiltered_server_controls(server_control_payloads, request):
410418
request_payload = request.model_dump(mode="json", exclude_none=True)
411419
headers: dict[str, str] = {}
412-
if trace_id:
413-
headers["X-Trace-Id"] = trace_id
414-
if span_id:
415-
headers["X-Span-Id"] = span_id
420+
if resolved_trace_id:
421+
headers["X-Trace-Id"] = resolved_trace_id
422+
if resolved_span_id:
423+
headers["X-Span-Id"] = resolved_span_id
416424

417425
response = await client.http_client.post(
418426
"/api/v1/evaluation",

‎sdks/python/src/agent_control/telemetry/trace_context.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def get_trace_context_from_provider() -> TraceContext | None:
4040
span_id = trace_context.get("span_id")
4141
if not isinstance(trace_id, str) or not isinstance(span_id, str):
4242
return None
43+
if not trace_id or not span_id:
44+
return None
4345

4446
return {
4547
"trace_id": trace_id,

‎sdks/python/tests/test_observability_updates.py‎

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
_map_applies_to,
1111
_merge_results,
1212
)
13+
from agent_control.telemetry.trace_context import (
14+
clear_trace_context_provider,
15+
set_trace_context_provider,
16+
)
1317
from agent_control_models import ControlDefinition
1418

1519
# =============================================================================
@@ -326,6 +330,9 @@ def test_fallback_warning_logged_only_once(self):
326330
class TestCheckEvaluationWithLocal:
327331
"""Tests for check_evaluation_with_local event emission and non_matches."""
328332

333+
def teardown_method(self) -> None:
334+
clear_trace_context_provider()
335+
329336
@pytest.mark.asyncio
330337
async def test_emits_events_when_trace_context_provided(self):
331338
"""Should emit observability events when trace_id and span_id are passed."""
@@ -398,7 +405,7 @@ async def test_emits_events_when_trace_context_provided(self):
398405

399406
@pytest.mark.asyncio
400407
async def test_emits_events_without_trace_context(self):
401-
"""Should still emit events when trace_id/span_id not provided (fallback IDs)."""
408+
"""Should resolve trace context from the provider when IDs are omitted."""
402409
from agent_control_models import EvaluationResponse, Step
403410

404411
mock_response = EvaluationResponse(
@@ -424,6 +431,12 @@ async def test_emits_events_without_trace_context(self):
424431
client = MagicMock()
425432
client.http_client = AsyncMock()
426433
step = Step(type="llm", name="test-step", input="hello")
434+
set_trace_context_provider(
435+
lambda: {
436+
"trace_id": "a" * 32,
437+
"span_id": "b" * 16,
438+
}
439+
)
427440

428441
with patch("agent_control.evaluation.ControlEngine", return_value=mock_engine), \
429442
patch("agent_control.evaluation.list_evaluators", return_value=["regex"]), \
@@ -438,8 +451,8 @@ async def test_emits_events_without_trace_context(self):
438451
)
439452
mock_emit.assert_called_once()
440453
call_args = mock_emit.call_args
441-
assert call_args[0][3] is None # trace_id passed as None
442-
assert call_args[0][4] is None # span_id passed as None
454+
assert call_args[0][3] == "a" * 32
455+
assert call_args[0][4] == "b" * 16
443456

444457
@pytest.mark.asyncio
445458
async def test_forwards_trace_headers_to_server(self):
@@ -492,6 +505,59 @@ async def test_forwards_trace_headers_to_server(self):
492505
assert headers["X-Trace-Id"] == "aaaa1111bbbb2222cccc3333dddd4444"
493506
assert headers["X-Span-Id"] == "eeee5555ffff6666"
494507

508+
@pytest.mark.asyncio
509+
async def test_forwards_provider_trace_headers_to_server_when_ids_omitted(self):
510+
"""Server POST should resolve trace headers from the provider when omitted."""
511+
from agent_control_models import Step
512+
513+
controls = [{
514+
"id": 1,
515+
"name": "server-ctrl",
516+
"control": {
517+
"condition": {
518+
"evaluator": {"name": "regex", "config": {"pattern": "test"}},
519+
"selector": {"path": "input"},
520+
},
521+
"action": {"decision": "deny"},
522+
"execution": "server",
523+
},
524+
}]
525+
526+
mock_http_response = MagicMock()
527+
mock_http_response.json.return_value = {
528+
"is_safe": True,
529+
"confidence": 1.0,
530+
"matches": None,
531+
"errors": None,
532+
"non_matches": None,
533+
}
534+
mock_http_response.raise_for_status = MagicMock()
535+
536+
client = MagicMock()
537+
client.http_client = AsyncMock()
538+
client.http_client.post = AsyncMock(return_value=mock_http_response)
539+
step = Step(type="llm", name="test-step", input="hello")
540+
set_trace_context_provider(
541+
lambda: {
542+
"trace_id": "c" * 32,
543+
"span_id": "d" * 16,
544+
}
545+
)
546+
547+
with patch("agent_control.evaluation.list_evaluators", return_value=["regex"]):
548+
await evaluation.check_evaluation_with_local(
549+
client=client,
550+
agent_name="agent-000000000001",
551+
step=step,
552+
stage="pre",
553+
controls=controls,
554+
)
555+
556+
call_kwargs = client.http_client.post.call_args
557+
headers = call_kwargs.kwargs.get("headers", {})
558+
assert headers["X-Trace-Id"] == "c" * 32
559+
assert headers["X-Span-Id"] == "d" * 16
560+
495561

496562
# =============================================================================
497563
# control_decorators non_matches dict conversion

‎sdks/python/tests/test_trace_context.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,14 @@ def test_get_trace_context_from_provider_returns_none_for_invalid_shape() -> Non
5252
)
5353

5454
assert get_trace_context_from_provider() is None
55+
56+
57+
def test_get_trace_context_from_provider_returns_none_for_empty_ids() -> None:
58+
set_trace_context_provider(
59+
lambda: {
60+
"trace_id": "",
61+
"span_id": "",
62+
}
63+
)
64+
65+
assert get_trace_context_from_provider() is None

0 commit comments

Comments
 (0)