Skip to content

feat: vendor Mustache templating engine & add Interpolator (AIC-2695)#172

Open
ctawiah wants to merge 7 commits into
mainfrom
feat/AIC-2695/ai-sdk-vendor-mustache
Open

feat: vendor Mustache templating engine & add Interpolator (AIC-2695)#172
ctawiah wants to merge 7 commits into
mainfrom
feat/AIC-2695/ai-sdk-vendor-mustache

Conversation

@ctawiah

@ctawiah ctawiah commented Jun 8, 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-2695 — Vendor the Mustache templating library + add the Interpolator. Part of epic AIC-2629.

Stacked on #171 (feat/AIC-2662/ai-sdk-datamodel). It builds on the internal.LDValueConverter introduced there. The base will be retargeted to main once #171 merges.

Describe the solution you've provided

  • Vendored sourcecom.launchdarkly.sdk.server.ai.internal.mustache (7 files). Kept byte-for-byte from upstream 1.15 aside from a provenance banner and the relocated package declaration. It is now compiled from source and ships inside this jar with no third-party runtime dependency.
  • Java 8 bytecode. Because we compile from source against our targetCompatibility = 1.8, the vendored classes are emitted as class-version 52 (Java 8). This sidesteps the UnsupportedClassVersionError the external 1.16 jar hit on JDK 8.
  • Interpolator (internal) renders AI Config message/instruction templates using the cross-SDK policy shared with JS/Python: no HTML escaping, missing/null variables render empty, and the reserved ldctx variable (derived from the eval context) always wins. Compiled templates are cached in a ConcurrentHashMap; the compiler and compiled Templates are immutable/thread-safe.
  • License/compliance. Upstream copyright headers retained; THIRD-PARTY-NOTICES.txt records the upstream BSD 3-Clause license (Copyright (c) 2010 Michael Bayne).
  • Build hygiene. The vendored package is excluded from checkstyle (third-party style) and is already covered by the internal/** exclusion for the published javadoc/sources jars. Verified the main jar contains the compiled vendored classes while the sources jar excludes all internal code.

Validated with ./gradlew clean build (checkstyle + all unit tests, including the 10 InterpolatorTest parity cases, green).

Describe alternatives you've considered

  • Linking the external jmustache artifact — rejected on supply-chain grounds (it was the impetus for this ticket).
  • Forking jmustache into the org and depending on the fork — more maintenance overhead and still a separate runtime artifact; vendoring keeps everything in-tree with zero added runtime deps.

Additional context

This must land before the v1.0 release (AIC-2666).

Made with Cursor


Note

Medium Risk
Large in-tree third-party surface and template rendering will affect AI prompt text once wired, though changes stay internal with tests; supply-chain risk is reduced by removing the external artifact.

Overview
Vendors JMustache 1.15 into com.launchdarkly.sdk.server.ai.internal.mustache so AI Config templating ships in the jar with no com.samskivert:jmustache runtime dependency, and documents the BSD license in THIRD-PARTY-NOTICES.txt.

Adds an internal Interpolator that renders Mustache templates for AI messages/instructions using the cross-SDK rules: no HTML escaping, missing/null → empty string, and ldctx built from LDContext (including multi-kind layout) merged last so it overrides caller-supplied ldctx. Templates are compiled once and cached in a ConcurrentHashMap.

build.gradle updates the Mustache supply-chain note and excludes the vendored package from checkstyleMain. InterpolatorTest locks parity behavior (escaping, ldctx, nesting, cache).

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

@ctawiah ctawiah marked this pull request as ready for review June 8, 2026 17:23
@ctawiah ctawiah requested a review from a team as a code owner June 8, 2026 17:23
@@ -0,0 +1,43 @@
This product includes vendored third-party source code. The relevant licenses

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.

This was only added to give attribution to the vendored lib but if theres existing attribution process, I'm happy to switch to that.

if (context == null || !context.isValid()) {
return new HashMap<>();
}
LDValue asValue = LDValue.parse(JsonSerialization.serialize(context));

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 shouldn't need to go through serialization to get to a map. That's not very efficient. This should probably be something that walks the graph. At the leafs you can use your LDValueConverter in the target PR.

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.

Consider putting this as a utility in common package next to the context implementation.

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.

On serialization: I've updated the logic so it no longer round-trips through serialize/parse.

On the common utility: agreed, the context-encoding logic belongs in common. Since we already have a follow-up ticket to move LDValueConverter into common (AIC-2715), I've folded this into it so both move together when we update common.

@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 b8778a2. 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 requested a review from tanderson-ld June 11, 2026 01:47
Base automatically changed from feat/AIC-2662/ai-sdk-datamodel to main June 11, 2026 14:28
* {@code ldctx}, without round-tripping through JSON serialization. A single-kind context becomes
* a map of its attributes; a multi-kind context becomes {@code {"kind":"multi", <kind>: {...}}}
* with one nested map per individual context.
*/

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.

Had AI double check this against other impls. Looks like .NET always emits anonymous true/false and not only when true. Also .NET puts the fully qualified key next to Multi at the top level.

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.

Also need to discuss with SDK team if private attributes should be going into the templating space. That will happen in a few hours.

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.

@tanderson-ld what decision came out of the discussion with the SDK team

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.

Good to template with those attributes. Did the .NET disparity get investigated / addressed?

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.

Yep, investigated it against the .NET source (ConfigFactory.cs) and addressed both items in this PR - good catch

ctawiah and others added 7 commits June 11, 2026 17:19
Vendor the com.samskivert:jmustache:1.15 source into the relocated internal
package com.launchdarkly.sdk.server.ai.internal.mustache instead of linking the
external artifact, per the SDK team's supply-chain guidance. The library is now
compiled from source and ships inside this jar with no third-party runtime
dependency, and compiles to Java 8 bytecode against our target (avoiding the
class-version mismatch the external jar hit on JDK 8).

Also (re)adds the internal Interpolator, which renders AI Config message and
instruction templates using the cross-SDK policy (no HTML escaping, missing/null
render empty, reserved ldctx wins) and caches compiled templates.

- Vendored source kept byte-for-byte from upstream aside from a provenance
  banner and the relocated package declaration.
- THIRD-PARTY-NOTICES.txt records the upstream BSD 3-Clause license.
- Vendored package excluded from checkstyle; internal package already excluded
  from the published javadoc/sources jars.

Co-authored-by: Cursor <cursoragent@cursor.com>
Build the ldctx template variable by walking the LDContext's attributes
directly instead of serializing it to a JSON string and parsing it back
into an LDValue. This removes the serialize-then-deserialize round trip
flagged in review, following the generic-encoder shape used by the iOS
SDK. Custom attribute values are still converted via LDValueConverter so
nested objects/arrays remain addressable, and multi-kind contexts are
exposed as {"kind":"multi", <kind>: {...}}.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ts (AIC-2695)

Match LaunchDarkly's standard context JSON, where per-kind objects under a
multi-kind context omit "kind" (it is implied by the property key). Keeps
{{ldctx.<kind>.kind}} consistent with the JS and Python SDKs.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Always expose ldctx.anonymous as true/false rather than only when true.
- Expose the canonical fully-qualified key at the top level of a multi-kind
  ldctx so {{ldctx.key}} resolves, matching the .NET (and other) SDKs.

Adds InterpolatorTest cases for both.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…C-2695)

Co-authored-by: Cursor <cursoragent@cursor.com>
@ctawiah ctawiah force-pushed the feat/AIC-2695/ai-sdk-vendor-mustache branch from 68a0c99 to b1b318e Compare June 11, 2026 21:28
@ctawiah ctawiah requested a review from tanderson-ld June 11, 2026 21:30
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