Back to Baml

State of BAML↔Java completeness

baml_language/sdks/agent-docs/bridge-ref/ref-java-state-of-completeness.md

0.226.124.5 KB
Original Source

State of BAML↔Java completeness

This document tracks the state of the BAML/Java (JVM) bridge: what is supported / partially supported / not supported. It is the Java copy of [State of BAML Python completeness]; Python is the reference bridge, so the parity target for every row is Python's status (shown in the Python column). Per the Bridge Week ground rules, a row is only flipped to ✅ once the matching parity test passes in sdk_tests.

Cells in the Java columns marked (proposed) are design intent, not implemented behavior.

Architecture: minimum supported Java is 17 (records + sealed interfaces; the de-facto library baseline — Spring Boot 3+ requires it). That rules out Panama/FFM (final only in JDK 22+), so the native binding is JNI. Proposed shape: a bridge_java Rust cdylib using the jni crate that links bridge_cffi in-process — the JVM analog of bridge_python (pyo3) / bridge_nodejs (napi) — speaking the same baml_bridge.cffi.v1 protobuf envelopes. In-process linking also gives a true sync call path (like pyo3/napi) instead of synthesizing sync by blocking on the async-callback-only C ABI; async siblings return CompletableFuture<T> completed from the result callback. (Alternative if we ever need pure-dlopen: the bridge_go model over bridge.h, at the cost of hand-rolled JNI glue or a JNA/JNR dependency.)

Status (2026-07-17): the full value-model slice is green — type_shapes runs 110/110 against the live engine (primitives incl. bigint round-trip beyond i64 — test_round_trip_bigint + test_return_bigint, added 2026-07-20, closing the last 🚧 value-table row; unions 6/6, optional 5/5, lists 5/5, maps 4/4, enums 5/5, media 9/9, handles 4/4 (baml.fs.File/baml.http.Response shells incl. cursor-state encode-back), stream partial-values 5/5 (host-constructed Resume$stream/Foo$stream round-trips — engine accepts them; $-preserved in-package companions, TS-aligned, decided 2026-07-17), classes/refs/recursion/routing all green, generics 6/6 + 2/2, aliases 3/3, forward-refs 4/4); All 21 test files compile and pass — the compile-exclude list is empty. Streaming callsBamlStream — have now landed (the last unimplemented capability): llm_functions TestStreamingE2e runs 6/6 against a keyless in-process replay harness (native BridgeEnv setenv shim + BAML replay server), and llm_functions is now in the enforced gate (21/21, javac+junit, fully deterministic offline). function_calls is now in the enforced CI gate too, all slices landed: 157 run / 153 pass / 0 fail / 4 skips (155 + 2 new value-level async host-callable tests) (the skips are @Disabled/xfail cases: host-callable release-on-GC, a partial-subscript arity xfail, and two negative-runtime-case placeholders) — smoke 2/2, static+instance methods with _async siblings 7/7, optional args 4/4, stdlib entrypoints 3/3, @throws Javadoc 8/8, cancellation 7/7, errors/panics/os-exit green, generics (inferred + explicit) green, host-callables green (incl. a concurrent registry-isolation test); the compile-exclude list is empty. The bridge registers with the versioned C ABI at init (BridgeLanguage::Java = 7, canonical-version validated). Unions follow the cross-language team decision: the runtime's generic arity family baml_bridge.Union2<A,B>Union10<...> (sealed, nested records Arm0..Arm{n-1} in declaration order → exhaustive switch on Java 21+) with type-directed decode — generated bindings pass a type-descriptor string; the wire/protobuf layer is unchanged. Recursive type aliases keep a nominal sealed type named after the alias. baml generate accepts output_type = "java". 🚧 = implemented/emitted but not yet parity-verified. Packaging (2026-07-16): com.boundaryml:baml-bridge:0.15.0-nightly.1 is live on Maven Central — GPG-signed main + linux-x86_64 natives + sources/javadoc jars; verified by a clean consumer resolving from Central only (no local copies) and executing BAML functions through the embedded native library. Remaining packaging work: full platform matrix via CI, GMM variant auto-selection, Gradle plugin (in progress).

Function-call forms (how a BAML callable is invoked from Java)

Call formPythonJavaBAML shapeJava call form (proposed)
Free function (sync)function classify(...) -> Tstatic method on the namespace's generated class (BAML has free functions, Java doesn't — exact shape TBD)
Free function (async)same..._async sibling returning CompletableFuture<T>
Static methodclass Resume { function parse(...) }Resume.parse(...) (static method)
Instance methodclass Agent { function reply(self, ...) }agent.reply(...) (instance method; receiver encoded as required param 0)
Required args (positional)function classify(text: string) -> Tordinary positional Java parameters
Required args (keyword)n/asameJava has no keyword arguments
Optional args (omitted → default)function classify(text: string, lang: string = "en")trailing generated options object/builder; unset fields omitted from the wire so the engine evaluates defaults
Optional args (supplied)sameset on the options object; explicit null encodes BAML null (distinct from unset)
Streamingclassify$stream(...)classify$stream(...) companion ($-preserved) returning BamlStream<TPartial, TFinal> — runtime-owned wrapper around an ai.stream.Stream tagged handle. Decode retains the handle's concrete class FQN; next()/get_final() (+ _async) derive <FQN>.next/<FQN>.final. Parity-verified by llm_functions TestStreamingE2e (6/6, keyless replay harness).
$build_request companionclassify$build_request(...)BAML name preserved verbatim ($-legal, TS-style — no mangling). Parity-verified by the llm_functions TestMain $build_request api-key tests (via the native BridgeEnv hook)
Generic function / method (inferred)function classify<T>(...)classify(...) (type args inferred engine-side)
Generic function / method (explicit)function classify<T>(...)explicit type-args overload taking BamlType tokens (Java has no _types= kwarg or subscript; shape TBD)
Host callback paramfunction run_agent(query: string, tool: (string) -> string) -> Tjava.util.function.* / generated functional interface, registered in the host-value registry

Runtime-behavior forms (what a call does at the boundary, beyond returning a value)

The bridge decodes the BamlOutboundResult envelope and dispatches its ok / error / panic arm. These rows are the control-flow outcomes of a call, orthogonal to the value it carries.

Runtime behaviorPythonJavaTrigger (BAML side / caller)Java outcome (proposed)
Normal returnok armdecoded value (see value table)
BAML error✅ (docs-only)error arm; throws E written or inferredthrow unchecked BamlError carrying decoded .value(), .baml_trace(), .class_name(); thrown types documented via Javadoc @throws, not checked exceptions. baml.errors.TypeMismatch → native IllegalArgumentException (decided 2026-07-17, mirrors Python's TypeError; BAML frames synthesized into the Java stack trace — in progress)
BAML panicpanic arm (non-exit)throw unchecked BamlPanic(value, bamlTrace, className) (re-parenting to Error — the analog of Python's BaseException choice — decided 2026-07-17, in progress)
Java error (host callback)Java exception thrown inside a passed-in host callable, surfaced back through the enginerehydrate and rethrow the original exception object by identity via the host-value registry (baml.errors.HostCallable handle)
Cancellationcaller cancels the future, ctx.abort(), or engine returns baml.panics.Cancelledtrailing BamlCallContext overloads (f(req…, ctx)); engine-driven abort → BamlCancelledError extends CancellationException (future counts as cancelled, join()/get() throw it directly); future.cancel(true)cancel_function_call(call_id) + raw CancellationException; sync abort → BamlPanic(Cancelled) (full Python parity incl. sync)
OS exitpanic with is_exit_panic (baml.sys.exit)run registered best-effort flush hooks (BamlFfi.registerExitFlushHooks socket — empty until telemetry ships; exceptions swallowed) then Runtime.getRuntime().halt(exitCode) — hard exit, bypasses shutdown hooks (analog of Python os._exit)

Value kinds supported across the BAML/Java bridge

Directional shorthand: in = Java→BAML encode (host encoder → baml_inbound.protovalue_decode.rs), out = BAML→Java decode (value_encode.rsbaml_outbound.proto → host decoder).

Rows marked ❌ in the Python column have no host-side path in the reference bridge either; they are out of parity scope unless the shared plan changes.

Value table

Value kindPythonJavaJava value type (proposed)BEV type (BexExternalValue)Wire (in / out)
PrimitivenullNullabsent oneof / null_value
Primitiveboolean / BooleanBoolbool_value
Primitivelong / LongIntint_value
Primitive (outside i64)java.math.BigIntegerBigintbigint_value (hex string, sign-prefixed). Encode is magnitude-directed: an i64-range BigInteger rides int_value, only out-of-range values take the hex bigint_value channel (ProtoWriter); the engine's outbound encode always emits a Bigint BEV as bigint_value regardless of magnitude (value_encode.rs, no i64 canonicalization). Decode is capped at MAX_BIGINT_HEX_LEN = 2²⁸ bits (DoS guard, not a value cap). Parity-verified by type_shapes TestPrimitives test_round_trip_bigint (2⁸⁰, −2⁸⁰, and the 2⁶⁴ hex boundary — all beyond i64 so they exercise the hex channel) + test_return_bigint (decode of 12345678901234567890), ported 1:1 from the python_pydantic2 twin.
Primitivedouble / DoubleFloatfloat_value
PrimitiveStringStringstring_value
Primitivebyte[]Uint8Arrayuint8array_value
Containerjava.util.List<T>Array{element_type,items}list_value (explicit presence for empty lists)
Containerjava.util.Map<String,V> (all keys stringified engine-side)Map{key_type,value_type,entries}map_value (explicit presence for empty maps)
Enumgenerated Java enum + wire-name serializer map (Java must encode enum→variant name explicitly — TS gets this free from string enums)Variant{enum_name,variant_name}enum_value (FQN + variant)
Classgenerated class (record vs POJO TBD — records fit immutability but can't carry hidden handle/type-args fields)Instance{class_name,type_args:[],fields}class_value w/ class_ty.name = FQN; out: FQN→ctor via typemap registry
Generic explicitly reified by BAML-known type🚧generated generic class + hidden BamlType[] type-args side-channel (Java generics are erased; mirrors TS $generic/$types)Instance{class_name,type_args:[Int],fields}class_ty.type_args in; type_args out reparameterize the instance
Generic implicitly reified by BAML-known typesameInstance{class_name,type_args:[],fields}type args filled via engine generic inference
Generic reified by host-only typerejected (parity with Python)
Union✅ (metadata dropped)generic arity family baml_bridge.Union2<A,B>Union10 (team decision 2026-07-16): sealed interfaces with nested generic records Arm0..Arm{n-1} in BAML declaration order — exhaustive switch via record patterns on Java 21+, instanceof on 17. Decode is type-directed (descriptor from the generated binding; arms matched against the declared list in source order — the wire's value_option_name/order is never trusted); encode unwraps to the bare inner value. T|null collapses to boxed nullable T; same-base literal unions erase to the base; recursive aliases keep a nominal sealed type named after the alias; arity > 10 pends the threshold/alias policyUnion{value,metadata}in: inner value only; out: union_variant_value (metadata present but not relied upon)
BAML interfaceout of scope (Python codegen is Any)
Mediaruntime-owned Image/Audio/Video/Pdf handle-backed classes (re-exported, never code-generated) wrapping BamlHandle (Cleaner + AutoCloseable lifecycle); ctors via JNI over the same Rust the C ABI baml_media_from_url/file/base64 funnel through; wire key cloned per send (engine drains it)Adt(Media)in: class_value w/ stdlib FQN + _data handle; out: handle_value ADT_MEDIA_*
StreamBamlStream<TPartial,TFinal> wrapping a typed tagged handle; next/get_final + _async siblings call <carried-class-FQN>.next / .final. Exhaustion returns a baml_sdk.ai.stream.Done VALUE (runtime-owned, registered under ai.stream.Done); next() is declared TPartial but bound to Object so instanceof Done compilesAdt(TaggedHeapHandle{ty,heap_handle})in: inner handle (cloned key); out: ADT_TAGGED_HEAP_HANDLE → retain ty.class_ty.nameBamlStream.fromHandle (generic args erased, concrete class identity preserved)
Host callablefunctional interface, registered in host-value registry; dispatched via register_host_dispatch_callback → executor → complete_host_callHostValue{Callable}in: handle HOST_VALUE_CALLABLE; out ok-path: bare handle (identity lost); error-path: rehydrated
Host callable (async)callable returning CompletableFuture<T>, awaited on the dispatch executor via whenComplete (async detected at the VALUE level, design point C); failure path drives the same design-point-D identity rehydration. Parity-verified by function_calls TestHostCallables (test_async_callable_returning_future_is_awaited_by_bridge + ..._future_completing_exceptionally_round_trips_original)HostValue{Callable}same as above
BAML closurebare BamlHandle (not callable back) — parity with PythonFunctionRef{global_index}out only: handle_value FUNCTION_REF
BAML type reference valuesout of scope (no encoder/decoder arm in Python either)Adt(Type(RuntimeTy))
BAML type definition valuesengine-rejected
BAML $rust_type values: baml.io.File, baml.net.UdpSocket, etcgenerated shell class holding a private BamlHandle; lifecycle via java.lang.ref.Cleaner + AutoCloseablebaml_handle_release (JVM finalizers are deprecated)RustData(Arc<dyn Any>)round-trips as handle (UNTAGGED_RUST_DATA) in the shell's private field
native Java exception thrown by a host callbackoriginal exception object kept in host-value registry, rehydrated by key on the error pathHostValue{Opaque}out: HOST_VALUE_OPAQUE handle
n/a — unused in SDKs (Handle, Collector, PromptAst)🚧bare BamlHandle via catch-all (only reachable with copy_objects=false, which the SDK never uses)Handle / Adt(Collector) / Adt(PromptAst)handle_value
BAML builtin type (Future / UnscheduledFuture)engine-rejected
arbitrary unsupported Java objectencoder throws IllegalArgumentException naming the offending argument
cyclic / self-referential objectsunbounded recursion (parity with Python; wishlist)

Wishlist

  • mutability across the boundary (prerequisite: cyclic/self-referential objects preserved correctly)
  • interfaces (as args, as return values)
  • function visibility (mechanism for hiding baml std functions from Java)
  • serde delegation to BAML (bamlFoo.fromJson() / bamlFoo.toJson() delegating to BAML function calls)
  • Kotlin-friendly surface (data-class interop, suspend-function wrappers over the async siblings)