@boundaryml/baml-core-node 0.11.2-nightly.20260604.d
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.
- package/LICENSE +201 -0
- package/dist/ctx_manager.d.ts +22 -0
- package/dist/ctx_manager.d.ts.map +1 -0
- package/dist/ctx_manager.js +91 -0
- package/dist/ctx_manager.js.map +1 -0
- package/dist/define_function.d.ts +27 -0
- package/dist/define_function.d.ts.map +1 -0
- package/dist/define_function.js +105 -0
- package/dist/define_function.js.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +75 -0
- package/dist/errors.js.map +1 -0
- package/dist/exit_hook.d.ts +9 -0
- package/dist/exit_hook.d.ts.map +1 -0
- package/dist/exit_hook.js +27 -0
- package/dist/exit_hook.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +280 -0
- package/dist/native.js +619 -0
- package/dist/proto/baml_cffi.d.ts +5694 -0
- package/dist/proto/baml_cffi.js +15459 -0
- package/dist/proto.d.ts +42 -0
- package/dist/proto.d.ts.map +1 -0
- package/dist/proto.js +665 -0
- package/dist/proto.js.map +1 -0
- package/dist/stream.d.ts +23 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +65 -0
- package/dist/stream.js.map +1 -0
- package/dist/typemap.d.ts +37 -0
- package/dist/typemap.d.ts.map +1 -0
- package/dist/typemap.js +110 -0
- package/dist/typemap.js.map +1 -0
- package/package.json +71 -0
package/dist/proto.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Source: baml_language/crates/bridge_nodejs/typescript_src/
|
|
5
|
+
* Proto: baml_language/crates/bridge_ctypes/types/baml_core/cffi/v1/*.proto
|
|
6
|
+
* Build: cd baml_language/crates/bridge_nodejs && pnpm build:debug
|
|
7
|
+
*/
|
|
8
|
+
// proto.ts — mirrors bridge_python/python_src/baml_py/proto.py
|
|
9
|
+
//
|
|
10
|
+
// Encodes TS objects → CallFunctionArgs protobuf bytes (for sending to Rust)
|
|
11
|
+
// Decodes the BamlOutboundResult envelope → TS objects (call results), and
|
|
12
|
+
// bare BamlOutboundValue bytes → TS objects (host-callable args).
|
|
13
|
+
import { baml_core } from './proto/baml_cffi.js';
|
|
14
|
+
import { BamlHandle, putHandleIntoTable, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, } from './native.js';
|
|
15
|
+
import { BamlStream } from './stream.js';
|
|
16
|
+
import { BamlError, BamlPanic } from './errors.js';
|
|
17
|
+
import { getTypeMap } from './typemap.js';
|
|
18
|
+
const CallFunctionArgs = baml_core.cffi.v1.CallFunctionArgs;
|
|
19
|
+
const BamlOutboundValue = baml_core.cffi.v1.BamlOutboundValue;
|
|
20
|
+
const BamlOutboundResult = baml_core.cffi.v1.BamlOutboundResult;
|
|
21
|
+
const InboundValue = baml_core.cffi.v1.InboundValue;
|
|
22
|
+
const HostCallableError = baml_core.cffi.v1.HostCallableError;
|
|
23
|
+
const HostCallableErrorCategory = baml_core.cffi.v1.HostCallableErrorCategory;
|
|
24
|
+
const BamlHandleType = baml_core.cffi.v1.BamlHandleType;
|
|
25
|
+
// ─── Inbound (TS → Rust) ───
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when a host callable (a JS `function`) is passed to the
|
|
28
|
+
* *synchronous* call path. See {@link encodeCallArgs} for why this can't work.
|
|
29
|
+
*/
|
|
30
|
+
export class HostCallableSyncError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'HostCallableSyncError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function setInboundValue(iv, value, ctx) {
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return; // Leave oneof unset → null
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === 'boolean') {
|
|
41
|
+
iv.boolValue = value;
|
|
42
|
+
}
|
|
43
|
+
else if (typeof value === 'number') {
|
|
44
|
+
if (Number.isInteger(value)) {
|
|
45
|
+
iv.intValue = value;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
iv.floatValue = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (typeof value === 'bigint') {
|
|
52
|
+
// Hex / base sixteen on the wire (see Phase 10 of the bigint plan).
|
|
53
|
+
// BigInt.prototype.toString(16) yields e.g. "-2a"; signed values
|
|
54
|
+
// round-trip via num-bigint's LowerHex impl on the Rust side.
|
|
55
|
+
iv.bigintValue = value.toString(16);
|
|
56
|
+
}
|
|
57
|
+
else if (typeof value === 'string') {
|
|
58
|
+
iv.stringValue = value;
|
|
59
|
+
}
|
|
60
|
+
else if (value instanceof Uint8Array) {
|
|
61
|
+
iv.uint8arrayValue = value;
|
|
62
|
+
}
|
|
63
|
+
else if (value instanceof BamlHandle) {
|
|
64
|
+
// A round-tripped host callable arrives as a handle, not a raw
|
|
65
|
+
// function — apply the same sync-path fast-fail so `callFunctionSync`
|
|
66
|
+
// can't hang waiting on a callback that the blocked main thread can
|
|
67
|
+
// never run. HOST_VALUE_CALLABLE is currently the only handle type
|
|
68
|
+
// that dispatches back into the host; the rest are engine-side ADT/
|
|
69
|
+
// heap handles that need no host callback and so are safe on the sync
|
|
70
|
+
// path. Any future dispatch-backed handle type must be guarded here.
|
|
71
|
+
if (ctx.syncMode && value.handleType === BamlHandleType.HOST_VALUE_CALLABLE) {
|
|
72
|
+
throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
|
|
73
|
+
'(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
|
|
74
|
+
'thread, so the host callback can never run and the call would hang.');
|
|
75
|
+
}
|
|
76
|
+
// The Rust inbound decoder drains handle-table entries. Send a fresh
|
|
77
|
+
// cloned key so the JS-owned handle remains valid for later calls.
|
|
78
|
+
iv.handle = { key: putHandleIntoTable(value), handleType: value.handleType };
|
|
79
|
+
}
|
|
80
|
+
else if (value instanceof BamlStream) {
|
|
81
|
+
// Stream wrapper → its inner TaggedHeapHandle. Mirrors the BamlHandle
|
|
82
|
+
// branch above; the engine re-binds it to the heap value on decode.
|
|
83
|
+
const h = value._toHandle();
|
|
84
|
+
iv.handle = { key: h.key, handleType: h.handleType };
|
|
85
|
+
}
|
|
86
|
+
else if (value instanceof BamlImage
|
|
87
|
+
|| value instanceof BamlAudio
|
|
88
|
+
|| value instanceof BamlVideo
|
|
89
|
+
|| value instanceof BamlPdf) {
|
|
90
|
+
// Stdlib media wrappers → their backing ADT_MEDIA_* handle. `_toHandle`
|
|
91
|
+
// clones the table row so the wrapper stays usable after encode.
|
|
92
|
+
const h = value._toHandle();
|
|
93
|
+
iv.handle = { key: h.key, handleType: h.handleType };
|
|
94
|
+
}
|
|
95
|
+
else if (typeof value === 'function') {
|
|
96
|
+
// Host callables cannot work on the synchronous call path —
|
|
97
|
+
// fast-fail before any blocking happens (and before we register a
|
|
98
|
+
// tsfn, which would otherwise be orphaned).
|
|
99
|
+
if (ctx.syncMode) {
|
|
100
|
+
throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
|
|
101
|
+
'(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
|
|
102
|
+
'thread, so the host callback can never run and the call would hang.');
|
|
103
|
+
}
|
|
104
|
+
// JS callable → register a dispatch wrapper in the host-value
|
|
105
|
+
// registry and emit `Handle{key, HOST_VALUE_CALLABLE}`. The Rust
|
|
106
|
+
// side decodes this into `BexExternalValue::HostValue` and binds it
|
|
107
|
+
// to an `Object::HostClosure`; BAML invocations land back in
|
|
108
|
+
// `hostCallableDispatch` below via the ThreadsafeFunction.
|
|
109
|
+
const key = registerHostCallable(makeHostCallableDispatch(value));
|
|
110
|
+
// Remember the key so a later encode failure can release it.
|
|
111
|
+
ctx.registered.push(key);
|
|
112
|
+
iv.handle = { key, handleType: BamlHandleType.HOST_VALUE_CALLABLE };
|
|
113
|
+
}
|
|
114
|
+
else if (Array.isArray(value)) {
|
|
115
|
+
const listVal = [];
|
|
116
|
+
for (const item of value) {
|
|
117
|
+
const child = {};
|
|
118
|
+
setInboundValue(child, item, ctx);
|
|
119
|
+
listVal.push(child);
|
|
120
|
+
}
|
|
121
|
+
iv.listValue = { values: listVal };
|
|
122
|
+
}
|
|
123
|
+
else if (value !== null && typeof value === 'object') {
|
|
124
|
+
// Any remaining object — a plain object OR a codegen-emitted class
|
|
125
|
+
// instance (e.g. `new Resume({...})`) — encodes as `map_value` with
|
|
126
|
+
// no FQN tag. The Rust side's `coerce_arg_to_declared_type` reshapes
|
|
127
|
+
// it against the function's declared parameter type (the 10a
|
|
128
|
+
// typemap-free encode simplification). `Object.entries` yields the
|
|
129
|
+
// class's own enumerable fields, set by the constructor's
|
|
130
|
+
// `Object.assign(this, init)`. The specific built-in wrappers
|
|
131
|
+
// (BamlHandle/BamlStream/media) are handled by the instanceof
|
|
132
|
+
// branches above, so they never reach here.
|
|
133
|
+
//
|
|
134
|
+
// Class instances additionally carry their instance-method bindings
|
|
135
|
+
// (`m = defineInstanceFunction(...).bind(this)`) as own enumerable
|
|
136
|
+
// fields. Those are behavior, not state — skip function-valued fields
|
|
137
|
+
// on a class instance so re-encoding a handle-backed value (e.g. a
|
|
138
|
+
// `baml.fs.File` with `read`/`text` bindings) sends only its data
|
|
139
|
+
// (the `_handle`). Plain objects keep every field, so a host callable
|
|
140
|
+
// nested in a plain object still encodes as a callable.
|
|
141
|
+
const proto = Object.getPrototypeOf(value);
|
|
142
|
+
const isClassInstance = proto !== Object.prototype && proto !== null;
|
|
143
|
+
// Handle-backed stdlib types (e.g. `baml.fs.File`, `baml.http.Response`)
|
|
144
|
+
// decode to a class instance that carries the engine's handle in a
|
|
145
|
+
// field (`_handle` / `_body`). The engine resolves these from a
|
|
146
|
+
// FQN-tagged `class_value` (not a bare `map` — which has no FQN — nor a
|
|
147
|
+
// bare `handle`), so re-sending the same handle inside the named class
|
|
148
|
+
// value lets it resolve the same object and preserve cursor/connection
|
|
149
|
+
// state across FFI calls. The FQN comes from the typemap reverse map.
|
|
150
|
+
if (isClassInstance && Object.values(value).some(v => v instanceof BamlHandle)) {
|
|
151
|
+
const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
|
|
152
|
+
if (fqn) {
|
|
153
|
+
const classFields = [];
|
|
154
|
+
for (const [k, v] of Object.entries(value)) {
|
|
155
|
+
if (typeof v === 'function')
|
|
156
|
+
continue;
|
|
157
|
+
const childVal = {};
|
|
158
|
+
setInboundValue(childVal, v, ctx);
|
|
159
|
+
classFields.push({ stringKey: k, value: childVal });
|
|
160
|
+
}
|
|
161
|
+
iv.classValue = { name: fqn, fields: classFields };
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const [k, v] of Object.entries(value)) {
|
|
167
|
+
if (isClassInstance && typeof v === 'function')
|
|
168
|
+
continue;
|
|
169
|
+
const entry = { stringKey: k };
|
|
170
|
+
const childVal = {};
|
|
171
|
+
setInboundValue(childVal, v, ctx);
|
|
172
|
+
entry.value = childVal;
|
|
173
|
+
entries.push(entry);
|
|
174
|
+
}
|
|
175
|
+
iv.mapValue = { entries };
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new TypeError(`Cannot encode value of type ${Object.prototype.toString.call(value)} to protobuf`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Encode kwargs into `CallFunctionArgs` bytes.
|
|
183
|
+
*
|
|
184
|
+
* `syncMode` (default false) selects the sync guard: a host callable in the
|
|
185
|
+
* kwargs of a *synchronous* call rejects with {@link HostCallableSyncError}
|
|
186
|
+
* before any work, rather than registering a tsfn and then hanging.
|
|
187
|
+
*
|
|
188
|
+
* Release tradeoff: a callable that encodes successfully is registered in the
|
|
189
|
+
* host-value table and is normally released only when the engine GCs the
|
|
190
|
+
* `HostClosure` it allocated and fires the C release callback (a GC-timed
|
|
191
|
+
* release, drained by the engine after collection).
|
|
192
|
+
* Because the Node tsfn is built with `weak::<false>` it keeps a strong libuv
|
|
193
|
+
* ref, so a *leaked* registry entry can also keep the Node process from
|
|
194
|
+
* exiting — which is exactly why the encode-error rollback below matters: if a
|
|
195
|
+
* later kwarg fails, the engine never sees (and so never releases) the keys we
|
|
196
|
+
* already registered, so we release them here.
|
|
197
|
+
*/
|
|
198
|
+
export function encodeCallArgs(kwargs, syncMode = false) {
|
|
199
|
+
const ctx = { syncMode, registered: [] };
|
|
200
|
+
try {
|
|
201
|
+
const entries = [];
|
|
202
|
+
for (const [key, value] of Object.entries(kwargs)) {
|
|
203
|
+
const entry = { stringKey: key };
|
|
204
|
+
const iv = {};
|
|
205
|
+
setInboundValue(iv, value, ctx);
|
|
206
|
+
entry.value = iv;
|
|
207
|
+
entries.push(entry);
|
|
208
|
+
}
|
|
209
|
+
const msg = CallFunctionArgs.create({ kwargs: entries });
|
|
210
|
+
return Buffer.from(CallFunctionArgs.encode(msg).finish());
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
// Roll back any host callables registered before the failure so
|
|
214
|
+
// they don't leak in the registry (and pin the libuv loop) for the
|
|
215
|
+
// life of the process — the call never reaches the engine, so the
|
|
216
|
+
// engine would never release them.
|
|
217
|
+
for (const k of ctx.registered) {
|
|
218
|
+
try {
|
|
219
|
+
releaseHostCallable(k);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Best-effort cleanup; never mask the original error.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ─── Outbound (Rust → TS) ───
|
|
229
|
+
// Hex / base sixteen on the wire (see Phase 10 of the bigint plan). Shared
|
|
230
|
+
// by `bigint_value` (runtime values) and `bigint_literal` (type literals)
|
|
231
|
+
// since both fields use the same wire format. BigInt() accepts a "0x"-prefixed
|
|
232
|
+
// hex literal; strip a leading minus so we can parse the magnitude. Guard
|
|
233
|
+
// against empty or sign-only inputs — `BigInt("0x")` throws `SyntaxError`,
|
|
234
|
+
// so we surface a clearer error instead.
|
|
235
|
+
// Workspace bigint cap = 2^28 bits ⇒ at most (2^28)/4 hex digits, plus a
|
|
236
|
+
// small slack to match the Rust-side `MAX_BIGINT_HEX_LEN` constant in
|
|
237
|
+
// `bridge_ctypes/src/value_decode.rs`. Reject longer inputs before calling
|
|
238
|
+
// `BigInt()` so a megabyte-scale payload can't drive an unbounded allocation.
|
|
239
|
+
const MAX_BIGINT_HEX_LEN = (1 << 28) / 4 + 2;
|
|
240
|
+
function parseHexBigint(s) {
|
|
241
|
+
const magnitude = s.startsWith('-') ? s.slice(1) : s;
|
|
242
|
+
if (magnitude.length === 0 || !/^[0-9a-fA-F]+$/.test(magnitude)) {
|
|
243
|
+
throw new Error(`Invalid bigint hex on the wire: ${JSON.stringify(s)}`);
|
|
244
|
+
}
|
|
245
|
+
if (magnitude.length > MAX_BIGINT_HEX_LEN) {
|
|
246
|
+
throw new Error(`bigint hex exceeds the workspace cap (${magnitude.length} chars, limit ${MAX_BIGINT_HEX_LEN})`);
|
|
247
|
+
}
|
|
248
|
+
return s.startsWith('-')
|
|
249
|
+
? -BigInt(`0x${magnitude}`)
|
|
250
|
+
: BigInt(`0x${magnitude}`);
|
|
251
|
+
}
|
|
252
|
+
function decodeValueHolder(holder, typeMap) {
|
|
253
|
+
if (holder.nullValue != null)
|
|
254
|
+
return null;
|
|
255
|
+
if (holder.stringValue != null)
|
|
256
|
+
return holder.stringValue;
|
|
257
|
+
if (holder.intValue != null)
|
|
258
|
+
return Number(holder.intValue);
|
|
259
|
+
if (holder.bigintValue != null) {
|
|
260
|
+
return parseHexBigint(holder.bigintValue);
|
|
261
|
+
}
|
|
262
|
+
if (holder.floatValue != null)
|
|
263
|
+
return holder.floatValue;
|
|
264
|
+
if (holder.boolValue != null)
|
|
265
|
+
return holder.boolValue;
|
|
266
|
+
if (holder.uint8arrayValue != null)
|
|
267
|
+
return holder.uint8arrayValue;
|
|
268
|
+
if (holder.classValue) {
|
|
269
|
+
return decodeClass(holder.classValue, typeMap);
|
|
270
|
+
}
|
|
271
|
+
if (holder.enumValue) {
|
|
272
|
+
return decodeEnum(holder.enumValue, typeMap);
|
|
273
|
+
}
|
|
274
|
+
if (holder.literalValue) {
|
|
275
|
+
if (holder.literalValue.stringLiteral != null)
|
|
276
|
+
return holder.literalValue.stringLiteral.value;
|
|
277
|
+
if (holder.literalValue.intLiteral != null)
|
|
278
|
+
return Number(holder.literalValue.intLiteral.value);
|
|
279
|
+
if (holder.literalValue.boolLiteral != null)
|
|
280
|
+
return holder.literalValue.boolLiteral.value;
|
|
281
|
+
// Hex / base sixteen on the wire, matching `bigint_value`. `value`
|
|
282
|
+
// is a required field on `BamlTyLiteralBigint`, so its absence
|
|
283
|
+
// indicates a corrupted / malformed wire message — reject loudly
|
|
284
|
+
// rather than silently coercing a missing field to `0n`.
|
|
285
|
+
if (holder.literalValue.bigintLiteral != null) {
|
|
286
|
+
const hex = holder.literalValue.bigintLiteral.value;
|
|
287
|
+
if (hex == null) {
|
|
288
|
+
throw new Error('wire message: BamlTyLiteralBigint missing required `value`');
|
|
289
|
+
}
|
|
290
|
+
return parseHexBigint(hex);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (holder.listValue) {
|
|
294
|
+
return (holder.listValue.items || []).map(item => decodeValueHolder(item, typeMap));
|
|
295
|
+
}
|
|
296
|
+
if (holder.mapValue) {
|
|
297
|
+
const obj = Object.create(null);
|
|
298
|
+
for (const entry of holder.mapValue.entries || []) {
|
|
299
|
+
if (entry.key != null && entry.value) {
|
|
300
|
+
obj[entry.key] = decodeValueHolder(entry.value, typeMap);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return obj;
|
|
304
|
+
}
|
|
305
|
+
if (holder.unionVariantValue && holder.unionVariantValue.value) {
|
|
306
|
+
return decodeValueHolder(holder.unionVariantValue.value, typeMap);
|
|
307
|
+
}
|
|
308
|
+
// handle_value: pass the protobufjs Long directly as the key — BamlHandle's
|
|
309
|
+
// constructor accepts { low, high } which is layout-compatible with Long.
|
|
310
|
+
// Dispatch on handle_type so media handles decode to their typed wrapper.
|
|
311
|
+
if (holder.handleValue) {
|
|
312
|
+
const ht = holder.handleValue.handleType ?? 0;
|
|
313
|
+
if (ht === BamlHandleType.HANDLE_UNSPECIFIED) {
|
|
314
|
+
// Never a valid decoded handle (mirrors Python's _decode_handle).
|
|
315
|
+
throw new BamlError('decoded handle has HANDLE_UNSPECIFIED handle_type');
|
|
316
|
+
}
|
|
317
|
+
const handle = new BamlHandle(holder.handleValue.key, ht);
|
|
318
|
+
if (ht === BamlHandleType.ADT_MEDIA_IMAGE)
|
|
319
|
+
return BamlImage._fromHandle(handle);
|
|
320
|
+
if (ht === BamlHandleType.ADT_MEDIA_AUDIO)
|
|
321
|
+
return BamlAudio._fromHandle(handle);
|
|
322
|
+
if (ht === BamlHandleType.ADT_MEDIA_VIDEO)
|
|
323
|
+
return BamlVideo._fromHandle(handle);
|
|
324
|
+
if (ht === BamlHandleType.ADT_MEDIA_PDF)
|
|
325
|
+
return BamlPdf._fromHandle(handle);
|
|
326
|
+
// ADT_MEDIA_GENERIC has no typed wrapper — stays a bare BamlHandle.
|
|
327
|
+
// TODO: ADT_TAGGED_HEAP_HANDLE / RustData re-encode (handle-backed
|
|
328
|
+
// stdlib types like baml.fs.File) needs cross-call handle-lifecycle
|
|
329
|
+
// work; for now non-media handles decode to a bare BamlHandle.
|
|
330
|
+
return handle;
|
|
331
|
+
}
|
|
332
|
+
// Inline media / prompt AST are not expected on the Node FFI path — they
|
|
333
|
+
// travel via `handle_value`. Reject loudly rather than silently collapsing
|
|
334
|
+
// to null (mirrors bridge_python's proto.py, which raises here).
|
|
335
|
+
if (holder.mediaValue || holder.promptAstValue) {
|
|
336
|
+
const which = holder.mediaValue ? 'media_value' : 'prompt_ast_value';
|
|
337
|
+
throw new BamlError(`BEX emitted ${which} on the FFI path — media/prompt AST are expected ` +
|
|
338
|
+
`via handle_value, not inline`);
|
|
339
|
+
}
|
|
340
|
+
// Any remaining unset oneof is a legitimate null: an all-default holder is a
|
|
341
|
+
// null BAML result.
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Decode a `class_value` to a typed instance via the typemap. When the FQN is
|
|
346
|
+
* in the typemap (the generated-SDK path), construct `new Cls(fieldDict)`
|
|
347
|
+
* (codegen emits `constructor(init) { Object.assign(this, init); }`). The five
|
|
348
|
+
* stdlib media wrappers unwrap their `_data` envelope to the wrapper itself.
|
|
349
|
+
* When the FQN is absent (the bare bridge has no typemap, or an unmapped
|
|
350
|
+
* class), fall back to a plain object — preserving the pre-typemap behavior.
|
|
351
|
+
*/
|
|
352
|
+
function decodeClass(classValue, typeMap) {
|
|
353
|
+
const fieldDict = {};
|
|
354
|
+
for (const entry of classValue.fields || []) {
|
|
355
|
+
if (entry.key != null && entry.value) {
|
|
356
|
+
fieldDict[entry.key] = decodeValueHolder(entry.value, typeMap);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const fqn = classValue.name?.name ?? '';
|
|
360
|
+
if (fqn) {
|
|
361
|
+
let Cls;
|
|
362
|
+
try {
|
|
363
|
+
Cls = typeMap.getClass(fqn);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
Cls = undefined; // unmapped FQN — fall back below
|
|
367
|
+
}
|
|
368
|
+
if (Cls !== undefined) {
|
|
369
|
+
// Stdlib media wrappers: the decoded `_data` is already the typed
|
|
370
|
+
// wrapper (its inner handle_value decoded via the media branch);
|
|
371
|
+
// unwrap the envelope per the spec's Instance row.
|
|
372
|
+
if ((Cls === BamlImage || Cls === BamlAudio || Cls === BamlVideo || Cls === BamlPdf)
|
|
373
|
+
&& '_data' in fieldDict) {
|
|
374
|
+
return fieldDict._data;
|
|
375
|
+
}
|
|
376
|
+
const Ctor = Cls;
|
|
377
|
+
return new Ctor(fieldDict);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Fallback: plain object (null-prototype, matching the prior behavior).
|
|
381
|
+
const obj = Object.create(null);
|
|
382
|
+
for (const [k, v] of Object.entries(fieldDict))
|
|
383
|
+
obj[k] = v;
|
|
384
|
+
return obj;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Decode an `enum_value` to a typed enum member via the typemap. Falls back to
|
|
388
|
+
* the raw variant string when the FQN is unmapped (bare bridge / unmapped enum).
|
|
389
|
+
*/
|
|
390
|
+
function decodeEnum(enumValue, typeMap) {
|
|
391
|
+
const fqn = enumValue.name?.name ?? '';
|
|
392
|
+
const variant = enumValue.value;
|
|
393
|
+
if (fqn && variant != null) {
|
|
394
|
+
let En;
|
|
395
|
+
try {
|
|
396
|
+
En = typeMap.getEnum(fqn);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
En = undefined;
|
|
400
|
+
}
|
|
401
|
+
if (En !== undefined && variant in En) {
|
|
402
|
+
return En[variant];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return variant;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Decode a bare `BamlOutboundValue` to a JS value. Used for the host-callable
|
|
409
|
+
* args path, where the engine sends a list-shaped `BamlOutboundValue` rather
|
|
410
|
+
* than the call-result `BamlOutboundResult` envelope.
|
|
411
|
+
*/
|
|
412
|
+
export function decodeOutboundValue(data) {
|
|
413
|
+
const msg = BamlOutboundValue.decode(data instanceof Buffer ? data : Buffer.from(data));
|
|
414
|
+
return decodeValueHolder(msg, getTypeMap());
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Decode the thrown value off the wire holder. Returns the fully decoded BAML
|
|
418
|
+
* `value` (a generated class instance when the FQN is mapped, else a plain
|
|
419
|
+
* object / primitive), the class FQN (`className`), and a readable `message`
|
|
420
|
+
* lifted from the value's `message` field when present. Mirrors
|
|
421
|
+
* bridge_python's `decode_value` + `_outbound_class_fqn` so the surfaced
|
|
422
|
+
* `BamlError`/`BamlPanic` carries the decoded value, not just a string.
|
|
423
|
+
*
|
|
424
|
+
* Decoding is defensive: a malformed/unsupported thrown payload must not mask
|
|
425
|
+
* the original error/panic, so a decode failure degrades to an undefined value
|
|
426
|
+
* (the formatted message and className are still surfaced).
|
|
427
|
+
*/
|
|
428
|
+
function decodeThrown(holder) {
|
|
429
|
+
const className = holder?.classValue?.name?.name ?? undefined;
|
|
430
|
+
let value;
|
|
431
|
+
try {
|
|
432
|
+
value = holder ? decodeValueHolder(holder, getTypeMap()) : undefined;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
value = undefined;
|
|
436
|
+
}
|
|
437
|
+
let message = '';
|
|
438
|
+
if (value != null && typeof value === 'object' && 'message' in value) {
|
|
439
|
+
const m = value.message;
|
|
440
|
+
if (typeof m === 'string')
|
|
441
|
+
message = m;
|
|
442
|
+
}
|
|
443
|
+
return { value, className, message };
|
|
444
|
+
}
|
|
445
|
+
function formatThrownMessage(kind, className, message, trace) {
|
|
446
|
+
const label = className || `baml.${kind}`;
|
|
447
|
+
let text = `baml ${kind}: ${label}`;
|
|
448
|
+
if (message)
|
|
449
|
+
text += `: ${message}`;
|
|
450
|
+
if (trace.length)
|
|
451
|
+
text += '\n' + trace.map(l => ' ' + l).join('\n');
|
|
452
|
+
return text;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Decode a `BamlOutboundResult` envelope (the engine's call-result wire shape
|
|
456
|
+
* after 31c/31e). The `ok` arm returns the decoded value; the `error`/`panic`
|
|
457
|
+
* arms **throw** a `BamlError`/`BamlPanic` carrying the fully decoded thrown
|
|
458
|
+
* value (`.value`), the BAML trace (`.bamlTrace`), and the class FQN
|
|
459
|
+
* (`.className`), with a readable formatted `.message`. An `is_exit_panic`
|
|
460
|
+
* (clean `baml.sys.exit`) terminates the process via `process.exit(code)`
|
|
461
|
+
* rather than throwing.
|
|
462
|
+
*/
|
|
463
|
+
export function decodeCallResult(data) {
|
|
464
|
+
const buf = data instanceof Buffer ? data : Buffer.from(data);
|
|
465
|
+
const result = BamlOutboundResult.decode(buf);
|
|
466
|
+
switch (result.result) {
|
|
467
|
+
case 'error': {
|
|
468
|
+
const { value, className, message } = decodeThrown(result.error?.value);
|
|
469
|
+
const trace = result.error?.trace ?? [];
|
|
470
|
+
throw new BamlError(formatThrownMessage('error', className ?? '', message, trace), { value, bamlTrace: trace, className });
|
|
471
|
+
}
|
|
472
|
+
case 'panic': {
|
|
473
|
+
const panic = result.panic;
|
|
474
|
+
if (panic?.isExitPanic) {
|
|
475
|
+
// Clean process-exit panic: exit after flushing telemetry (the
|
|
476
|
+
// registered `process.once('exit', flushEvents)` hook fires
|
|
477
|
+
// synchronously inside process.exit), rather than throwing.
|
|
478
|
+
const code = Number(panic.exitCode ?? 0);
|
|
479
|
+
process.exit(code);
|
|
480
|
+
}
|
|
481
|
+
const { value, className, message } = decodeThrown(panic?.value);
|
|
482
|
+
const trace = panic?.trace ?? [];
|
|
483
|
+
throw new BamlPanic(formatThrownMessage('panic', className ?? '', message, trace), { value, bamlTrace: trace, className });
|
|
484
|
+
}
|
|
485
|
+
case 'ok':
|
|
486
|
+
default:
|
|
487
|
+
// `ok` (or an absent oneof — an all-default envelope is a null `ok`).
|
|
488
|
+
return result.ok ? decodeValueHolder(result.ok, getTypeMap()) : null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// ─── Host-callable dispatch (BAML → JS) ───
|
|
492
|
+
//
|
|
493
|
+
// When BAML invokes a `HostValue` registered via `registerHostCallable`, the
|
|
494
|
+
// engine fires the C `HostDispatchFn`, which schedules the per-callable
|
|
495
|
+
// `ThreadsafeFunction` (built from the wrapper below) onto the libuv event
|
|
496
|
+
// loop with `(callId, argsBytes)`. The wrapper decodes args, invokes the
|
|
497
|
+
// user function, encodes the result (or error), and forwards the result
|
|
498
|
+
// back to the engine via `completeHostCall`.
|
|
499
|
+
//
|
|
500
|
+
// Mirrors the Python bridge's `dispatch_in_python` flow
|
|
501
|
+
// (sdks/python/rust/bridge_python/src/host_value.rs:152), but the Python
|
|
502
|
+
// side calls into `_decode_value_holder` / `_set_inbound_value` directly
|
|
503
|
+
// inside the Rust dispatch callback (under the GIL) rather than going
|
|
504
|
+
// through a JS-side wrapper. Node's tsfn model makes the wrapper natural.
|
|
505
|
+
function makeHostCallableDispatch(userFn) {
|
|
506
|
+
return (callId, argsBytes) => {
|
|
507
|
+
// Every reachable exit from this wrapper must complete `callId`
|
|
508
|
+
// exactly once — if it doesn't, the engine awaits the in-flight call
|
|
509
|
+
// forever (there is no timeout). The outer try/catch is a last-resort
|
|
510
|
+
// net: if anything below throws *after* deciding not to complete (or
|
|
511
|
+
// the normal error path itself throws), we still fire one generic
|
|
512
|
+
// completion. The branches below never both complete and fall
|
|
513
|
+
// through, so we never double-complete.
|
|
514
|
+
try {
|
|
515
|
+
let args;
|
|
516
|
+
try {
|
|
517
|
+
// Host-callable args arrive as a bare list-shaped
|
|
518
|
+
// BamlOutboundValue, not the call-result envelope.
|
|
519
|
+
const decoded = decodeOutboundValue(argsBytes);
|
|
520
|
+
if (!Array.isArray(decoded)) {
|
|
521
|
+
throw new TypeError(`host-callable args decoded to a non-list value (got ${typeof decoded})`);
|
|
522
|
+
}
|
|
523
|
+
args = decoded;
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
sendHostCallableError(callId, err);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
let result;
|
|
530
|
+
try {
|
|
531
|
+
result = userFn(...args);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
sendHostCallableError(callId, err);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// Async callables: the wrapper resolves the promise on the libuv
|
|
538
|
+
// loop and then forwards the result. The engine has released its
|
|
539
|
+
// heap permit while awaiting `complete_host_call`, so JS-side
|
|
540
|
+
// delay is safe (mirrors the Python `run_until_complete` model).
|
|
541
|
+
if (isPromiseLike(result)) {
|
|
542
|
+
// Adopt the thenable via `Promise.resolve` rather than calling
|
|
543
|
+
// its `.then` directly: a non-compliant thenable could invoke
|
|
544
|
+
// its callbacks more than once, but `Promise.resolve(...)`
|
|
545
|
+
// collapses to a single settlement, so the call completes
|
|
546
|
+
// exactly once. The handlers only call the defended send*
|
|
547
|
+
// helpers (they can't throw synchronously).
|
|
548
|
+
Promise.resolve(result).then((resolved) => sendHostCallableResult(callId, resolved), (err) => sendHostCallableError(callId, err));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
sendHostCallableResult(callId, result);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
// Reached only if a send* helper or the promise plumbing threw
|
|
556
|
+
// *and* did not complete the call. Last-resort completion.
|
|
557
|
+
completeHostCallLastResort(callId, err);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function isPromiseLike(value) {
|
|
562
|
+
return (value != null &&
|
|
563
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
564
|
+
typeof value.then === 'function');
|
|
565
|
+
}
|
|
566
|
+
function sendHostCallableResult(callId, value) {
|
|
567
|
+
let bytes;
|
|
568
|
+
// Result-encode path (host → engine): no sync guard (we're already on
|
|
569
|
+
// libuv). We do track registrations, though — a callable nested in the
|
|
570
|
+
// result is registered before encoding finishes, and if encoding then
|
|
571
|
+
// throws, the bytes never reach the engine, so it never decodes (and
|
|
572
|
+
// never releases) the callable. Roll those back on failure, mirroring the
|
|
573
|
+
// argument-path rollback in `encodeCallArgs`.
|
|
574
|
+
const ctx = { syncMode: false, registered: [] };
|
|
575
|
+
try {
|
|
576
|
+
const iv = {};
|
|
577
|
+
setInboundValue(iv, value, ctx);
|
|
578
|
+
const msg = InboundValue.create(iv);
|
|
579
|
+
bytes = Buffer.from(InboundValue.encode(msg).finish());
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
for (const k of ctx.registered) {
|
|
583
|
+
try {
|
|
584
|
+
releaseHostCallable(k);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Best-effort cleanup; never mask the original error.
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
sendHostCallableError(callId, err);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
completeHostCall(callId, 0, bytes);
|
|
594
|
+
}
|
|
595
|
+
function sendHostCallableError(callId, err) {
|
|
596
|
+
// This is the normal error path, but it must not be able to leave
|
|
597
|
+
// the call uncompleted. If building/encoding the `HostCallableError`
|
|
598
|
+
// throws (e.g. `describeError`, proto `create`/`encode`, or the native
|
|
599
|
+
// `completeHostCall` itself), fall back to a completion that does the
|
|
600
|
+
// minimum possible work.
|
|
601
|
+
try {
|
|
602
|
+
const { className, message, stack } = describeError(err);
|
|
603
|
+
const msg = HostCallableError.create({
|
|
604
|
+
className,
|
|
605
|
+
message,
|
|
606
|
+
traceback: stack,
|
|
607
|
+
language: 'nodejs',
|
|
608
|
+
category: HostCallableErrorCategory.HOST_CALLABLE_HOST_ERROR,
|
|
609
|
+
});
|
|
610
|
+
const bytes = Buffer.from(HostCallableError.encode(msg).finish());
|
|
611
|
+
completeHostCall(callId, 1, bytes);
|
|
612
|
+
}
|
|
613
|
+
catch (innerErr) {
|
|
614
|
+
completeHostCallLastResort(callId, innerErr);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Absolute last-resort completion. Encodes a fixed, minimal
|
|
619
|
+
* `HostCallableError` with no dependence on the original error object, so the
|
|
620
|
+
* only ways it can fail are a broken proto runtime or a broken native
|
|
621
|
+
* binding — at which point nothing can complete the call. We swallow any
|
|
622
|
+
* throw here to avoid surfacing an unhandled rejection on the libuv loop; the
|
|
623
|
+
* engine's lack of completion would then be the (unavoidable) failure mode.
|
|
624
|
+
*/
|
|
625
|
+
function completeHostCallLastResort(callId, err) {
|
|
626
|
+
try {
|
|
627
|
+
const msg = HostCallableError.create({
|
|
628
|
+
className: 'InternalError',
|
|
629
|
+
message: `host callable dispatch failed and the error could not be reported: ${safeStringify(err)}`,
|
|
630
|
+
language: 'nodejs',
|
|
631
|
+
category: HostCallableErrorCategory.HOST_CALLABLE_HOST_ERROR,
|
|
632
|
+
});
|
|
633
|
+
const bytes = Buffer.from(HostCallableError.encode(msg).finish());
|
|
634
|
+
completeHostCall(callId, 1, bytes);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
// Nothing more we can safely do; avoid throwing on the libuv loop.
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/** `String(err)` that cannot itself throw (e.g. a Proxy with a throwing
|
|
641
|
+
* `toString`). */
|
|
642
|
+
function safeStringify(err) {
|
|
643
|
+
try {
|
|
644
|
+
return String(err);
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return '<unstringifiable error>';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function describeError(err) {
|
|
651
|
+
if (err instanceof Error) {
|
|
652
|
+
return {
|
|
653
|
+
className: err.name || err.constructor.name || 'Error',
|
|
654
|
+
message: err.message || String(err),
|
|
655
|
+
stack: err.stack ?? undefined,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (err != null && typeof err === 'object') {
|
|
659
|
+
const ctor = err.constructor;
|
|
660
|
+
const className = ctor?.name && ctor.name !== 'Object' ? ctor.name : 'Error';
|
|
661
|
+
return { className, message: String(err), stack: undefined };
|
|
662
|
+
}
|
|
663
|
+
return { className: 'Error', message: String(err), stack: undefined };
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=proto.js.map
|