Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
".": "0.25.0",
"adk": "0.25.0"
".": "0.26.0",
"adk": "0.26.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

* **tracing:** emit OTel metrics for async span queue depth, batch drain, and SGP export success/failure (HTTP status labels). Disable SDK-side recording with ``AGENTEX_TRACING_METRICS=0``.

## 0.26.0 (2026-08-31)

Full Changelog: [agentex-client-v0.25.0...agentex-client-v0.26.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.25.0...agentex-client-v0.26.0)

### Features

* **tracing:** add opt-in commit SHA stamping for SGP spans ([#505](https://github.com/scaleapi/scale-agentex-python/issues/505)) ([76252a9](https://github.com/scaleapi/scale-agentex-python/commit/76252a98f28663e8c95777456d07e42171592c62))

## 0.25.0 (2026-08-26)

Full Changelog: [agentex-client-v0.24.0...agentex-client-v0.25.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.24.0...agentex-client-v0.25.0)
Expand Down
8 changes: 8 additions & 0 deletions adk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.26.0 (2026-08-31)

Full Changelog: [agentex-sdk-v0.25.0...agentex-sdk-v0.26.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.25.0...agentex-sdk-v0.26.0)

### Chores

* **agentex-sdk:** Synchronize agentex versions

## 0.25.0 (2026-08-26)

Full Changelog: [agentex-sdk-v0.24.0...agentex-sdk-v0.25.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.24.0...agentex-sdk-v0.25.0)
Expand Down
2 changes: 1 addition & 1 deletion adk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# (agentex/{__init__.py, _*.py, types/, resources/}) ships from the slim
# sibling package `agentex-client` which is pinned as a runtime dep.
name = "agentex-sdk"
version = "0.25.0"
version = "0.26.0"
description = "Agent Development Kit (ADK) overlay for the Agentex API — FastACP server, Temporal workflows, LLM provider integrations, observability"
license = "Apache-2.0"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# overlay (formerly `src/agentex/lib/*`) now lives in `adk/` and ships
# as the sibling `agentex-sdk` package — see `adk/pyproject.toml`.
name = "agentex-client"
version = "0.25.0"
version = "0.26.0"
description = "The official Python REST client for the Agentex API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion src/agentex/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "agentex"
__version__ = "0.25.0" # x-release-please-version
__version__ = "0.26.0" # x-release-please-version
4 changes: 4 additions & 0 deletions src/agentex/lib/adk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -73,6 +76,7 @@
"TurnSpan",
# Lineage data-source refs (SGP-6513)
"lineage",
"code_revision",
"DataSourceRef",
"data_sources",
# Checkpointing / LangGraph
Expand Down
105 changes: 105 additions & 0 deletions src/agentex/lib/core/tracing/code_revision.py
Original file line number Diff line number Diff line change
@@ -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__:<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 ``<image-name>-<sha>`` 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}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Reject invalid commit lengths

If an opted-in deployment supplies a 41–63-character hexadecimal value, this regex accepts it even though it cannot be a SHA-1 or SHA-256 object name, causing SGP to store misleading __commit_sha__ metadata that cannot identify a real commit.

Suggested change
_GIT_SHA_RE = re.compile(r"[0-9a-fA-F]{7,64}")
_GIT_SHA_RE = re.compile(r"(?:[0-9a-fA-F]{7,40}|[0-9a-fA-F]{64})")

Knowledge Base Used: Tracing pipeline

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/core/tracing/code_revision.py
Line: 32

Comment:
**Reject invalid commit lengths**

If an opted-in deployment supplies a 41–63-character hexadecimal value, this regex accepts it even though it cannot be a SHA-1 or SHA-256 object name, causing SGP to store misleading `__commit_sha__` metadata that cannot identify a real commit.

```suggestion
_GIT_SHA_RE = re.compile(r"(?:[0-9a-fA-F]{7,40}|[0-9a-fA-F]{64})")
```

**Knowledge Base Used:** [Tracing pipeline](https://app.greptile.com/scale-ai/-/custom-context/knowledge-base/scaleapi/scale-agentex-python/-/docs/tracing-pipeline.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex


_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
28 changes: 26 additions & 2 deletions src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
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
from scale_gp_beta.lib.tracing import create_span, flush_queue
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
Expand Down Expand Up @@ -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)
Expand All @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions src/agentex/lib/environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions tests/lib/core/tracing/processors/test_sgp_tracing_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading