Repository navigation
Conversation
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 abdad41. Configure here.
… when they can't be prepared
# Conflicts: # packages/client/src/client.ts # packages/client/src/lifecycle.ts # packages/client/src/tracking.ts
XieX
left a comment
There was a problem hiding this comment.
Comment-only review. The configFromVariation(args, evaluate) seam is a neat way to do this: the wrapper hands core its evaluation, with no subclass and no copied fields, and plain config() is unchanged. There's one bug inline, plus one difference from the Python SDK.
| return (async function* () { | ||
| for await (const event of events) { | ||
| if (event.type === 'done') { | ||
| startShadows({ userInput, context, variables, history }); |
There was a problem hiding this comment.
When the caller binds a conversation id around the stream() call only and iterates later, the shadow loses it. Core's stream() binds the id at call time through bindConversationId, which re-enters it only around the inner generator's next(). This outer generator runs in the iterating caller's context, so startShadows, and the shadow's spans, run unbound.
Reproduced with an AsyncLocalStorageContextManager and handlers that read the conversation key:
const gen = withConversationId('conv-1', () => shadowConfig({ ... }).stream('hi', ctx));
for await (const _ of gen) {}
// primary handler sees 'conv-1'; shadow handler sees undefinedinvoke() is fine, and so is iterating inside the block. The Python SDK keeps the id on both paths.
A fix that works: capture the active OTel context when stream() is called, and start the shadows inside it, e.g. const callerContext = otelContext.active() up front and otelContext.with(callerContext, () => startShadows(...)) here. (The context parameter shadows the OTel context import in this function, hence the alias.) That also parents the shadow's spans to the caller's trace, as invoke() does. Wrapping the outer generator in bindConversationId(...) fixes only the id. A regression test like the snippet above would be worth adding.
| const events = run.stream(userInput, context, variables, history); | ||
| return (async function* () { | ||
| for await (const event of events) { | ||
| if (event.type === 'done') { |
There was a problem hiding this comment.
Difference from the Python SDK: this done is the public one, which core yields after judges run, so stream shadows start after judges. Python starts stream shadows on the internal done, before judges. On invoke(), both start after judges. Neither delays the caller, but it'd be good for the two SDKs to agree.
|
These have been fixed, and I had Claude confirm. Christie, could you take a look next week? From Claude: Fixed Christie's comments in 32e0293, merged with main in bd10137.
|

Why
Shadow requests let a team try a candidate AI Config variation on real traffic without serving it, and compare it on the normal AI metrics. The SDK owners asked for this to be merged as experimental (TESTING.md §0). Earlier versions (#187, #193) either added an invocation hook to core or changed
config()for everyone; this one is an opt-in wrapper, so core behavior is unchanged.Summary
Adds
shadowConfig,runShadow,ShadowTask,ShadowResultandShadowConfigArgsto@launchdarkly/ai-server/experimental.shadowConfig()evaluates the flag once withvariationDetail, runs the served variation through the same code asconfig(), and starts the sampled shadows in the background once it succeeds (or hands them toonTask). Core gets only generic internal seams:configFromVariationbehindconfigInternal, optionalrunId,extraTrackDataandspanAttributesonexecuteAndTrack,runIdonexecuteAndStream, and a symbol key thatsetLdSpanAttributesreads. The root API report does not change, andconfig()still callsvariation().Testing approaches
config(), and that shadows start only after it succeeds.onTask,runShadow, shutdown and usage IDs.config()never callsvariationDetailand runs no shadows.Feature flags
N/A
Note
Overview
Adds experimental shadow requests on
@launchdarkly/ai-server/experimental:shadowConfig()mirrorsconfig()but, after a successful primaryinvoke()or streamdone, runs LaunchDarkly-configured candidate variations in the background (or hands them toonTaskforrunShadow()). Served responses, usage, and primary events stay aligned with plainconfig(); shadows reuse the primaryrunId, emit normal$ld:ai:*events with shadow metadata, and never get tools, judges, or user-visible output.Core changes are opt-in plumbing:
configFromVariation,fetchVariation/parseVariation(reason viavariationDetailonly when needed), sharedrunIdand extra tracking/span hooks onexecuteAndTrack, plus conversation helpers for deferred shadow runs.config()still evaluates withvariation()only.Reviewed by Cursor Bugbot for commit 32e0293. Bugbot is set up for automated code reviews on this repo. Configure here.