baml_language/sdks/agent-docs/bridge-ref/ref-java-state-of-completeness.md
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 calls — BamlStream — 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).
| Call form | Python | Java | BAML shape | Java call form (proposed) |
|---|---|---|---|---|
| Free function (sync) | ✅ | ✅ | function classify(...) -> T | static 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 method | ✅ | ✅ | class Resume { function parse(...) } | Resume.parse(...) (static method) |
| Instance method | ✅ | ✅ | class Agent { function reply(self, ...) } | agent.reply(...) (instance method; receiver encoded as required param 0) |
| Required args (positional) | ✅ | ✅ | function classify(text: string) -> T | ordinary positional Java parameters |
| Required args (keyword) | ✅ | n/a | same | Java 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) | ✅ | ✅ | same | set on the options object; explicit null encodes BAML null (distinct from unset) |
| Streaming | ✅ | ✅ | classify$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 companion | ✅ | ✅ | classify$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 param | ✅ | ✅ | function run_agent(query: string, tool: (string) -> string) -> T | java.util.function.* / generated functional interface, registered in the host-value registry |
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 behavior | Python | Java | Trigger (BAML side / caller) | Java outcome (proposed) |
|---|---|---|---|---|
| Normal return | ✅ | ✅ | ok arm | decoded value (see value table) |
| BAML error | ✅ (docs-only) | ✅ | error arm; throws E written or inferred | throw 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 panic | ✅ | ✅ | panic 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 engine | rehydrate and rethrow the original exception object by identity via the host-value registry (baml.errors.HostCallable handle) |
| Cancellation | ✅ | ✅ | caller cancels the future, ctx.abort(), or engine returns baml.panics.Cancelled | trailing 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 exit | ✅ | ✅ | panic 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) |
Directional shorthand: in = Java→BAML encode (host encoder → baml_inbound.proto → value_decode.rs), out = BAML→Java decode (value_encode.rs → baml_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 kind | Python | Java | Java value type (proposed) | BEV type (BexExternalValue) | Wire (in / out) |
|---|---|---|---|---|---|
| Primitive | ✅ | ✅ | null | Null | absent oneof / null_value |
| Primitive | ✅ | ✅ | boolean / Boolean | Bool | bool_value |
| Primitive | ✅ | ✅ | long / Long | Int | int_value |
| Primitive (outside i64) | ✅ | ✅ | java.math.BigInteger | Bigint | bigint_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. |
| Primitive | ✅ | ✅ | double / Double | Float | float_value |
| Primitive | ✅ | ✅ | String | String | string_value |
| Primitive | ✅ | ✅ | byte[] | Uint8Array | uint8array_value |
| Container | ✅ | ✅ | java.util.List<T> | Array{element_type,items} | list_value (explicit presence for empty lists) |
| Container | ✅ | ✅ | java.util.Map<String,V> (all keys stringified engine-side) | Map{key_type,value_type,entries} | map_value (explicit presence for empty maps) |
| Enum | ✅ | ✅ | generated 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) |
| Class | ✅ | ✅ | generated 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 type | ✅ | ✅ | same | Instance{class_name,type_args:[],fields} | type args filled via engine generic inference |
| Generic reified by host-only type | ❌ | — | rejected (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 policy | Union{value,metadata} | in: inner value only; out: union_variant_value (metadata present but not relied upon) |
| BAML interface | ❌ | — | out of scope (Python codegen is Any) | — | — |
| Media | ✅ | ✅ | runtime-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_* |
| Stream | ✅ | ✅ | BamlStream<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 compiles | Adt(TaggedHeapHandle{ty,heap_handle}) | in: inner handle (cloned key); out: ADT_TAGGED_HEAP_HANDLE → retain ty.class_ty.name → BamlStream.fromHandle (generic args erased, concrete class identity preserved) |
| Host callable | ✅ | ✅ | functional interface, registered in host-value registry; dispatched via register_host_dispatch_callback → executor → complete_host_call | HostValue{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 closure | ❌ | — | bare BamlHandle (not callable back) — parity with Python | FunctionRef{global_index} | out only: handle_value FUNCTION_REF |
| BAML type reference values | ❌ | — | out of scope (no encoder/decoder arm in Python either) | Adt(Type(RuntimeTy)) | — |
| BAML type definition values | ❌ | — | engine-rejected | — | — |
BAML $rust_type values: baml.io.File, baml.net.UdpSocket, etc | ✅ | ❌ | generated shell class holding a private BamlHandle; lifecycle via java.lang.ref.Cleaner + AutoCloseable → baml_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 callback | ✅ | ✅ | original exception object kept in host-value registry, rehydrated by key on the error path | HostValue{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 object | ❌ | — | encoder throws IllegalArgumentException naming the offending argument | — | — |
| cyclic / self-referential objects | ❌ | — | unbounded recursion (parity with Python; wishlist) | — | — |
bamlFoo.fromJson() / bamlFoo.toJson() delegating to BAML function calls)