Skip to content

fix(AIC-3598)!: make Python packaging and public types GA-ready - #218

Merged
jeffdupont merged 12 commits into
mainfrom
fix/AIC-3598-packaging-ga
Oct 29, 2026
Merged

jeffdupont merged 12 commits into
mainfrom
fix/AIC-3598-packaging-ga

Conversation

@jeffdupont

@jeffdupont jeffdupont commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Python half of AIC-3598. The spec change is launchdarkly/ai-sdks-monorepo#91.

Packaging

  • Classifiers. All 14 packages say Development Status :: 5 - Production/Stable. The two vercel-* packages had no classifiers at all.

  • Third-party floors move to the version CI tests wherever the old floor crossed a breaking line (a major for 1.x+ libraries, a minor for 0.x):

    • langchain-core>=1.6, langgraph>=1.2, openai>=2.44, openai-agents>=0.17
    • anthropic>=0.113, httpx>=0.28, typesafe-sdk>=0.7

    The rule, as the shared spec states it: a floor sits on the line CI tests, the same major as the tested version for a 1.x+ library and the same minor for a 0.x one. The README says so.

    Floors already on the tested major stay put (opentelemetry-api>=1.25, litellm>=1.83).

  • [ld] extra on launchdarkly-ai-server and launchdarkly-ai-python installs launchdarkly-server-sdk>=9.18.3,<10. It's an extra rather than a dependency so bring-your-own-client installs stay lean, as client/AGENTS.md requires. The READMEs and the init_client error message point to it.

  • Undeclared LangChain provider packages, found by the new check. Both langchain packages imported langchain_openai and langchain_anthropic without declaring them, the Python side of the JS @langchain/anthropic gap:

    • langchain-agents now depends on langchain-openai, its default model, and offers [anthropic] and [aws] extras.
    • langchain-messages now depends on langchain-openai, its default model, as langchain-agents does, and offers [anthropic] and [aws].
    • The Anthropic and OpenAI imports are now guarded with an install hint, as Bedrock's already was.
  • Internal ranges: every package pins launchdarkly-ai-server>=0.6,<2. The native graph adapters import NativeGraphRunner, which published server 0.5.0 lacks, so >=0.4.1 would let a handler resolve to a core it can't import from. >=0.6 resolves whichever lands first, this PR's 0.6.0 or the 1.0.0 cut; chore(AIC-3672): release every Python package as 1.0.0 #242 (AIC-3672) tightens it to >=1.0.0,<2.

Public types

  • LDClientInterface becomes a Protocol. It was a plain class with async methods, which matched neither the synchronous ldclient.LDClient nor the "maybe awaitable" way core calls it. ldclient.LDClient now satisfies it structurally; checked with mypy in both directions (a client missing track is rejected). init_client and get_client return it. variation, flush and close may be async; track must be synchronous (-> None), because no call site awaits it.
  • tool_handlers on the handler protocol and in every handler is typed Mapping[str, Callable[..., Any] | NativeTool] instead of dict[str, Any], so a caller's precisely typed dict is accepted. Offline evaluations pass a NativeTool to the handler unwrapped, so langchain-agents now checks callable() before calling. It used to raise TypeError: 'NativeTool' object is not callable; there's a test, and it fails without the fix.
  • Native graph adapters (to_lang_graph, to_openai_agents, to_claude_agents, to_litellm_agents, to_vercel_agents):
    • take Awaitable[GraphDefinition]
    • take a per-package options TypedDict exported from the root
    • return the new core NativeGraphRunner protocol. They return a small class rather than a SimpleNamespace, because mypy accepts any SimpleNamespace as any protocol.

Packaging check (make check-packaging, new CI job packaging)

scripts/check_packaging.py installs each wheel alone, with no extras, into an empty venv, which proves core loads without launchdarkly-server-sdk. It imports every public module and __all__ name. Then it checks that every module the source imports unconditionally, lazy importlib.import_module calls included, comes from a distribution the package declares; installed through some other dependency is not enough. That found undeclared pydantic in four packages and openai in litellm-agents, now declared. Each declared extra gets its own venv and must import what EXTRA_IMPORTS lists for it; an extra with no entry fails. Internal dependencies resolve to this commit's wheels and third-party ones from PyPI. Each import is resolved by its full dotted name to the distribution that owns it, so a namespace package such as opentelemetry.sdk needs opentelemetry-sdk declared; that found langgraph.prebuilt undeclared in langchain-agents. The 22 runs go through a thread pool.

Checks

  • make lint, make format-check, make typecheck (no issues in 104 files), make api-report-check (15 reports current)
  • make test: 3,651 passed, 18 skipped
  • make check-packaging: all 14 packages and 8 extras pass; removing pydantic from vercel-agents makes it fail with pydantic (from pydantic, not declared)

Versioning

The README documents the 1.0 rule: from 1.0.0, every package is released at the same version.

🤖 Generated with Claude Code


Note

Overview
Prepares the Python AI SDK monorepo for GA by tightening packaging, install docs, and public types, plus a CI gate that catches undeclared dependencies workspace tests miss.

Packaging and installs. All packages move to Production/Stable classifiers; third-party floors are raised to versions CI exercises (LangChain 1.x, OpenAI 2.x, etc.). launchdarkly-ai-server[ld] and launchdarkly-ai-python[ld] now install bounded launchdarkly-server-sdk; READMEs drop separate launchdarkly-server-sdk lines. Handlers pin launchdarkly-ai-server>=0.6,<2. LangChain packages declare langchain-openai and add [anthropic] / [aws] extras with guarded import errors; missing deps like pydantic and langgraph-prebuilt are added where the new checker found gaps.

make check-packaging + CI packaging job. New scripts/check_packaging.py builds wheels, installs each package (and each extra) in an isolated venv, validates public __all__ imports, and asserts every unconditional/lazy import is owned by a declared distribution—not a transitive install.

Public API typing. LDClientInterface is a Protocol aligned with real ldclient usage; get_client / init_client return it. tool_handlers is Mapping[str, Callable[..., Any] | NativeTool] across core and handlers (LangChain agents reject unwrapped NativeTool). Native graph to_* adapters take Awaitable[GraphDefinition], export per-package options TypedDicts, and return NativeGraphRunner instead of untyped namespaces.

README adds 1.0 synchronized versioning policy for the SDK.

Reviewed by Cursor Bugbot for commit bdd635b. Bugbot is set up for automated code reviews on this repo. Configure here.

jeffdupont and others added 3 commits October 26, 2026 12:52
- Production/Stable classifiers on all 14 packages (vercel-* had none).
- Raise third-party floors that crossed a breaking line to the tested
  versions in uv.lock (langchain-core, langgraph, openai, openai-agents,
  anthropic, httpx, typesafe-sdk).
- Add an optional [ld] extra for launchdarkly-server-sdk on
  launchdarkly-ai-server and launchdarkly-ai-python; READMEs install it.
- LDClientInterface becomes a Protocol that ldclient.LDClient satisfies;
  init_client and get_client return it.
- Handler protocol and every handler type tool_handlers as
  dict[str, Callable | NativeTool]; offline evaluations pass a NativeTool
  through unwrapped, so langchain-agents now guards with callable().
- Native graph adapters take Awaitable[GraphDefinition], a per-package
  options TypedDict, and return NativeGraphRunner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck

- scripts/check_packaging.py (make check-packaging, CI job "packaging")
  installs each wheel into an empty virtualenv with its documented install
  command, imports every public module and __all__ name, and checks every
  module the source imports unconditionally is installed, lazy
  importlib.import_module calls included. Internal dependencies resolve to
  this commit's wheels; third-party ones from PyPI.
- The check found langchain-agents and langchain-messages importing
  langchain_openai and langchain_anthropic without declaring them, the
  Python side of the JS @langchain/anthropic gap. langchain-agents now
  depends on langchain-openai (its default model) and offers [anthropic]
  and [aws] extras; langchain-messages offers [openai], [anthropic], [aws].
  The Anthropic and OpenAI imports are guarded with an install hint, like
  Bedrock's.
- README: document the 1.0 versioning rule (lockstep per language).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeffdupont jeffdupont changed the title fix(AIC-3598): make Python packaging and public types GA-ready fix(AIC-3598)!: make Python packaging and public types GA-ready Oct 27, 2026
…itle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeffdupont added a commit to launchdarkly/js-ai-sdk that referenced this pull request Oct 27, 2026
…angchain/anthropic (#170)

TypeScript half of
[AIC-3598](https://launchdarkly.atlassian.net/browse/AIC-3598). The spec
is in launchdarkly/ai-sdks-monorepo#91 (TESTING.md §5.3, §1.4, A.21).
The Python half is launchdarkly/python-ai-sdk#218.

## Changes

- **`toolHandlers` on the handler contract.** `ProviderHandler.stream`
and `createHandler`'s input types took `Record<string, any>`. They now
take `Record<string, ToolHandlerFn | NativeTool>`, which matches the
blocking call signature and what apps already pass.
- The union is the real contract, not functions only: offline
evaluations (`evaluations-runner.ts`) pass the trajectory recorder's map
to the handler without `wrapToolHandlers`, so a `NativeTool` arrives
unwrapped.
- Five handlers typed the map narrower, which the old `any` hid:
`claude-agents`, `claude-messages`, `langchain-agents`,
`litellm-messages` and `openai-messages`. They now accept the union.
- Runtime behavior is unchanged, except in `claude-agents`' MCP tool
executor. It now guards with `typeof handler !== 'function'`, so a raw
`NativeTool` gets the "No handler registered" error instead of `handler
is not a function`. There's a test, and it fails without the fix.
- **`@langchain/anthropic`** is declared by
`@launchdarkly/ai-langchain-agents` as an optional peer plus a
devDependency, the same pattern as `@langchain/aws`. The package
dynamically imports it for Anthropic nodes. `toolHandlers` on the
app-facing API was already typed.
- **`zod`** is declared by `@launchdarkly/ai-langchain-agents`
(`^4.4.3`, like its siblings). `native-graph.ts` imports it statically
and was getting it through LangChain.
- **Packaging check, `yarn check:packaging`**, added to the CI build
job:
- It fails when a package's built `dist` imports a package it doesn't
declare, static or dynamic.
- It then packs every workspace, installs all the tarballs into one
empty project, and loads every `exports` subpath through both
`require()` and `import()`.
- The scan has unit tests (`yarn test:check-packaging`). It found the
`zod` gap above.
- **README** documents the 1.0 rule: from 1.0.0, every package is
released at the same version.

## Breaking (the reason for the `!`)

`ProviderHandler.stream`'s declaration changed, so
`scripts/api-breaking.mjs` will flag it. A custom handler whose `stream`
typed `toolHandlers` narrower than the union no longer type-checks.
Before 1.0 the `!` bumps the minor version, and AIC-3673 cuts 1.0 next.
If owners judge it compatible, `api-compatible` is the alternative.

## Checks

- `yarn build`, `yarn typecheck` (0 errors), `yarn code:check`, `yarn
sherif`
- `yarn api:check` (only `ai-server.api.md` changed, the `stream` line)
and `yarn api:surface`
- `yarn test`: every workspace passes
- `yarn check:packaging`: 14 packages declare every import, and all 16
entry points load
- `yarn.lock` is unchanged

## Known, out of scope

- `@langchain/anthropic@1.5.1` (the pinned dev version, as in
`langchain-messages`) asks for `@langchain/core@^1.2.1`, while the
lockfile resolves 1.1.49. That predates this change.
- In both SDKs, `claude-agents` recognises a native tool only through
the tracking stub's `NATIVE_TOOL_KEY`. On the offline evaluations path a
raw `NativeTool` is therefore not wired to Claude's built-in tool.
That's an evaluations behavior, so it's left for a follow-up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


[AIC-3598]:
https://launchdarkly.atlassian.net/browse/AIC-3598?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Tightens the public **handler contract** so `toolHandlers` is
consistently `Record<string, ToolHandlerFn | NativeTool>` on blocking
and streaming paths (replacing `Record<string, any>` on
`ProviderHandler.stream` and internal handler helpers). Provider
packages that narrowed the map are aligned, and **claude-agents** MCP
execution now rejects unwrapped `NativeTool` values (offline
evaluations) with the same “no handler” error instead of calling a
non-function.
> 
> **Packaging and release hygiene:** adds `yarn check:packaging` (plus
unit tests and CI after full build) to flag undeclared imports in built
`dist` and to verify packed tarballs install and every export loads via
`require()` and `import()`. **@launchdarkly/ai-langchain-agents**
declares missing **`zod`** and optional peer **`@langchain/anthropic`**
for its dynamic Anthropic import. README documents **lockstep versioning
from 1.0.0**.
> 
> **Breaking:** `ProviderHandler.stream`'s `toolHandlers` type changed;
custom handlers with a narrower `stream` signature may need updates.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
dd78326. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved by keeping both sides:
- claude-agents: ClaudeNativeGraphOptions export plus main's skills handler.
- openai-agents: OpenAINativeGraphOptions plus main's create_openai_agents_handler
  rename; build_output_type stays private, as main made it in #224.
- opencode-agents handler: main's secrets import plus this branch's NativeTool.
- API reports regenerated with make api-report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeffdupont
jeffdupont marked this pull request as ready for review October 28, 2026 16:39
@jeffdupont
jeffdupont requested a review from a team as a code owner October 28, 2026 16:39

@XieX XieX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few comments from a review pass, each checked against the code at this head:

  • The biggest one is the LDClientInterface.track() contract (types.py): the new docstring and return type allow an async track, but the SDK never awaits it.
  • The rest are about packaging-check coverage, tool_handlers typing, dependency floors, install hints and CI speed.

Comment thread packages/client/src/launchdarkly_ai_server/types.py Outdated
Comment thread packages/client/src/launchdarkly_ai_server/types.py Outdated
Comment thread scripts/check_packaging.py Outdated
Comment thread packages/openai-messages/pyproject.toml
Comment thread packages/langchain-messages/src/launchdarkly_ai_langchain_messages/handler.py Outdated
Comment thread scripts/check_packaging.py Outdated
…e packaging check

- LDClientInterface.track() is typed -> None again and documented as synchronous:
  no call site awaits it, so an async track would drop every $ld:ai:* event.
- tool_handlers takes Mapping[str, Callable[..., Any] | NativeTool] everywhere it is
  only read, so a caller's precisely typed dict is accepted; drops the dict() copies.
- check_packaging installs every declared extra in its own venv and imports what it
  promises; an extra with no EXTRA_IMPORTS entry fails. Override extras are derived
  from pyproject. Runs go through a thread pool with buffered output.
- LangChain install hints name the package extra; the READMEs list the extras.
- README states that dependency floors are the CI-tested versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jeffdupont jeffdupont left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-review of 1304692. Four of the six earlier points are resolved: track() is synchronous again, tool_handlers is a Mapping, the extras are installed and import-checked, and the install hints name the extras. A mypy probe against the head types accepts a real ldclient.LDClient, rejects a client with async def track, and accepts a dict[str, Callable[[dict[str, Any]], str]] passed as tool_handlers. I found no call site that awaits track(), and none that mutates tool_handlers.

Three things are still open:

  1. The packaging check can't fail on an undeclared import that another dependency installs. Today pydantic is imported unconditionally by openai-agents, litellm-agents, vercel-agents and vercel-messages, and openai by litellm-agents, and none of them declares it. TESTING.md section 5.3 in ai-sdks-monorepo#91 requires each one to be declared.
  2. Core's import scan only runs in a venv that has [ld] installed. An unguarded import ldclient in core would pass, even though section 5.3 says core must load without the base SDK.
  3. The internal floor is still launchdarkly-ai-server>=0.4.1. No 0.4.1 release exists, and the handlers now import NativeGraphRunner, which the published 0.5.0 does not have. #242 raises the floor to >=1.0.0,<2, so this PR is only safe if #242 merges before the next release PR.

The floors policy (your point 4) is now written down, but its wording disagrees with the spec text and with some of the floors. See the inline comment on README.md.

Comment thread scripts/check_packaging.py Outdated
Comment thread scripts/check_packaging.py Outdated
Comment thread packages/claude-agents/pyproject.toml Outdated
Comment thread README.md Outdated
…out extras

- check_packaging: an unconditional import must come from a distribution the
  package declares. Installed is no longer enough: a module another dependency
  pulls in fails. Base runs install with no extras, so core is proven to load
  without launchdarkly-server-sdk; [ld] is now an extra run like the others.
- Declare what the stricter check found: pydantic in openai-agents,
  litellm-agents, vercel-agents and vercel-messages; openai in litellm-agents.
- Internal pins: launchdarkly-ai-server>=1.0.0,<2 everywhere. The native graph
  adapters import NativeGraphRunner, which published server 0.5.0 lacks.
- README states the floor rule spec 5.3 uses: same major as the tested version
  for 1.x+, same minor for 0.x.
- Tests: core is checked without extras, an undeclared dynamic import fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/ai/pyproject.toml Outdated
jeffdupont and others added 2 commits October 28, 2026 15:41
…resolves

>=1.0.0,<2 could not resolve if a release went out before release-as 1.0.0
lands (Bugbot). >=0.6 still excludes server 0.5.0, which lacks
NativeGraphRunner; this PR's breaking change makes core's next 0.x release
0.6.0, and 1.0.0 satisfies it too. #242 tightens it to >=1.0.0,<2 at the cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g-ga

# Conflicts:
#	packages/client/api-report/launchdarkly_ai_server.txt

@XieX XieX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for working through all of this. I checked each fix at this head: track() is synchronous and documented that way, tool_handlers is a Mapping with the copies gone, every extra is installed and import-checked, the install hints name the extras, the checks run in parallel, and the floor rule is written down. The declared-distribution check, the core-without-extras run and the >=0.6,<2 internal floor all look right too. Release PR #231 already takes core to 0.6.0, so the floor resolves whichever release goes out first.

Approving. A few non-blocking nits, none of which need to hold this up:

  1. Private-spec reference. The module docstring of tests/test_check_packaging.py (line 1) cites the private spec document and a section number. #234, merged to main today but not yet in this branch, removed every such reference from the .py files, so merging this would add one back. Stating the rule in plain words, as #234 did elsewhere, would fix it. The PR description cites section numbers as well.
  2. Namespace packages in the declared-import check (latent). The scan reduces each import to its top-level name, so a module like opentelemetry maps to several distributions. A future unguarded lazy from opentelemetry.sdk ... in core would pass, because opentelemetry-api is declared, even though opentelemetry-sdk is only in [otel]. Every such import in core is guarded today, so nothing is broken. Mapping the full dotted module to its distribution (or special-casing namespace packages) would close the gap.
  3. LangChain default-model consistency. Both LangChain packages default to OpenAI, but langchain-agents depends on langchain-openai directly while langchain-messages offers it as [openai]. A bare pip install launchdarkly-ai-langchain-messages therefore fails on its default path, though with a clear hint. If that difference is intentional, ignore this.

jeffdupont and others added 2 commits October 29, 2026 08:39
…chain-openai is a dependency

Review nits from XieX:
- check_packaging keeps each import's full dotted name and maps it to the
  distribution that owns its file, so a namespace package such as
  opentelemetry.sdk needs opentelemetry-sdk declared, not any opentelemetry-*.
  It found langgraph.prebuilt (from langgraph-prebuilt) undeclared in
  langchain-agents; declared at >=1.1.
- langchain-messages depends on langchain-openai, its default model, as
  langchain-agents does, so a bare install works; the [openai] extra is gone.
- The test module docstring no longer cites the private spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeffdupont

Copy link
Copy Markdown
Contributor Author

Thanks @XieX. All three nits are in f3fbe8a:

  1. Private-spec reference. The test module docstring no longer cites the spec, and the PR description no longer cites section numbers. I also merged main, so fix(AIC-3568)!: give the barrel core's __all__, and drop private-spec references #234's cleanup is in this branch; a search of the .py files and scripts/ for the spec name or section signs now finds nothing.
  2. Namespace packages. The scan keeps each import's full dotted name, and inside the venv each module resolves to the distribution that owns its file, falling back to the top-level lookup only for a namespace leaf with no file. So opentelemetry.sdk needs opentelemetry-sdk declared. It caught a real one straight away: langchain-agents imports langgraph.prebuilt, which comes from langgraph-prebuilt, a dependency of langgraph that we didn't declare. It's now declared at >=1.1. test_a_namespace_package_needs_its_own_distribution_declared covers the case.
  3. LangChain default model. Not intentional. langchain-messages now depends on langchain-openai, as langchain-agents does, so a bare install works on the default path. The [openai] extra is gone (it was never released), and the README says so.

Local: make test 3,711 passed, make check-packaging 22/22 (14 packages, 8 extras).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f3fbe8a. Configure here.

Comment thread packages/client/README.md
launchdarkly-server-sdk is now an extra, so an install without it succeeds and
then init_client() raises. The handler and barrel READMEs and the client
telemetry install now match the root README and the other handlers (Bugbot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeffdupont
jeffdupont merged commit 5860b40 into main Oct 29, 2026
10 checks passed
@jeffdupont
jeffdupont deleted the fix/AIC-3598-packaging-ga branch October 29, 2026 15:53
@github-actions github-actions Bot mentioned this pull request Oct 29, 2026
jeffdupont added a commit that referenced this pull request Oct 29, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants