Skip to content

Commit d720896

Browse files
jeffdupontclaude
andcommitted
Merge origin/main into jdupont/aic-3672-release-1-0
Takes main's pyproject.toml files (with #218's new dependencies) and keeps this branch's internal pin, launchdarkly-ai-server>=1.0.0,<2, in all 13. Also fixes release-please-config.json: 12727f3 wrote the release-as lines with literal backslashes (\"release-as\": \"1.0.0\"), which is invalid JSON. The file now parses, with release-as 1.0.0 on all 14 packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 12727f3 + 5860b40 commit d720896

133 files changed

Lines changed: 1786 additions & 665 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,21 @@ jobs:
8686
echo "Building $pkg..."
8787
(cd "$pkg" && uv build --out-dir dist)
8888
done
89+
90+
# ─── Packaging ──────────────────────────────────────────────────────────────
91+
# Installs each wheel into an empty virtualenv with its documented install
92+
# command, then imports its public modules and every module its source
93+
# imports, lazy ones included. The workspace test run cannot catch an
94+
# undeclared dependency, because it installs every package's at once.
95+
packaging:
96+
name: Packaging check
97+
needs: [lint, typecheck, test]
98+
runs-on: ubuntu-latest
99+
steps:
100+
- uses: actions/checkout@v4
101+
- uses: astral-sh/setup-uv@v5
102+
with:
103+
python-version: "3.12"
104+
- run: uv lock
105+
- run: uv sync --all-packages
106+
- run: make check-packaging

‎AGENTS.md‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,12 @@ Every public module has a committed API report at `packages/<package>/api-report
8686

8787
PRs are squash-merged with the PR title as the commit message, and release-please builds each package's changelog from those messages.
8888

89-
- **A change to an experimental feature uses that feature's scope:** `evaluations` (Evaluations from Code). `feat`, `fix` and `perf` commits with that scope are listed under **Experimental** (`changelog-sections` in `release-please-config.json`), for example `feat(evaluations): score rows in parallel`. Any other scope, such as `client` or `skills`, lists the change as a core feature or fix. Agent Skills is core, so `feat(skills):` lands under Features.
90-
- **Use exactly one scope.** `feat(evaluations,client):` matches no Experimental row, so it lands under Features.
89+
- **A change to an experimental feature uses that feature's scope.** `feat`, `fix` and `perf` commits with that scope are listed under **Experimental** (`changelog-sections` in `release-please-config.json`). No feature is experimental now. Any other scope, such as `client`, `skills` or `evaluations`, lists the change as a core feature or fix. Agent Skills and Evaluations from Code are core, so `feat(skills):` and `feat(evaluations):` land under Features.
90+
- **Use exactly one scope.** `feat(beta,client):` matches no Experimental row, so it lands under Features.
9191
- **A PR that changes both core and experimental behaviour** should be split. If it can't be, add a `BEGIN_COMMIT_OVERRIDE` block to its description with one conventional-commit line per change. An override also corrects a merged PR's title, as long as the release PR hasn't merged yet.
92-
- **A breaking experimental change** uses `!` while the package is 0.x: `feat(evaluations)!:` bumps the minor version, which is what an experimental break is allowed. After 1.0, `!` would force a major release. Use `feat(evaluations):` without `!` instead, and say the change is breaking in the title.
92+
- **A breaking experimental change** uses `!` while the package is 0.x: `feat(beta)!:` bumps the minor version, which is what an experimental break is allowed. After 1.0, `!` would force a major release. Use `feat(beta):` without `!` instead, and say the change is breaking in the title.
9393
- **When a feature is promoted to core,** remove its rows from `changelog-sections`.
9494

95-
The rule is specified in `TESTING.md` §0.3 in `launchdarkly/ai-sdks-monorepo`.
96-
9795
## Package Hierarchy
9896

9997
The monorepo is organized into three tiers plus a convenience barrel. Dependencies only flow **downward** — never sideways between packages in the same tier, and never upward.

‎Makefile‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: start test typecheck lint format format-check lint-fix build clean api-report api-report-check
1+
.PHONY: start test typecheck lint format format-check lint-fix build check-packaging clean api-report api-report-check
22

33
# Run main.py (uv run python main.py)
44
start:
@@ -49,6 +49,14 @@ build:
4949
(cd $$pkg && uv build --out-dir dist); \
5050
done
5151

52+
# Install each built wheel into an empty virtualenv and check its imports resolve there
53+
check-packaging:
54+
rm -rf dist
55+
@for pkg in packages/*/; do \
56+
(cd $$pkg && uv build --quiet --wheel --out-dir ../../dist); \
57+
done
58+
uv run python scripts/check_packaging.py dist
59+
5260
# Clean build artifacts
5361
clean:
5462
rm -rf packages/*/dist

‎README.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,26 @@ Tier 0 — Core Client (launchdarkly-ai-server)
9191
| [`launchdarkly-ai-vercel-agents`](packages/vercel-agents/README.md) | `*` (any) | `agent` | Native Vercel `ai.Agent` loop and graph adapter |
9292
| [`launchdarkly-ai-typesafe`](packages/typesafe/README.md) | TypeSafe | `messages` | TypeSafe Jev judge handler |
9393

94+
### Versioning
95+
96+
From 1.0.0 every package in this SDK is released at the same version, so install them at matching versions. Each package depends on `launchdarkly-ai-server` within the same major (`>=1.0,<2`), and a breaking change in any package is a new major for all of them. Names exported from `launchdarkly_ai_server.experimental` are the exception: they may change in a minor release.
97+
98+
Every third-party floor sits on the release line CI tests: the same major as the tested version for a 1.x or later library, the same minor for a 0.x one, where a minor release may break. A floor that was below that line was raised to the tested version; a floor already on it stayed where it was.
99+
94100
## Quick Start
95101

96102
### 1. Install
97103

98104
```bash
99-
pip install launchdarkly-ai-python launchdarkly-server-sdk launchdarkly-ai-openai-messages
105+
pip install "launchdarkly-ai-python[ld]" launchdarkly-ai-openai-messages
100106
```
101107

102-
`launchdarkly-ai-python` is a thin barrel that re-exports all of `launchdarkly-ai-server`. The base LaunchDarkly Python SDK is a peer dependency rather than a bundled one, so install `launchdarkly-server-sdk` alongside it. `init_client()` discovers it at runtime, or you can skip it entirely by passing a pre-initialized client with `init_client(client=my_client)`.
108+
`launchdarkly-ai-python` is a thin barrel that re-exports all of `launchdarkly-ai-server`. The base LaunchDarkly Python SDK is an optional dependency rather than a bundled one; the `[ld]` extra installs `launchdarkly-server-sdk`. `init_client()` discovers it at runtime, or you can skip it entirely by passing a pre-initialized client with `init_client(client=my_client)`.
103109

104110
**With telemetry** (recommended for production) — traces export to the LaunchDarkly Observability dashboard:
105111

106112
```bash
107-
pip install "launchdarkly-ai-python[otel]" launchdarkly-server-sdk launchdarkly-ai-openai-messages
113+
pip install "launchdarkly-ai-python[ld,otel]" launchdarkly-ai-openai-messages
108114
```
109115

110116
No code changes are needed — `init_client()` detects whether the OTel packages are present at runtime and configures the tracer provider automatically. If they are absent, the SDK logs a one-time warning and continues normally.
@@ -574,7 +580,7 @@ When running inside `graph()`, every node's events carry the graph key, tool inv
574580

575581
### Optional dependencies
576582

577-
The OpenTelemetry SDK packages are **optional** — detected at runtime via `importlib`. The LaunchDarkly server SDK (`launchdarkly-server-sdk`) is also an optional dependency; pass a pre-initialized client to `init_client(client=...)` if you bring your own.
583+
The OpenTelemetry SDK packages are **optional** — detected at runtime via `importlib`. The LaunchDarkly server SDK (`launchdarkly-server-sdk`) is also an optional dependency, installed by the `[ld]` extra; pass a pre-initialized client to `init_client(client=...)` if you bring your own.
578584

579585
**OTel packages** (installed via `pip install "launchdarkly-ai-python[otel]"` or `pip install "launchdarkly-ai-server[otel]"`):
580586
- **If installed:** `init_client()` sets up a `TracerProvider` with a GZIP-compressed OTLP HTTP exporter and W3C trace-context/baggage propagators — no code changes needed.

‎packages/ai/README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Convenience barrel package for the LaunchDarkly AI Python SDK. Re-exports the co
55
## Installation
66

77
```bash
8-
pip install launchdarkly-ai-python launchdarkly-ai-openai-messages
8+
pip install "launchdarkly-ai-python[ld]" launchdarkly-ai-openai-messages
99
```
1010

1111
To enable trace export to the LaunchDarkly Observability dashboard, install the `otel` extras group:
@@ -70,10 +70,10 @@ Experimental features are not re-exported from this package. They live in
7070
`launchdarkly-ai-server`, the package this one depends on, and need a `launchdarkly-ai-server`
7171
release that includes them. Import them from `launchdarkly_ai_server.experimental`; if that
7272
import fails, upgrade `launchdarkly-ai-server` itself, since upgrading this package alone can
73-
leave an older `launchdarkly-ai-server` in place. The experimental feature is:
73+
leave an older `launchdarkly-ai-server` in place. No feature is experimental now.
7474

75-
- **Evaluations from Code**, in `launchdarkly_ai_server.experimental.evaluations`; see the
76-
[evaluations guide](https://github.com/launchdarkly/python-ai-sdk/blob/main/packages/client/README.md#run-an-evaluation-from-code-experimental).
75+
Evaluations from Code is core, so this package re-exports it. See the
76+
[evaluations guide](https://github.com/launchdarkly/python-ai-sdk/blob/main/packages/client/README.md#run-an-evaluation-from-code).
7777

7878
---
7979

‎packages/ai/pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ license = "Apache-2.0"
99
authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}]
1010
keywords = ["launchdarkly", "ai", "feature-flags", "sdk"]
1111
classifiers = [
12-
"Development Status :: 3 - Alpha",
12+
"Development Status :: 5 - Production/Stable",
1313
"Intended Audience :: Developers",
1414
"License :: OSI Approved :: Apache Software License",
1515
"Programming Language :: Python :: 3",
@@ -24,6 +24,7 @@ Repository = "https://github.com/launchdarkly/python-ai-sdk"
2424

2525
[project.optional-dependencies]
2626
otel = ["launchdarkly-ai-server[otel]>=1.0.0,<2"]
27+
ld = ["launchdarkly-ai-server[ld]>=1.0.0,<2"]
2728

2829
[build-system]
2930
requires = ["hatchling>=1.32,<2"]

‎packages/ai/src/launchdarkly_ai_python/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
__version__ = "0.4.0" # x-release-please-version
77

88
from launchdarkly_ai_server import * # noqa: F403
9+
from launchdarkly_ai_server import __all__ as __all__
910
from launchdarkly_ai_server import register_ai_sdk_package
1011

1112
register_ai_sdk_package("launchdarkly-ai-python", __version__)

‎packages/ai/tests/test_reexports.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""
2-
Tests for §5 launchdarkly-ai-python re-export barrel.
3-
Reference: TESTING.md §5
2+
Tests for the launchdarkly-ai-python re-export barrel.
43
"""
54

65
import importlib
@@ -16,6 +15,12 @@ def test_all_named_exports_from_server_are_reexported(self) -> None:
1615
f"launchdarkly_ai_python is missing re-export: {name!r}"
1716
)
1817

18+
def test_all_matches_server(self) -> None:
19+
server = importlib.import_module("launchdarkly_ai_server")
20+
barrel = importlib.import_module("launchdarkly_ai_python")
21+
22+
assert barrel.__all__ == server.__all__
23+
1924
def test_reexported_values_are_identical_references(self) -> None:
2025
server = importlib.import_module("launchdarkly_ai_server")
2126
barrel = importlib.import_module("launchdarkly_ai_python")
@@ -27,9 +32,9 @@ def test_reexported_values_are_identical_references(self) -> None:
2732
f"launchdarkly_ai_python.{name} is not the same object as launchdarkly_ai_server.{name}"
2833
)
2934

30-
def test_experimental_evaluations_are_not_reexported(self) -> None:
31-
importlib.import_module("launchdarkly_ai_server.experimental.evaluations")
35+
def test_evaluations_are_reexported(self) -> None:
36+
server = importlib.import_module("launchdarkly_ai_server")
3237
barrel = importlib.import_module("launchdarkly_ai_python")
3338

3439
for name in ("init_evaluations", "EvalTool", "Judge", "Scorer", "DatasetRow"):
35-
assert not hasattr(barrel, name), name
40+
assert getattr(barrel, name) is getattr(server, name), name

‎packages/claude-agents/README.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Anthropic Claude handler for `launchdarkly-ai-server` using the **Claude Agent S
77
## Installation
88

99
```bash
10-
pip install launchdarkly-ai-server launchdarkly-ai-claude-agents
10+
pip install "launchdarkly-ai-server[ld]" launchdarkly-ai-claude-agents
1111
```
1212

1313
Set `ANTHROPIC_API_KEY` in your environment (the Anthropic SDK reads it automatically).
@@ -74,6 +74,15 @@ to_claude_agents(
7474
)
7575
```
7676
77+
### Output limit and thinking
78+
79+
- `max_tokens` in the config's model parameters sets the output-token limit for each model request. The Agent SDK has no option for it, so the handler sets `CLAUDE_CODE_MAX_OUTPUT_TOKENS` in the environment of the Claude Code process. Without `max_tokens`, Claude Code uses its own default. A `CLAUDE_CODE_MAX_OUTPUT_TOKENS` in the `env` of a Claude Code settings file, including `~/.claude.json`, wins over `max_tokens`.
80+
- Thinking is off unless the config sets `thinking` or `max_thinking_tokens`. Models that reject disabled thinking, such as `claude-fable-5`, still use their own default. Claude Code lowers a thinking budget to one less than `max_tokens`, so when a config sets both, keep `max_tokens` above the budget.
81+
82+
#### Migrating from a version that used Claude Code's thinking default
83+
84+
Earlier versions passed no thinking setting when the config had none, so Claude Code chose one: adaptive thinking on models that support it, and a 31,999-token budget on others such as `claude-haiku-4-5`. A run now thinks only when its config asks. To keep thinking, set it in the config's model parameters, for example `{"thinking": {"type": "adaptive"}}`.
85+
7786
### Convenience wrapper
7887

7988
```python

‎packages/claude-agents/agents.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def claude_graph(key: str, *, cwd=None, setting_sources=None, **options): ...
5555

5656
# Native graph adapter — builds a Claude code-agents graph from a resolved GraphDefinition.
5757
# opts: context, tool_handlers, and the host options cwd and setting_sources
58-
def to_claude_agents(def_promise: Awaitable[dict], opts: dict | None = None): ...
58+
def to_claude_agents(def_promise: Awaitable[GraphDefinition], opts: ClaudeNativeGraphOptions | None = None) -> NativeGraphRunner: ...
5959

6060
# NativeTool sentinels for Claude Code built-in capabilities
6161
ClaudeBash: NativeTool
@@ -141,7 +141,7 @@ The CLI also writes its own `AssistantMessage`, with `model` set to the literal
141141
an API call fails and after a response stops at `max_output_tokens`. It is not a model response:
142142
it gets no `chat` span and never joins the conversation a later span reports.
143143

144-
**Important:** a bare `return` inside `async for` abandons the generator. Python's asyncio finalizer will try to `aclose()` it later and may raise `RuntimeError: aclose(): asynchronous generator is already running` if the generator is still suspended inside a real SDK `await`. Always use `break` + explicit `aclose()`. See **Appendix A.4** in `TESTING.md`.
144+
**Important:** a bare `return` inside `async for` abandons the generator. Python's asyncio finalizer will try to `aclose()` it later and may raise `RuntimeError: aclose(): asynchronous generator is already running` if the generator is still suspended inside a real SDK `await`. Always use `break` + explicit `aclose()`.
145145

146146
### 4. Telemetry
147147

@@ -236,6 +236,6 @@ Span names and attributes are described in [Implementation Details → Telemetry
236236
## Common Pitfalls
237237

238238
- **MCP tool name prefix**: tools registered in the MCP server are accessible as `mcp__tool-mcp__<name>`. The `allowed_tools` list must use this prefix; omitting it will cause the agent to not invoke any tools.
239-
- **Async generator teardown**: always use `break` inside `async for` (not `return`) and wrap the loop in `try/finally: await gen.aclose()`. See TESTING.md Appendix A.4.
239+
- **Async generator teardown**: always use `break` inside `async for` (not `return`) and wrap the loop in `try/finally: await gen.aclose()`.
240240
- **`message.usage` shape**: the raw usage dict from the `claude_agent_sdk` is passed through directly to `parse_usage`. It accepts `input_tokens`/`output_tokens` which the SDK provides.
241241
- **Native tools** (`ClaudeWebSearch`, etc.) are registered in `tool_handlers` as `NativeTool` sentinel instances (not callables). The handler's `_partition_tools` function separates them from user-defined tools — native tools go into `native_tool_names` for the agent's built-in access, user tools go through the MCP server.

0 commit comments

Comments
 (0)