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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/agentex-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -44,7 +44,7 @@ jobs:
permissions:
contents: read
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/agentex-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Expand Down Expand Up @@ -87,7 +87,7 @@ jobs:
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/agentex-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
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.20.0",
"adk": "0.20.0"
".": "0.21.0",
"adk": "0.21.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-b2df5f506330ad5fba5a0d518ab8a4bcf876e8c3684a4fe0d0cc5102fd9c569e.yml
openapi_spec_hash: 132e9efdb0535d9594abadf799431cf5
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-644a4ec06aa1f055c614cbef3379684819a4edd84eeb20d2fb29ae01663622a3.yml
openapi_spec_hash: a6a4dc0c09691ac9783bf38e9653a464
config_hash: 593e89b291976a5e84e4c3c3f8324354
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@

* **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.21.0 (2026-07-28)

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

### Features

* **api:** add include_live parameter to schedules list method ([199fd6a](https://github.com/scaleapi/scale-agentex-python/commit/199fd6a633bc82626a4629ee1ac52a40e6f10271))
* **codex:** republish todo_list revisions as they are ticked off ([#473](https://github.com/scaleapi/scale-agentex-python/issues/473)) ([0252fcc](https://github.com/scaleapi/scale-agentex-python/commit/0252fcc1d00c96066e4500d09b07a6b0da836437))
* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([23c1b6b](https://github.com/scaleapi/scale-agentex-python/commit/23c1b6b8dc60f16c5e19a1aab68d244d286caffc))


### Bug Fixes

* **api:** remove params field from task list response ([152bc75](https://github.com/scaleapi/scale-agentex-python/commit/152bc75b242c2ff4179e0cfac0f33edbc6bcae38))

## 0.20.0 (2026-07-16)

Full Changelog: [agentex-client-v0.19.0...agentex-client-v0.20.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.19.0...agentex-client-v0.20.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.21.0 (2026-07-28)

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

### Chores

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

## 0.20.0 (2026-07-16)

Full Changelog: [agentex-sdk-v0.19.0...agentex-sdk-v0.20.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.19.0...agentex-sdk-v0.20.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.20.0"
version = "0.21.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
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@

logger = make_logger(__name__)

# These reference MCP servers still import the mcp 1.x API (``McpError``), which
# mcp 2.0.0 renamed to ``MCPError``. uvx gives each server its own isolated env and
# resolves ``mcp`` unpinned there, ignoring the version this project pins, so without
# this constraint every server dies at import and the agent silently makes zero tool
# calls. Drop the pin once the servers support mcp 2.x.
_MCP_PIN = ["--with", "mcp<2"]

MCP_SERVERS = [
StdioServerParameters(
command="uvx",
args=["mcp-server-time", "--local-timezone", "America/Los_Angeles"],
args=[*_MCP_PIN, "mcp-server-time", "--local-timezone", "America/Los_Angeles"],
),
StdioServerParameters(
command="uvx",
args=["openai-websearch-mcp"],
args=[*_MCP_PIN, "openai-websearch-mcp"],
env={
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "")
}
),
StdioServerParameters(
command="uvx",
args=["mcp-server-fetch"],
args=[*_MCP_PIN, "mcp-server-fetch"],
),
]

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.20.0"
version = "0.21.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.20.0" # x-release-please-version
__version__ = "0.21.0" # x-release-please-version
47 changes: 43 additions & 4 deletions src/agentex/lib/adk/_modules/_codex_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@
a synthetic ToolRequestContent Full is emitted before the response.
mcp_tool_call -> same as command_execution
web_search -> same as command_execution
todo_list -> same as command_execution
todo_list -> same as command_execution, plus:
item.updated -> StreamTaskMessageFull(ToolResponseContent)
Codex ticks one in-place todo_list item through
item.updated; each revision is republished under
the same tool_call_id so consumers can render the
checklist filling in rather than jumping to its
final state at end of turn.
collab_tool_call -> same as command_execution
error (item type) -> StreamTaskMessageFull(TextContent, error text) on completed only

Expand All @@ -75,9 +81,10 @@
without this module needing to know about spans.
item.updated (reasoning): the intermediate cumulative text is discarded;
only item.completed carries the final text.
item.updated (tool): tool item types other than agent_message do not
emit updates; item.started opens the request and
item.completed closes it.
item.updated (tool): only todo_list is republished (see above). For the
other tool item types item.started opens the request
and item.completed closes it; any updates in between
carry no state a consumer could act on.
"""

from __future__ import annotations
Expand Down Expand Up @@ -112,6 +119,11 @@ def _truncate(text: str, max_len: int = _MAX_RESULT_LENGTH) -> str:
return str(text)[:max_len]


# Tool items codex revises in place rather than reopening. Their item.updated
# events carry real intermediate state, so they are forwarded as responses.
_PROGRESSIVE_TOOL_ITEMS = frozenset({"todo_list"})


def _tool_name_for(item_type: str, payload: dict[str, Any]) -> str:
"""Derive a canonical tool name from a codex item type."""
if item_type == "command_execution":
Expand Down Expand Up @@ -467,6 +479,33 @@ def _handle_item(self, evt_type: str, item: dict[str, Any]) -> list[StreamTaskMe
)
out.append(StreamTaskMessageDone(type="done", index=req_idx))

elif evt_type == "item.updated" and item_type in _PROGRESSIVE_TOOL_ITEMS:
# Codex revises its plan in place: one todo_list item is opened
# at the start of the turn and ticked off through item.updated,
# with item.completed only arriving at the very end. Forwarding
# each revision as a response for the SAME tool_call_id lets a
# consumer show the checklist filling in as the turn runs; the
# last response received is the current state.
if item_id in self._tool_open:
actual_type = self._tool_item_types.get(item_id, item_type)
result_text, is_error = _tool_output_for(actual_type, item)
resp_content: dict[str, Any] = {"result": result_text}
if is_error:
resp_content["is_error"] = True
out.append(
StreamTaskMessageFull(
type="full",
index=self._alloc(),
content=ToolResponseContent(
type="tool_response",
author="agent",
tool_call_id=tool_call_id,
name=_tool_name_for(actual_type, item),
content=resp_content,
),
)
)

elif evt_type == "item.completed":
# file_change items may only emit item.completed (no started).
if item_id not in self._tool_open:
Expand Down
83 changes: 83 additions & 0 deletions src/agentex/lib/core/tracing/obs_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Correlate adk business spans with the active observability trace.

The adk business ``trace_id`` is the agent **task id** (run-level: it spans the
whole agent run across many requests -- task/create, then each message/send turn),
so we must NOT overwrite it with a per-request observability trace_id. Doing so
would collapse the run-level grouping.

Instead, each business span is *tagged* with the active observability
trace_id/span_id (this is the OpenTelemetry "span link" pattern -- correlate
across trace granularities rather than merging them). You can then pivot from a
persisted business span to the Tempo/Datadog trace for the turn that produced it,
while the business trace still groups the entire run by task id.

Source selection follows SGP_OBS_MODE, matching egp-api-backend:
- unset / "dd_only": ddtrace context (current stack)
- "dual": OTel/LGTM preferred, ddtrace fallback
- "lgtm": OTel/LGTM only

This never fabricates ids -- if no observability context is active, it returns
an empty dict and the span is simply not tagged.
"""
from __future__ import annotations

import os
from typing import Dict, Tuple, Optional

__all__ = ("get_obs_mode", "obs_correlation")

DD_ONLY = "dd_only"
DUAL = "dual"
LGTM = "lgtm"
_DEFAULT_MODE = DD_ONLY
_VALID_MODES = (DD_ONLY, DUAL, LGTM)


def get_obs_mode() -> str:
"""Unset/empty/unrecognized -> ``dd_only`` (current behavior)."""
raw = os.getenv("SGP_OBS_MODE")
if not raw:
return _DEFAULT_MODE
mode = raw.strip().lower()
return mode if mode in _VALID_MODES else _DEFAULT_MODE


def _lgtm_ids() -> Optional[Tuple[str, str]]:
try:
from opentelemetry import trace
except ImportError:
return None
ctx = trace.get_current_span().get_span_context()
if ctx and ctx.is_valid:
return format(ctx.trace_id, "032x"), format(ctx.span_id, "016x")
return None


def _ddtrace_ids() -> Optional[Tuple[str, str]]:
try:
from ddtrace.trace import tracer
except ImportError:
return None
ctx = tracer.current_trace_context()
if ctx and ctx.trace_id:
return format(ctx.trace_id, "032x"), format(ctx.span_id or 0, "016x")
return None


def obs_correlation() -> Dict[str, str]:
"""Return ``{"obs.trace_id": ..., "obs.span_id": ...}`` for the active
observability context, or ``{}`` if none is active.

Never fabricates ids -- this is a correlation tag, not the span's id.
"""
mode = get_obs_mode()
if mode == LGTM:
ids = _lgtm_ids()
elif mode == DUAL:
ids = _lgtm_ids() or _ddtrace_ids()
else: # dd_only
ids = _ddtrace_ids()

if not ids:
return {}
return {"obs.trace_id": ids[0], "obs.span_id": ids[1]}
13 changes: 13 additions & 0 deletions src/agentex/lib/core/tracing/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from agentex.types.span import Span
from agentex.lib.utils.logging import make_logger
from agentex.lib.utils.model_utils import recursive_model_dump
from agentex.lib.core.tracing.obs_ids import obs_correlation
from agentex.lib.core.tracing.span_error import set_span_error
from agentex.lib.core.tracing.span_queue import (
SpanEventType,
Expand Down Expand Up @@ -79,6 +80,12 @@ def start_span(

serialized_input = recursive_model_dump(input) if input else None
serialized_data = recursive_model_dump(data) if data else None
# Tag the business span with the active observability trace_id/span_id
# (OTel/ddtrace) so it can be correlated to the per-turn obs trace. The
# business trace_id stays the run-level task id -- see obs_ids.py.
obs = obs_correlation()
if obs:
serialized_data = {**(serialized_data or {}), **obs}
id = str(uuid.uuid4())

span = Span(
Expand Down Expand Up @@ -229,6 +236,12 @@ async def start_span(

serialized_input = recursive_model_dump(input) if input else None
serialized_data = recursive_model_dump(data) if data else None
# Tag the business span with the active observability trace_id/span_id
# (OTel/ddtrace) so it can be correlated to the per-turn obs trace. The
# business trace_id stays the run-level task id -- see obs_ids.py.
obs = obs_correlation()
if obs:
serialized_data = {**(serialized_data or {}), **obs}
id = str(uuid.uuid4())

span = Span(
Expand Down
22 changes: 20 additions & 2 deletions src/agentex/resources/agents/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ def list(
self,
agent_id: str,
*,
include_live: bool | Omit = omit,
limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
Expand All @@ -289,6 +290,8 @@ def list(
List run schedules for an agent.

Args:
include_live: Include live Temporal state and upcoming action times.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -306,7 +309,13 @@ def list(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
query=maybe_transform({"limit": limit}, schedule_list_params.ScheduleListParams),
query=maybe_transform(
{
"include_live": include_live,
"limit": limit,
},
schedule_list_params.ScheduleListParams,
),
),
cast_to=ScheduleListResponse,
)
Expand Down Expand Up @@ -1056,6 +1065,7 @@ async def list(
self,
agent_id: str,
*,
include_live: bool | Omit = omit,
limit: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
Expand All @@ -1068,6 +1078,8 @@ async def list(
List run schedules for an agent.

Args:
include_live: Include live Temporal state and upcoming action times.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -1085,7 +1097,13 @@ async def list(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
query=await async_maybe_transform({"limit": limit}, schedule_list_params.ScheduleListParams),
query=await async_maybe_transform(
{
"include_live": include_live,
"limit": limit,
},
schedule_list_params.ScheduleListParams,
),
),
cast_to=ScheduleListResponse,
)
Expand Down
Loading
Loading