Repository navigation
fix(AIC-3598)!: make Python packaging and public types GA-ready - #218
Conversation
- 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>
…itle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
XieX
left a comment
There was a problem hiding this comment.
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 asynctrack, but the SDK never awaits it. - The rest are about packaging-check coverage,
tool_handlerstyping, dependency floors, install hints and CI speed.
…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
left a comment
There was a problem hiding this comment.
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:
- The packaging check can't fail on an undeclared import that another dependency installs. Today
pydanticis imported unconditionally by openai-agents, litellm-agents, vercel-agents and vercel-messages, andopenaiby litellm-agents, and none of them declares it. TESTING.md section 5.3 in ai-sdks-monorepo#91 requires each one to be declared. - Core's import scan only runs in a venv that has
[ld]installed. An unguardedimport ldclientin core would pass, even though section 5.3 says core must load without the base SDK. - The internal floor is still
launchdarkly-ai-server>=0.4.1. No 0.4.1 release exists, and the handlers now importNativeGraphRunner, 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.
…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>
…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
left a comment
There was a problem hiding this comment.
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:
- 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.pyfiles, 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. - Namespace packages in the declared-import check (latent). The scan reduces each import to its top-level name, so a module like
opentelemetrymaps to several distributions. A future unguarded lazyfrom opentelemetry.sdk ...in core would pass, becauseopentelemetry-apiis declared, even thoughopentelemetry-sdkis 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. - LangChain default-model consistency. Both LangChain packages default to OpenAI, but langchain-agents depends on
langchain-openaidirectly while langchain-messages offers it as[openai]. A barepip install launchdarkly-ai-langchain-messagestherefore fails on its default path, though with a clear hint. If that difference is intentional, ignore this.
…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>
|
Thanks @XieX. All three nits are in f3fbe8a:
Local: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
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>
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>

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 twovercel-*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.17anthropic>=0.113,httpx>=0.28,typesafe-sdk>=0.7The 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 onlaunchdarkly-ai-serverandlaunchdarkly-ai-pythoninstallslaunchdarkly-server-sdk>=9.18.3,<10. It's an extra rather than a dependency so bring-your-own-client installs stay lean, asclient/AGENTS.mdrequires. The READMEs and theinit_clienterror message point to it.Undeclared LangChain provider packages, found by the new check. Both langchain packages imported
langchain_openaiandlangchain_anthropicwithout declaring them, the Python side of the JS@langchain/anthropicgap:langchain-agentsnow depends onlangchain-openai, its default model, and offers[anthropic]and[aws]extras.langchain-messagesnow depends onlangchain-openai, its default model, as langchain-agents does, and offers[anthropic]and[aws].Internal ranges: every package pins
launchdarkly-ai-server>=0.6,<2. The native graph adapters importNativeGraphRunner, which published server 0.5.0 lacks, so>=0.4.1would let a handler resolve to a core it can't import from.>=0.6resolves 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
LDClientInterfacebecomes aProtocol. It was a plain class withasyncmethods, which matched neither the synchronousldclient.LDClientnor the "maybe awaitable" way core calls it.ldclient.LDClientnow satisfies it structurally; checked with mypy in both directions (a client missingtrackis rejected).init_clientandget_clientreturn it.variation,flushandclosemay be async;trackmust be synchronous (-> None), because no call site awaits it.tool_handlerson the handler protocol and in every handler is typedMapping[str, Callable[..., Any] | NativeTool]instead ofdict[str, Any], so a caller's precisely typeddictis accepted. Offline evaluations pass aNativeToolto the handler unwrapped, solangchain-agentsnow checkscallable()before calling. It used to raiseTypeError: 'NativeTool' object is not callable; there's a test, and it fails without the fix.to_lang_graph,to_openai_agents,to_claude_agents,to_litellm_agents,to_vercel_agents):Awaitable[GraphDefinition]TypedDictexported from the rootNativeGraphRunnerprotocol. They return a small class rather than aSimpleNamespace, because mypy accepts anySimpleNamespaceas any protocol.Packaging check (
make check-packaging, new CI jobpackaging)scripts/check_packaging.pyinstalls each wheel alone, with no extras, into an empty venv, which proves core loads withoutlaunchdarkly-server-sdk. It imports every public module and__all__name. Then it checks that every module the source imports unconditionally, lazyimportlib.import_modulecalls included, comes from a distribution the package declares; installed through some other dependency is not enough. That found undeclaredpydanticin four packages andopenaiin litellm-agents, now declared. Each declared extra gets its own venv and must import whatEXTRA_IMPORTSlists 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 asopentelemetry.sdkneedsopentelemetry-sdkdeclared; that foundlanggraph.prebuiltundeclared 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 skippedmake check-packaging: all 14 packages and 8 extras pass; removingpydanticfrom vercel-agents makes it fail withpydantic (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]andlaunchdarkly-ai-python[ld]now install boundedlaunchdarkly-server-sdk; READMEs drop separatelaunchdarkly-server-sdklines. Handlers pinlaunchdarkly-ai-server>=0.6,<2. LangChain packages declarelangchain-openaiand add[anthropic]/[aws]extras with guarded import errors; missing deps likepydanticandlanggraph-prebuiltare added where the new checker found gaps.make check-packaging+ CIpackagingjob. Newscripts/check_packaging.pybuilds 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.
LDClientInterfaceis aProtocolaligned with realldclientusage;get_client/init_clientreturn it.tool_handlersisMapping[str, Callable[..., Any] | NativeTool]across core and handlers (LangChain agents reject unwrappedNativeTool). Native graphto_*adapters takeAwaitable[GraphDefinition], export per-package optionsTypedDicts, and returnNativeGraphRunnerinstead 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.