@boundaryml/baml-bridge-web 0.0.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +16 -14
  4. package/dist/index.js.map +1 -1
  5. package/dist/native.d.ts +50 -14
  6. package/dist/native.d.ts.map +1 -1
  7. package/dist/native.js +327 -70
  8. package/dist/native.js.map +1 -1
  9. package/dist/platform.d.ts +4 -0
  10. package/dist/platform.d.ts.map +1 -0
  11. package/dist/platform.js +5 -0
  12. package/dist/platform.js.map +1 -0
  13. package/dist/shared/call_context.d.ts +7 -0
  14. package/dist/shared/call_context.d.ts.map +1 -0
  15. package/dist/shared/call_context.js +11 -0
  16. package/dist/shared/call_context.js.map +1 -0
  17. package/dist/shared/define_function.d.ts.map +1 -1
  18. package/dist/shared/define_function.js +13 -24
  19. package/dist/shared/define_function.js.map +1 -1
  20. package/dist/shared/errors.d.ts +4 -4
  21. package/dist/shared/errors.d.ts.map +1 -1
  22. package/dist/shared/errors.js +16 -10
  23. package/dist/shared/errors.js.map +1 -1
  24. package/dist/shared/host_value_registry.d.ts +5 -3
  25. package/dist/shared/host_value_registry.d.ts.map +1 -1
  26. package/dist/shared/host_value_registry.js +10 -6
  27. package/dist/shared/host_value_registry.js.map +1 -1
  28. package/dist/shared/platform.d.ts +4 -0
  29. package/dist/shared/platform.d.ts.map +1 -0
  30. package/dist/shared/platform.js +5 -0
  31. package/dist/shared/platform.js.map +1 -0
  32. package/dist/shared/proto/baml_cffi.d.ts +35 -8
  33. package/dist/shared/proto/baml_cffi.js +138 -25
  34. package/dist/shared/proto.d.ts +9 -5
  35. package/dist/shared/proto.d.ts.map +1 -1
  36. package/dist/shared/proto.js +197 -104
  37. package/dist/shared/proto.js.map +1 -1
  38. package/dist/shared/stream.d.ts +3 -2
  39. package/dist/shared/stream.d.ts.map +1 -1
  40. package/dist/shared/stream.js +22 -15
  41. package/dist/shared/stream.js.map +1 -1
  42. package/dist/wasm/bridge_web_core.d.ts +99 -68
  43. package/dist/wasm/bridge_web_core.js +519 -492
  44. package/dist/wasm/bridge_web_core_bg.wasm +0 -0
  45. package/dist/wasm/bridge_web_core_bg.wasm.d.ts +41 -27
  46. package/dist/wasm/package.json +1 -1
  47. package/dist/workerd/index.d.ts +3 -3
  48. package/dist/workerd/index.d.ts.map +1 -1
  49. package/dist/workerd/index.js +21 -14
  50. package/dist/workerd/index.js.map +1 -1
  51. package/dist/workerd/native.d.ts +50 -14
  52. package/dist/workerd/native.d.ts.map +1 -1
  53. package/dist/workerd/native.js +327 -70
  54. package/dist/workerd/shared/call_context.d.ts +7 -0
  55. package/dist/workerd/shared/call_context.d.ts.map +1 -0
  56. package/dist/workerd/shared/call_context.js +11 -0
  57. package/dist/workerd/shared/call_context.js.map +1 -0
  58. package/dist/workerd/shared/define_function.d.ts.map +1 -1
  59. package/dist/workerd/shared/define_function.js +13 -24
  60. package/dist/workerd/shared/define_function.js.map +1 -1
  61. package/dist/workerd/shared/errors.d.ts +4 -4
  62. package/dist/workerd/shared/errors.d.ts.map +1 -1
  63. package/dist/workerd/shared/errors.js +16 -10
  64. package/dist/workerd/shared/errors.js.map +1 -1
  65. package/dist/workerd/shared/host_value_registry.d.ts +5 -3
  66. package/dist/workerd/shared/host_value_registry.d.ts.map +1 -1
  67. package/dist/workerd/shared/host_value_registry.js +10 -6
  68. package/dist/workerd/shared/host_value_registry.js.map +1 -1
  69. package/dist/workerd/shared/platform.d.ts +4 -0
  70. package/dist/workerd/shared/platform.d.ts.map +1 -0
  71. package/dist/workerd/shared/platform.js +5 -0
  72. package/dist/workerd/shared/platform.js.map +1 -0
  73. package/dist/workerd/shared/proto/baml_cffi.d.ts +35 -8
  74. package/dist/workerd/shared/proto/baml_cffi.js +140 -25
  75. package/dist/workerd/shared/proto.d.ts +9 -5
  76. package/dist/workerd/shared/proto.d.ts.map +1 -1
  77. package/dist/workerd/shared/proto.js +197 -104
  78. package/dist/workerd/shared/proto.js.map +1 -1
  79. package/dist/workerd/shared/stream.d.ts +3 -2
  80. package/dist/workerd/shared/stream.d.ts.map +1 -1
  81. package/dist/workerd/shared/stream.js +22 -15
  82. package/dist/workerd/shared/stream.js.map +1 -1
  83. package/dist/workerd-wasm/bridge_web_core.d.ts +62 -45
  84. package/dist/workerd-wasm/bridge_web_core.js +3 -5
  85. package/dist/workerd-wasm/bridge_web_core_bg.js +519 -493
  86. package/dist/workerd-wasm/bridge_web_core_bg.wasm +0 -0
  87. package/dist/workerd-wasm/bridge_web_core_bg.wasm.d.ts +41 -27
  88. package/dist/workerd-wasm/package.json +1 -1
  89. package/package.json +22 -5
@@ -4,10 +4,11 @@
4
4
  // Decodes the BamlOutboundResult envelope → TS objects (call results), and
5
5
  // bare BamlOutboundValue bytes → TS objects (host-callable args).
6
6
  import { baml_bridge } from './proto/baml_cffi.js';
7
- import { BamlHandle, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, } from '../native.js';
7
+ import { BamlHandle, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, getRuntime, newFunctionCall, } from '../native.js';
8
8
  import { BamlStream } from './stream.js';
9
- import { BamlAbortError, BamlCancelledError, BamlError, BamlPanic } from './errors.js';
10
- import { registerHostOpaque, tryRehydrateHostValueByKey, } from './host_value_registry.js';
9
+ import { BamlAbortError, BamlCancelledError, BamlClientError, BamlError, BamlInvalidArgumentError, BamlPanic } from './errors.js';
10
+ import { handleExitPanic } from './platform.js';
11
+ import { registerHostOpaque, releaseHostOpaque, tryRehydrateHostValueByKey, } from './host_value_registry.js';
11
12
  import { getTypeMap } from './typemap.js';
12
13
  import { lowerTypeToWireTy, outboundTyToBamlType } from './wire_ty.js';
13
14
  const CallFunctionArgs = baml_bridge.cffi.v1.CallFunctionArgs;
@@ -30,6 +31,16 @@ export class HostCallableSyncError extends Error {
30
31
  this.name = 'HostCallableSyncError';
31
32
  }
32
33
  }
34
+ function rollbackHostCallables(keys) {
35
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
36
+ try {
37
+ releaseHostCallable(keys[index]);
38
+ }
39
+ catch {
40
+ // Best-effort cleanup; never mask the original encode failure.
41
+ }
42
+ }
43
+ }
33
44
  /**
34
45
  * Generic-class instances declare their TypeVar names (declaration order) in a
35
46
  * static `$generic` field that codegen emits; this reads it back. Returns the
@@ -81,8 +92,8 @@ function setInboundValue(iv, value, ctx) {
81
92
  // path. Any future dispatch-backed handle type must be guarded here.
82
93
  if (ctx.syncMode && value.handleType === BamlHandleType.HOST_VALUE_CALLABLE) {
83
94
  throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
84
- '(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
85
- 'thread, so the host callback can never run and the call would hang.');
95
+ '(callFunction) instead of callFunctionSync. The sync path occupies the single ' +
96
+ 'JavaScript/bridge event loop, so the host callback cannot await a future turn.');
86
97
  }
87
98
  // The Rust inbound decoder drains handle-table entries. Send a fresh
88
99
  // cloned key so the JS-owned handle remains valid for later calls.
@@ -113,8 +124,8 @@ function setInboundValue(iv, value, ctx) {
113
124
  // tsfn, which would otherwise be orphaned).
114
125
  if (ctx.syncMode) {
115
126
  throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
116
- '(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
117
- 'thread, so the host callback can never run and the call would hang.');
127
+ '(callFunction) instead of callFunctionSync. The sync path occupies the single ' +
128
+ 'JavaScript/bridge event loop, so the host callback cannot await a future turn.');
118
129
  }
119
130
  // JS callable → register a dispatch wrapper in the host-value
120
131
  // registry and emit `Handle{key, HOST_VALUE_CALLABLE}`. The Rust
@@ -136,13 +147,12 @@ function setInboundValue(iv, value, ctx) {
136
147
  iv.listValue = { values: listVal };
137
148
  }
138
149
  else if (value !== null && typeof value === 'object') {
139
- // Any remaining object a plain object OR a codegen-emitted class
140
- // instance (e.g. `new Resume({...})`) encodes as `map_value` with
141
- // no FQN tag. The Rust side's `coerce_arg_to_declared_type` reshapes
142
- // it against the function's declared parameter type (the 10a
143
- // typemap-free encode simplification). `Object.entries` yields the
144
- // class's own enumerable fields, set by the constructor's
145
- // `Object.assign(this, init)`. The specific built-in wrappers
150
+ // Any remaining object is either a plain object or a codegen-emitted
151
+ // class instance. Plain objects encode as `map_value` and are reshaped
152
+ // against contextual types; generated instances carry a sparse nominal
153
+ // annotation when the typemap identifies their constructor.
154
+ // `Object.entries` yields the class's own enumerable fields, set by the
155
+ // constructor's `Object.assign(this, init)`. The specific built-in wrappers
146
156
  // (BamlHandle/BamlStream/media) are handled by the instanceof
147
157
  // branches above, so they never reach here.
148
158
  //
@@ -173,30 +183,27 @@ function setInboundValue(iv, value, ctx) {
173
183
  setInboundValue(childVal, v, ctx);
174
184
  classFields.push({ stringKey: k, value: childVal });
175
185
  }
176
- iv.classValue = { classTy: { name: fqn }, fields: classFields };
186
+ iv.valueType = { classTy: { name: fqn } };
187
+ iv.classValue = { fields: classFields };
177
188
  return;
178
189
  }
179
190
  }
180
- // Generic class instance a FQN-tagged `class_value` carrying the
181
- // value-level class type-args channel (`class_ty`). Unlike a non-generic
182
- // class instance (which encodes as a bare `map_value` for the engine to
183
- // reshape against the declared param type), a generic instance MUST send
184
- // its concrete type args: the engine strictly rejects coercing a bare map
185
- // into a generic class slot ("a bare map carries no class type
186
- // arguments"). The args come from the optional `$types` instance field;
187
- // an absent binding lowers to the unknown/top type. Mirrors
188
- // bridge_python's pydantic-generic-metadata path in proto.py, which sets
189
- // `class_value.class_ty` for a generic instance.
191
+ // A codegen class instance has nominal identity even though its payload
192
+ // is structurally object-shaped. Preserve that identity with a sparse
193
+ // node annotation. For a generic class, include exact args when every
194
+ // `$types` entry is present; otherwise send only the class name. The
195
+ // engine may refine that nominal hint from one contextual class but
196
+ // will not use it to choose between two concrete instantiations of the
197
+ // same generic class in a union.
190
198
  if (isClassInstance) {
199
+ const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
191
200
  const params = genericParamNames(value);
192
- if (params) {
193
- const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
201
+ if (fqn) {
194
202
  const userTypes = value.$types;
195
- const typeArgs = params.map((p) => lowerTypeToWireTy(userTypes?.[p]));
196
203
  const classFields = [];
197
204
  for (const [k, v] of Object.entries(value)) {
198
205
  // Skip method bindings (behavior, not state) and the synthetic
199
- // `$types` carrier (it rides `class_ty`, not the field list).
206
+ // `$types` carrier (it rides `value_type`, not the field list).
200
207
  if (typeof v === 'function')
201
208
  continue;
202
209
  if (k === '$types')
@@ -205,7 +212,14 @@ function setInboundValue(iv, value, ctx) {
205
212
  setInboundValue(childVal, v, ctx);
206
213
  classFields.push({ stringKey: k, value: childVal });
207
214
  }
208
- iv.classValue = { classTy: { name: fqn, typeArgs }, fields: classFields };
215
+ if (params && userTypes !== undefined && params.every((p) => userTypes[p] !== undefined)) {
216
+ const typeArgs = params.map((p) => lowerTypeToWireTy(userTypes[p]));
217
+ iv.valueType = { classTy: { name: fqn, typeArgs } };
218
+ }
219
+ else {
220
+ iv.valueType = { classTy: { name: fqn, typeArgs: [] } };
221
+ }
222
+ iv.classValue = { fields: classFields };
209
223
  return;
210
224
  }
211
225
  }
@@ -236,8 +250,8 @@ function setInboundValue(iv, value, ctx) {
236
250
  * host-value table and is normally released only when the engine GCs the
237
251
  * `HostClosure` it allocated and fires the C release callback (a GC-timed
238
252
  * release, drained by the engine after collection).
239
- * Because the Node tsfn is built with `weak::<false>` it keeps a strong libuv
240
- * ref, so a *leaked* registry entry can also keep the Node process from
253
+ * Because the host bridge is built with `weak::<false>` it keeps a strong host event loop
254
+ * ref, so a *leaked* registry entry can also keep the JavaScript realm from
241
255
  * exiting — which is exactly why the encode-error rollback below matters: if a
242
256
  * later kwarg fails, the engine never sees (and so never releases) the keys we
243
257
  * already registered, so we release them here.
@@ -247,6 +261,9 @@ export function encodeCallArgs(kwargs, options) {
247
261
  if (callId === 0n) {
248
262
  throw new TypeError('callId must be a nonzero uint64');
249
263
  }
264
+ if (options.functionName !== undefined && options.functionHandle !== undefined) {
265
+ throw new TypeError('exactly one BAML call target may be set');
266
+ }
250
267
  const ctx = { syncMode: options.syncMode ?? false, registered: [] };
251
268
  try {
252
269
  const entries = [];
@@ -265,22 +282,17 @@ export function encodeCallArgs(kwargs, options) {
265
282
  kwargs: entries,
266
283
  callId: callId.toString(),
267
284
  typeArgs,
285
+ functionName: options.functionName,
286
+ functionHandle: options.functionHandle,
268
287
  });
269
288
  return Uint8Array.from(CallFunctionArgs.encode(msg).finish());
270
289
  }
271
290
  catch (err) {
272
291
  // Roll back any host callables registered before the failure so
273
- // they don't leak in the registry (and pin the libuv loop) for the
274
- // life of the process — the call never reaches the engine, so the
292
+ // they don't leak in the registry (and pin the host event loop loop) for the
293
+ // life of the runtime — the call never reaches the engine, so the
275
294
  // engine would never release them.
276
- for (const k of ctx.registered) {
277
- try {
278
- releaseHostCallable(k);
279
- }
280
- catch {
281
- // Best-effort cleanup; never mask the original error.
282
- }
283
- }
295
+ rollbackHostCallables(ctx.registered);
284
296
  throw err;
285
297
  }
286
298
  }
@@ -376,6 +388,9 @@ function decodeValueHolder(holder, typeMap) {
376
388
  throw new BamlError('decoded handle has HANDLE_UNSPECIFIED handle_type');
377
389
  }
378
390
  const handle = new BamlHandle(holder.handleValue.key, ht);
391
+ if (ht === BamlHandleType.FUNCTION_REF) {
392
+ return decodeBamlClosure(handle, holder.handleValue.ty?.function);
393
+ }
379
394
  if (ht === BamlHandleType.ADT_MEDIA_IMAGE)
380
395
  return BamlImage._fromHandle(handle);
381
396
  if (ht === BamlHandleType.ADT_MEDIA_AUDIO)
@@ -387,14 +402,14 @@ function decodeValueHolder(holder, typeMap) {
387
402
  if (ht === BamlHandleType.ADT_TAGGED_HEAP_HANDLE) {
388
403
  // Dispatch via the typemap: every tagged-heap class self-registers
389
404
  // under its engine FQN (codegen emits the entry, e.g.
390
- // `baml.llm.Stream → BamlStream`), so any class is reachable without
405
+ // `ai.stream.Stream → BamlStream`), so any class is reachable without
391
406
  // special-casing here. Mirrors bridge_python's `_decode_handle`
392
407
  // ADT_TAGGED_HEAP_HANDLE arm (sdks/python/.../proto.py).
393
408
  // The handle's `ty` is a full `BamlTy`; the typed-wrapper FQN lives
394
409
  // on its class variant (a non-class `ty` reads back as `''`).
395
410
  const fqn = holder.handleValue.ty?.classTy?.name ?? '';
396
411
  const Cls = typeMap.getClass(fqn);
397
- return Cls._fromHandle(handle);
412
+ return Cls._fromHandle(handle, fqn);
398
413
  }
399
414
  // ADT_MEDIA_GENERIC has no typed wrapper — stays a bare BamlHandle.
400
415
  return handle;
@@ -411,6 +426,45 @@ function decodeValueHolder(holder, typeMap) {
411
426
  // null BAML result.
412
427
  return null;
413
428
  }
429
+ function decodeBamlClosure(handle, functionTy) {
430
+ const params = functionTy?.params ?? [];
431
+ const required = params.filter((param) => param.mode !== baml_bridge.cffi.v1.BamlTyFunctionParamMode.BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL);
432
+ const optional = params.filter((param) => param.mode === baml_bridge.cffi.v1.BamlTyFunctionParamMode.BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL);
433
+ const requiredNames = required.map((param, index) => param.name ?? `arg${index}`);
434
+ const optionalNames = new Set(optional.map((param, index) => param.name ?? `arg${required.length + index}`));
435
+ return (...args) => {
436
+ if (args.length > requiredNames.length + 1) {
437
+ throw new TypeError(`got ${args.length} arguments but this BAML closure accepts ` +
438
+ `${requiredNames.length} required arguments and one optional-arguments object`);
439
+ }
440
+ const kwargs = {};
441
+ const positionalCount = Math.min(args.length, requiredNames.length);
442
+ for (let index = 0; index < positionalCount; index += 1) {
443
+ kwargs[requiredNames[index]] = args[index];
444
+ }
445
+ if (args.length > requiredNames.length) {
446
+ const options = args[requiredNames.length];
447
+ if (options === null || Array.isArray(options) || typeof options !== 'object') {
448
+ throw new TypeError('optional BAML closure arguments must be passed as an object');
449
+ }
450
+ for (const [name, value] of Object.entries(options)) {
451
+ if (!optionalNames.has(name)) {
452
+ throw new TypeError(`unknown optional argument ${JSON.stringify(name)}`);
453
+ }
454
+ if (value !== undefined)
455
+ kwargs[name] = value;
456
+ }
457
+ }
458
+ const runtime = getRuntime();
459
+ const callId = BigInt(newFunctionCall());
460
+ const encodedArgs = encodeCallArgs(kwargs, {
461
+ syncMode: true,
462
+ callId,
463
+ functionHandle: handle.key,
464
+ });
465
+ return decodeCallResult(runtime.callFunctionSync(encodedArgs));
466
+ };
467
+ }
414
468
  /**
415
469
  * Decode a `class_value` to a typed instance via the typemap. When the FQN is
416
470
  * in the typemap (the generated-SDK path), construct `new Cls(fieldDict)`
@@ -448,7 +502,7 @@ function decodeClass(classValue, typeMap) {
448
502
  // Repopulate a generic instance's `$types` from the wire's positional
449
503
  // `type_args` (the value-level type channel), keyed by the class's
450
504
  // static `$generic` param names — the decode-side mirror of the
451
- // encoder's `class_ty`. Defined non-enumerable so it neither perturbs
505
+ // encoder's sparse `value_type`. Defined non-enumerable so it neither perturbs
452
506
  // structural equality (`toEqual`) nor re-encodes as a data field,
453
507
  // while still being readable by a later generic instance-method call.
454
508
  const params = Array.isArray(Ctor.$generic) ? Ctor.$generic : null;
@@ -602,13 +656,25 @@ function cancellationAbortError(message) {
602
656
  reason: new BamlCancelledError(message),
603
657
  });
604
658
  }
659
+ function errorForClassName(formatted, detail) {
660
+ switch (detail.className) {
661
+ case 'baml.errors.InvalidArgument':
662
+ return new BamlInvalidArgumentError(formatted, detail);
663
+ case 'baml.errors.GenericSdkError':
664
+ case 'baml.errors.CompilationError':
665
+ case 'baml.errors.AccessError':
666
+ return new BamlClientError(formatted, detail);
667
+ default:
668
+ return new BamlError(formatted, detail);
669
+ }
670
+ }
605
671
  /**
606
672
  * Decode a `BamlOutboundResult` envelope (the engine's call-result wire shape
607
673
  * after 31c/31e). The `ok` arm returns the decoded value; the `error`/`panic`
608
674
  * arms **throw** a `BamlError`/`BamlPanic` carrying the fully decoded thrown
609
675
  * value (`.value`), the BAML trace (`.bamlTrace`), and the class FQN
610
676
  * (`.className`), with a readable formatted `.message`. An `is_exit_panic`
611
- * (clean `baml.sys.exit`) terminates the process via `process.exit(code)`
677
+ * (clean `baml.sys.exit`) terminates the runtime via `runtime.exit(code)`
612
678
  * rather than throwing.
613
679
  */
614
680
  export function decodeCallResult(data) {
@@ -619,10 +685,10 @@ export function decodeCallResult(data) {
619
685
  const { value, className, message } = decodeThrown(result.error?.value);
620
686
  const trace = result.error?.trace ?? [];
621
687
  // Same-host rehydration: a `baml.errors.HostCallable` carrying a
622
- // `_handle` that still resolves in this process's host-value
688
+ // `_handle` that still resolves in this runtime's host-value
623
689
  // registry re-throws the *original* JS error object the bridge
624
690
  // registered on the inbound throw — preserving `raised === caught`
625
- // identity. Foreign runtimes (a different Node process, the
691
+ // identity. Foreign runtimes (a different JavaScript realm, the
626
692
  // Python bridge) and released keys fall through to the
627
693
  // metadata-bearing `BamlError(HostCallable)` wrapper below.
628
694
  if (className === 'baml.errors.HostCallable' && value !== null && typeof value === 'object') {
@@ -636,24 +702,21 @@ export function decodeCallResult(data) {
636
702
  if (className === CANCELLED_PANIC_CLASS) {
637
703
  throw cancellationAbortError(formatted);
638
704
  }
639
- throw new BamlError(formatted, { value, bamlTrace: trace, className });
705
+ throw errorForClassName(formatted, { value, bamlTrace: trace, className });
640
706
  }
641
707
  case 'panic': {
642
708
  const panic = result.panic;
643
- if (panic?.isExitPanic) {
644
- // Clean process-exit panic: exit after flushing telemetry (the
645
- // registered `process.once('exit', flushEvents)` hook fires
646
- // synchronously inside process.exit), rather than throwing.
647
- const code = Number(panic.exitCode ?? 0);
648
- process.exit(code);
649
- }
650
709
  const { value, className, message } = decodeThrown(panic?.value);
651
710
  const trace = panic?.trace ?? [];
652
711
  const formatted = formatThrownMessage('panic', className ?? '', message, trace);
653
712
  if (className === CANCELLED_PANIC_CLASS) {
654
713
  throw cancellationAbortError(formatted);
655
714
  }
656
- throw new BamlPanic(formatted, { value, bamlTrace: trace, className });
715
+ const fallback = new BamlPanic(formatted, { value, bamlTrace: trace, className });
716
+ if (panic?.isExitPanic) {
717
+ handleExitPanic(Number(panic.exitCode ?? 0), fallback);
718
+ }
719
+ throw fallback;
657
720
  }
658
721
  case 'ok':
659
722
  default:
@@ -665,7 +728,7 @@ export function decodeCallResult(data) {
665
728
  //
666
729
  // When BAML invokes a `HostValue` registered via `registerHostCallable`, the
667
730
  // engine fires the C `HostDispatchFn`, which schedules the per-callable
668
- // `ThreadsafeFunction` (built from the wrapper below) onto the libuv event
731
+ // `ThreadsafeFunction` (built from the wrapper below) onto the host event loop event
669
732
  // loop with `(callId, argsBytes)`. The wrapper decodes args, invokes the
670
733
  // user function, encodes the result (or error), and forwards the result
671
734
  // back to the engine via `completeHostCall`.
@@ -675,7 +738,7 @@ export function decodeCallResult(data) {
675
738
  // side calls into `_decode_value_holder` / `_set_inbound_value` directly
676
739
  // inside the Rust dispatch callback (under the GIL) rather than going
677
740
  // through a JS-side wrapper. Node's tsfn model makes the wrapper natural.
678
- function makeHostCallableDispatch(userFn) {
741
+ export function makeHostCallableDispatch(userFn) {
679
742
  return (callId, argsBytes) => {
680
743
  // Every reachable exit from this wrapper must complete `callId`
681
744
  // exactly once — if it doesn't, the engine awaits the in-flight call
@@ -706,7 +769,7 @@ function makeHostCallableDispatch(userFn) {
706
769
  sendHostCallableError(callId, err);
707
770
  return;
708
771
  }
709
- // Async callables: the wrapper resolves the promise on the libuv
772
+ // Async callables: the wrapper resolves the promise on the host event loop
710
773
  // loop and then forwards the result. The engine has released its
711
774
  // heap permit while awaiting `complete_host_call`, so JS-side
712
775
  // delay is safe (mirrors the Python `run_until_complete` model).
@@ -738,7 +801,7 @@ function isPromiseLike(value) {
738
801
  function sendHostCallableResult(callId, value) {
739
802
  let bytes;
740
803
  // Result-encode path (host → engine): no sync guard (we're already on
741
- // libuv). We do track registrations, though — a callable nested in the
804
+ // host event loop). We do track registrations, though — a callable nested in the
742
805
  // result is registered before encoding finishes, and if encoding then
743
806
  // throws, the bytes never reach the engine, so it never decodes (and
744
807
  // never releases) the callable. Roll those back on failure, mirroring the
@@ -751,14 +814,7 @@ function sendHostCallableResult(callId, value) {
751
814
  bytes = Uint8Array.from(InboundValue.encode(msg).finish());
752
815
  }
753
816
  catch (err) {
754
- for (const k of ctx.registered) {
755
- try {
756
- releaseHostCallable(k);
757
- }
758
- catch {
759
- // Best-effort cleanup; never mask the original error.
760
- }
761
- }
817
+ rollbackHostCallables(ctx.registered);
762
818
  sendHostCallableError(callId, err);
763
819
  return;
764
820
  }
@@ -788,14 +844,12 @@ function buildHostCallableInbound(className, message, traceback, handleKey) {
788
844
  stringField('message', message),
789
845
  stringField('class_name', className),
790
846
  stringField('language', 'nodejs'),
847
+ stringField('traceback', traceback ?? ''),
791
848
  ];
792
- if (traceback != null) {
793
- fields.push(stringField('traceback', traceback));
794
- }
795
849
  fields.push(handleField);
796
850
  return InboundValue.create({
851
+ valueType: { classTy: { name: 'baml.errors.HostCallable' } },
797
852
  classValue: InboundClassValue.create({
798
- classTy: { name: 'baml.errors.HostCallable' },
799
853
  fields,
800
854
  }),
801
855
  });
@@ -811,9 +865,40 @@ function buildHostCallableInbound(className, message, traceback, handleKey) {
811
865
  // skips `0`), so a real registered key can never collide.
812
866
  //
813
867
  // Cast through `as unknown as HandleKey` because `HandleKey` is a native
814
- // napi class with private fields; protobufjs only reads the public
868
+ // native class with private fields; protobufjs only reads the public
815
869
  // `{low, high}` shape and the wire serializes them identically.
816
870
  const UNRESOLVED_HOST_ERROR_KEY = { low: 0, high: 0 };
871
+ /** Encode a BamlError's wrapped value with its concrete generated-class FQN.
872
+ * Ordinary function arguments may use a bare map because the declared
873
+ * parameter supplies the target class. A thrown value has no such coercion
874
+ * context: the engine must see the class identity before it can validate the
875
+ * callable's declared `throws` contract. */
876
+ function setInboundTypedThrowValue(iv, value, ctx) {
877
+ if (value !== null && typeof value === 'object') {
878
+ const proto = Object.getPrototypeOf(value);
879
+ const isClassInstance = proto !== Object.prototype && proto !== null;
880
+ if (isClassInstance) {
881
+ const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
882
+ if (fqn) {
883
+ const params = genericParamNames(value);
884
+ const userTypes = value.$types;
885
+ const typeArgs = params?.map((param) => lowerTypeToWireTy(userTypes?.[param])) ?? [];
886
+ const fields = [];
887
+ for (const [key, fieldValue] of Object.entries(value)) {
888
+ if (typeof fieldValue === 'function' || key === '$types')
889
+ continue;
890
+ const child = {};
891
+ setInboundValue(child, fieldValue, ctx);
892
+ fields.push({ stringKey: key, value: child });
893
+ }
894
+ iv.valueType = { classTy: { name: fqn, typeArgs } };
895
+ iv.classValue = { fields };
896
+ return;
897
+ }
898
+ }
899
+ }
900
+ setInboundValue(iv, value, ctx);
901
+ }
817
902
  function sendHostCallableError(callId, err) {
818
903
  // Normal error path: never leaves the call uncompleted. If building or
819
904
  // encoding the Instance throws (e.g. `describeError`, proto `create` /
@@ -845,7 +930,7 @@ function sendHostCallableError(callId, err) {
845
930
  const ctx = { syncMode: false, registered: [] };
846
931
  try {
847
932
  const iv = InboundValue.create({});
848
- setInboundValue(iv, err.value, ctx);
933
+ setInboundTypedThrowValue(iv, err.value, ctx);
849
934
  const bytes = Uint8Array.from(InboundValue.encode(iv).finish());
850
935
  completeHostCall(callId, 1, bytes);
851
936
  return;
@@ -855,25 +940,18 @@ function sendHostCallableError(callId, err) {
855
940
  // registrations its nested fields triggered and fall through
856
941
  // to the opaque-handle path below. The fall-through is
857
942
  // intentional: the throw must still reach the engine even if
858
- // the typed-value encode broke, so we never leave the call
943
+ // the sparse typed-node encode broke, so we never leave the call
859
944
  // hanging. Each release is wrapped in its own try/catch so
860
945
  // a single bad entry doesn't abort the rest of the rollback
861
946
  // and leak the remaining registrations (mirrors the other
862
947
  // rollback sites in `setInboundValue` and
863
948
  // `sendHostCallableResult`).
864
- for (const key of ctx.registered) {
865
- try {
866
- releaseHostCallable(key);
867
- }
868
- catch {
869
- // Best-effort cleanup; never mask the original error.
870
- }
871
- }
949
+ rollbackHostCallables(ctx.registered);
872
950
  }
873
951
  }
874
952
  // Opaque-handle path: a `baml.errors.HostCallable` Instance carrying
875
953
  // a handle to the originating JS error so the BAML→host decoder on
876
- // the same Node process can rehydrate the *same* exception object
954
+ // the same JavaScript realm can rehydrate the *same* exception object
877
955
  // on round-trip (`raised === caught` identity). Foreign runtimes /
878
956
  // released keys fall back to metadata.
879
957
  //
@@ -887,19 +965,20 @@ function sendHostCallableError(callId, err) {
887
965
  // original Proxy. Identity loss here is the right trade — the
888
966
  // alternative is hanging the call.
889
967
  //
890
- // Registration-leak edge case (rare, bounded): if encoding succeeds
891
- // through `registerHostOpaque` but `buildHostCallableInbound` or
892
- // `InboundValue.encode` fails after, the TS map entry stays alive
893
- // with no corresponding engine-side `HostValueArc` to release it,
894
- // so it lives until process exit. Both downstream calls are deeply
895
- // mechanical (build a proto message, serialize fixed-shape fields)
896
- // and don't depend on `err`'s shape, so this is effectively
897
- // unreachable outside protobufjs / native-binding corruption.
968
+ // Register transactionally: if fixed-shape payload construction,
969
+ // encoding, or delivery fails before Rust retains the handle, remove
970
+ // the opaque entry before falling back to the last-resort payload.
898
971
  const { className, message, stack } = describeError(err);
899
972
  const handleKey = registerHostOpaque(err);
900
- const inbound = buildHostCallableInbound(className, message, stack, handleKey);
901
- const bytes = Uint8Array.from(InboundValue.encode(inbound).finish());
902
- completeHostCall(callId, 1, bytes);
973
+ try {
974
+ const inbound = buildHostCallableInbound(className, message, stack, handleKey);
975
+ const bytes = Uint8Array.from(InboundValue.encode(inbound).finish());
976
+ completeHostCall(callId, 1, bytes);
977
+ }
978
+ catch (innerErr) {
979
+ releaseHostOpaque(handleKey);
980
+ throw innerErr;
981
+ }
903
982
  }
904
983
  catch (innerErr) {
905
984
  completeHostCallLastResort(callId, innerErr);
@@ -911,7 +990,7 @@ function sendHostCallableError(callId, err) {
911
990
  * error object, so the only ways it can fail are a broken proto runtime or a
912
991
  * broken native binding — at which point nothing can complete the call. We
913
992
  * swallow any throw here to avoid surfacing an unhandled rejection on the
914
- * libuv loop; the engine's lack of completion would then be the
993
+ * host event loop loop; the engine's lack of completion would then be the
915
994
  * (unavoidable) failure mode.
916
995
  */
917
996
  function completeHostCallLastResort(callId, err) {
@@ -922,7 +1001,7 @@ function completeHostCallLastResort(callId, err) {
922
1001
  }
923
1002
  catch (innerErr) {
924
1003
  // Nothing more we can safely do — a throw here would surface as an
925
- // unhandled rejection on the libuv loop. The engine's lack of
1004
+ // unhandled rejection on the host event loop loop. The engine's lack of
926
1005
  // completion will then be the (unavoidable) failure mode; log so the
927
1006
  // failure is at least attributable.
928
1007
  console.error('BAML internal: last-resort host-call completion failed; call will hang.', { callId, originalError: safeStringify(err), lastResortError: innerErr });
@@ -940,17 +1019,31 @@ function safeStringify(err) {
940
1019
  }
941
1020
  function describeError(err) {
942
1021
  if (err instanceof Error) {
1022
+ const rawName = err.name;
1023
+ const rawMessage = err.message;
1024
+ const rawStack = err.stack;
943
1025
  return {
944
- className: err.name || err.constructor.name || 'Error',
945
- message: err.message || String(err),
946
- stack: err.stack ?? undefined,
1026
+ className: typeof rawName === 'string' && rawName.length > 0
1027
+ ? rawName
1028
+ : err.constructor.name || 'Error',
1029
+ message: typeof rawMessage === 'string' && rawMessage.length > 0
1030
+ ? rawMessage
1031
+ : safeStringify(err),
1032
+ stack: rawStack == null
1033
+ ? undefined
1034
+ : typeof rawStack === 'string'
1035
+ ? rawStack
1036
+ : safeStringify(rawStack),
947
1037
  };
948
1038
  }
949
1039
  if (err != null && typeof err === 'object') {
950
1040
  const ctor = err.constructor;
951
- const className = ctor?.name && ctor.name !== 'Object' ? ctor.name : 'Error';
952
- return { className, message: String(err), stack: undefined };
1041
+ const rawName = ctor?.name;
1042
+ const className = typeof rawName === 'string' && rawName.length > 0 && rawName !== 'Object'
1043
+ ? rawName
1044
+ : 'Error';
1045
+ return { className, message: safeStringify(err), stack: undefined };
953
1046
  }
954
- return { className: 'Error', message: String(err), stack: undefined };
1047
+ return { className: 'Error', message: safeStringify(err), stack: undefined };
955
1048
  }
956
1049
  //# sourceMappingURL=proto.js.map