Skip to content

Commit f4d0b3e

Browse files
cyntwang99claude
andcommitted
feat(tracing): add opt-in commit SHA stamping for SGP spans
Answers "which commit handled this request?" from the SGP Traces UI: search `__commit_sha__:<sha>` and read it in the span-detail Metadata panel. Agent spans never reach the OTel collector -- the SDK POSTs them straight to SGP -- so span metadata is the only carrier, and the value has to be present in the agent's own process. Opt-in, modelled on the lineage registry next door: the module global's default is the off state, so `commit_sha()` returns None and the stamp site needs no flag. Nothing is emitted until an agent calls `adk.code_revision.enable()`, and no agent inherits the field by upgrading the SDK. Deliberately separate from the automatic `__agent_version__`, which carries the deployed image tag verbatim. That tag is a real commit on GCP/Azure CI but an "<image-name>-<sha>" composite on AWS ECR, "latest" on local deploys, and an arbitrary string on manual dispatch -- so a field named for a commit must not mirror it. Values that are not git object names are refused with a warning, and `__commit_sha__` therefore only ever holds one. Value precedence: explicit argument, then AGENT_COMMIT_SHA, then AGENT_VERSION only when it is already SHA-shaped -- so a platform-deployed agent that opts in needs no extra plumbing, while a custom build path bakes AGENT_COMMIT_SHA into the image. Stamped in the SGP processor rather than at span creation. That scopes it to the SGP backend, which is the user story here (and 92 of the fleet's agents), and it re-runs at span end, so the value survives an agent replacing span.data mid-span -- which real agent code does today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0fa93b6 commit f4d0b3e

6 files changed

Lines changed: 260 additions & 0 deletions

File tree

src/agentex/lib/adk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
# Data-source refs for lineage (SGP-6513); implementation lives in core.tracing
3333
from agentex.lib.core.tracing import lineage
34+
35+
# Opt-in commit-SHA stamping (AGX1-969); implementation in core.tracing
36+
from agentex.lib.core.tracing import code_revision
3437
from agentex.lib.core.tracing.lineage import DataSourceRef, data_sources
3538

3639
# Unified harness surface (AGX1-375)
@@ -73,6 +76,7 @@
7376
"TurnSpan",
7477
# Lineage data-source refs (SGP-6513)
7578
"lineage",
79+
"code_revision",
7680
"DataSourceRef",
7781
"data_sources",
7882
# Checkpointing / LangGraph
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Opt-in stamping of the agent's source commit onto its spans.
2+
3+
Nothing is stamped until the agent calls :func:`enable`, mirroring the
4+
``lineage`` registry next door: a process-wide switch the agent sets once at
5+
import, rather than automatic behaviour every agent inherits. When enabled the
6+
resolved commit lands in span data under ``__commit_sha__`` and is searchable in
7+
the SGP Traces UI as ``__commit_sha__:<sha>``.
8+
9+
This is deliberately separate from ``__agent_version__``, which is automatic and
10+
carries the deployed image tag verbatim ("image tag or git sha"). That tag is a
11+
real commit on some build paths but an ``<image-name>-<sha>`` composite (AWS
12+
ECR), ``latest``, or a hand-passed tag on others -- so a field named for a commit
13+
must not simply mirror it. Values that are not git object names are refused, and
14+
a field named ``__commit_sha__`` therefore only ever holds one.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import os
20+
import re
21+
22+
from agentex.lib.utils.logging import make_logger
23+
24+
__all__ = ("COMMIT_SHA_KEY", "enable", "disable", "is_enabled", "commit_sha")
25+
26+
logger = make_logger(__name__)
27+
28+
COMMIT_SHA_KEY = "__commit_sha__"
29+
30+
# A git object name: 40 hex for SHA-1, 64 for SHA-256, or an abbreviation down to
31+
# git's own 7-character minimum.
32+
_GIT_SHA_RE = re.compile(r"[0-9a-fA-F]{7,64}")
33+
34+
_COMMIT_SHA_ENV = "AGENT_COMMIT_SHA"
35+
# Fallback only: automatic, and only usable when it happens to be SHA-shaped.
36+
_AGENT_VERSION_ENV = "AGENT_VERSION"
37+
38+
# Resolved once at enable() rather than per span: the value is fixed for the
39+
# life of the process, and resolving eagerly means a bad value is reported at
40+
# startup instead of silently producing unstamped spans.
41+
_commit_sha: str | None = None
42+
43+
44+
def enable(commit_sha: str | None = None) -> None:
45+
"""Opt this process in to stamping ``__commit_sha__`` onto every span.
46+
47+
Value precedence: the explicit ``commit_sha`` argument, else
48+
``AGENT_COMMIT_SHA``, else ``AGENT_VERSION`` when the deployment happened to
49+
set it to a bare commit SHA. A value that is not a git object name is
50+
refused with a warning and leaves stamping off -- better an absent field
51+
than one named for a commit that holds an image tag.
52+
"""
53+
global _commit_sha
54+
55+
for value, source in (
56+
(commit_sha, "the commit_sha argument"),
57+
(os.environ.get(_COMMIT_SHA_ENV), _COMMIT_SHA_ENV),
58+
(os.environ.get(_AGENT_VERSION_ENV), _AGENT_VERSION_ENV),
59+
):
60+
candidate = (value or "").strip()
61+
if not candidate:
62+
continue
63+
if _GIT_SHA_RE.fullmatch(candidate):
64+
_commit_sha = candidate
65+
logger.info("code revision stamping enabled from %s", source)
66+
return
67+
# An explicit argument or AGENT_COMMIT_SHA is a direct statement of
68+
# intent, so a bad value there is worth surfacing. AGENT_VERSION is only
69+
# a fallback and is expected to be a non-SHA tag much of the time, so
70+
# falling through it quietly is correct, not a silent failure.
71+
if source != _AGENT_VERSION_ENV:
72+
logger.warning(
73+
"%s=%r is not a git commit SHA; __commit_sha__ will not be stamped.",
74+
source,
75+
candidate,
76+
)
77+
_commit_sha = None
78+
return
79+
80+
_commit_sha = None
81+
logger.warning(
82+
"code revision stamping was enabled but no commit SHA was found "
83+
"(checked the commit_sha argument, %s, and %s); __commit_sha__ will not "
84+
"be stamped. Set %s in the agent's environment -- e.g. bake it at build "
85+
"time with a Dockerfile ARG/ENV.",
86+
_COMMIT_SHA_ENV,
87+
_AGENT_VERSION_ENV,
88+
_COMMIT_SHA_ENV,
89+
)
90+
91+
92+
def disable() -> None:
93+
"""Turn stamping back off (also used for test isolation)."""
94+
global _commit_sha
95+
_commit_sha = None
96+
97+
98+
def is_enabled() -> bool:
99+
"""Whether a commit SHA resolved and will be stamped."""
100+
return _commit_sha is not None
101+
102+
103+
def commit_sha() -> str | None:
104+
"""The resolved commit SHA, or ``None`` when stamping is not enabled."""
105+
return _commit_sha

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from scale_gp_beta.lib.tracing.span import Span as SGPSpan
1212

1313
from agentex.types.span import Span
14+
from agentex.lib.core.tracing import code_revision
1415
from agentex.lib.types.tracing import SGPTracingProcessorConfig
1516
from agentex.lib.utils.logging import make_logger
1617
from agentex.lib.core.observability import tracing_metrics_recording as _metrics
@@ -67,6 +68,11 @@ def _add_source_to_span(span: Span, env_vars: EnvironmentVariables) -> None:
6768
span.data["__agent_id__"] = env_vars.AGENT_ID
6869
if env_vars.AGENT_VERSION is not None:
6970
span.data["__agent_version__"] = env_vars.AGENT_VERSION
71+
# Opt-in only (adk.code_revision.enable()); None unless the agent asked
72+
# for it, so no agent inherits this by upgrading the SDK.
73+
commit_sha = code_revision.commit_sha()
74+
if commit_sha is not None:
75+
span.data[code_revision.COMMIT_SHA_KEY] = commit_sha
7076

7177

7278
def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan:

src/agentex/lib/environment_variables.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class EnvVarKeys(str, Enum):
2525
AGENT_DESCRIPTION = "AGENT_DESCRIPTION"
2626
AGENT_ID = "AGENT_ID"
2727
AGENT_VERSION = "AGENT_VERSION"
28+
AGENT_COMMIT_SHA = "AGENT_COMMIT_SHA"
2829
AGENT_API_KEY = "AGENT_API_KEY"
2930
# ACP Configuration
3031
ACP_URL = "ACP_URL"
@@ -67,6 +68,12 @@ class EnvironmentVariables(BaseModel):
6768
AGENT_ID: str | None = None
6869
# Build/version discriminator (image tag or git sha), set by the deployment
6970
AGENT_VERSION: str | None = None
71+
# The agent's source commit, baked into the image or set by the deployment.
72+
# Unlike AGENT_VERSION this is expected to be a git SHA and nothing else, and
73+
# it is OPT-IN: nothing is stamped unless the agent calls
74+
# `adk.code_revision.enable()`, which also refuses a value that is not a git
75+
# object name. See agentex.lib.core.tracing.code_revision.
76+
AGENT_COMMIT_SHA: str | None = None
7077
AGENT_API_KEY: str | None = None
7178
ACP_TYPE: str | None = "async"
7279
AGENT_INPUT_TYPE: str | None = None

tests/lib/core/tracing/processors/test_sgp_tracing_processor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,35 @@ def test_agent_identity_and_version_stamped_into_span_data(self):
5454
"__agent_version__": "sha-abc123",
5555
}
5656

57+
def test_commit_sha_is_not_stamped_without_opt_in(self, monkeypatch):
58+
"""Upgrading the SDK must not start emitting __commit_sha__ on its own,
59+
even when the environment carries a perfectly good SHA."""
60+
from agentex.lib.core.tracing import code_revision
61+
from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span
62+
63+
monkeypatch.setenv("AGENT_COMMIT_SHA", "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d")
64+
code_revision.disable()
65+
66+
env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None)
67+
span = _make_span()
68+
_add_source_to_span(span, env)
69+
assert "__commit_sha__" not in span.data
70+
71+
def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch):
72+
from agentex.lib.core.tracing import code_revision
73+
from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span
74+
75+
sha = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d"
76+
monkeypatch.setenv("AGENT_COMMIT_SHA", sha)
77+
code_revision.enable()
78+
try:
79+
env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None)
80+
span = _make_span()
81+
_add_source_to_span(span, env)
82+
assert span.data["__commit_sha__"] == sha
83+
finally:
84+
code_revision.disable()
85+
5786
def test_unset_identity_fields_are_omitted(self):
5887
from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span
5988

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Opt-in commit-SHA stamping.
2+
3+
The contract that matters: an agent that does not call ``enable()`` gets nothing,
4+
so upgrading the SDK never starts emitting this field on its own.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import pytest
10+
11+
from agentex.lib.core.tracing import code_revision
12+
13+
SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d"
14+
15+
16+
@pytest.fixture(autouse=True)
17+
def _reset():
18+
"""State is process-wide (like the lineage registry), so isolate each test."""
19+
code_revision.disable()
20+
yield
21+
code_revision.disable()
22+
23+
24+
class TestOptIn:
25+
def test_disabled_by_default(self, monkeypatch):
26+
"""Even with the env fully populated, nothing resolves until enable()."""
27+
monkeypatch.setenv("AGENT_COMMIT_SHA", SHA)
28+
monkeypatch.setenv("AGENT_VERSION", SHA)
29+
assert code_revision.commit_sha() is None
30+
assert code_revision.is_enabled() is False
31+
32+
def test_enable_reads_agent_commit_sha(self, monkeypatch):
33+
monkeypatch.setenv("AGENT_COMMIT_SHA", SHA)
34+
code_revision.enable()
35+
assert code_revision.commit_sha() == SHA
36+
assert code_revision.is_enabled() is True
37+
38+
def test_explicit_argument_wins(self, monkeypatch):
39+
monkeypatch.setenv("AGENT_COMMIT_SHA", SHA)
40+
code_revision.enable("7f3a91c2")
41+
assert code_revision.commit_sha() == "7f3a91c2"
42+
43+
def test_disable_turns_it_back_off(self, monkeypatch):
44+
monkeypatch.setenv("AGENT_COMMIT_SHA", SHA)
45+
code_revision.enable()
46+
code_revision.disable()
47+
assert code_revision.commit_sha() is None
48+
49+
50+
class TestValueIsAlwaysACommit:
51+
"""A field named for a commit must never hold an image tag."""
52+
53+
@pytest.mark.parametrize(
54+
"value",
55+
[
56+
"latest",
57+
"v1.2.3",
58+
"0.2.4-v4",
59+
"rocket_mock_agent-b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d", # AWS ECR composite
60+
"abc", # shorter than git's 7-char minimum
61+
"z" * 40, # right length, not hex
62+
],
63+
)
64+
def test_non_sha_is_refused(self, monkeypatch, value):
65+
monkeypatch.setenv("AGENT_COMMIT_SHA", value)
66+
code_revision.enable()
67+
assert code_revision.commit_sha() is None
68+
69+
@pytest.mark.parametrize("value", [SHA, SHA.upper(), "b362b17", "a" * 64])
70+
def test_git_object_names_are_accepted(self, monkeypatch, value):
71+
monkeypatch.setenv("AGENT_COMMIT_SHA", value)
72+
code_revision.enable()
73+
assert code_revision.commit_sha() == value
74+
75+
def test_whitespace_only_is_refused(self, monkeypatch):
76+
monkeypatch.setenv("AGENT_COMMIT_SHA", " ")
77+
code_revision.enable()
78+
assert code_revision.commit_sha() is None
79+
80+
def test_enable_with_nothing_available_is_a_no_op(self, monkeypatch):
81+
monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False)
82+
monkeypatch.delenv("AGENT_VERSION", raising=False)
83+
code_revision.enable()
84+
assert code_revision.commit_sha() is None
85+
86+
87+
class TestAgentVersionFallback:
88+
def test_falls_back_to_agent_version_when_sha_shaped(self, monkeypatch):
89+
"""A platform deploy already sets AGENT_VERSION; on GCP/Azure it is a
90+
bare SHA, so an opting-in agent needs no extra plumbing."""
91+
monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False)
92+
monkeypatch.setenv("AGENT_VERSION", SHA)
93+
code_revision.enable()
94+
assert code_revision.commit_sha() == SHA
95+
96+
def test_does_not_fall_back_to_a_non_sha_agent_version(self, monkeypatch):
97+
"""AGENT_VERSION is 'latest' or an AWS composite much of the time."""
98+
monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False)
99+
monkeypatch.setenv("AGENT_VERSION", "latest")
100+
code_revision.enable()
101+
assert code_revision.commit_sha() is None
102+
103+
def test_bad_explicit_value_does_not_fall_through(self, monkeypatch):
104+
"""An explicit AGENT_COMMIT_SHA is a statement of intent: if it is wrong,
105+
say so rather than silently substituting the image tag."""
106+
monkeypatch.setenv("AGENT_COMMIT_SHA", "not-a-sha")
107+
monkeypatch.setenv("AGENT_VERSION", SHA)
108+
code_revision.enable()
109+
assert code_revision.commit_sha() is None

0 commit comments

Comments
 (0)