From 0fa93b6d254788a2213708873d46225d6f5132e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:56:58 +0000 Subject: [PATCH 1/6] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index ee6f43e93..e45c32e4f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-7acaeb315af90255109ae17afc71e32a8e5851bb8a956a2a284cb4d344dfab51.yml -openapi_spec_hash: 3044e94b48d60311b6048e8df88e7552 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-7074b9156acbeefa63e9ca2173e9c22768268e894a48f511ec902fdcff043407.yml +openapi_spec_hash: 400e8dc4ce4d49db45e2943f67fe255a config_hash: 593e89b291976a5e84e4c3c3f8324354 From 76252a98f28663e8c95777456d07e42171592c62 Mon Sep 17 00:00:00 2001 From: Cynthia Wang Date: Mon, 31 Aug 2026 11:08:39 -0400 Subject: [PATCH 2/6] feat(tracing): add opt-in commit SHA stamping for SGP spans (#505) Co-authored-by: Claude Opus 5 --- src/agentex/lib/adk/__init__.py | 4 + src/agentex/lib/core/tracing/code_revision.py | 105 +++++++++++++++++ .../processors/sgp_tracing_processor.py | 28 ++++- src/agentex/lib/environment_variables.py | 7 ++ .../processors/test_sgp_tracing_processor.py | 60 ++++++++++ tests/lib/core/tracing/test_code_revision.py | 109 ++++++++++++++++++ 6 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/agentex/lib/core/tracing/code_revision.py create mode 100644 tests/lib/core/tracing/test_code_revision.py diff --git a/src/agentex/lib/adk/__init__.py b/src/agentex/lib/adk/__init__.py index d5be0ac52..c05f8f3ea 100644 --- a/src/agentex/lib/adk/__init__.py +++ b/src/agentex/lib/adk/__init__.py @@ -31,6 +31,9 @@ # Data-source refs for lineage (SGP-6513); implementation lives in core.tracing from agentex.lib.core.tracing import lineage + +# Opt-in commit-SHA stamping (AGX1-969); implementation in core.tracing +from agentex.lib.core.tracing import code_revision from agentex.lib.core.tracing.lineage import DataSourceRef, data_sources # Unified harness surface (AGX1-375) @@ -73,6 +76,7 @@ "TurnSpan", # Lineage data-source refs (SGP-6513) "lineage", + "code_revision", "DataSourceRef", "data_sources", # Checkpointing / LangGraph diff --git a/src/agentex/lib/core/tracing/code_revision.py b/src/agentex/lib/core/tracing/code_revision.py new file mode 100644 index 000000000..7b08dd45f --- /dev/null +++ b/src/agentex/lib/core/tracing/code_revision.py @@ -0,0 +1,105 @@ +"""Opt-in stamping of the agent's source commit onto its spans. + +Nothing is stamped until the agent calls :func:`enable`, mirroring the +``lineage`` registry next door: a process-wide switch the agent sets once at +import, rather than automatic behaviour every agent inherits. When enabled the +resolved commit lands in span data under ``__commit_sha__`` and is searchable in +the SGP Traces UI as ``__commit_sha__:``. + +This is deliberately separate from ``__agent_version__``, which is automatic and +carries the deployed image tag verbatim ("image tag or git sha"). That tag is a +real commit on some build paths but an ``-`` composite (AWS +ECR), ``latest``, or a hand-passed tag on others -- so a field named for a commit +must not simply mirror it. Values that are not git object names are refused, and +a field named ``__commit_sha__`` therefore only ever holds one. +""" + +from __future__ import annotations + +import os +import re + +from agentex.lib.utils.logging import make_logger + +__all__ = ("COMMIT_SHA_KEY", "enable", "disable", "is_enabled", "commit_sha") + +logger = make_logger(__name__) + +COMMIT_SHA_KEY = "__commit_sha__" + +# A git object name: 40 hex for SHA-1, 64 for SHA-256, or an abbreviation down to +# git's own 7-character minimum. +_GIT_SHA_RE = re.compile(r"[0-9a-fA-F]{7,64}") + +_COMMIT_SHA_ENV = "AGENT_COMMIT_SHA" +# Fallback only: automatic, and only usable when it happens to be SHA-shaped. +_AGENT_VERSION_ENV = "AGENT_VERSION" + +# Resolved once at enable() rather than per span: the value is fixed for the +# life of the process, and resolving eagerly means a bad value is reported at +# startup instead of silently producing unstamped spans. +_commit_sha: str | None = None + + +def enable(commit_sha: str | None = None) -> None: + """Opt this process in to stamping ``__commit_sha__`` onto every span. + + Value precedence: the explicit ``commit_sha`` argument, else + ``AGENT_COMMIT_SHA``, else ``AGENT_VERSION`` when the deployment happened to + set it to a bare commit SHA. A value that is not a git object name is + refused with a warning and leaves stamping off -- better an absent field + than one named for a commit that holds an image tag. + """ + global _commit_sha + + for value, source in ( + (commit_sha, "the commit_sha argument"), + (os.environ.get(_COMMIT_SHA_ENV), _COMMIT_SHA_ENV), + (os.environ.get(_AGENT_VERSION_ENV), _AGENT_VERSION_ENV), + ): + candidate = (value or "").strip() + if not candidate: + continue + if _GIT_SHA_RE.fullmatch(candidate): + _commit_sha = candidate + logger.info("code revision stamping enabled from %s", source) + return + # An explicit argument or AGENT_COMMIT_SHA is a direct statement of + # intent, so a bad value there is worth surfacing. AGENT_VERSION is only + # a fallback and is expected to be a non-SHA tag much of the time, so + # falling through it quietly is correct, not a silent failure. + if source != _AGENT_VERSION_ENV: + logger.warning( + "%s=%r is not a git commit SHA; __commit_sha__ will not be stamped.", + source, + candidate, + ) + _commit_sha = None + return + + _commit_sha = None + logger.warning( + "code revision stamping was enabled but no commit SHA was found " + "(checked the commit_sha argument, %s, and %s); __commit_sha__ will not " + "be stamped. Set %s in the agent's environment -- e.g. bake it at build " + "time with a Dockerfile ARG/ENV.", + _COMMIT_SHA_ENV, + _AGENT_VERSION_ENV, + _COMMIT_SHA_ENV, + ) + + +def disable() -> None: + """Turn stamping back off (also used for test isolation).""" + global _commit_sha + _commit_sha = None + + +def is_enabled() -> bool: + """Whether a commit SHA resolved and will be stamped.""" + return _commit_sha is not None + + +def commit_sha() -> str | None: + """The resolved commit SHA, or ``None`` when stamping is not enabled.""" + return _commit_sha diff --git a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py index a1c0edca2..9ee269231 100644 --- a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py +++ b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py @@ -3,7 +3,7 @@ import os import asyncio import weakref -from typing import cast, override +from typing import Any, cast, override import scale_gp_beta.lib.tracing as tracing from scale_gp_beta import SGPClient, AsyncSGPClient @@ -11,6 +11,7 @@ from scale_gp_beta.lib.tracing.span import Span as SGPSpan from agentex.types.span import Span +from agentex.lib.core.tracing import code_revision from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.lib.core.observability import tracing_metrics_recording as _metrics @@ -69,6 +70,29 @@ def _add_source_to_span(span: Span, env_vars: EnvironmentVariables) -> None: span.data["__agent_version__"] = env_vars.AGENT_VERSION +def _sgp_metadata(span: Span) -> Any: + """Metadata for the SGP write: ``span.data`` plus the opt-in commit SHA. + + Returns a COPY rather than mutating ``span``. ``trace.py`` hands the same + Span instance to every registered processor, so anything written onto + ``span.data`` here would also be serialized by the Agentex processor and + show up in caller-visible span data. ``__commit_sha__`` is opt-in and + SGP-scoped, so it must not leak that way. + + (The ``__source__`` / ``__agent_*`` keys set by ``_add_source_to_span`` do + leak like that today. Left as-is: changing five long-shipped fields is not + this change's business.) + """ + commit_sha = code_revision.commit_sha() + if commit_sha is None: + return span.data + if isinstance(span.data, dict): + return {**span.data, code_revision.COMMIT_SHA_KEY: commit_sha} + # List-shaped data is an accepted `data` shape and has nowhere to put a + # metadata key; leave it untouched rather than dropping the caller's data. + return span.data + + def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan: """Build an SGPSpan from an agentex Span. Idempotent on span_id at the SGP backend.""" _add_source_to_span(span, env_vars) @@ -82,7 +106,7 @@ def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan: trace_id=span.trace_id, input=span.input, output=span.output, - metadata=span.data, + metadata=_sgp_metadata(span), ), ) sgp_span.start_time = span.start_time.isoformat() # type: ignore[union-attr] diff --git a/src/agentex/lib/environment_variables.py b/src/agentex/lib/environment_variables.py index 7d893e462..00dbbaada 100644 --- a/src/agentex/lib/environment_variables.py +++ b/src/agentex/lib/environment_variables.py @@ -25,6 +25,7 @@ class EnvVarKeys(str, Enum): AGENT_DESCRIPTION = "AGENT_DESCRIPTION" AGENT_ID = "AGENT_ID" AGENT_VERSION = "AGENT_VERSION" + AGENT_COMMIT_SHA = "AGENT_COMMIT_SHA" AGENT_API_KEY = "AGENT_API_KEY" # ACP Configuration ACP_URL = "ACP_URL" @@ -67,6 +68,12 @@ class EnvironmentVariables(BaseModel): AGENT_ID: str | None = None # Build/version discriminator (image tag or git sha), set by the deployment AGENT_VERSION: str | None = None + # The agent's source commit, baked into the image or set by the deployment. + # Unlike AGENT_VERSION this is expected to be a git SHA and nothing else, and + # it is OPT-IN: nothing is stamped unless the agent calls + # `adk.code_revision.enable()`, which also refuses a value that is not a git + # object name. See agentex.lib.core.tracing.code_revision. + AGENT_COMMIT_SHA: str | None = None AGENT_API_KEY: str | None = None ACP_TYPE: str | None = "async" AGENT_INPUT_TYPE: str | None = None diff --git a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py index 4a233fb72..6cd324f01 100644 --- a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py +++ b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py @@ -54,6 +54,66 @@ def test_agent_identity_and_version_stamped_into_span_data(self): "__agent_version__": "sha-abc123", } + SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + + def test_commit_sha_is_not_stamped_without_opt_in(self, monkeypatch): + """Upgrading the SDK must not start emitting __commit_sha__ on its own, + even when the environment carries a perfectly good SHA.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.disable() + + span = _make_span(); span.data = {} + assert "__commit_sha__" not in (_sgp_metadata(span) or {}) + + def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch): + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.enable() + try: + span = _make_span(); span.data = {"caller": "kept"} + metadata = _sgp_metadata(span) + assert metadata["__commit_sha__"] == self.SHA + assert metadata["caller"] == "kept" + finally: + code_revision.disable() + + def test_commit_sha_does_not_leak_onto_the_shared_span(self, monkeypatch): + """trace.py hands ONE Span to every processor. If the commit SHA were + written onto span.data, a co-registered Agentex processor would + serialize it too, and it would surface in caller-visible span data.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + from agentex.lib.core.tracing.processors.agentex_tracing_processor import _create_kwargs + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.enable() + try: + span = _make_span(); span.data = {} + assert _sgp_metadata(span)["__commit_sha__"] == self.SHA # SGP sees it + assert "__commit_sha__" not in span.data # the span does not + assert "__commit_sha__" not in (_create_kwargs(span)["data"] or {}) + finally: + code_revision.disable() + + def test_list_shaped_data_is_left_alone(self, monkeypatch): + """`data` may be a list of dicts; there is nowhere to put a metadata key, + and dropping the caller's data would be worse than omitting the field.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.enable() + try: + span = _make_span(); span.data = [{"a": 1}] + assert _sgp_metadata(span) == [{"a": 1}] + finally: + code_revision.disable() + def test_unset_identity_fields_are_omitted(self): from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span diff --git a/tests/lib/core/tracing/test_code_revision.py b/tests/lib/core/tracing/test_code_revision.py new file mode 100644 index 000000000..0b89b88f2 --- /dev/null +++ b/tests/lib/core/tracing/test_code_revision.py @@ -0,0 +1,109 @@ +"""Opt-in commit-SHA stamping. + +The contract that matters: an agent that does not call ``enable()`` gets nothing, +so upgrading the SDK never starts emitting this field on its own. +""" + +from __future__ import annotations + +import pytest + +from agentex.lib.core.tracing import code_revision + +SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + + +@pytest.fixture(autouse=True) +def _reset(): + """State is process-wide (like the lineage registry), so isolate each test.""" + code_revision.disable() + yield + code_revision.disable() + + +class TestOptIn: + def test_disabled_by_default(self, monkeypatch): + """Even with the env fully populated, nothing resolves until enable().""" + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + monkeypatch.setenv("AGENT_VERSION", SHA) + assert code_revision.commit_sha() is None + assert code_revision.is_enabled() is False + + def test_enable_reads_agent_commit_sha(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable() + assert code_revision.commit_sha() == SHA + assert code_revision.is_enabled() is True + + def test_explicit_argument_wins(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable("7f3a91c2") + assert code_revision.commit_sha() == "7f3a91c2" + + def test_disable_turns_it_back_off(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable() + code_revision.disable() + assert code_revision.commit_sha() is None + + +class TestValueIsAlwaysACommit: + """A field named for a commit must never hold an image tag.""" + + @pytest.mark.parametrize( + "value", + [ + "latest", + "v1.2.3", + "0.2.4-v4", + "rocket_mock_agent-b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d", # AWS ECR composite + "abc", # shorter than git's 7-char minimum + "z" * 40, # right length, not hex + ], + ) + def test_non_sha_is_refused(self, monkeypatch, value): + monkeypatch.setenv("AGENT_COMMIT_SHA", value) + code_revision.enable() + assert code_revision.commit_sha() is None + + @pytest.mark.parametrize("value", [SHA, SHA.upper(), "b362b17", "a" * 64]) + def test_git_object_names_are_accepted(self, monkeypatch, value): + monkeypatch.setenv("AGENT_COMMIT_SHA", value) + code_revision.enable() + assert code_revision.commit_sha() == value + + def test_whitespace_only_is_refused(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", " ") + code_revision.enable() + assert code_revision.commit_sha() is None + + def test_enable_with_nothing_available_is_a_no_op(self, monkeypatch): + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.delenv("AGENT_VERSION", raising=False) + code_revision.enable() + assert code_revision.commit_sha() is None + + +class TestAgentVersionFallback: + def test_falls_back_to_agent_version_when_sha_shaped(self, monkeypatch): + """A platform deploy already sets AGENT_VERSION; on GCP/Azure it is a + bare SHA, so an opting-in agent needs no extra plumbing.""" + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.setenv("AGENT_VERSION", SHA) + code_revision.enable() + assert code_revision.commit_sha() == SHA + + def test_does_not_fall_back_to_a_non_sha_agent_version(self, monkeypatch): + """AGENT_VERSION is 'latest' or an AWS composite much of the time.""" + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.setenv("AGENT_VERSION", "latest") + code_revision.enable() + assert code_revision.commit_sha() is None + + def test_bad_explicit_value_does_not_fall_through(self, monkeypatch): + """An explicit AGENT_COMMIT_SHA is a statement of intent: if it is wrong, + say so rather than silently substituting the image tag.""" + monkeypatch.setenv("AGENT_COMMIT_SHA", "not-a-sha") + monkeypatch.setenv("AGENT_VERSION", SHA) + code_revision.enable() + assert code_revision.commit_sha() is None From f394ce7f1dfd0088eb63e7cdf64ff37307990706 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:15:08 +0000 Subject: [PATCH 3/6] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e45c32e4f..955f7e2ac 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 75 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-7074b9156acbeefa63e9ca2173e9c22768268e894a48f511ec902fdcff043407.yml -openapi_spec_hash: 400e8dc4ce4d49db45e2943f67fe255a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-ee0c521f0612c31b874bd595b90cd9209545bab603983552a1a7a87f38ed931e.yml +openapi_spec_hash: 917a1ffe9e353bed2740524dec786ed2 config_hash: 593e89b291976a5e84e4c3c3f8324354 From 0db6037e63ca1b24b80ae0d38883f7687ae5b9e5 Mon Sep 17 00:00:00 2001 From: Rishav Chakravarti Date: Wed, 9 Sep 2026 10:13:03 -0400 Subject: [PATCH 4/6] fix: keep agent output streaming alive on an unreadable line, and honor LOG_LEVEL (#509) --- src/agentex/lib/cli/debug/debug_handlers.py | 3 + src/agentex/lib/cli/handlers/run_handlers.py | 60 ++++++- src/agentex/lib/cli/utils/cli_utils.py | 12 ++ src/agentex/lib/utils/logging.py | 21 ++- tests/lib/cli/test_run_handlers_streaming.py | 180 +++++++++++++++++++ tests/lib/utils/test_logging_level.py | 66 +++++++ 6 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 tests/lib/cli/test_run_handlers_streaming.py create mode 100644 tests/lib/utils/test_logging_level.py diff --git a/src/agentex/lib/cli/debug/debug_handlers.py b/src/agentex/lib/cli/debug/debug_handlers.py index 98746387f..a27d682cd 100644 --- a/src/agentex/lib/cli/debug/debug_handlers.py +++ b/src/agentex/lib/cli/debug/debug_handlers.py @@ -16,6 +16,7 @@ pass from agentex.lib.utils.logging import make_logger +from agentex.lib.cli.utils.cli_utils import SUBPROCESS_STREAM_LIMIT from .debug_config import DebugConfig, resolve_debug_port @@ -66,6 +67,7 @@ async def start_temporal_worker_debug( env=debug_env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, + limit=SUBPROCESS_STREAM_LIMIT, ) @@ -119,6 +121,7 @@ async def start_acp_server_debug( env=debug_env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, + limit=SUBPROCESS_STREAM_LIMIT, ) diff --git a/src/agentex/lib/cli/handlers/run_handlers.py b/src/agentex/lib/cli/handlers/run_handlers.py index 3a43e95dd..18ee84e93 100644 --- a/src/agentex/lib/cli/handlers/run_handlers.py +++ b/src/agentex/lib/cli/handlers/run_handlers.py @@ -12,6 +12,7 @@ from agentex.lib.cli.debug import DebugConfig, start_acp_server_debug, start_temporal_worker_debug from agentex.lib.utils.logging import make_logger from agentex.config.agent_manifest import AgentManifest +from agentex.lib.cli.utils.cli_utils import SUBPROCESS_STREAM_LIMIT from agentex.lib.cli.utils.path_utils import ( get_file_paths, calculate_uvicorn_target_for_local, @@ -23,6 +24,11 @@ logger = make_logger(__name__) console = Console() +# How many consecutive unreadable lines to skip before giving up on the stream. +# Skipping is only known-safe for the limit-overrun case; this bounds the damage +# if some other error repeats without consuming anything. +MAX_CONSECUTIVE_READ_ERRORS = 100 + class RunError(Exception): """An error occurred during agent run""" @@ -215,6 +221,7 @@ async def start_acp_server( env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, + limit=SUBPROCESS_STREAM_LIMIT, ) @@ -234,23 +241,68 @@ async def start_temporal_worker( env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, + limit=SUBPROCESS_STREAM_LIMIT, ) async def stream_process_output(process: asyncio.subprocess.Process, prefix: str): - """Stream process output with prefix""" + """Stream process output with prefix. + + This loop is the only reader of the child's stdout pipe. If it ever stops + reading, the pipe fills and the child blocks forever inside ``write()``, + which presents as a silent freeze: 0% CPU, no further logs, no traceback. + So a single unreadable line must never end the loop. + """ try: if process.stdout is None: return + consecutive_read_errors = 0 while True: - line = await process.stdout.readline() + try: + line = await process.stdout.readline() + except ValueError as e: + # readline() raises ValueError when a line exceeds the stream limit. + # In *that* case it has already discarded the line and resumed the + # transport, so skipping it makes guaranteed progress. Any other + # ValueError carries no such guarantee, and retrying it forever would + # spin without draining. We cannot tell the two apart (readline + # flattens LimitOverrunError into a bare ValueError), so bound the + # retries and let the outer handler report the hang risk. + consecutive_read_errors += 1 + if consecutive_read_errors > MAX_CONSECUTIVE_READ_ERRORS: + raise + logger.warning( + f"Skipping an unreadable line from {prefix}: {e!r} " + f"(consecutive failure {consecutive_read_errors}/{MAX_CONSECUTIVE_READ_ERRORS}). " + f"If this says the chunk exceeded the limit, raise limit= on this " + f"process's create_subprocess_exec." + ) + continue + + consecutive_read_errors = 0 + if not line: break - decoded_line = line.decode("utf-8").rstrip() + + try: + decoded_line = line.decode("utf-8").rstrip() + except UnicodeDecodeError as e: + logger.warning(f"Dropped an undecodable log line from {prefix} ({e}).") + continue + if decoded_line: # Only print non-empty lines console.print(f"[dim]{prefix}:[/dim] {decoded_line}") except Exception as e: - logger.debug(f"Output streaming ended for {prefix}: {e}") + # The escalation path, including for the re-raise above. Anything reaching + # here ends the loop, so the child is now at risk of blocking on a full pipe. + # Warning rather than debug: this used to be a debug() that make_logger could + # never emit, which is why three freezes produced no clue. + # CancelledError derives from BaseException, so the auto-reload path that + # cancels these tasks passes straight through and is unaffected. + logger.warning( + f"Output streaming for {prefix} stopped on {e!r}. " + f"Nothing is draining its stdout now, so {prefix} will hang once the pipe fills." + ) async def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None): diff --git a/src/agentex/lib/cli/utils/cli_utils.py b/src/agentex/lib/cli/utils/cli_utils.py index 43b3fba62..4238e8fd9 100644 --- a/src/agentex/lib/cli/utils/cli_utils.py +++ b/src/agentex/lib/cli/utils/cli_utils.py @@ -5,6 +5,18 @@ console = Console() +# asyncio's StreamReader defaults to 64 KiB, and a single log line above that makes +# readline() raise. Agents legitimately emit large lines (serialized charts, payloads +# echoed back by validation errors), so give the reader room before it has to drop one. +# +# Lives here rather than beside its users so that both the normal spawns in +# cli/handlers/run_handlers.py and the debug spawns in cli/debug/debug_handlers.py can +# import it: run_handlers imports cli.debug, so the constant cannot live in either one. +# Keep the two in step. A subprocess left on the asyncio default overruns far more +# easily, and enough consecutive overruns exhaust the reader's retry bound and stop it +# draining, which is the deadlock the bound is there to avoid. +SUBPROCESS_STREAM_LIMIT = 8 * 1024 * 1024 + def handle_questionary_cancellation( result: str | None, operation: str = "operation" diff --git a/src/agentex/lib/utils/logging.py b/src/agentex/lib/utils/logging.py index 5bbaf61ac..a0d39331b 100644 --- a/src/agentex/lib/utils/logging.py +++ b/src/agentex/lib/utils/logging.py @@ -11,6 +11,25 @@ ctx_var_request_id = contextvars.ContextVar[str]("request_id") +DEFAULT_LOG_LEVEL = logging.INFO + + +def resolve_log_level() -> int: + """Read the log level from ``LOG_LEVEL``, falling back to INFO. + + Read straight from the environment rather than through ``EnvVarKeys``, since + ``environment_variables`` imports this module and the reverse would be a cycle. + + ``getLevelName`` returns the string ``"Level FOO"`` for anything it does not + recognise, so the isinstance check is what stops a typo in ``LOG_LEVEL`` from + silently turning logging off. + """ + configured = os.getenv("LOG_LEVEL") + if not configured: + return DEFAULT_LOG_LEVEL + level = logging.getLevelName(configured.strip().upper()) + return level if isinstance(level, int) else DEFAULT_LOG_LEVEL + class CustomJSONFormatter(json_log_formatter.JSONFormatter): def json_record(self, message: str, extra: dict, record: logging.LogRecord) -> dict: # type: ignore[override] @@ -51,7 +70,7 @@ def make_logger(name: str) -> logging.Logger: """ # Create a console object to print colored text logger = logging.getLogger(name) - logger.setLevel(logging.INFO) + logger.setLevel(resolve_log_level()) environment = os.getenv("ENVIRONMENT") if environment == "local": diff --git a/tests/lib/cli/test_run_handlers_streaming.py b/tests/lib/cli/test_run_handlers_streaming.py new file mode 100644 index 000000000..8f0ab13b5 --- /dev/null +++ b/tests/lib/cli/test_run_handlers_streaming.py @@ -0,0 +1,180 @@ +"""Tests for run_handlers output streaming. + +stream_process_output is the only reader of a child's stdout pipe. If it stops +reading, the pipe fills and the child blocks forever inside write(), which +presents as a silent freeze with no traceback. These tests pin the behaviour +that prevents that: a line the reader cannot handle is skipped, not fatal. +""" + +from __future__ import annotations + +import sys +import asyncio +from typing import Any + +import pytest + +from agentex.lib.cli.debug import DebugMode, DebugConfig +from agentex.lib.cli.handlers import run_handlers +from agentex.lib.cli.debug.debug_handlers import ( + start_acp_server_debug, + start_temporal_worker_debug, +) +from agentex.lib.cli.handlers.run_handlers import ( + SUBPROCESS_STREAM_LIMIT, + start_acp_server, + start_temporal_worker, + stream_process_output, +) + +# Emits a line of MARKER over the reader's limit, then enough further output to +# more than fill a 64 KiB pipe. If the reader stops draining, the child cannot +# finish its writes and never exits. +MARKER = "X" + +CHILD_SCRIPT = """ +print("before") +print("{marker}" * {oversized}) +for i in range(2000): + print("after", i, "y" * 60) +print("done") +""" + + +async def _drain(limit: int, oversized: int) -> int | None: + """Run the child under stream_process_output. None means it never exited.""" + process = await asyncio.create_subprocess_exec( + sys.executable, + "-c", + CHILD_SCRIPT.format(marker=MARKER, oversized=oversized), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + limit=limit, + ) + streamer = asyncio.create_task(stream_process_output(process, "TEST")) + try: + await asyncio.wait_for(asyncio.gather(streamer, process.wait()), timeout=60) + except TimeoutError: + process.kill() + await process.wait() + return None + return process.returncode + + +async def test_oversized_line_is_skipped_without_stalling_the_child( + capsys: pytest.CaptureFixture[str], +) -> None: + """A line past the reader's limit is dropped, and streaming continues. + + Before this was handled per line, readline() raised, the loop exited, and the + child deadlocked on a full pipe. The child reaching exit is the assertion. + """ + limit = 64 * 1024 + oversized = limit + 16_000 + + returncode = await _drain(limit=limit, oversized=oversized) + out = capsys.readouterr().out + + assert returncode == 0, "child did not exit: the reader stopped draining its pipe" + # The offending line is gone, but everything after it still streamed. + assert out.count(MARKER) == 0 + assert "done" in out + + +async def test_large_line_within_the_limit_is_streamed_in_full( + capsys: pytest.CaptureFixture[str], +) -> None: + """A line over asyncio's 64 KiB default still reaches the console under our limit. + + Counts marker characters rather than matching the line, because rich wraps + long output across terminal-width lines. + """ + oversized = 82_000 + + returncode = await _drain(limit=SUBPROCESS_STREAM_LIMIT, oversized=oversized) + out = capsys.readouterr().out + + assert returncode == 0 + assert out.count(MARKER) == oversized, "the large line was dropped rather than streamed" + + +class _AlwaysFailingReader: + """A reader whose readline() raises without consuming anything. + + The dangerous shape: skipping it makes no progress, so an unbounded retry + would spin at 100% CPU while still not draining the pipe. + """ + + def __init__(self) -> None: + self.attempts = 0 + + async def readline(self) -> bytes: + self.attempts += 1 + raise ValueError("unreadable, and nothing was consumed") + + +class _FakeProcess: + def __init__(self, stdout: Any) -> None: + self.stdout = stdout + + +async def test_repeated_unreadable_lines_give_up_instead_of_spinning() -> None: + """A ValueError that consumes nothing must not loop forever.""" + reader = _AlwaysFailingReader() + + await asyncio.wait_for( + stream_process_output(_FakeProcess(reader), "TEST"), timeout=30 + ) + + assert reader.attempts == run_handlers.MAX_CONSECUTIVE_READ_ERRORS + 1 + + +async def test_cancellation_is_not_swallowed() -> None: + """The auto-reload path cancels these tasks, so cancel must propagate. + + CancelledError derives from BaseException, so the outer `except Exception` + does not catch it. This pins that, since swallowing it would hang restarts. + """ + + class _NeverReturns: + async def readline(self) -> bytes: + await asyncio.sleep(3600) + return b"" + + task = asyncio.create_task(stream_process_output(_FakeProcess(_NeverReturns()), "TEST")) + await asyncio.sleep(0) + task.cancel() + + with pytest.raises(asyncio.CancelledError): + await task + + +async def test_every_spawn_uses_the_larger_limit( + monkeypatch: pytest.MonkeyPatch, tmp_path: Any +) -> None: + """Every spawn must pass limit=, including the debug ones. + + A subprocess left on asyncio's default overruns far more easily, and enough + consecutive overruns exhaust MAX_CONSECUTIVE_READ_ERRORS and stop the reader + draining, which is the deadlock the bound exists to avoid. + """ + seen: list[int | None] = [] + + async def fake_exec(*_args: Any, **kwargs: Any) -> None: + seen.append(kwargs.get("limit")) + + monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec) + monkeypatch.setattr(run_handlers, "calculate_uvicorn_target_for_local", lambda *_: "project.acp") + + await start_acp_server(tmp_path / "acp.py", 8000, {}, tmp_path) + await start_temporal_worker(tmp_path / "run_worker.py", {}, tmp_path) + + # BOTH, since each helper refuses unless its own mode is enabled. + debug_config = DebugConfig( + enabled=True, mode=DebugMode.BOTH, port=5678, wait_for_attach=False, auto_port=False + ) + await start_acp_server_debug(tmp_path / "acp.py", 8000, {}, debug_config) + await start_temporal_worker_debug(tmp_path / "run_worker.py", {}, debug_config) + + assert seen == [SUBPROCESS_STREAM_LIMIT] * 4, f"a spawn is missing limit=: {seen}" + assert SUBPROCESS_STREAM_LIMIT > 64 * 1024, "asyncio's default is what breaks readline()" diff --git a/tests/lib/utils/test_logging_level.py b/tests/lib/utils/test_logging_level.py new file mode 100644 index 000000000..16b171e33 --- /dev/null +++ b/tests/lib/utils/test_logging_level.py @@ -0,0 +1,66 @@ +"""Tests for log level resolution in agentex.lib.utils.logging. + +The level used to be pinned to INFO with no override, so a debug() call could +never be emitted on any configuration. That is not just a missing feature: it +made diagnostics that were already written into the SDK unreachable. +""" + +from __future__ import annotations + +import logging + +import pytest + +from agentex.lib.utils.logging import ( + DEFAULT_LOG_LEVEL, + make_logger, + resolve_log_level, +) + + +def test_defaults_to_info_when_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LOG_LEVEL", raising=False) + + assert resolve_log_level() == DEFAULT_LOG_LEVEL == logging.INFO + + +@pytest.mark.parametrize( + ("configured", "expected"), + [ + ("DEBUG", logging.DEBUG), + ("debug", logging.DEBUG), + (" WaRnInG ", logging.WARNING), + ("ERROR", logging.ERROR), + ("CRITICAL", logging.CRITICAL), + ], +) +def test_reads_level_from_env( + monkeypatch: pytest.MonkeyPatch, configured: str, expected: int +) -> None: + monkeypatch.setenv("LOG_LEVEL", configured) + + assert resolve_log_level() == expected + + +@pytest.mark.parametrize("configured", ["", " ", "VERBOSE", "10x", "TRUE"]) +def test_falls_back_to_info_on_an_unusable_value( + monkeypatch: pytest.MonkeyPatch, configured: str +) -> None: + """A typo must not silently disable logging. + + logging.getLevelName returns the string "Level FOO" for anything it does not + recognise, which would otherwise be handed straight to setLevel. + """ + monkeypatch.setenv("LOG_LEVEL", configured) + + assert resolve_log_level() == logging.INFO + + +def test_make_logger_applies_the_configured_level(monkeypatch: pytest.MonkeyPatch) -> None: + """The regression that mattered: a debug() call must be able to emit.""" + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + + logger = make_logger("agentex.tests.level_from_env") + + assert logger.level == logging.DEBUG + assert logger.isEnabledFor(logging.DEBUG) From 4eb9ab97dc6092063aa8973d06447c7bd2dc6361 Mon Sep 17 00:00:00 2001 From: Michael Xu Date: Wed, 9 Sep 2026 16:05:54 -0500 Subject: [PATCH 5/6] fix(templates): register the local Agentex tracing processor so the developer UI traces tab shows spans Every framework template registered only SGPTracingProcessorConfig, which disables itself when SGP_API_KEY or SGP_ACCOUNT_ID is empty. Nothing else creates a tracing processor, so spans derived by the unified harness were never written to the backend /spans API and a scaffolded agent always showed "No spans found for this task" in the developer UI, contradicting the README ("open the traces tab"). Register AgentexTracingProcessorConfig() ahead of the SGP block in all 16 framework templates (sync/default/temporal x openai-agents, pydantic-ai, langgraph, claude-code, codex, plus the local-sandbox variant), fix a sync-langgraph comment that claimed to register the Agentex processor, and add a parametrized test asserting every framework template registers it. Verified on a scaffolded sync Claude Code agent and a Temporal Claude Code agent against a local backend: with the registration, message/turn spans appear via POST /spans; without it, none do. Claude-Session: https://claude.ai/code/session_01HCVKnA7LeJZ44nxZz1uzF3 --- .../default-claude-code/project/acp.py.j2 | 5 ++++- .../templates/default-codex/project/acp.py.j2 | 5 ++++- .../default-langgraph/project/acp.py.j2 | 5 ++++- .../default-openai-agents/project/acp.py.j2 | 5 ++++- .../default-pydantic-ai/project/acp.py.j2 | 5 ++++- .../sync-claude-code/project/acp.py.j2 | 5 ++++- .../templates/sync-codex/project/acp.py.j2 | 5 ++++- .../sync-langgraph/project/acp.py.j2 | 7 +++++-- .../project/acp.py.j2 | 5 ++++- .../sync-openai-agents/project/acp.py.j2 | 5 ++++- .../sync-pydantic-ai/project/acp.py.j2 | 5 ++++- .../project/workflow.py.j2 | 5 ++++- .../temporal-codex/project/workflow.py.j2 | 5 ++++- .../temporal-langgraph/project/workflow.py.j2 | 5 ++++- .../project/workflow.py.j2 | 5 ++++- .../project/workflow.py.j2 | 5 ++++- tests/lib/cli/test_init_templates.py | 20 +++++++++++++++++++ 17 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 index 42512c601..d662c9e21 100644 --- a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 @@ -25,7 +25,7 @@ from agentex.lib.adk import ClaudeCodeTurn from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -33,6 +33,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index f676ef137..55616b85b 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -35,7 +35,7 @@ from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskP from agentex.types.text_content import TextContent from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.lib.utils.model_utils import BaseModel from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -43,6 +43,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 index da5d37905..0254deec8 100644 --- a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 @@ -20,7 +20,7 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.adk import LangGraphTurn @@ -29,6 +29,9 @@ from project.graph import create_graph logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 index 66ee31243..1548ca816 100644 --- a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 @@ -25,7 +25,7 @@ from agents import Agent, Runner, function_tool, set_tracing_disabled from agentex.lib import adk from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.utils.model_utils import BaseModel @@ -48,6 +48,9 @@ if _litellm_key and not os.environ.get("OPENAI_API_KEY"): _sgp_api_key = os.environ.get("SGP_API_KEY", "") _sgp_account_id = os.environ.get("SGP_ACCOUNT_ID", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if _sgp_api_key and _sgp_account_id: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 index 245f9ec38..c42b4fda3 100644 --- a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 @@ -27,7 +27,7 @@ import agentex.lib.adk as adk from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.utils.model_utils import BaseModel @@ -42,6 +42,9 @@ logger = make_logger(__name__) # so they show up in the per-task spans dropdown out of the box. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 index 33a89a51e..020fd95aa 100644 --- a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 @@ -25,7 +25,7 @@ import agentex.lib.adk as adk from agentex.lib.adk import ClaudeCodeTurn from agentex.lib.types.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -35,6 +35,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 index 0bc5d66a7..5ae59a37b 100644 --- a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 @@ -34,7 +34,7 @@ import agentex.lib.adk as adk from agentex.lib.adk import CodexTurn from agentex.lib.types.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -44,6 +44,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 index 32d261093..3e2d98102 100644 --- a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 @@ -12,7 +12,7 @@ from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.protocol.acp import SendMessageParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.adk import LangGraphTurn @@ -33,7 +33,10 @@ from project.graph import create_graph logger = make_logger(__name__) -# Register the Agentex tracing processor so spans are shipped to the backend +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + +# Register the Scale GenAI Platform (SGP) tracing processor add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 index 14af98351..63538a06d 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 @@ -21,7 +21,7 @@ load_dotenv() from agentex.lib import adk from project.agent import run_agent from agentex.protocol.acp import SendMessageParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -42,6 +42,9 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 index 41029f2ce..08945a142 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 @@ -6,7 +6,7 @@ from agentex.lib.adk.providers._modules.sync_provider import SyncStreamingProvid from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.protocol.acp import SendMessageParams from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.model_utils import BaseModel from agentex.types.task_message_update import TaskMessageUpdate, StreamTaskMessageFull @@ -32,6 +32,9 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 index 1a3c6f0a9..7a66e0e1c 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 @@ -20,7 +20,7 @@ from project.agent import MODEL_NAME, create_agent import agentex.lib.adk as adk from agentex.protocol.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP @@ -36,6 +36,9 @@ logger = make_logger(__name__) # processor that's lazy-initialised on first span. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 index 8191ad80f..d1fde0362 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 @@ -26,7 +26,7 @@ from temporalio import workflow from agentex.lib import adk from agentex.lib.types.acp import SendEventParams, CreateTaskParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.environment_variables import EnvironmentVariables @@ -37,6 +37,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce with workflow.unsafe.imports_passed_through(): from project.activities import RunClaudeCodeTurnParams, run_claude_code_turn +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 index 1004ebfb8..68fc3f36a 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 @@ -28,7 +28,7 @@ from temporalio import workflow from agentex.lib import adk from agentex.lib.types.acp import SendEventParams, CreateTaskParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.environment_variables import EnvironmentVariables @@ -39,6 +39,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce with workflow.unsafe.imports_passed_through(): from project.activities import RunCodexTurnParams, run_codex_turn +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + add_tracing_processor_config( SGPTracingProcessorConfig( sgp_api_key=os.environ.get("SGP_API_KEY", ""), diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 index 14bafabc1..f8467eb98 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 @@ -36,7 +36,7 @@ from agentex.lib import adk from project.graph import GRAPH_NAME, build_graph from agentex.lib.adk import emit_langgraph_messages from agentex.protocol.acp import SendEventParams, CreateTaskParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.environment_variables import EnvironmentVariables @@ -48,6 +48,9 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce # the default processor that is lazy-initialised on first span). SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 index af8b7a299..62fd99f70 100644 --- a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 @@ -25,7 +25,7 @@ from project.activities import get_weather from agentex.lib.core.tracing.tracing_processor_manager import ( add_tracing_processor_config, ) -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from datetime import timedelta @@ -39,6 +39,9 @@ if environment_variables.AGENT_NAME is None: logger = make_logger(__name__) +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + # Setup tracing for SGP (Scale GenAI Platform) # This enables visibility into your agent's execution in the SGP dashboard add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 index 6dcca3002..2e500b603 100644 --- a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 @@ -24,7 +24,7 @@ from project.agent import TaskDeps, temporal_agent from agentex.lib import adk from agentex.protocol.acp import SendEventParams, CreateTaskParams -from agentex.lib.types.tracing import SGPTracingProcessorConfig +from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib.environment_variables import EnvironmentVariables @@ -39,6 +39,9 @@ if TYPE_CHECKING: # via the default Agentex processor that's lazy-initialised on first span. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") +# Local Agentex backend: spans appear in the developer UI's traces tab. +add_tracing_processor_config(AgentexTracingProcessorConfig()) + if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/tests/lib/cli/test_init_templates.py b/tests/lib/cli/test_init_templates.py index ec809cbbf..f0ecbe2cb 100644 --- a/tests/lib/cli/test_init_templates.py +++ b/tests/lib/cli/test_init_templates.py @@ -137,3 +137,23 @@ def test_requirements_include_langgraph_plugin_and_temporal(self, tmp_path: Path requirements = (project_dir / "requirements.txt").read_text() assert "temporalio[langgraph]>=1.27.0" in requirements assert "langchain-openai" in requirements + + +_FRAMEWORK_TEMPLATES = [t for t in TemplateType if t not in (TemplateType.DEFAULT, TemplateType.SYNC, TemplateType.TEMPORAL)] + + +@pytest.mark.parametrize("template_type", _FRAMEWORK_TEMPLATES) +def test_framework_templates_register_local_tracing_processor(tmp_path: Path, template_type: TemplateType): + """Every framework template registers the Agentex tracing processor. + + Without it, spans derived by the unified harness only go to the (optional) + SGP processor, and the developer UI's traces tab stays empty for a locally + scaffolded agent. + """ + project_dir = _render_project(tmp_path, template_type) + entrypoints = [p for p in project_dir.rglob("*.py") if p.name in ("acp.py", "workflow.py")] + assert entrypoints, f"{template_type.value} has no acp.py/workflow.py" + joined = "\n".join(p.read_text() for p in entrypoints) + assert "add_tracing_processor_config(AgentexTracingProcessorConfig())" in joined, ( + f"{template_type.value} does not register AgentexTracingProcessorConfig" + ) From 8a049a71ee7ed54994f0ac91f666ecf9cdd70c33 Mon Sep 17 00:00:00 2001 From: Michael Xu Date: Thu, 10 Sep 2026 12:20:06 -0500 Subject: [PATCH 6/6] fix(templates): skip the local tracing processor when the backend is disabled The runtime treats an explicitly empty AGENTEX_BASE_URL as "no backend" and skips agent registration; registering the Agentex tracing processor unconditionally would then make every span export fail. Guard the registration on the same condition in all 16 framework templates. Claude-Session: https://claude.ai/code/session_01HCVKnA7LeJZ44nxZz1uzF3 --- .../cli/templates/default-claude-code/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/default-codex/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/default-langgraph/project/acp.py.j2 | 7 +++++-- .../cli/templates/default-openai-agents/project/acp.py.j2 | 7 +++++-- .../cli/templates/default-pydantic-ai/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/sync-claude-code/project/acp.py.j2 | 7 +++++-- src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/sync-langgraph/project/acp.py.j2 | 7 +++++-- .../sync-openai-agents-local-sandbox/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/sync-openai-agents/project/acp.py.j2 | 7 +++++-- .../lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 | 7 +++++-- .../templates/temporal-claude-code/project/workflow.py.j2 | 7 +++++-- .../cli/templates/temporal-codex/project/workflow.py.j2 | 7 +++++-- .../templates/temporal-langgraph/project/workflow.py.j2 | 7 +++++-- .../temporal-openai-agents/project/workflow.py.j2 | 7 +++++-- .../templates/temporal-pydantic-ai/project/workflow.py.j2 | 7 +++++-- 16 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 index d662c9e21..391a6f5cb 100644 --- a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 @@ -33,8 +33,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index 55616b85b..c53a1e367 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -43,8 +43,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 index 0254deec8..5bc72f881 100644 --- a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 @@ -29,8 +29,11 @@ from project.graph import create_graph logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 index 1548ca816..cee6d5087 100644 --- a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 @@ -48,8 +48,11 @@ if _litellm_key and not os.environ.get("OPENAI_API_KEY"): _sgp_api_key = os.environ.get("SGP_API_KEY", "") _sgp_account_id = os.environ.get("SGP_ACCOUNT_ID", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if _sgp_api_key and _sgp_account_id: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 index c42b4fda3..9bc5e3e84 100644 --- a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 @@ -42,8 +42,11 @@ logger = make_logger(__name__) # so they show up in the per-task spans dropdown out of the box. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 index 020fd95aa..b48371736 100644 --- a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 @@ -35,8 +35,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 index 5ae59a37b..b0469943b 100644 --- a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 @@ -44,8 +44,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 index 3e2d98102..f327b2f1f 100644 --- a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 @@ -33,8 +33,11 @@ from project.graph import create_graph logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) # Register the Scale GenAI Platform (SGP) tracing processor add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 index 63538a06d..6cb578204 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 @@ -42,8 +42,11 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 index 08945a142..28e450450 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 @@ -32,8 +32,11 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 index 7a66e0e1c..995f68248 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 @@ -36,8 +36,11 @@ logger = make_logger(__name__) # processor that's lazy-initialised on first span. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 index d1fde0362..69fd24d7f 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 @@ -37,8 +37,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce with workflow.unsafe.imports_passed_through(): from project.activities import RunClaudeCodeTurnParams, run_claude_code_turn -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 index 68fc3f36a..9f19bbfa3 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 @@ -39,8 +39,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce with workflow.unsafe.imports_passed_through(): from project.activities import RunCodexTurnParams, run_codex_turn -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) add_tracing_processor_config( SGPTracingProcessorConfig( diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 index f8467eb98..ab734e9a4 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 @@ -48,8 +48,11 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce # the default processor that is lazy-initialised on first span). SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config( diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 index 62fd99f70..833aab650 100644 --- a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 @@ -39,8 +39,11 @@ if environment_variables.AGENT_NAME is None: logger = make_logger(__name__) -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) # Setup tracing for SGP (Scale GenAI Platform) # This enables visibility into your agent's execution in the SGP dashboard diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 index 2e500b603..b7f37c43f 100644 --- a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 @@ -39,8 +39,11 @@ if TYPE_CHECKING: # via the default Agentex processor that's lazy-initialised on first span. SGP_API_KEY = os.environ.get("SGP_API_KEY", "") SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "") -# Local Agentex backend: spans appear in the developer UI's traces tab. -add_tracing_processor_config(AgentexTracingProcessorConfig()) +# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped +# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend +# as disabled and skips agent registration too). +if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"): + add_tracing_processor_config(AgentexTracingProcessorConfig()) if SGP_API_KEY and SGP_ACCOUNT_ID: add_tracing_processor_config(