Repository navigation
feat(client)!: add result variant and value to the feature_flag span event - #186
Conversation
extractVariation now returns the raw variation it already gets from client.variation, and invoke, stream, inline judges and graph nodes hand it to handlers as variables.__ldValue, next to __ld. It is kept out of TrackData on purpose: TrackData is the payload of every $ld:ai:* track event, and the value includes the prompt.
…vent AI Config monitoring only finds traces whose feature_flag event carries feature_flag.result.value, which the LaunchDarkly observability plugins always send. Write the variation key as feature_flag.result.variant (conditionally required by the OpenTelemetry feature-flag conventions) and the evaluated value as feature_flag.result.value, serialized with JSON.stringify exactly like TracingHook with includeValue. Both are omitted when there is nothing to write, for example on native-graph spans.
The raw variation holds the prompt, so writing it on every run's span event bypassed the content-capture opt-in. setLdSpanAttributes now takes a captureContent argument (default false). With capture on and a raw variation present, feature_flag.result.value is its JSON, as before. Otherwise it is the JSON-encoded variation key, and it is omitted when there is neither. Every handler passes the capture setting it already uses for content attributes, as does vercelEvaluate. Native graph and client graph spans carry no raw variation, so they keep the default.
Move the content-capture gate for feature_flag.result.value out of the handlers and into executeAndTrack/executeAndStream. The core now sets variables.__ldValue only when the handler that runs has captureContent on (ProviderHandler.captureContent, the same gate judge reasoning uses), so the raw config never reaches a capture-off handler. Config runs, judges and graph nodes all go through these two functions. setLdSpanAttributes goes back to its original signature, and the handler call sites and the API report match main again. The helper writes the raw variation's JSON when __ldValue is present, the JSON-encoded variation key otherwise, and nothing when there is neither.
jeffdupont
left a comment
There was a problem hiding this comment.
Looks good, and it matches the updated #113 spec. Gating on handler.captureContent in executeAndTrack / executeAndStream keeps the raw config away from capture-off handlers, and since createHandlerInternal (utils.ts:74) sets that property for every handler, it is a single check. The fallback also closes the vercelEvaluate / JudgeTask gap from the first version.
- One handler-level capture-on test. The four handler tests only assert the capture-off value (
JSON.stringify('variation-a')). §1.5 asks handler tests to cover capture on too. A single test that goes throughconfig().invokewithcreateXHandler({ captureContent: true })and assertsresult.value === JSON.stringify(rawVariation)would cover the wiring from start to finish. - Serialization question. gonfalon's flag Monitoring Traces tab groups chart series by the exact
feature_flag.result.valuestring, matched againstJSON.stringify(variation.value)(static/ld/components/feature-monitoring/hooks/useVariationChartConfig.ts:8,45-49). JS already matches with capture on. The capture-off"variation-a"fallback matches no variation, so if AI Config Monitoring uses that chart, those traces land in an unlabeled series. Do you know which UI readsresult.valuefor AI Configs? Not blocking.
Happy to approve once the capture-on test is in.
Run config().invoke with a handler built by createHandler with captureContent on and check that the root feature_flag event's feature_flag.result.value is the JSON of the raw variation extractVariation returned.
jeffdupont
left a comment
There was a problem hiding this comment.
bda9e7b adds the end-to-end capture-on test I asked for: it goes through config().invoke with capture on and asserts the raw variation as the value. Approving.
This matches launchdarkly/ai-sdks-monorepo#113. setLdSpanAttributes writes feature_flag.result.variant only for a non-empty variation key. It writes feature_flag.result.value as JSON.stringify of the raw variation, which is how the node-server-sdk-otel TracingHook writes it. The core sets __ldValue only when handler.captureContent is true. Otherwise the value is the JSON-encoded key, and it is left off when there is neither. The event is still written only by the shared helper on the root span. The tests cover capture on (client.test.ts), capture off and unset (tracking.test.ts, for track and stream), and the case with neither (utils.test.ts). The change is additive. extractVariation gains a return field, and no public type or API report changes, so API compatibility passes.
On parity with python-ai-sdk#240, the span output agrees, with two differences in how it gets there. First, JS gates capture in the core and Python gates it in the helper. Second, a judge run from a JudgeTask writes the raw value in Python but only the key here, because the JS JudgeTask path has no raw variation. Both stay within the spec.
….result.value A deferred judge (buildJudgeTasks, then runJudge in a worker) wrote the variation key as feature_flag.result.value even when the judge handler captures content, because the task had no raw variation to pass. JudgeTask now carries it as judgeValue, and runJudge hands it to the core as the evaluated value, which writes it only for a capture-on handler.
…tion captureContentFor returns a handler's own captureContent when it states one, otherwise the process-wide default (off). The evaluated-variation gate in executeAndTrack and executeAndStream now goes through it, so a later process-wide setting changes one place. Behavior is unchanged.
…on the graph root The launchdarkly.graph span wrote the variation key as feature_flag.result.value even when content capture is on, because the raw graph variation never reached it. buildGraph now returns the variation it fetched, and graph().stream (which invoke() walks) passes it as __ldValue when captureContentFor(undefined) is true. The root has no single handler, so it follows the process-wide default.
… ag/ff-result-variant
jeffdupont
left a comment
There was a problem hiding this comment.
Since my approval at bda9e7b: 8398f4e puts the judge variation on JudgeTask.judgeValue, and runJudge gates it with the judge's handler. That closes the deferred-judge gap I mentioned, and the structuredClone test is a good touch. 4ad0f34 gives the launchdarkly.graph root the graph variation under the process default. de89d4a routes both through captureContentFor. The main merge had no conflict resolutions.
About the red API compatibility runs: they come from 8398f4e. JudgeTask is public, so the new field changed ai-server.api.md, and the check counts any changed declaration as breaking. The rename to feat(client)! at 16:34 made it pass, and the PR is clean now. Adding an optional field doesn't break anyone, though. If you'd rather not have a BREAKING CHANGES entry in the changelog, the api-compatible label on a title without ! is the more accurate route. Your call.
One thing to settle with #190. I merged #190 onto this branch locally. It merges with no conflicts, but leaves two captureContentFor functions. The one in content.ts keeps the hard-coded false default, and tracking.ts and graph.ts still import it. The one in sdk-options.ts follows configure(), and judges.ts imports it. Handlers built with createHandler are fine, because #190 makes captureContent a getter that always returns a boolean. A plain function handler, and the graph root, would still send only the key while the default is on. A quick vitest probe on the merged tree confirms it for the plain function, and the full suite still passes, because graph.test.ts mocks content.js. Whichever of the two merges second should delete the content.ts version, point tracking.ts and graph.ts at sdk-options.ts, and use configure() in the graph test instead of the mock. A test with a plain function handler under the built-in default would keep it fixed.
Nit: executeAndTrack and executeAndStream spread the caller's variables before adding __ldValue. A capture-off handler therefore writes a caller-supplied variables.__ldValue. Overwriting or deleting it the way __ld is handled would close that.
executeAndTrack and executeAndStream spread the caller's variables before adding __ldValue, so a capture-off handler still received a caller-supplied __ldValue and wrote it as feature_flag.result.value. Both now build the handler's variables in one place that sets __ldValue only for a capture-on handler and removes it otherwise, as __ld is already overwritten.
… ag/ff-result-variant
#111 replaced createHandler's positional stream and captureContent arguments with an options object, and the positional form now throws. The two client tests this branch added still used it.
…ure() (#190) Fixes [AIC-3703](https://launchdarkly.atlassian.net/browse/AIC-3703). The spec change for TESTING.md §3.14 and Appendix A.8 (`createHandler` returning a new function) is in launchdarkly/ai-sdks-monorepo#113. BEGIN_COMMIT_OVERRIDE feat(client)!: capture conversation content on spans by default and add configure() BREAKING CHANGE: conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. fix(client): return a new handler from createHandler instead of changing the function passed in BREAKING CHANGE: createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. END_COMMIT_OVERRIDE ## Why The cross-SDK spec (TESTING.md §3.9a) says conversation content is captured on spans by default and that a process-wide `configure()` sets that default, with TypeScript marked pending. The Python SDK already works this way. This SDK did the opposite: every handler factory defaulted `captureContent` to `false`, evaluation spans had their own `false` default, and there was no `configure()`. This PR brings the TypeScript SDK in line with the spec and with Python. ## What changed - New `configure({ options })` and `SDKOptions` (`{ captureContent?: boolean }`, built-in default `true`) exported from `@launchdarkly/ai-server`. The options live on the lifecycle singleton, not on `initClient`. `configure` can replace them until the first AI invocation. Every call to a handler built with `createHandler` or a factory counts, whether the SDK makes it or your code calls `await handler(config, input)` directly, as does the start of a handler's stream, an SDK call to a plain function handler, and a `vercelEvaluate` call. After that, identical options are accepted and different ones throw. This matches Python, where every handler call latches. `shutdown()` restores the built-in defaults, even when no client was ever initialized. - `createHandler` now returns a new function that latches `configure` and then calls the one you passed, with the same `this` and arguments, returning its result. It keeps the original function's own properties, so wrapping a handler again keeps its `stream` and `providers`. The options object, `createHandler(providesFor, fn, { stream, captureContent, providers })`, is unchanged. It no longer returns the function it was given, and that function no longer carries `providesFor`, so code that calls `createHandler(p, fn)` and keeps using `fn` must use the returned handler instead. When the `captureContent` option is omitted and the function is already a handler with an explicit override, the new handler keeps that override; an unset handler stays unset and keeps following `configure()`. An omitted `providers` option likewise keeps the wrapped handler's list, and a passed one replaces it on the new handler only. - `handler.captureContent` on anything built with `createHandler` is now resolved when it is read: the handler's explicit `true` or `false` if it has one, otherwise the configured default, otherwise `true`. Assigning to it sets the handler's override and leaves the process default alone. - Every handler factory (claude-agents, claude-messages, langchain-agents, langchain-messages, litellm-agents, litellm-messages, openai-agents, openai-messages, typesafe, vercel-agents, vercel-messages) leaves the option unset when it is omitted and resolves it at the start of each run and stream, so a handler built before `configure()` follows it. Convenience wrappers forward an omitted option as unset. `vercelEvaluate` follows the same rule. - With #186 merged, `feature_flag.result.value` also goes through this resolver. #186 added a second `captureContentFor` in `content.ts` with a fixed `false` default; it is deleted, and `tracking.ts` and `graph.ts` now use the one in `sdk-options.ts`. So the graph root and plain function handlers (not built with `createHandler`) follow `configure()` and its built-in `true`, and write the raw variation by default. - Judge explanations and the spans the evaluations runner writes use the same resolver. A plain function judge or generation handler with no flag follows the process default. - The content helpers no longer throw when a handler passes a text part with no text. That could only happen with capture on, so it only mattered once capture became the default. - Docs (root AGENTS.md, package agents.md files, client, claude-agents, litellm-messages and vercel-messages READMEs) now describe the resolution order instead of "off by default". The client README has a new "Conversation content on spans" section, with migration notes for both breaking changes. AGENTS.md and the root README no longer say `createHandler` returns the same function. The API report gains `configure` and `SDKOptions`, both additions already listed as joining in `api-surface/ai-server.txt`. ## Behaviour change If you never set `captureContent`, your spans will now include conversation content: prompts and system instructions, model output, tool definitions, tool arguments and results, judge explanations, and the same fields on offline evaluation spans. The full variation is also exported as `feature_flag.result.value`, on handler spans and on the graph root, because #186 gates it on content capture, which now resolves to `true` by default. Model names, token counts, timings and tool names were already exported and are unchanged. `createHandler` also changed: it returns a new function, and the function you pass in is left without `providesFor`. Use the returned handler. ## How to opt out For the whole process, once at startup, before the first AI call: ```ts import { configure } from '@launchdarkly/ai-server'; configure({ options: { captureContent: false } }); ``` For one handler, pass `captureContent: false` to its factory, for example `createOpenAIMessagesHandler({ captureContent: false })`. An explicit `true` on a factory still captures when the process default is `false`. ## Testing - New client tests for `configure`: default on, read at run time for a handler built earlier, replacement before the first invocation, explicit `false` and `true` overrides in both directions, the property setter, rejection after an invocation and acceptance of identical options, latching on a plain function call through the SDK, on a direct `await handler(...)` call (as soon as it starts, and when wrapped twice), on a handler stream that is never iterated, and on a streamed SDK call, `shutdown()` restoring the default, `captureContent` on `initClient` being ignored with a warning, and `runJudges` writing `gen_ai.evaluation.explanation` only when the judge's effective flag is true. - Re-wrapping tests: `createHandler(q, createHandler(p, fn, { captureContent: false })).captureContent` stays `false` under a configured `true`, a re-wrapped unset handler still follows a later `configure()`, and `runJudges` omits the explanation for a re-wrapped judge that kept an explicit `false`. - The deferred `runJudge(task, handlers)` path writes `gen_ai.evaluation.explanation` only when the judge's effective flag is true: built-in default, explicit `false`, `configure` turning it off, and explicit `true` over a configured `false`. - `inspectConfig` reads a variation without latching `configure`. - `openaiMessages` and `claudeAgents`: an explicit `captureContent: false` beats a configured `true`, and an omitted option is forwarded as unset, so the handler follows `configure()`. - `createHandler` tests check that the returned handler forwards `this` and arguments, returns the original result, latches `configure` when called directly, keeps properties attached to the wrapped function, and keeps or replaces `providers` on a re-wrap. - `vercelEvaluate` tests check that a call latches `configure` (also when the model call fails), that identical options are still accepted afterwards, and that a call rejected for a disabled config does not latch. - Evaluation span tests now check that the transcript is exported under the built-in default, removed by `configure` or an explicit `false`, kept by an explicit `true` over a configured `false`, and that a handler built before `configure` follows it. - Each handler package has tests for: on by default, explicit `false`, `configure` turning it off, a handler built before `configure`, and explicit `true` over a configured `false`. - `feature_flag.result.value`: a plain function handler under the built-in default gets the raw variation through `executeAndTrack` and through `config().invoke`, and gets the variation key once `configure` turns capture off. The graph root writes the raw graph variation under the built-in default and under a configured `true`, and the variation key under a configured `false`. The graph suite drives this with `configure()` and `resetSdkOptions()` instead of mocking the resolver. These tests fail against the old fixed `false`. #186's tests, including the caller-supplied `__ldValue` one, still pass. - `yarn test`, `yarn typecheck`, `yarn code:check`, `yarn lint:pkg`, `yarn api:update` (no report change), `yarn api:check`, `yarn api:surface`, `yarn test:api-surface`, `yarn test:api-breaking`, `yarn check:packaging` and `scripts/api-breaking.mjs pr` all pass locally. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Aligns the TypeScript SDK with the cross-SDK spec and Python: **conversation content on OpenTelemetry spans is on by default**, and integrators can set process-wide defaults with new **`configure({ options: { captureContent } })`**. > > **Content capture** no longer defaults to `false` on every handler factory. Unset `captureContent` follows `configure()` (built-in **`true`**), resolved **when each run starts** so handlers created before `configure()` still pick up the default. Per-handler `captureContent: false` (or convenience-wrapper overrides) still wins. The same gate applies to judge **`gen_ai.evaluation.explanation`**, offline evaluation runner spans, and **`feature_flag.result.value`** (full variation when capture is on, variation key when off). **`shutdown()`** restores built-in defaults and clears the post-invocation lock on `configure`. > > **`createHandler`** now returns a **new wrapper** that carries `providesFor`, latches the first AI call, and forwards to the function you pass in—the original function is **not** mutated and must not be used for routing. Handler **`captureContent`** is a runtime getter with an assignable override. > > Docs, examples, and tests across the client and all provider packages were updated for the new defaults and resolution order. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ede7625. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> [AIC-3703]: https://launchdarkly.atlassian.net/browse/AIC-3703?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
🤖 I have created a release *beep* *boop* --- <details><summary>@launchdarkly/ai-claude-agents: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-claude-agents-0.5.0...@launchdarkly/ai-claude-agents-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **client:** add result variant and value to the feature_flag span event ([#186](#186)) * **SEC-9507:** enable Claude native tools only when the active variation attaches them * **claude-agents:** honour the config's max_tokens and disable thinking unless the config asks ([#183](#183)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **client:** add result variant and value to the feature_flag span event ([#186](#186)) ([8dbe2ec](8dbe2ec)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) * **skills:** give Claude Agents runs the skills attached to their variation ([#136](#136)) ([df16ba0](df16ba0)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) ([d3e7c5f](d3e7c5f)) ### Bug Fixes * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **claude-agents:** honour the config's max_tokens and disable thinking unless the config asks ([#183](#183)) ([f58ce21](f58ce21)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) * **SEC-9507:** dispatch litellm-messages tool calls only to tools offered for the request ([6926e8a](6926e8a)) * **SEC-9507:** enable Claude native tools only when the active variation attaches them ([6926e8a](6926e8a)) * **SEC-9507:** pass handlers only the tools attached to the active variation ([6926e8a](6926e8a)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-claude-messages: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-claude-messages-0.5.0...@launchdarkly/ai-claude-messages-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-langchain-agents: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-langchain-agents-0.5.0...@launchdarkly/ai-langchain-agents-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **client:** add result variant and value to the feature_flag span event ([#186](#186)) * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) ([9371975](9371975)) * **client:** add result variant and value to the feature_flag span event ([#186](#186)) ([8dbe2ec](8dbe2ec)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3592:** make native LangGraph hand-offs complete ([#192](#192)) ([025f2ce](025f2ce)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-langchain-messages: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-langchain-messages-0.5.0...@launchdarkly/ai-langchain-messages-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **client:** add result variant and value to the feature_flag span event ([#186](#186)) * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) ### Features * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) ([9371975](9371975)) * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) ([15a0d37](15a0d37)) * **client:** add result variant and value to the feature_flag span event ([#186](#186)) ([8dbe2ec](8dbe2ec)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3726:** realign three usage IDs with the 1.0 factory names ([#197](#197)) ([fc83c7a](fc83c7a)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-litellm-agents: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-litellm-agents-0.2.0...@launchdarkly/ai-litellm-agents-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-litellm-messages: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-litellm-messages-0.2.0...@launchdarkly/ai-litellm-messages-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **SEC-9507:** enable Claude native tools only when the active variation attaches them * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) * **SEC-9507:** dispatch litellm-messages tool calls only to tools offered for the request ([6926e8a](6926e8a)) * **SEC-9507:** enable Claude native tools only when the active variation attaches them ([6926e8a](6926e8a)) * **SEC-9507:** pass handlers only the tools attached to the active variation ([6926e8a](6926e8a)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-node: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-node-0.4.0...@launchdarkly/ai-node-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **evaluations:** promote Evaluations from Code to core ([#181](#181)) * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) ### Features * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) ([15a0d37](15a0d37)) * **evaluations:** promote Evaluations from Code to core ([#181](#181)) ([676a2ed](676a2ed)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) ([d3e7c5f](d3e7c5f)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-openai-agents: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-openai-agents-0.5.0...@launchdarkly/ai-openai-agents-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **client:** add result variant and value to the feature_flag span event ([#186](#186)) * **AIC-3687:** rename createOpenAIAgentHandler to createOpenAIAgentsHandler ([#174](#174)) ### Features * **AIC-3687:** rename createOpenAIAgentHandler to createOpenAIAgentsHandler ([#174](#174)) ([cfba4d2](cfba4d2)) * **client:** add result variant and value to the feature_flag span event ([#186](#186)) ([8dbe2ec](8dbe2ec)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3726:** realign three usage IDs with the 1.0 factory names ([#197](#197)) ([fc83c7a](fc83c7a)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-openai-messages: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-openai-messages-0.5.0...@launchdarkly/ai-openai-messages-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) ([15a0d37](15a0d37)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **AIC-3726:** realign three usage IDs with the 1.0 factory names ([#197](#197)) ([fc83c7a](fc83c7a)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-otel: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-otel-0.3.0...@launchdarkly/ai-otel-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** spans carry launchdarkly.project_id instead of highlight.project_id ### Bug Fixes * **client:** stamp launchdarkly.project_id on the telemetry resource ([#176](#176)) ([93fb412](93fb412)) </details> <details><summary>@launchdarkly/ai-server: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-server-0.5.0...@launchdarkly/ai-server-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. * **client:** add result variant and value to the feature_flag span event ([#186](#186)) * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) * **SEC-9507:** enable Claude native tools only when the active variation attaches them * **evaluations:** export each offline row as one trace ([#175](#175)) * **evaluations:** promote Evaluations from Code to core ([#181](#181)) * **evaluations:** remove default pass rate threshold ([#184](#184)) * **client:** spans carry launchdarkly.project_id instead of highlight.project_id * **AIC-3687:** rename createOpenAIAgentHandler to createOpenAIAgentsHandler ([#174](#174)) * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ### Features * **AIC-3439:** let wildcard handlers accept only named providers ([#111](#111)) ([9371975](9371975)) * **AIC-3686:** rename the LangChain and OpenAI messages factories to *MessagesHandler ([#173](#173)) ([15a0d37](15a0d37)) * **AIC-3687:** rename createOpenAIAgentHandler to createOpenAIAgentsHandler ([#174](#174)) ([cfba4d2](cfba4d2)) * **client:** add result variant and value to the feature_flag span event ([#186](#186)) ([8dbe2ec](8dbe2ec)) * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) * **client:** point judge spans at the run they graded ([#200](#200)) ([ba9c936](ba9c936)) * **evaluations:** export each offline row as one trace ([#175](#175)) ([05a7eb1](05a7eb1)) * **evaluations:** promote Evaluations from Code to core ([#181](#181)) ([676a2ed](676a2ed)) * **evaluations:** remove default pass rate threshold ([#184](#184)) ([f3a3e37](f3a3e37)) * **skills:** give Claude Agents runs the skills attached to their variation ([#136](#136)) ([df16ba0](df16ba0)) * **skills:** rename FDv2SkillStore to LaunchDarklySkillStore ([#172](#172)) ([d3e7c5f](d3e7c5f)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **AIC-3598:** type toolHandlers on the handler contract; declare @langchain/anthropic ([#170](#170)) ([c6c00fc](c6c00fc)) * **AIC-3601:** stop multi-edge routing text from asking a router for a final answer ([#177](#177)) ([10cce54](10cce54)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) * **client:** stamp launchdarkly.project_id on the telemetry resource ([#176](#176)) ([93fb412](93fb412)) * **evaluations:** update stale test import ([#189](#189)) ([f4d8083](f4d8083)) * **evaluations:** warn once when a scorer returns an invalid score ([#199](#199)) ([f43c888](f43c888)) * **graph:** report a node's own judge scores on its node_done stream event ([#182](#182)) ([1230f02](1230f02)) * **SEC-9507:** dispatch litellm-messages tool calls only to tools offered for the request ([6926e8a](6926e8a)) * **SEC-9507:** enable Claude native tools only when the active variation attaches them ([6926e8a](6926e8a)) * **SEC-9507:** pass handlers only the tools attached to the active variation ([6926e8a](6926e8a)) </details> <details><summary>@launchdarkly/ai-typesafe: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-typesafe-0.2.0...@launchdarkly/ai-typesafe-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-vercel-agents: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-vercel-agents-0.3.0...@launchdarkly/ai-vercel-agents-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> <details><summary>@launchdarkly/ai-vercel-messages: 1.0.0</summary> ## [1.0.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-vercel-messages-0.3.0...@launchdarkly/ai-vercel-messages-1.0.0) (2026-10-30) ### ⚠ BREAKING CHANGES * **client:** conversation content is now captured on spans by default. To keep it off, call configure({ options: { captureContent: false } }) once at startup. * **client:** createHandler returns a new wrapper function rather than adding providesFor to the function you pass and returning that same function. Use the handler createHandler returns; the function you passed in no longer carries providesFor. ### Features * **client:** capture conversation content on spans by default and add configure() ([d9c7fc3](d9c7fc3)) ### Bug Fixes * **AIC-3573:** never send a NativeTool to the model as a function tool ([#194](#194)) ([ec43b85](ec43b85)) * **client:** return a new handler from createHandler instead of changing the function passed in ([d9c7fc3](d9c7fc3)) ### Dependencies * The following workspace dependencies were updated * dependencies * @launchdarkly/ai-server bumped from ^0.5.0 to ^1.0.0 </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > This **release-please** PR cuts **1.0.0** for every workspace package (`@launchdarkly/ai-server`, `@launchdarkly/ai-node`, provider adapters, `@launchdarkly/ai-otel`, `@launchdarkly/ai-typesafe`) by updating `.release-please-manifest.json`, each `package.json` / `src/version.ts`, and adding **1.0.0** changelog sections. Provider packages bump their `@launchdarkly/ai-server` dependency to **^1.0.0**. > > The release notes bundled here flag **breaking** upgrades integrators must handle: **`configure()`** with **conversation content on spans by default** (opt out via `captureContent: false`); **`createHandler`** now returns a **new wrapper** instead of mutating the passed function; renamed factories (**`*MessagesHandler`**, **`createOpenAIAgentsHandler`**); **`LaunchDarklySkillStore`** (was `FDv2SkillStore`); **Evaluations promoted to core** with offline export/trace and threshold behavior changes; telemetry **`launchdarkly.project_id`** (was `highlight.project_id`); richer **`feature_flag`** span events; and **SEC-9507** tool scoping (native tools and handlers only see tools on the active variation). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f87c9a6. 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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
Runs made through the turnkey SDK don't show up on an AI Config's monitoring traces tab. Turnkey writes the
feature_flagspan event withoutfeature_flag.result.value. Since launchdarkly/observability#2553, a trace search for a flag key only scans hours where afeature_flagevent with a value was recorded, so turnkey runs are never found. The LaunchDarkly observability plugins always send the value (the SDK tracing hook withincludeValue: true).This adds two attributes to the event
setLdSpanAttributeswrites:feature_flag.result.variant: the variation key. The OpenTelemetry feature-flag conventions make it conditionally required when the provider has variants.feature_flag.result.value: with content capture on, the evaluated variation, serialized withJSON.stringifyexactly asTracingHookdoes. With capture off, the variation key, JSON-encoded like any flag value (for example"opus"), so the prompt is never sent unless capture is on.What changed
extractVariationreturns the raw variation it already receives fromclient.variation.invoke,stream, inline judges and graph nodes pass it to handlers asvariables.__ldValue, next to__ld.JudgeTaskhas a new optional field,judgeValue: the judge variation exactly as the LaunchDarkly SDK returned it.buildJudgeTasksfills it, andrunJudgepasses it on under the same capture check as the inline path. It is plain JSON, so a task still survives structured clone, and it adds no new kind of content, since the task already carriesjudgeConfigandactualOutput.launchdarkly.graphroot span gets the value too.graph().invoke()andgraph().stream()share one code path, which now passes the graph flag's raw variation to the root span. The root has no single handler, so it follows the process-wide content capture default, which is off today. With the default off, the root keeps the JSON-encoded variation key.TrackData.TrackDatais the payload of every$ld:ai:*track event, and the value includes the prompt.captureContenton.executeAndTrackandexecuteAndStreamset__ldValueonly when the handler'scaptureContentis true, the same check judge reasoning uses, so the raw config never reaches a capture-off handler. That check, and the graph root's, go through one function,captureContentFor, which returns a handler's owncaptureContentor the process-wide default when there is no handler or it states none. This uses the existingcaptureContenthandler option; there is no new setting.setLdSpanAttributeswrites the variant when the variation key is non-empty. For the value it writes the raw variation's JSON when__ldValueis present, otherwise the JSON-encoded variation key, and leaves it off when there is neither (native-graph spans).JudgeTask.judgeValuefield.setLdSpanAttributeskeeps its signature and the handlers are unchanged.vercelEvaluatehas no raw variation at hand, so its value is always the variation key.Testing
__ldValueonly to a capture-on handler, for bothexecuteAndTrackandexecuteAndStream; the value isJSON.stringifyof the raw variation when present, the JSON-encoded variation key otherwise, and absent with neither;__ldValuereaches the handler and stays out ofTrackDataand track payloads.JudgeTaskbuilt from a judge variation carries it asjudgeValuethrough structured clone, and itsrunJudgerun writes the raw variation asfeature_flag.result.valuewith capture on and the variation key with capture off. Thelaunchdarkly.graphroot writes the variation key with the default off and the raw graph variation with it on, for both invoke and stream.tsc --noEmitin every package,api:check,api:surfaceand biome.The cross-SDK spec change is in launchdarkly/ai-sdks-monorepo#113.
Note
Overview
Turnkey SDK runs were missing from AI Config monitoring traces because the root
feature_flagevent lackedfeature_flag.result.value, which observability now requires to correlate traces with a flag. This PR addsfeature_flag.result.variant(served variation key, omitted when empty) andfeature_flag.result.valueon the eventsetLdSpanAttributeswrites, aligned with LaunchDarkly OTelTracingHookserialization.extractVariationnow returns the raw SDKvalue, threaded throughinvoke/stream, graph nodes, and judges as optionalevaluatedValue. The core exposes it to handlers only asvariables.__ldValuewhencaptureContentFor(handler)is true (handlercaptureContentor process default); otherwise the event gets JSON-encoded variation key only. Raw variation never lands inTrackDataor track payloads. Deferred judges store it on optionalJudgeTask.judgeValue; thelaunchdarkly.graphroot uses the graph flag raw value only when the process-wide capture default is on.captureContentForcentralizes that gate. Handler package tests and AGENTS.md are updated; the only public API addition isJudgeTask.judgeValue.Reviewed by Cursor Bugbot for commit 869636a. Bugbot is set up for automated code reviews on this repo. Configure here.