chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC_ROOT = REPO_ROOT / "src"
|
||||
|
||||
|
||||
def _run_python(script: str) -> dict[str, object]:
|
||||
env = os.environ.copy()
|
||||
pythonpath = env.get("PYTHONPATH")
|
||||
if pythonpath:
|
||||
env["PYTHONPATH"] = f"{SRC_ROOT}:{pythonpath}"
|
||||
else:
|
||||
env["PYTHONPATH"] = str(SRC_ROOT)
|
||||
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
payload = json.loads(completed.stdout)
|
||||
if not isinstance(payload, dict):
|
||||
raise AssertionError("Subprocess payload must be a JSON object.")
|
||||
return cast(dict[str, object], payload)
|
||||
|
||||
|
||||
def test_import_agents_has_no_tracing_side_effects() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import gc
|
||||
import json
|
||||
import httpx
|
||||
|
||||
clients_before = sum(1 for obj in gc.get_objects() if isinstance(obj, httpx.Client))
|
||||
import agents # noqa: F401
|
||||
from agents.tracing import processors as tracing_processors
|
||||
from agents.tracing import setup as tracing_setup
|
||||
clients_after = sum(1 for obj in gc.get_objects() if isinstance(obj, httpx.Client))
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"client_delta": clients_after - clients_before,
|
||||
"provider_initialized": tracing_setup.GLOBAL_TRACE_PROVIDER is not None,
|
||||
"exporter_initialized": tracing_processors._global_exporter is not None,
|
||||
"processor_initialized": tracing_processors._global_processor is not None,
|
||||
"shutdown_handler_registered": tracing_setup._SHUTDOWN_HANDLER_REGISTERED,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["client_delta"] == 0
|
||||
assert payload["provider_initialized"] is False
|
||||
assert payload["exporter_initialized"] is False
|
||||
assert payload["processor_initialized"] is False
|
||||
assert payload["shutdown_handler_registered"] is False
|
||||
|
||||
|
||||
def test_import_agents_does_not_require_sqlite3() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import importlib.abc
|
||||
import json
|
||||
import sys
|
||||
|
||||
class BlockSqlite3(importlib.abc.MetaPathFinder):
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if fullname in {"sqlite3", "_sqlite3"}:
|
||||
raise ModuleNotFoundError(f"blocked optional backend module: {fullname}")
|
||||
return None
|
||||
|
||||
sys.meta_path.insert(0, BlockSqlite3())
|
||||
|
||||
import agents
|
||||
from agents import Agent, Runner
|
||||
from agents.memory import Session, SessionSettings
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"agent_name": Agent.__name__,
|
||||
"runner_name": Runner.__name__,
|
||||
"session_name": Session.__name__,
|
||||
"settings_name": SessionSettings.__name__,
|
||||
"sqlite3_loaded": "sqlite3" in sys.modules,
|
||||
"private_sqlite3_loaded": "_sqlite3" in sys.modules,
|
||||
"sqlite_session_loaded": "agents.memory.sqlite_session" in sys.modules,
|
||||
"sqlite_session_exported": "SQLiteSession" in agents.__all__,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["agent_name"] == "Agent"
|
||||
assert payload["runner_name"] == "Runner"
|
||||
assert payload["session_name"] == "Session"
|
||||
assert payload["settings_name"] == "SessionSettings"
|
||||
assert payload["sqlite3_loaded"] is False
|
||||
assert payload["private_sqlite3_loaded"] is False
|
||||
assert payload["sqlite_session_loaded"] is False
|
||||
assert payload["sqlite_session_exported"] is True
|
||||
|
||||
|
||||
def test_sqlite_session_top_level_export_is_lazy() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
import agents
|
||||
|
||||
loaded_after_import = "agents.memory.sqlite_session" in sys.modules
|
||||
|
||||
from agents import SQLiteSession
|
||||
|
||||
loaded_after_export = "agents.memory.sqlite_session" in sys.modules
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"sqlite_session_name": SQLiteSession.__name__,
|
||||
"loaded_after_import": loaded_after_import,
|
||||
"loaded_after_export": loaded_after_export,
|
||||
"sqlite3_loaded": "sqlite3" in sys.modules,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["sqlite_session_name"] == "SQLiteSession"
|
||||
assert payload["loaded_after_import"] is False
|
||||
assert payload["loaded_after_export"] is True
|
||||
assert payload["sqlite3_loaded"] is True
|
||||
|
||||
|
||||
def test_get_trace_provider_lazily_initializes_defaults() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import json
|
||||
|
||||
from agents.tracing import setup as tracing_setup
|
||||
from agents.tracing import processors as tracing_processors
|
||||
|
||||
provider_before = tracing_setup.GLOBAL_TRACE_PROVIDER
|
||||
exporter_before = tracing_processors._global_exporter
|
||||
processor_before = tracing_processors._global_processor
|
||||
shutdown_before = tracing_setup._SHUTDOWN_HANDLER_REGISTERED
|
||||
|
||||
provider = tracing_setup.get_trace_provider()
|
||||
|
||||
provider_after = tracing_setup.GLOBAL_TRACE_PROVIDER
|
||||
exporter_after = tracing_processors._global_exporter
|
||||
processor_after = tracing_processors._global_processor
|
||||
shutdown_after = tracing_setup._SHUTDOWN_HANDLER_REGISTERED
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"provider_before": provider_before is not None,
|
||||
"exporter_before": exporter_before is not None,
|
||||
"processor_before": processor_before is not None,
|
||||
"shutdown_before": shutdown_before,
|
||||
"provider_after": provider_after is not None,
|
||||
"exporter_after": exporter_after is not None,
|
||||
"processor_after": processor_after is not None,
|
||||
"shutdown_after": shutdown_after,
|
||||
"provider_matches_global": provider_after is provider,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["provider_before"] is False
|
||||
assert payload["exporter_before"] is False
|
||||
assert payload["processor_before"] is False
|
||||
assert payload["shutdown_before"] is False
|
||||
|
||||
assert payload["provider_after"] is True
|
||||
assert payload["exporter_after"] is True
|
||||
assert payload["processor_after"] is True
|
||||
assert payload["shutdown_after"] is True
|
||||
assert payload["provider_matches_global"] is True
|
||||
|
||||
|
||||
def test_get_trace_provider_bootstraps_once() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import json
|
||||
|
||||
from agents.tracing import processors as tracing_processors
|
||||
from agents.tracing import setup as tracing_setup
|
||||
|
||||
registrations = []
|
||||
|
||||
def fake_register(fn):
|
||||
registrations.append(fn)
|
||||
return fn
|
||||
|
||||
tracing_setup.atexit.register = fake_register
|
||||
tracing_setup.GLOBAL_TRACE_PROVIDER = None
|
||||
tracing_setup._SHUTDOWN_HANDLER_REGISTERED = False
|
||||
tracing_processors._global_exporter = None
|
||||
tracing_processors._global_processor = None
|
||||
|
||||
first = tracing_setup.get_trace_provider()
|
||||
second = tracing_setup.get_trace_provider()
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"same_provider": first is second,
|
||||
"shutdown_registration_count": sum(
|
||||
1
|
||||
for fn in registrations
|
||||
if getattr(fn, "__name__", "") == "_shutdown_global_trace_provider"
|
||||
),
|
||||
"provider_initialized": tracing_setup.GLOBAL_TRACE_PROVIDER is not None,
|
||||
"exporter_initialized": tracing_processors._global_exporter is not None,
|
||||
"processor_initialized": tracing_processors._global_processor is not None,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["same_provider"] is True
|
||||
assert payload["shutdown_registration_count"] == 1
|
||||
assert payload["provider_initialized"] is True
|
||||
assert payload["exporter_initialized"] is True
|
||||
assert payload["processor_initialized"] is True
|
||||
|
||||
|
||||
def test_set_trace_provider_skips_default_bootstrap() -> None:
|
||||
payload = _run_python(
|
||||
"""
|
||||
import json
|
||||
|
||||
from agents.tracing import processors as tracing_processors
|
||||
from agents.tracing import setup as tracing_setup
|
||||
from agents.tracing.provider import DefaultTraceProvider
|
||||
|
||||
registrations = []
|
||||
|
||||
def fake_register(fn):
|
||||
registrations.append(fn)
|
||||
return fn
|
||||
|
||||
tracing_setup.atexit.register = fake_register
|
||||
tracing_setup.GLOBAL_TRACE_PROVIDER = None
|
||||
tracing_setup._SHUTDOWN_HANDLER_REGISTERED = False
|
||||
tracing_processors._global_exporter = None
|
||||
tracing_processors._global_processor = None
|
||||
|
||||
custom_provider = DefaultTraceProvider()
|
||||
tracing_setup.set_trace_provider(custom_provider)
|
||||
retrieved_provider = tracing_setup.get_trace_provider()
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"custom_provider_returned": retrieved_provider is custom_provider,
|
||||
"shutdown_registration_count": sum(
|
||||
1
|
||||
for fn in registrations
|
||||
if getattr(fn, "__name__", "") == "_shutdown_global_trace_provider"
|
||||
),
|
||||
"exporter_initialized": tracing_processors._global_exporter is not None,
|
||||
"processor_initialized": tracing_processors._global_processor is not None,
|
||||
}
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
assert payload["custom_provider_returned"] is True
|
||||
assert payload["shutdown_registration_count"] == 1
|
||||
assert payload["exporter_initialized"] is False
|
||||
assert payload["processor_initialized"] is False
|
||||
@@ -0,0 +1,5 @@
|
||||
from agents.tracing import logger as tracing_logger
|
||||
|
||||
|
||||
def test_tracing_logger_is_configured() -> None:
|
||||
assert tracing_logger.logger.name == "openai.agents.tracing"
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from agents.tracing.processors import BackendSpanExporter
|
||||
from agents.tracing.spans import Span
|
||||
from agents.tracing.traces import Trace
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_processor_api_key(monkeypatch):
|
||||
# If the API key is not set, it should be None
|
||||
monkeypatch.delenv("OPENAI_API_KEY", None)
|
||||
processor = BackendSpanExporter()
|
||||
assert processor.api_key is None
|
||||
|
||||
# If we set it afterwards, it should be the new value
|
||||
processor.set_api_key("test_api_key")
|
||||
assert processor.api_key == "test_api_key"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_processor_api_key_from_env(monkeypatch):
|
||||
# If the API key is not set at creation time but set before access time, it should be the new
|
||||
# value
|
||||
monkeypatch.delenv("OPENAI_API_KEY", None)
|
||||
processor = BackendSpanExporter()
|
||||
|
||||
# If we set it afterwards, it should be the new value
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "foo_bar_123")
|
||||
assert processor.api_key == "foo_bar_123"
|
||||
|
||||
|
||||
def test_exporter_uses_item_api_keys(monkeypatch):
|
||||
class DummyItem:
|
||||
def __init__(self, key: str | None, payload: dict[str, str]):
|
||||
self.tracing_api_key = key
|
||||
self._payload = payload
|
||||
|
||||
def export(self) -> dict[str, str]:
|
||||
return self._payload
|
||||
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
def fake_post(*, url, headers, json):
|
||||
calls.append({"url": url, "headers": headers, "json": json})
|
||||
return SimpleNamespace(status_code=200, text="ok")
|
||||
|
||||
exporter = BackendSpanExporter()
|
||||
exporter.set_api_key("global-key")
|
||||
monkeypatch.setattr(exporter, "_client", SimpleNamespace(post=fake_post))
|
||||
|
||||
exporter.export(
|
||||
cast(
|
||||
list[Trace | Span[Any]],
|
||||
[
|
||||
DummyItem("key-a", {"id": "a"}),
|
||||
DummyItem(None, {"id": "b"}),
|
||||
DummyItem("key-b", {"id": "c"}),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert len(calls) == 3
|
||||
auth_by_first_item = {
|
||||
tuple(entry["id"] for entry in call["json"]["data"]): call["headers"]["Authorization"]
|
||||
for call in calls
|
||||
}
|
||||
assert ("a",) in auth_by_first_item
|
||||
assert ("b",) in auth_by_first_item
|
||||
assert ("c",) in auth_by_first_item
|
||||
assert auth_by_first_item[("a",)] == "Bearer key-a"
|
||||
assert auth_by_first_item[("c",)] == "Bearer key-b"
|
||||
assert auth_by_first_item[("b",)] == "Bearer global-key"
|
||||
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
from agents.tracing.processors import BackendSpanExporter
|
||||
|
||||
|
||||
def test_set_api_key_preserves_env_fallback(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Test that set_api_key doesn't break environment variable fallback."""
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "env-key")
|
||||
|
||||
exporter = BackendSpanExporter()
|
||||
|
||||
# Initially should use env var
|
||||
assert exporter.api_key == "env-key"
|
||||
|
||||
# Set explicit key
|
||||
exporter.set_api_key("explicit-key")
|
||||
assert exporter.api_key == "explicit-key"
|
||||
|
||||
# Clear explicit key and verify env fallback works
|
||||
exporter._api_key = None
|
||||
if "api_key" in exporter.__dict__:
|
||||
del exporter.__dict__["api_key"]
|
||||
assert exporter.api_key == "env-key"
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from agents.tracing import (
|
||||
processors as tracing_processors,
|
||||
provider as tracing_provider,
|
||||
setup as tracing_setup,
|
||||
)
|
||||
|
||||
|
||||
class _DummyProvider:
|
||||
def __init__(self) -> None:
|
||||
self.shutdown_calls = 0
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.shutdown_calls += 1
|
||||
|
||||
|
||||
class _DefaultProviderWithTimeout(tracing_provider.DefaultTraceProvider):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.shutdown_timeout: float | None = None
|
||||
|
||||
def shutdown(self, timeout: float | None = None) -> None:
|
||||
self.shutdown_timeout = timeout
|
||||
|
||||
|
||||
class _BootstrapProvider:
|
||||
def __init__(self) -> None:
|
||||
self.processors: list[Any] = []
|
||||
self.shutdown_calls = 0
|
||||
|
||||
def register_processor(self, processor: Any) -> None:
|
||||
self.processors.append(processor)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.shutdown_calls += 1
|
||||
|
||||
|
||||
def test_shutdown_global_trace_provider_calls_shutdown(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
provider = _DummyProvider()
|
||||
monkeypatch.setattr(tracing_setup, "GLOBAL_TRACE_PROVIDER", provider)
|
||||
|
||||
tracing_setup._shutdown_global_trace_provider()
|
||||
|
||||
assert provider.shutdown_calls == 1
|
||||
|
||||
|
||||
def test_shutdown_global_trace_provider_passes_timeout_to_default_provider(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
provider = _DefaultProviderWithTimeout()
|
||||
monkeypatch.setattr(tracing_setup, "GLOBAL_TRACE_PROVIDER", provider)
|
||||
|
||||
tracing_setup._shutdown_global_trace_provider()
|
||||
|
||||
assert provider.shutdown_timeout == tracing_setup._DEFAULT_SHUTDOWN_TIMEOUT
|
||||
|
||||
|
||||
def test_set_trace_provider_registers_shutdown_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
registrations: list[Any] = []
|
||||
|
||||
def fake_register(callback: Any) -> Any:
|
||||
registrations.append(callback)
|
||||
return callback
|
||||
|
||||
first = _DummyProvider()
|
||||
second = _DummyProvider()
|
||||
|
||||
monkeypatch.setattr(atexit, "register", fake_register)
|
||||
monkeypatch.setattr(tracing_setup, "GLOBAL_TRACE_PROVIDER", None)
|
||||
monkeypatch.setattr(tracing_setup, "_SHUTDOWN_HANDLER_REGISTERED", False)
|
||||
|
||||
tracing_setup.set_trace_provider(cast(Any, first))
|
||||
tracing_setup.set_trace_provider(cast(Any, second))
|
||||
|
||||
assert cast(Any, tracing_setup.GLOBAL_TRACE_PROVIDER) is second
|
||||
assert registrations == [tracing_setup._shutdown_global_trace_provider]
|
||||
|
||||
|
||||
def test_get_trace_provider_returns_existing_provider(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
provider = _DummyProvider()
|
||||
|
||||
def fail_register(_: Any) -> None:
|
||||
raise AssertionError("atexit.register should not be called for an existing provider.")
|
||||
|
||||
monkeypatch.setattr(atexit, "register", fail_register)
|
||||
monkeypatch.setattr(tracing_setup, "GLOBAL_TRACE_PROVIDER", provider)
|
||||
|
||||
assert cast(Any, tracing_setup.get_trace_provider()) is provider
|
||||
|
||||
|
||||
def test_get_trace_provider_bootstraps_provider_in_process(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
registrations: list[Any] = []
|
||||
default_processor = object()
|
||||
|
||||
def fake_register(callback: Any) -> Any:
|
||||
registrations.append(callback)
|
||||
return callback
|
||||
|
||||
monkeypatch.setattr(atexit, "register", fake_register)
|
||||
monkeypatch.setattr(tracing_setup, "GLOBAL_TRACE_PROVIDER", None)
|
||||
monkeypatch.setattr(tracing_setup, "_SHUTDOWN_HANDLER_REGISTERED", False)
|
||||
monkeypatch.setattr(tracing_processors, "default_processor", lambda: default_processor)
|
||||
monkeypatch.setattr(tracing_provider, "DefaultTraceProvider", _BootstrapProvider)
|
||||
|
||||
provider = tracing_setup.get_trace_provider()
|
||||
|
||||
assert isinstance(provider, _BootstrapProvider)
|
||||
assert provider.processors == [default_processor]
|
||||
assert tracing_setup.GLOBAL_TRACE_PROVIDER is provider
|
||||
assert registrations == [tracing_setup._shutdown_global_trace_provider]
|
||||
@@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
import agents.tracing.traces as trace_module
|
||||
from agents.tracing import TracingConfig, set_tracing_disabled, trace
|
||||
from agents.tracing.context import create_trace_for_run
|
||||
from agents.tracing.scope import Scope
|
||||
from agents.tracing.traces import (
|
||||
NoOpTrace,
|
||||
ReattachedTrace,
|
||||
TraceImpl,
|
||||
TraceState,
|
||||
_started_trace_ids,
|
||||
_started_trace_ids_lock,
|
||||
)
|
||||
|
||||
|
||||
def _new_trace_id() -> str:
|
||||
return f"trace_{uuid4().hex}"
|
||||
|
||||
|
||||
def _clear_started_trace_ids() -> None:
|
||||
with _started_trace_ids_lock:
|
||||
_started_trace_ids.clear()
|
||||
|
||||
|
||||
def _mark_trace_as_started(
|
||||
*,
|
||||
workflow_name: str = "workflow",
|
||||
group_id: str | None = "group-1",
|
||||
metadata: dict[str, str] | None = None,
|
||||
tracing_api_key: str | None = None,
|
||||
) -> TraceState:
|
||||
metadata = metadata or {"key": "value"}
|
||||
trace_id = _new_trace_id()
|
||||
Scope.set_current_trace(None)
|
||||
set_tracing_disabled(False)
|
||||
|
||||
original = trace(
|
||||
workflow_name=workflow_name,
|
||||
trace_id=trace_id,
|
||||
group_id=group_id,
|
||||
metadata=metadata,
|
||||
tracing={"api_key": tracing_api_key} if tracing_api_key is not None else None,
|
||||
)
|
||||
assert isinstance(original, TraceImpl)
|
||||
original.start()
|
||||
original.finish()
|
||||
|
||||
trace_state = TraceState.from_trace(original)
|
||||
assert trace_state is not None
|
||||
return trace_state
|
||||
|
||||
|
||||
def test_create_trace_for_run_reattaches_matching_started_trace() -> None:
|
||||
trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=trace_state.trace_id,
|
||||
group_id=trace_state.group_id,
|
||||
metadata=dict(trace_state.metadata or {}),
|
||||
tracing={"api_key": "trace-key"},
|
||||
disabled=False,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, ReattachedTrace)
|
||||
assert created.trace_id == trace_state.trace_id
|
||||
|
||||
|
||||
def test_create_trace_for_run_does_not_reattach_after_trace_state_reload() -> None:
|
||||
trace_state = _mark_trace_as_started()
|
||||
_clear_started_trace_ids()
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=trace_state.trace_id,
|
||||
group_id=trace_state.group_id,
|
||||
metadata=dict(trace_state.metadata or {}),
|
||||
tracing=None,
|
||||
disabled=False,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, TraceImpl)
|
||||
assert not isinstance(created, ReattachedTrace)
|
||||
|
||||
|
||||
def test_create_trace_for_run_does_not_reattach_when_trace_id_differs() -> None:
|
||||
trace_state = _mark_trace_as_started()
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=_new_trace_id(),
|
||||
group_id=trace_state.group_id,
|
||||
metadata=dict(trace_state.metadata or {}),
|
||||
tracing=None,
|
||||
disabled=False,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, TraceImpl)
|
||||
assert not isinstance(created, ReattachedTrace)
|
||||
|
||||
|
||||
def test_create_trace_for_run_reattaches_stripped_trace_key_with_matching_resume_key() -> None:
|
||||
trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
|
||||
stripped_trace_state = TraceState.from_json(trace_state.to_json())
|
||||
assert stripped_trace_state is not None
|
||||
assert stripped_trace_state.tracing_api_key is None
|
||||
assert stripped_trace_state.tracing_api_key_hash == trace_state.tracing_api_key_hash
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=stripped_trace_state.trace_id,
|
||||
group_id=stripped_trace_state.group_id,
|
||||
metadata=dict(stripped_trace_state.metadata or {}),
|
||||
tracing={"api_key": "trace-key"},
|
||||
disabled=False,
|
||||
trace_state=stripped_trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, ReattachedTrace)
|
||||
assert created.tracing_api_key == "trace-key"
|
||||
|
||||
|
||||
def test_create_trace_for_run_does_not_reattach_stripped_trace_key_with_mismatch() -> None:
|
||||
trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
|
||||
stripped_trace_state = TraceState.from_json(trace_state.to_json())
|
||||
assert stripped_trace_state is not None
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=stripped_trace_state.trace_id,
|
||||
group_id=stripped_trace_state.group_id,
|
||||
metadata=dict(stripped_trace_state.metadata or {}),
|
||||
tracing={"api_key": "other-trace-key"},
|
||||
disabled=False,
|
||||
trace_state=stripped_trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, TraceImpl)
|
||||
assert not isinstance(created, ReattachedTrace)
|
||||
|
||||
|
||||
def test_create_trace_for_run_does_not_reattach_when_settings_mismatch() -> None:
|
||||
trace_state = _mark_trace_as_started(tracing_api_key="trace-key")
|
||||
|
||||
mismatch_cases: list[tuple[str, str | None, dict[str, str], TracingConfig]] = [
|
||||
(
|
||||
"workflow-override",
|
||||
trace_state.group_id,
|
||||
dict(trace_state.metadata or {}),
|
||||
{"api_key": "trace-key"},
|
||||
),
|
||||
(
|
||||
"workflow",
|
||||
"group-override",
|
||||
dict(trace_state.metadata or {}),
|
||||
{"api_key": "trace-key"},
|
||||
),
|
||||
(
|
||||
"workflow",
|
||||
trace_state.group_id,
|
||||
{"key": "override"},
|
||||
{"api_key": "trace-key"},
|
||||
),
|
||||
(
|
||||
"workflow",
|
||||
trace_state.group_id,
|
||||
dict(trace_state.metadata or {}),
|
||||
{"api_key": "other-trace-key"},
|
||||
),
|
||||
]
|
||||
|
||||
for workflow_name, group_id, metadata, tracing in mismatch_cases:
|
||||
Scope.set_current_trace(None)
|
||||
created = create_trace_for_run(
|
||||
workflow_name=workflow_name,
|
||||
trace_id=trace_state.trace_id,
|
||||
group_id=group_id,
|
||||
metadata=metadata,
|
||||
tracing=tracing,
|
||||
disabled=False,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, TraceImpl)
|
||||
assert not isinstance(created, ReattachedTrace)
|
||||
|
||||
|
||||
def test_create_trace_for_run_respects_disabled_flag_for_resume() -> None:
|
||||
trace_state = _mark_trace_as_started()
|
||||
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=trace_state.trace_id,
|
||||
group_id=trace_state.group_id,
|
||||
metadata=dict(trace_state.metadata or {}),
|
||||
tracing=None,
|
||||
disabled=True,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert isinstance(created, NoOpTrace)
|
||||
|
||||
|
||||
def test_create_trace_for_run_uses_existing_current_trace() -> None:
|
||||
trace_state = _mark_trace_as_started()
|
||||
outer_trace = trace(workflow_name="outer", trace_id=_new_trace_id())
|
||||
assert isinstance(outer_trace, TraceImpl)
|
||||
|
||||
with outer_trace:
|
||||
created = create_trace_for_run(
|
||||
workflow_name="workflow",
|
||||
trace_id=trace_state.trace_id,
|
||||
group_id=trace_state.group_id,
|
||||
metadata=dict(trace_state.metadata or {}),
|
||||
tracing=None,
|
||||
disabled=False,
|
||||
trace_state=trace_state,
|
||||
reattach_resumed_trace=True,
|
||||
)
|
||||
|
||||
assert created is None
|
||||
|
||||
|
||||
def test_trace_logs_warning_when_current_trace_exists(
|
||||
caplog,
|
||||
) -> None:
|
||||
Scope.set_current_trace(None)
|
||||
outer_trace = trace(workflow_name="outer", trace_id=_new_trace_id())
|
||||
assert isinstance(outer_trace, TraceImpl)
|
||||
|
||||
with outer_trace:
|
||||
with caplog.at_level(logging.WARNING, logger="openai.agents"):
|
||||
inner_trace = trace(workflow_name="inner", trace_id=_new_trace_id())
|
||||
|
||||
assert isinstance(inner_trace, TraceImpl)
|
||||
assert "Trace already exists" in caplog.text
|
||||
|
||||
|
||||
def test_started_trace_id_cache_is_bounded(monkeypatch) -> None:
|
||||
_clear_started_trace_ids()
|
||||
monkeypatch.setattr(trace_module, "_MAX_STARTED_TRACE_IDS", 2)
|
||||
|
||||
first = _mark_trace_as_started(metadata={"key": "first"})
|
||||
second = _mark_trace_as_started(metadata={"key": "second"})
|
||||
third = _mark_trace_as_started(metadata={"key": "third"})
|
||||
|
||||
assert len(_started_trace_ids) == 2
|
||||
assert list(_started_trace_ids) == [second.trace_id, third.trace_id]
|
||||
assert first.trace_id not in _started_trace_ids
|
||||
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from agents.tracing.processor_interface import TracingProcessor
|
||||
from agents.tracing.scope import Scope
|
||||
from agents.tracing.spans import Span
|
||||
from agents.tracing.traces import NoOpTrace, Trace, TraceImpl, TraceState, reattach_trace
|
||||
|
||||
|
||||
class DummyProcessor(TracingProcessor):
|
||||
def __init__(self) -> None:
|
||||
self.started: list[str] = []
|
||||
self.ended: list[str] = []
|
||||
|
||||
def on_trace_start(self, trace: Trace) -> None:
|
||||
self.started.append(trace.trace_id)
|
||||
|
||||
def on_trace_end(self, trace: Trace) -> None:
|
||||
self.ended.append(trace.trace_id)
|
||||
|
||||
def on_span_start(self, span: Span[Any]) -> None:
|
||||
return None
|
||||
|
||||
def on_span_end(self, span: Span[Any]) -> None:
|
||||
return None
|
||||
|
||||
def shutdown(self) -> None:
|
||||
return None
|
||||
|
||||
def force_flush(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_no_op_trace_double_enter_logs_error(caplog) -> None:
|
||||
Scope.set_current_trace(None)
|
||||
trace = NoOpTrace()
|
||||
with caplog.at_level(logging.ERROR):
|
||||
trace.start()
|
||||
trace.__enter__()
|
||||
trace.__enter__() # Second entry should log missing context token error
|
||||
assert trace._started is True
|
||||
trace.__exit__(None, None, None)
|
||||
|
||||
|
||||
def test_trace_impl_lifecycle_sets_scope() -> None:
|
||||
Scope.set_current_trace(None)
|
||||
processor = DummyProcessor()
|
||||
trace = TraceImpl(
|
||||
name="test-trace",
|
||||
trace_id="trace-123",
|
||||
group_id="group-1",
|
||||
metadata={"k": "v"},
|
||||
processor=processor,
|
||||
)
|
||||
|
||||
assert Scope.get_current_trace() is None
|
||||
with trace as current:
|
||||
assert current.trace_id == "trace-123"
|
||||
assert Scope.get_current_trace() is trace
|
||||
assert processor.started == ["trace-123"]
|
||||
|
||||
assert processor.ended == ["trace-123"]
|
||||
assert Scope.get_current_trace() is None
|
||||
assert trace.export() == {
|
||||
"object": "trace",
|
||||
"id": "trace-123",
|
||||
"workflow_name": "test-trace",
|
||||
"group_id": "group-1",
|
||||
"metadata": {"k": "v"},
|
||||
}
|
||||
|
||||
|
||||
def test_trace_impl_double_start_and_finish_without_start(caplog) -> None:
|
||||
Scope.set_current_trace(None)
|
||||
processor = DummyProcessor()
|
||||
trace = TraceImpl(
|
||||
name="double-start",
|
||||
trace_id=None,
|
||||
group_id=None,
|
||||
metadata=None,
|
||||
processor=processor,
|
||||
)
|
||||
|
||||
trace.start()
|
||||
trace.start() # should no-op when already started
|
||||
trace.finish(reset_current=True)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
trace._started = True
|
||||
trace._prev_context_token = None
|
||||
trace.__enter__() # logs when started but no context token
|
||||
trace.finish(reset_current=True)
|
||||
|
||||
fresh = TraceImpl(
|
||||
name="finish-no-start",
|
||||
trace_id=None,
|
||||
group_id=None,
|
||||
metadata=None,
|
||||
processor=processor,
|
||||
)
|
||||
fresh.finish(reset_current=True) # should not raise when never started
|
||||
|
||||
|
||||
def test_reattached_trace_restores_scope_without_reemitting_processor_events() -> None:
|
||||
Scope.set_current_trace(None)
|
||||
processor = DummyProcessor()
|
||||
original = TraceImpl(
|
||||
name="test-trace",
|
||||
trace_id="trace-123",
|
||||
group_id="group-1",
|
||||
metadata={"k": "v"},
|
||||
processor=processor,
|
||||
)
|
||||
|
||||
with original:
|
||||
pass
|
||||
|
||||
restored = reattach_trace(cast(TraceState, TraceState.from_trace(original)))
|
||||
assert restored is not None
|
||||
|
||||
with restored as current:
|
||||
assert current.trace_id == "trace-123"
|
||||
assert Scope.get_current_trace() is restored
|
||||
|
||||
assert processor.started == ["trace-123"]
|
||||
assert processor.ended == ["trace-123"]
|
||||
assert Scope.get_current_trace() is None
|
||||
@@ -0,0 +1,112 @@
|
||||
import logging
|
||||
|
||||
from agents.tracing.provider import DefaultTraceProvider
|
||||
from agents.tracing.scope import Scope
|
||||
from agents.tracing.span_data import AgentSpanData
|
||||
from agents.tracing.spans import NoOpSpan, SpanImpl
|
||||
from agents.tracing.traces import NoOpTrace, TraceImpl
|
||||
|
||||
|
||||
def test_env_read_on_first_use(monkeypatch):
|
||||
"""Env flag set before first trace disables tracing."""
|
||||
monkeypatch.setenv("OPENAI_AGENTS_DISABLE_TRACING", "1")
|
||||
provider = DefaultTraceProvider()
|
||||
|
||||
trace = provider.create_trace("demo")
|
||||
|
||||
assert isinstance(trace, NoOpTrace)
|
||||
|
||||
|
||||
def test_env_cached_after_first_use(monkeypatch):
|
||||
"""Env flag is cached after the first trace and later env changes do not flip it."""
|
||||
monkeypatch.setenv("OPENAI_AGENTS_DISABLE_TRACING", "0")
|
||||
provider = DefaultTraceProvider()
|
||||
|
||||
first = provider.create_trace("first")
|
||||
assert isinstance(first, TraceImpl)
|
||||
|
||||
# Change env after first use; cached value should keep tracing enabled.
|
||||
monkeypatch.setenv("OPENAI_AGENTS_DISABLE_TRACING", "1")
|
||||
second = provider.create_trace("second")
|
||||
|
||||
assert isinstance(second, TraceImpl)
|
||||
|
||||
|
||||
def test_manual_override_after_cache(monkeypatch):
|
||||
"""Manual toggle still works after env value is cached."""
|
||||
monkeypatch.setenv("OPENAI_AGENTS_DISABLE_TRACING", "0")
|
||||
provider = DefaultTraceProvider()
|
||||
|
||||
provider.create_trace("warmup")
|
||||
provider.set_disabled(True)
|
||||
disabled = provider.create_trace("disabled")
|
||||
assert isinstance(disabled, NoOpTrace)
|
||||
|
||||
provider.set_disabled(False)
|
||||
enabled = provider.create_trace("enabled")
|
||||
assert isinstance(enabled, TraceImpl)
|
||||
|
||||
|
||||
def test_manual_override_env_disable(monkeypatch):
|
||||
"""Manual enable can override env disable flag."""
|
||||
monkeypatch.setenv("OPENAI_AGENTS_DISABLE_TRACING", "1")
|
||||
provider = DefaultTraceProvider()
|
||||
|
||||
env_disabled = provider.create_trace("env_disabled")
|
||||
assert isinstance(env_disabled, NoOpTrace)
|
||||
|
||||
provider.set_disabled(False)
|
||||
reenabled = provider.create_trace("reenabled")
|
||||
|
||||
assert isinstance(reenabled, TraceImpl)
|
||||
|
||||
|
||||
def test_missing_active_trace_logs_debug_for_noop_span(caplog):
|
||||
Scope.set_current_trace(None)
|
||||
Scope.set_current_span(None)
|
||||
provider = DefaultTraceProvider()
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger="openai.agents"):
|
||||
span = provider.create_span(AgentSpanData(name="missing-trace"))
|
||||
|
||||
assert isinstance(span, NoOpSpan)
|
||||
assert "No active trace" in caplog.text
|
||||
assert not [record for record in caplog.records if record.levelno >= logging.ERROR]
|
||||
|
||||
|
||||
def test_noop_span_id_returns_noop_span_with_active_trace():
|
||||
Scope.set_current_trace(None)
|
||||
Scope.set_current_span(None)
|
||||
provider = DefaultTraceProvider()
|
||||
trace = provider.create_trace("active", trace_id="trace_123")
|
||||
trace_token = Scope.set_current_trace(trace)
|
||||
try:
|
||||
span = provider.create_span(AgentSpanData(name="invalid"), span_id="no-op")
|
||||
finally:
|
||||
Scope.reset_current_trace(trace_token)
|
||||
|
||||
assert isinstance(span, NoOpSpan)
|
||||
|
||||
|
||||
def test_noop_current_span_id_does_not_become_parent_id():
|
||||
Scope.set_current_trace(None)
|
||||
Scope.set_current_span(None)
|
||||
provider = DefaultTraceProvider()
|
||||
trace = provider.create_trace("active", trace_id="trace_123")
|
||||
invalid_parent = SpanImpl(
|
||||
trace_id="trace_123",
|
||||
span_id="no-op",
|
||||
parent_id=None,
|
||||
processor=provider._multi_processor,
|
||||
span_data=AgentSpanData(name="invalid-parent"),
|
||||
tracing_api_key=None,
|
||||
)
|
||||
trace_token = Scope.set_current_trace(trace)
|
||||
span_token = Scope.set_current_span(invalid_parent)
|
||||
try:
|
||||
span = provider.create_span(AgentSpanData(name="child"))
|
||||
finally:
|
||||
Scope.reset_current_span(span_token)
|
||||
Scope.reset_current_trace(trace_token)
|
||||
|
||||
assert isinstance(span, NoOpSpan)
|
||||
Reference in New Issue
Block a user