Skip to content
Draft
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
8 changes: 5 additions & 3 deletions .github/workflows/agentex-tutorials-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ jobs:

- name: Build AgentEx SDK
run: |
echo "🔨 Building AgentEx SDK wheel..."
uv build
echo "✅ SDK built successfully"
echo "🔨 Building both SDK wheels (slim client + heavy ADK overlay)..."
# uv workspace builds both members into the root dist/. --wheel: the
# heavy's cross-dir force-include can't build via the sdist default.
uv build --all-packages --wheel
echo "✅ Both SDK wheels built successfully"
ls -la dist/

- name: Test Tutorial
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ jobs:
version: '0.10.2'

- name: Install dependencies
run: uv sync --all-extras
run: uv sync --all-packages --all-extras

- name: Run lints
run: ./scripts/lint

- name: Check slim dependency set
run: ./scripts/check-slim-deps

build:
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
timeout-minutes: 10
Expand All @@ -51,10 +54,12 @@ jobs:
version: '0.10.2'

- name: Install dependencies
run: uv sync --all-extras
run: uv sync --all-packages --all-extras

- name: Run build
run: uv build
# Both workspace members. --wheel is load-bearing: the heavy's cross-dir
# force-include can't build via the sdist-then-wheel default.
run: uv build --all-packages --wheel

- name: Get GitHub OIDC Token
if: |-
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@ jobs:
run: |
bash ./bin/publish-pypi
env:
# Heavy `agentex-sdk` package token (existing PyPI name).
AGENTEX_PYPI_TOKEN: ${{ secrets.AGENTEX_PYPI_TOKEN }}
# Slim `agentex-client` package token (new PyPI name; needs
# to be added to repo secrets when the slim is registered).
AGENTEX_CLIENT_PYPI_TOKEN: ${{ secrets.AGENTEX_CLIENT_PYPI_TOKEN }}
# Back-compat fallback — used by bin/publish-pypi when the
# dedicated tokens above are unset.
PYPI_TOKEN: ${{ secrets.AGENTEX_PYPI_TOKEN || secrets.PYPI_TOKEN }}
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
".": "0.12.0"
".": "0.12.0",
"adk": "0.12.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: 64
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-46769b729d89151fb7e7ae15725678af99f55ef32d283e34a1e143057aa87b23.yml
openapi_spec_hash: 9115c9f283257e0636aba67fadfeda0a
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-f6bcef733992533e1b1d66c2207e42a934fa0ec9646e3d6a6268fad17e73c834.yml
openapi_spec_hash: 3aae4790b24edf6ea9469c1680d513ae
config_hash: 82cb83ac175dbf40265128506294218b
32 changes: 32 additions & 0 deletions adk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# agentex-sdk

The Agent Development Kit (ADK) overlay for the Agentex API.

## What's in here

This package ships everything under `agentex.lib.*`:

- **ACP server** (`agentex.lib.sdk.fastacp`) — FastAPI-based agent control plane.
- **Temporal workflows** (`agentex.lib.core.temporal`) — durable agent execution.
- **CLI** (`agentex.lib.cli`) — `agentex init`, `agentex run`, deploy helpers.
- **LLM provider integrations** (`agentex.lib.adk.providers`, `agentex.lib.core.temporal.plugins`) — OpenAI Agents, Claude Agent SDK, pydantic-ai, langgraph, litellm.
- **Observability** (`agentex.lib.core.tracing`, `agentex.lib.core.observability`) — SGP, Datadog, OpenTelemetry tracing processors.

## Installation

```sh
pip install agentex-sdk
```

This automatically pulls in [`agentex-client`](../) (the slim Stainless-generated REST client) so `from agentex import Agentex, AsyncAgentex` works the same as before.

## When to use this vs `agentex-client`

- **`agentex-sdk`** — you're authoring agents. Pulls everything: ACP server, Temporal, MCP, LLM providers, observability, CLI. ~37 deps.
- **`agentex-client`** — you only need to call the Agentex REST API. No agent authoring, no Temporal workflows, no FastACP server, no provider integrations. 6 deps.

The two packages contribute disjoint files to the `agentex.*` namespace — `agentex/lib/*` ships only from `agentex-sdk`.

## Repo layout

This package is hand-authored and lives at `adk/` inside [scaleapi/scale-agentex-python](https://github.com/scaleapi/scale-agentex-python). The Stainless generator preserves `adk/**` via `keep_files` so its codegen never touches anything here. The sibling `agentex-client` package lives at the repo root and IS Stainless-generated.
41 changes: 41 additions & 0 deletions adk/hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Builds the agentex/lib force-include map per-file so test files can be pruned
— force-include ignores `exclude` (hatchling #1395)."""

from __future__ import annotations

import os

from hatchling.builders.hooks.plugin.interface import BuildHookInterface

_SKIP_DIRS = {"__pycache__", "tests"}
_SKIP_NAMES = {"conftest.py", "pytest.ini", "run_tests.py"}
# Floor below the ~333 shippable files: a collapse means the walk broke — fail
# loud rather than ship a near-empty wheel.
_MIN_FILES = 320


def _is_test_file(name: str) -> bool:
return name in _SKIP_NAMES or (name.startswith("test_") and name.endswith(".py"))


class CustomBuildHook(BuildHookInterface):
PLUGIN_NAME = "custom"

def initialize(self, version: str, build_data: dict) -> None: # noqa: ARG002
lib_root = os.path.normpath(os.path.join(self.root, "..", "src", "agentex", "lib"))
force_include = build_data.setdefault("force_include", {})
collected = 0
for dirpath, dirnames, filenames in os.walk(lib_root):
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
for name in filenames:
if _is_test_file(name):
continue
src = os.path.join(dirpath, name)
rel = os.path.relpath(src, lib_root)
force_include[src] = os.path.join("agentex", "lib", rel)
collected += 1
if collected < _MIN_FILES:
raise RuntimeError(
f"agentex/lib force-include collected only {collected} files "
f"(expected >= {_MIN_FILES}); aborting build."
)
105 changes: 105 additions & 0 deletions adk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
[project]
# Hand-authored ADK overlay for agentex. This package contributes only
# `agentex/lib/*` to the agentex.* namespace; the REST client surface
# (agentex/{__init__.py, _*.py, types/, resources/}) ships from the slim
# sibling package `agentex-client` which is pinned as a runtime dep.
#
# This entire `adk/` directory must be preserved across Stainless codegen
# via `keep_files: ["adk/**"]` in the Stainless dashboard config.
name = "agentex-sdk"
version = "0.12.0"
description = "Agent Development Kit (ADK) overlay for the Agentex API — FastACP server, Temporal workflows, LLM provider integrations, observability"
license = "Apache-2.0"
authors = [
{ name = "Agentex", email = "roxanne.farhad@scale.com" },
]
readme = "README.md"

dependencies = [
# Co-released in lockstep; floor-only by design — a ceiling would
# eventually exclude the co-versioned slim (release-please can't bump it).
"agentex-client>=0.12.0",
# CLI surface (agentex.lib.cli.*, agentex.lib.sdk.config.*)
"typer>=0.16,<0.17",
"questionary>=2.0.1,<3",
"rich>=13.9.2,<14",
"yaspin>=3.1.0",
"pyyaml>=6.0.2,<7",
"python-on-whales>=0.73.0,<0.74",
"kubernetes>=25.0.0,<36.0.0",
"jsonref>=1.1.0,<2",
"jsonschema>=4.23.0,<5",
"jinja2>=3.1.3,<4",
"watchfiles>=0.24.0,<1.0",
# ACP server (FastAPI app surface)
"fastapi>=0.115.0",
"starlette>=0.49.1",
"uvicorn>=0.31.1",
"aiohttp>=3.10.10,<4",
# Temporal workflows
"temporalio>=1.26.0,<2",
"cloudpickle>=3.1.1",
# Async streaming infra
"redis>=5.2.0,<8",
# LLM provider integrations
"litellm>=1.83.7,<2",
"openai-agents>=0.14.3,<0.15",
"openai>=2.2,<3", # Required by openai-agents; litellm now supports openai 2.x (issue #13711 resolved: https://github.com/BerriAI/litellm/issues/13711)
"claude-agent-sdk>=0.1.0",
"pydantic-ai-slim>=1.0,<2",
"langgraph-checkpoint>=2.0.0",
"scale-gp>=0.1.0a59",
"scale-gp-beta>=0.2.0",
"mcp>=1.4.1",
# Observability
"ddtrace>=3.13.0",
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
"json_log_formatter>=1.1.1",
]

# agentex/lib/* uses `from typing import override` (3.12+) in 19 files.
# The slim agentex-client keeps 3.11 support.
requires-python = ">= 3.12,<4"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: Apache Software License",
]

[project.urls]
Homepage = "https://github.com/scaleapi/scale-agentex-python"
Repository = "https://github.com/scaleapi/scale-agentex-python"

[project.scripts]
agentex = "agentex.lib.cli.commands.main:app"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

# Ship only agentex/lib/*, pulled in from the parent repo's `src/agentex/lib`
# tree. The rest of agentex.* (the Stainless-generated client) ships from the
# sibling agentex-client package, which this package pins as a runtime dep.
# Stainless explicitly preserves `src/agentex/lib/` across codegen (per
# CONTRIBUTING.md), so it's safe to keep the source where it is.
[tool.hatch.build.targets.wheel]
bypass-selection = true

# Builds the ../src/agentex/lib force-include map per-file (see hatch_build.py)
# so test files can be pruned — force-include ignores `exclude` (hatchling #1395).
[tool.hatch.build.targets.wheel.hooks.custom]
path = "hatch_build.py"

# Sdist deferred: hatchling can't represent the wheel's ../src/agentex/lib
# force-include in an sdist include list. CI + bin/publish-pypi pass --wheel.
[tool.hatch.build.targets.sdist]
include = [
"/pyproject.toml",
"/README.md",
]
16 changes: 14 additions & 2 deletions bin/check-release-environment
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
#!/usr/bin/env bash

# This script is run by Release Doctor to validate the release environment.
# After the dual-package split (slim agentex-client + heavy agentex-sdk),
# both PyPI tokens must be present — one for each package name. If only
# PYPI_TOKEN is set, fall back to using it for both (back-compat for legacy
# single-token setups, which forces an account-scoped token).

errors=()

if [ -z "${PYPI_TOKEN}" ]; then
errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
# Heavy `agentex-sdk` token (existing PyPI name).
if [ -z "${AGENTEX_PYPI_TOKEN}" ] && [ -z "${PYPI_TOKEN}" ]; then
errors+=("The AGENTEX_PYPI_TOKEN secret has not been set (and no fallback PYPI_TOKEN). Add it in repo secrets so the heavy 'agentex-sdk' package can be published.")
fi

# Slim `agentex-client` token (new PyPI name).
if [ -z "${AGENTEX_CLIENT_PYPI_TOKEN}" ] && [ -z "${PYPI_TOKEN}" ]; then
errors+=("The AGENTEX_CLIENT_PYPI_TOKEN secret has not been set (and no fallback PYPI_TOKEN). Add it in repo secrets so the slim 'agentex-client' package can be published. Falling back to PYPI_TOKEN requires an account-scoped token.")
fi

lenErrors=${#errors[@]}
Expand Down
13 changes: 10 additions & 3 deletions bin/publish-pypi
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#!/usr/bin/env bash

# Publish slim (root) before heavy (adk/): heavy pins slim, so a slim-first
# failure aborts before shipping a heavy that needs an unreleased client.

set -eux

rm -rf dist
mkdir -p dist
uv build
uv publish --token=$PYPI_TOKEN
# --wheel: the heavy's cross-dir force-include can't build via sdist.
uv build --all-packages --wheel

# --check-url makes the per-component-tag double-trigger idempotent.
uv publish --check-url https://pypi.org/simple/ --token="${AGENTEX_CLIENT_PYPI_TOKEN:-$PYPI_TOKEN}" dist/agentex_client-*.whl
uv publish --check-url https://pypi.org/simple/ --token="${AGENTEX_PYPI_TOKEN:-$PYPI_TOKEN}" dist/agentex_sdk-*.whl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
The router and tools are ``async`` so LangGraph awaits them directly — a sync
callable would be offloaded via ``run_in_executor``, which Temporal's workflow
event loop does not support.

The in-workflow ``tools`` node is a plain ``async`` function rather than
LangGraph's ``ToolNode`` prebuilt on purpose. The plugin wraps an in-workflow
node in ``wrap_workflow``, whose closure captures the wrapped object. When that
object is itself a LangChain ``Runnable`` (as ``ToolNode`` is), LangGraph's
``compile()`` subgraph detection (``find_subgraph_pregel`` →
``get_function_nonlocals``) recurses through that wrapper without cycle
detection and never terminates, tripping Temporal's deadlock detector. A plain
function isn't a ``Runnable``, so compile stays trivial.
"""

from __future__ import annotations
Expand All @@ -26,12 +35,14 @@

from langgraph.graph import END, START, StateGraph
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode
from langchain_core.messages import SystemMessage
from langchain_core.messages import ToolMessage, SystemMessage
from langgraph.graph.message import add_messages

from project.tools import TOOLS

# Look up tools by name for the in-workflow tools node.
_TOOLS_BY_NAME = {tool.name: tool for tool in TOOLS}

# Name this graph is registered under in the LangGraphPlugin (acp.py / run_worker.py).
GRAPH_NAME = "at130-langgraph"
MODEL_NAME = "gpt-4o"
Expand All @@ -58,6 +69,28 @@ async def agent_node(state: AgentState) -> dict[str, Any]:
return {"messages": [await llm.ainvoke(messages)]}


async def tools_node(state: AgentState) -> dict[str, Any]:
"""Run the tool calls the model requested. Runs inline in the workflow.

A plain ``async`` function (not LangGraph's ``ToolNode``) — see the module
docstring for why a ``Runnable`` tools node can't be compiled here.
"""
last = state["messages"][-1]
results: list[Any] = []
for call in getattr(last, "tool_calls", None) or []:
tool = _TOOLS_BY_NAME.get(call["name"])
# Mirror ToolNode: surface an unknown/hallucinated tool name as an error
# ToolMessage so the graph keeps running instead of crashing the node.
if tool is None:
output = f"Error: unknown tool {call['name']!r}. Available: {list(_TOOLS_BY_NAME)}"
else:
output = await tool.ainvoke(call["args"])
results.append(
ToolMessage(content=str(output), tool_call_id=call["id"], name=call["name"])
)
return {"messages": results}


async def route_after_agent(state: AgentState) -> str:
"""Go to the tools node if the model requested tools, else finish (async router)."""
last = state["messages"][-1]
Expand All @@ -72,7 +105,7 @@ def build_graph() -> StateGraph:
agent_node,
metadata={"execute_in": "activity", "start_to_close_timeout": timedelta(minutes=5)},
)
builder.add_node("tools", ToolNode(TOOLS), metadata={"execute_in": "workflow"})
builder.add_node("tools", tools_node, metadata={"execute_in": "workflow"})
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", route_after_agent, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")
Expand Down
Loading
Loading