Skip to content

feat: AICONF config types & LDAIClient methods (AIC-2663)#173

Open
ctawiah wants to merge 4 commits into
feat/AIC-2695/ai-sdk-vendor-mustachefrom
feat/AIC-2663/ai-sdk-client
Open

feat: AICONF config types & LDAIClient methods (AIC-2663)#173
ctawiah wants to merge 4 commits into
feat/AIC-2695/ai-sdk-vendor-mustachefrom
feat/AIC-2663/ai-sdk-client

Conversation

@ctawiah

@ctawiah ctawiah commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

AIC-2663 — Step 3: AICONF config types & LDAIClient methods. Part of epic AIC-2629.

Stacked on #172 (feat/AIC-2695/ai-sdk-vendor-mustache), which is stacked on #171. It builds on the data model + parser (#171) and the vendored-Mustache Interpolator (#172). Base retargets up the stack as each merges.

Describe the solution you've provided

Implements the public AI Config types and the LDAIClient retrieval methods that evaluate a flag, validate its mode, interpolate prompt templates, and return a typed config. The behavior mirrors the JS reference the spec points to.

  • Config types.NET-style two-hierarchy design (AIConfig / AIConfigDefault bases): AICompletionConfig(+messages), AIAgentConfig(+instructions), AIJudgeConfig(+evaluationMetricKey) result types plus parallel *Default builder types, and AIAgentConfigRequest for batch agent retrieval. A generic base builder keeps the *Default builders DRY.
  • LDAIClient methodscompletionConfig, agentConfig, agentConfigs, judgeConfig:
    • serialize the caller default to a flag value (tagged with the requested mode) and pass it through jsonValueVariation, so an absent flag yields the default and the eval event records the correct default;
    • mode validation: a mismatch logs a single warning and returns a disabled config of the requested type — never a config that would NPE the caller;
    • interpolate messages/instructions via the vendored-Mustache Interpolator, exposing the context as {{ldctx}};
    • fire the spec'd usage events ($ld:ai:usage:completion-config, :agent-config, :agent-configs, :judge-config) and emit $ld:ai:sdk:info once at construction, guarded so an uninitialized client can't throw from the constructor.
  • evaluationMetricKey resolves to the first non-blank entry (handled by the feat: AgentControl data model, parsing & interpolation (AIC-2662) #171 parser).

Key decisions

  • Synchronous API (no CompletableFuture). Matches the core Java server SDK's blocking style and sidesteps Android API-level/threading concerns. variation is in-memory after init, so agentConfigs fan-out parallelism buys ~nothing while adding real complexity. Resolves the ticket's async-surface question.
  • No per-field merge of missing model/provider/instructions from the default (matches JS).
  • Tracking deferred to Step 4 (AIC-2664). LDAIConfigTracker is a placeholder interface with an internal no-op; configs expose createTracker() so Step 4 fills in behavior without reshaping the public config types.
  • Construction: new LDAIClientImpl(ldClient) (interface LDAIClient is provided for mocking/DI). A second constructor accepts an LDLogger.

Thread-safetyLDAIClientImpl holds only the thread-safe base client, a logger, and one shared Interpolator (thread-safe template cache); every returned config is immutable.

TestsLDAIClientImplTest (17 cases) covers usage events, SDK-info emission + constructor guard, typed retrieval, interpolation/ldctx, mode-mismatch (disabled + single warning, no NPE), default semantics (absent → default; no per-field merge), and agentConfigs ordering/count. LDValueConverterTest extended for the new inverse conversion. ./gradlew clean build green (checkstyle + Javadoc + all tests).

Describe alternatives you've considered

  • Single unified config type (default == result) — rejected; the ticket and sibling SDKs intentionally separate the caller-supplied default from the retrieved result (which carries the key + tracker + interpolated content).
  • Async CompletableFuture surface — rejected for a server SDK (see decision above).

Additional context

AISdkInfo.VERSION is currently a constant kept in step with gradle.properties; wiring it to a generated/build-time version can be a follow-up.

Made with Cursor


Note

Medium Risk
New public SDK API on the flag-evaluation path; incorrect defaults, mode handling, or interpolation would affect how apps load AI configs in production, though scope is additive with guarded telemetry.

Overview
Adds the public AI Config retrieval surface for the Java server AI SDK: immutable AICompletionConfig, AIAgentConfig, and AIJudgeConfig types (plus matching *Default builders and AIAgentConfigRequest for batch agents), wired through LDAIClient / LDAIClientImpl.

LDAIClientImpl evaluates flags via jsonValueVariation with a null sentinel, parses variations, validates mode (mismatch → disabled config + warning), and interpolates messages/instructions (including {{ldctx}}). Absent flags return the caller default with interpolation and no per-field merge from defaults when a variation is present. It emits $ld:ai:sdk:info at construction (failure-safe) and $ld:ai:usage:* metrics per method; LDAIConfigTracker is a placeholder with a no-op implementation until a later step.

The README now documents constructing LDAIClientImpl from LDClient and calling completionConfig. LDAIClientImplTest covers usage events, interpolation, mode mismatch, default semantics, and ordered agentConfigs.

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

@ctawiah ctawiah force-pushed the feat/AIC-2695/ai-sdk-vendor-mustache branch from b8778a2 to 97d46c1 Compare June 10, 2026 23:43
@ctawiah ctawiah force-pushed the feat/AIC-2663/ai-sdk-client branch 2 times, most recently from 72fc433 to ac6b827 Compare June 11, 2026 01:49
@ctawiah ctawiah marked this pull request as ready for review June 11, 2026 01:53
@ctawiah ctawiah requested a review from a team as a code owner June 11, 2026 01:53
Comment thread lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAIClient.java Outdated
return (AIJudgeConfig) evaluate(key, context, effectiveDefault, Mode.JUDGE, variables);
}

private AIAgentConfig evaluateAgent(

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.

We moved all the config building out of the client class to keep it more focused in dotnet. Not a requirement but something to consider.

@ctawiah ctawiah Jun 11, 2026

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.

I'm going to take that as a follow-up rather than fold it into this PR. These AI SDK PRs are stacked (config types -> tracker -> evals), and the tracker PR already touches the same buildConfig/buildConfigFromDefault paths, so extracting a ConfigFactory here would force a sizable rebase across the whole stack. I'll open a follow-up ticket to pull the building logic into its own factory (mirroring how .NET structured it) once the stacked PRs lands.

ctawiah added a commit that referenced this pull request Jun 11, 2026
- Return the caller's default config (not a hard-disabled config) on AI Config
  mode mismatch, per the recent spec change; drop the now-unused disabledConfig
  helper and update the LDAIClient docs/test accordingly.
- Remove the unnecessary try/catch around the SDK-info trackMetric call in the
  constructor (the call cannot throw) and the test that only passed via a
  throwing mock.
- Use release-please block markers on the AISdkInfo VERSION line and register
  AISdkInfo.java in the package's extra-files so the version is actually bumped.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ctawiah ctawiah requested a review from jsonbailey June 11, 2026 20:53
@ctawiah ctawiah force-pushed the feat/AIC-2695/ai-sdk-vendor-mustache branch from 68a0c99 to b1b318e Compare June 11, 2026 21:28
ctawiah and others added 4 commits June 11, 2026 17:28
Implement the public AI Config types and the LDAIClient retrieval methods that
evaluate a flag, validate its mode, interpolate prompt templates, and return a
typed config.

Config types follow the .NET-style two-hierarchy design (AIConfig /
AIConfigDefault bases): AICompletionConfig(+messages), AIAgentConfig
(+instructions), AIJudgeConfig(+evaluationMetricKey) results plus parallel
*Default builder types (a generic base builder keeps the defaults DRY), and
AIAgentConfigRequest for batch agent retrieval.

LDAIClientImpl mirrors the JS reference the spec points to:
- serialize the caller default to a flag value (with the requested mode) and
  pass it through jsonValueVariation, so an absent flag yields the default and
  the eval event records the correct default;
- validate mode: a mismatch logs a single warning and returns a disabled config
  of the requested type (never a config that would NPE the caller);
- interpolate messages/instructions via the vendored-Mustache Interpolator,
  exposing the context as {{ldctx}};
- fire the spec'd usage events ($ld:ai:usage:completion-config, :agent-config,
  :agent-configs, :judge-config) and emit $ld:ai:sdk:info once at construction,
  guarded so an uninitialized client can't throw from the constructor.

Design decisions (documented in the PR):
- Synchronous API (no CompletableFuture): matches the core Java server SDK and
  avoids Android-API/threading concerns; variation is in-memory post-init so
  agentConfigs fan-out parallelism buys ~nothing.
- No per-field merge of missing fields from the default (matches JS).
- Tracking is deferred to Step 4 (AIC-2664): LDAIConfigTracker is a placeholder
  interface with an internal no-op; configs expose createTracker() so Step 4
  fills in behavior without reshaping the public types.

Adds LDValueConverter.fromJavaObject (inverse conversion for default
serialization). LDAIClientImplTest covers usage events, typed retrieval,
interpolation/ldctx, mode-mismatch, default semantics, and agentConfigs.

Co-authored-by: Cursor <cursoragent@cursor.com>
…IC-2663)

When a flag is absent or unevaluable, build the typed AIConfig straight from
the caller's default rather than serializing the default to LDValue and parsing
it back. Drops the now-unused LDValueConverter.fromJavaObject helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
Update the AIConfig hierarchy, LDAIClientImpl, and tests to reference the
consolidated LDAIConfigTypes.{Mode,Message,Model,Provider,Tool,JudgeConfiguration}
types introduced on the data-model PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Return the caller's default config (not a hard-disabled config) on AI Config
  mode mismatch, per the recent spec change; drop the now-unused disabledConfig
  helper and update the LDAIClient docs/test accordingly.
- Remove the unnecessary try/catch around the SDK-info trackMetric call in the
  constructor (the call cannot throw) and the test that only passed via a
  throwing mock.
- Use release-please block markers on the AISdkInfo VERSION line and register
  AISdkInfo.java in the package's extra-files so the version is actually bumped.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ctawiah ctawiah force-pushed the feat/AIC-2663/ai-sdk-client branch from 3ace063 to dfc1386 Compare June 11, 2026 21:28

@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 dfc1386. Configure here.

}
result.put(
request.getKey(),
evaluateAgent(request.getKey(), context, request.getDefaultValue(), request.getVariables()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agent batch metric overcounts

Low Severity

agentConfigs reports usage with agentConfigs.size() before the loop, but null entries in the list are skipped without evaluation. The $ld:ai:usage:agent-configs metric value and count can exceed the number of configs actually retrieved.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dfc1386. Configure here.

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