@elizaos/capacitor-bun-runtime 2.0.3-beta.2

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 (61) hide show
  1. package/ElizaosCapacitorBunRuntime.podspec +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/dist/esm/definitions.d.ts +136 -0
  5. package/dist/esm/definitions.d.ts.map +1 -0
  6. package/dist/esm/definitions.js +14 -0
  7. package/dist/esm/definitions.js.map +1 -0
  8. package/dist/esm/index.d.ts +9 -0
  9. package/dist/esm/index.d.ts.map +1 -0
  10. package/dist/esm/index.js +11 -0
  11. package/dist/esm/index.js.map +1 -0
  12. package/dist/esm/web.d.ts +19 -0
  13. package/dist/esm/web.d.ts.map +1 -0
  14. package/dist/esm/web.js +44 -0
  15. package/dist/esm/web.js.map +1 -0
  16. package/dist/plugin.cjs.js +63 -0
  17. package/dist/plugin.cjs.js.map +1 -0
  18. package/dist/plugin.js +66 -0
  19. package/dist/plugin.js.map +1 -0
  20. package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
  21. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
  22. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
  23. package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
  24. package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
  25. package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
  26. package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
  27. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
  28. package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
  29. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
  30. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
  31. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
  32. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
  33. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
  34. package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
  35. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
  36. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
  37. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
  38. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
  39. package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
  40. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
  41. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
  42. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
  43. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
  44. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
  45. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
  46. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
  47. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
  48. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
  49. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
  50. package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
  51. package/package.json +68 -0
  52. package/src/bridge-contract.test.ts +127 -0
  53. package/src/definitions.d.ts +136 -0
  54. package/src/definitions.d.ts.map +1 -0
  55. package/src/definitions.ts +152 -0
  56. package/src/index.d.ts +9 -0
  57. package/src/index.d.ts.map +1 -0
  58. package/src/index.ts +16 -0
  59. package/src/web.d.ts +19 -0
  60. package/src/web.d.ts.map +1 -0
  61. package/src/web.ts +80 -0
@@ -0,0 +1,677 @@
1
+ import Foundation
2
+ import Darwin
3
+ #if ELIZA_IOS_FULL_BUN_ENGINE
4
+ import ElizaBunEngine
5
+ #endif
6
+
7
+ private let fullBunHostCallCallback: @convention(c) (
8
+ UnsafePointer<CChar>?,
9
+ UnsafePointer<CChar>?,
10
+ Int32
11
+ ) -> UnsafeMutablePointer<CChar>? = { methodPtr, payloadPtr, timeoutMs in
12
+ let method = methodPtr.map { String(cString: $0) } ?? ""
13
+ let payloadJson = payloadPtr.map { String(cString: $0) } ?? "null"
14
+ let response = FullBunEngineHost.shared.handleHostCall(
15
+ method: method,
16
+ payloadJson: payloadJson,
17
+ timeoutMs: timeoutMs
18
+ )
19
+ return strdup(response)
20
+ }
21
+
22
+ /// Host for the real Bun iOS engine framework.
23
+ ///
24
+ /// Full-engine/App Store builds link `ElizaBunEngine.xcframework` directly so
25
+ /// the shipped app does not import dynamic loader APIs. Compatibility builds
26
+ /// keep the optional loader path only in DEBUG development builds without
27
+ /// embedding the full engine framework. Release builds without the direct-link
28
+ /// flag fail closed.
29
+ ///
30
+ /// IPC security model (full-Bun path):
31
+ /// - Transport: NDJSON over anonymous stdio pipes. No TCP port is opened by the
32
+ /// native side. `bun_start(...)` receives the read end of the parent's stdin
33
+ /// pipe and the write end of its stdout pipe; no other network socket is
34
+ /// created by the shim.
35
+ /// - Input validation: the C shim (`eliza_bun_engine_shim.c`) validates all
36
+ /// NDJSON frames before dispatch: `id` must be numeric, `method` must be a
37
+ /// JSON string, `payload` is extracted as a bounded value field. The
38
+ /// `ELIZA_MAX_PROTOCOL_LINE_BYTES` (16 MiB) cap prevents unbounded reads.
39
+ /// - Host-call allowlist: only `llama_hardware_info`, `llama_load_model`,
40
+ /// `llama_generate`, `llama_free`, `llama_cancel`, `eliza_tts_synthesize`,
41
+ /// and `eliza_asr_transcribe` are dispatched by
42
+ /// `handleHostCall`. All other method names return `{"ok":false,"error":"..."}`.
43
+ /// - `http_fetch` (JSContext compat path): loopback/local-agent URLs are
44
+ /// rejected at `HTTPBridge.isLocalLoopback` before a URLRequest is created.
45
+ /// External fetches go through URLSession with the standard iOS ATS policy.
46
+ /// - `http_request` IPC (both paths): the path must begin with `/` and must not
47
+ /// contain `://`. Validated in `MobileAgentBridgePlugin.proxyHttpRequest` and
48
+ /// enforced by the Bun bridge contract at the agent layer.
49
+ /// - Filesystem (JSContext compat path): `FSBridge` does not restrict paths
50
+ /// beyond what the iOS app sandbox enforces. The agent bundle is signed and
51
+ /// staged inside the app bundle; paths visible to the JSContext are limited to
52
+ /// the app container by the OS.
53
+ /// - ABI version check: `load()` verifies the framework reports ABI version "3"
54
+ /// before accepting any other symbols. A version mismatch is a hard error.
55
+ final class FullBunEngineHost {
56
+ static let shared = FullBunEngineHost()
57
+ private static let expectedAbiVersion = "3"
58
+
59
+ private typealias AbiVersionFn = @convention(c) () -> UnsafePointer<CChar>?
60
+ private typealias LastErrorFn = @convention(c) () -> UnsafePointer<CChar>?
61
+ private typealias HostCallbackFn = @convention(c) (
62
+ UnsafePointer<CChar>?,
63
+ UnsafePointer<CChar>?,
64
+ Int32
65
+ ) -> UnsafeMutablePointer<CChar>?
66
+ private typealias SetHostCallbackFn = @convention(c) (HostCallbackFn?) -> Int32
67
+ private typealias StartFn = @convention(c) (
68
+ UnsafePointer<CChar>,
69
+ UnsafePointer<CChar>,
70
+ UnsafePointer<CChar>,
71
+ UnsafePointer<CChar>
72
+ ) -> Int32
73
+ private typealias StopFn = @convention(c) () -> Int32
74
+ private typealias IsRunningFn = @convention(c) () -> Int32
75
+ private typealias CallFn = @convention(c) (
76
+ UnsafePointer<CChar>,
77
+ UnsafePointer<CChar>
78
+ ) -> UnsafeMutablePointer<CChar>?
79
+ private typealias FreeFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
80
+
81
+ private var loaded = false
82
+ #if !ELIZA_IOS_FULL_BUN_ENGINE && DEBUG
83
+ private var handle: UnsafeMutableRawPointer?
84
+ #endif
85
+ private var abiVersionFn: AbiVersionFn?
86
+ private var lastErrorFn: LastErrorFn?
87
+ private var setHostCallbackFn: SetHostCallbackFn?
88
+ private var startFn: StartFn?
89
+ private var stopFn: StopFn?
90
+ private var isRunningFn: IsRunningFn?
91
+ private var callFn: CallFn?
92
+ private var freeFn: FreeFn?
93
+ private var running = false
94
+
95
+ private init() {}
96
+
97
+ var isAvailable: Bool {
98
+ do {
99
+ try load()
100
+ return true
101
+ } catch {
102
+ return false
103
+ }
104
+ }
105
+
106
+ var abiVersion: String {
107
+ guard let abi = abiVersionFn?() else { return "unknown" }
108
+ return String(cString: abi)
109
+ }
110
+
111
+ var isRunning: Bool {
112
+ do {
113
+ try load()
114
+ let engineRunning = isRunningFn?() == 1
115
+ if !engineRunning { running = false }
116
+ return engineRunning
117
+ } catch {
118
+ running = false
119
+ return false
120
+ }
121
+ }
122
+
123
+ func start(
124
+ bundlePath: String,
125
+ argv: [String],
126
+ env: [String: String],
127
+ appSupportDir: String
128
+ ) throws {
129
+ let startedAt = Date()
130
+ NSLog("[FullBunEngineHost] start requested bundle=\(bundlePath) appSupport=\(appSupportDir) argv=\(argv) envKeys=\(env.keys.sorted())")
131
+ try load()
132
+ if running {
133
+ if isRunningFn?() == 1 { return }
134
+ running = false
135
+ }
136
+ guard let startFn else {
137
+ throw makeError("ElizaBunEngine missing start symbol")
138
+ }
139
+ let argvJson = try encodeJSON(argv)
140
+ let envJson = try encodeJSON(env)
141
+ let code = bundlePath.withCString { bundlePtr in
142
+ argvJson.withCString { argvPtr in
143
+ envJson.withCString { envPtr in
144
+ appSupportDir.withCString { supportPtr in
145
+ startFn(bundlePtr, argvPtr, envPtr, supportPtr)
146
+ }
147
+ }
148
+ }
149
+ }
150
+ guard code == 0 else {
151
+ let detail = lastError()
152
+ let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
153
+ NSLog("[FullBunEngineHost] start failed code=\(code) durationMs=\(durationMs) detail=\(detail)")
154
+ throw makeError(
155
+ "ElizaBunEngine start failed with code \(code)" +
156
+ (detail.isEmpty ? "" : ": \(detail)")
157
+ )
158
+ }
159
+ running = true
160
+ let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
161
+ NSLog("[FullBunEngineHost] start succeeded abi=\(abiVersion) durationMs=\(durationMs)")
162
+ }
163
+
164
+ func stop() {
165
+ _ = stopFn?()
166
+ running = false
167
+ }
168
+
169
+ func call(method: String, payload: Any?) throws -> Any? {
170
+ try load()
171
+ guard let callFn else {
172
+ throw makeError("ElizaBunEngine missing call symbol")
173
+ }
174
+ let previousError = lastError()
175
+ let payloadJson = try encodeJSON(payload ?? NSNull())
176
+ let resultPtr = method.withCString { methodPtr in
177
+ payloadJson.withCString { payloadPtr in
178
+ callFn(methodPtr, payloadPtr)
179
+ }
180
+ }
181
+ guard let resultPtr else {
182
+ NSLog("[FullBunEngineHost] call returned null method=\(method) lastError=\(lastError())")
183
+ throw makeError("ElizaBunEngine call returned null for \(method)")
184
+ }
185
+ defer { freeFn?(UnsafeMutableRawPointer(resultPtr)) }
186
+ let resultJson = String(cString: resultPtr)
187
+ guard let data = resultJson.data(using: .utf8) else {
188
+ throw makeError("ElizaBunEngine returned non-UTF8 payload")
189
+ }
190
+ let decoded = try JSONSerialization.jsonObject(with: data)
191
+ if let dict = decoded as? [String: Any],
192
+ let ok = dict["ok"] as? Bool,
193
+ ok == false {
194
+ let message = dict["error"] as? String ?? "unknown full Bun engine error"
195
+ let currentError = lastError()
196
+ let diagnostic = !currentError.isEmpty && currentError != message
197
+ ? currentError
198
+ : previousError
199
+ let detail = diagnostic.isEmpty || diagnostic == message
200
+ ? ""
201
+ : " (engine error: \(diagnostic))"
202
+ NSLog("[FullBunEngineHost] call failed method=\(method) error=\(message) diagnostic=\(diagnostic)")
203
+ throw makeError("\(message)\(detail)")
204
+ }
205
+ if let dict = decoded as? [String: Any],
206
+ let ok = dict["ok"] as? Bool,
207
+ ok == true {
208
+ return dict["result"] ?? NSNull()
209
+ }
210
+ return decoded
211
+ }
212
+
213
+ private func load() throws {
214
+ if loaded { return }
215
+ NSLog("[FullBunEngineHost] loading ElizaBunEngine")
216
+ #if ELIZA_IOS_FULL_BUN_ENGINE
217
+ let loadedAbiVersionFn: AbiVersionFn = {
218
+ eliza_bun_engine_abi_version()
219
+ }
220
+ let loadedLastErrorFn: LastErrorFn = {
221
+ eliza_bun_engine_last_error()
222
+ }
223
+ let loadedSetHostCallbackFn: SetHostCallbackFn = { callback in
224
+ eliza_bun_engine_set_host_callback(callback)
225
+ }
226
+ let loadedStartFn: StartFn = { bundlePath, argvJson, envJson, appSupportDir in
227
+ eliza_bun_engine_start(bundlePath, argvJson, envJson, appSupportDir)
228
+ }
229
+ let loadedStopFn: StopFn = {
230
+ eliza_bun_engine_stop()
231
+ }
232
+ let loadedIsRunningFn: IsRunningFn = {
233
+ eliza_bun_engine_is_running()
234
+ }
235
+ let loadedCallFn: CallFn = { method, payload in
236
+ eliza_bun_engine_call(method, payload)
237
+ }
238
+ let loadedFreeFn: FreeFn = { ptr in
239
+ eliza_bun_engine_free(ptr)
240
+ }
241
+ try installLoadedEngineSymbols(
242
+ abiVersionFn: loadedAbiVersionFn,
243
+ lastErrorFn: loadedLastErrorFn,
244
+ setHostCallbackFn: loadedSetHostCallbackFn,
245
+ startFn: loadedStartFn,
246
+ stopFn: loadedStopFn,
247
+ isRunningFn: loadedIsRunningFn,
248
+ callFn: loadedCallFn,
249
+ freeFn: loadedFreeFn
250
+ )
251
+ #elseif DEBUG
252
+ let binaryPath = try locateFrameworkBinary()
253
+ guard let openedHandle = dlopen(binaryPath, RTLD_NOW | RTLD_LOCAL) else {
254
+ throw makeError(String(cString: dlerror()))
255
+ }
256
+ do {
257
+ let loadedAbiVersionFn: AbiVersionFn = try symbol(
258
+ "eliza_bun_engine_abi_version",
259
+ in: openedHandle
260
+ )
261
+ let loadedLastErrorFn: LastErrorFn = try symbol(
262
+ "eliza_bun_engine_last_error",
263
+ in: openedHandle
264
+ )
265
+ let loadedSetHostCallbackFn: SetHostCallbackFn = try symbol(
266
+ "eliza_bun_engine_set_host_callback",
267
+ in: openedHandle
268
+ )
269
+ let loadedStartFn: StartFn = try symbol("eliza_bun_engine_start", in: openedHandle)
270
+ let loadedStopFn: StopFn = try symbol("eliza_bun_engine_stop", in: openedHandle)
271
+ let loadedIsRunningFn: IsRunningFn = try symbol(
272
+ "eliza_bun_engine_is_running",
273
+ in: openedHandle
274
+ )
275
+ let loadedCallFn: CallFn = try symbol("eliza_bun_engine_call", in: openedHandle)
276
+ let loadedFreeFn: FreeFn = try symbol("eliza_bun_engine_free", in: openedHandle)
277
+ try installLoadedEngineSymbols(
278
+ abiVersionFn: loadedAbiVersionFn,
279
+ lastErrorFn: loadedLastErrorFn,
280
+ setHostCallbackFn: loadedSetHostCallbackFn,
281
+ startFn: loadedStartFn,
282
+ stopFn: loadedStopFn,
283
+ isRunningFn: loadedIsRunningFn,
284
+ callFn: loadedCallFn,
285
+ freeFn: loadedFreeFn
286
+ )
287
+ self.handle = openedHandle
288
+ } catch {
289
+ _ = dlclose(openedHandle)
290
+ throw error
291
+ }
292
+ #else
293
+ throw makeError(
294
+ "ElizaBunEngine direct-link symbols are not compiled into this release build"
295
+ )
296
+ #endif
297
+ }
298
+
299
+ private func installLoadedEngineSymbols(
300
+ abiVersionFn loadedAbiVersionFn: AbiVersionFn,
301
+ lastErrorFn loadedLastErrorFn: LastErrorFn,
302
+ setHostCallbackFn loadedSetHostCallbackFn: SetHostCallbackFn,
303
+ startFn loadedStartFn: StartFn,
304
+ stopFn loadedStopFn: StopFn,
305
+ isRunningFn loadedIsRunningFn: IsRunningFn,
306
+ callFn loadedCallFn: CallFn,
307
+ freeFn loadedFreeFn: FreeFn
308
+ ) throws {
309
+ guard let abiPointer = loadedAbiVersionFn() else {
310
+ throw makeError("ElizaBunEngine ABI version returned null")
311
+ }
312
+ let loadedAbiVersion = String(cString: abiPointer)
313
+ guard loadedAbiVersion == Self.expectedAbiVersion else {
314
+ throw makeError(
315
+ "ElizaBunEngine ABI mismatch: expected \(Self.expectedAbiVersion), got \(loadedAbiVersion)"
316
+ )
317
+ }
318
+ let callbackCode = loadedSetHostCallbackFn(fullBunHostCallCallback)
319
+ guard callbackCode == 0 else {
320
+ throw makeError("ElizaBunEngine failed to install host callback: \(callbackCode)")
321
+ }
322
+
323
+ self.abiVersionFn = loadedAbiVersionFn
324
+ self.lastErrorFn = loadedLastErrorFn
325
+ self.setHostCallbackFn = loadedSetHostCallbackFn
326
+ self.startFn = loadedStartFn
327
+ self.stopFn = loadedStopFn
328
+ self.isRunningFn = loadedIsRunningFn
329
+ self.callFn = loadedCallFn
330
+ self.freeFn = loadedFreeFn
331
+ self.loaded = true
332
+ NSLog("[FullBunEngineHost] loaded ElizaBunEngine abi=\(loadedAbiVersion)")
333
+ }
334
+
335
+ #if !ELIZA_IOS_FULL_BUN_ENGINE && DEBUG
336
+ private func locateFrameworkBinary() throws -> String {
337
+ let relative = "ElizaBunEngine.framework/ElizaBunEngine"
338
+ let candidates = [
339
+ Bundle.main.privateFrameworksURL?.appendingPathComponent(relative).path,
340
+ Bundle.main.bundleURL.appendingPathComponent("Frameworks").appendingPathComponent(relative).path,
341
+ Bundle.main.url(
342
+ forResource: "ElizaBunEngine",
343
+ withExtension: nil,
344
+ subdirectory: "Frameworks/ElizaBunEngine.framework"
345
+ )?.path,
346
+ ].compactMap { $0 }
347
+ for candidate in candidates where FileManager.default.fileExists(atPath: candidate) {
348
+ return candidate
349
+ }
350
+ throw makeError("ElizaBunEngine.framework is not embedded in the app bundle")
351
+ }
352
+
353
+ private func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
354
+ guard let pointer = dlsym(handle, name) else {
355
+ throw makeError("ElizaBunEngine missing symbol \(name)")
356
+ }
357
+ return unsafeBitCast(pointer, to: T.self)
358
+ }
359
+ #endif
360
+
361
+ private func encodeJSON(_ value: Any) throws -> String {
362
+ let data = try JSONSerialization.data(withJSONObject: value)
363
+ return String(data: data, encoding: .utf8) ?? "{}"
364
+ }
365
+
366
+ private func lastError() -> String {
367
+ guard let pointer = lastErrorFn?() else { return "" }
368
+ return String(cString: pointer)
369
+ }
370
+
371
+ private func makeError(_ message: String) -> NSError {
372
+ NSError(
373
+ domain: "ElizaBunEngine",
374
+ code: -1,
375
+ userInfo: [NSLocalizedDescriptionKey: message]
376
+ )
377
+ }
378
+
379
+ fileprivate func handleHostCall(
380
+ method: String,
381
+ payloadJson: String,
382
+ timeoutMs: Int32
383
+ ) -> String {
384
+ _ = timeoutMs
385
+ NSLog("[FullBunEngineHost] host call method=\(method) payloadBytes=\(payloadJson.lengthOfBytes(using: .utf8)) timeoutMs=\(timeoutMs)")
386
+ do {
387
+ let payload = try decodeHostPayload(payloadJson)
388
+ switch method {
389
+ case "llama_hardware_info":
390
+ return encodeHostEnvelope(ok: true, result: LlamaBridgeImpl.shared.hardwareInfo().asDict())
391
+ case "llama_load_model":
392
+ return try handleLoadModel(payload)
393
+ case "llama_generate":
394
+ return try handleGenerate(payload)
395
+ case "llama_free":
396
+ return handleFree(payload)
397
+ case "llama_cancel":
398
+ return handleCancel(payload)
399
+ case "eliza_tts_synthesize":
400
+ return handleTtsSynthesize(payload)
401
+ case "eliza_asr_transcribe":
402
+ return handleAsrTranscribe(payload)
403
+ default:
404
+ return encodeHostEnvelope(
405
+ ok: false,
406
+ error: "Unknown native host call method: \(method)"
407
+ )
408
+ }
409
+ } catch {
410
+ return encodeHostEnvelope(ok: false, error: error.localizedDescription)
411
+ }
412
+ }
413
+
414
+ private func handleLoadModel(_ payload: [String: Any]) throws -> String {
415
+ guard let path = stringValue(payload, "path") ?? stringValue(payload, "modelPath"),
416
+ !path.isEmpty else {
417
+ return encodeHostEnvelope(ok: false, error: "llama_load_model requires path")
418
+ }
419
+ let contextSize = uint32Value(payload, "context_size")
420
+ ?? uint32Value(payload, "contextSize")
421
+ ?? 4096
422
+ let useGPU = boolValue(payload, "use_gpu")
423
+ ?? boolValue(payload, "useGpu")
424
+ ?? true
425
+ let threads = int32Value(payload, "threads")
426
+ ?? int32Value(payload, "maxThreads")
427
+ let result = LlamaBridgeImpl.shared.loadModel(
428
+ path: path,
429
+ contextSize: contextSize,
430
+ useGPU: useGPU,
431
+ threads: threads
432
+ )
433
+ if let error = result.error {
434
+ return encodeHostEnvelope(ok: false, error: error)
435
+ }
436
+ guard let contextId = result.contextId else {
437
+ return encodeHostEnvelope(ok: false, error: "llama_load_model returned no context_id")
438
+ }
439
+ return encodeHostEnvelope(ok: true, result: [
440
+ "context_id": NSNumber(value: contextId),
441
+ "contextId": NSNumber(value: contextId),
442
+ "modelPath": path,
443
+ "contextSize": NSNumber(value: contextSize),
444
+ "useGpu": NSNumber(value: useGPU),
445
+ ])
446
+ }
447
+
448
+ private func handleGenerate(_ payload: [String: Any]) throws -> String {
449
+ guard let contextId = int64Value(payload, "context_id")
450
+ ?? int64Value(payload, "contextId") else {
451
+ return encodeHostEnvelope(ok: false, error: "llama_generate requires context_id")
452
+ }
453
+ guard let prompt = stringValue(payload, "prompt"), !prompt.isEmpty else {
454
+ return encodeHostEnvelope(ok: false, error: "llama_generate requires prompt")
455
+ }
456
+ let maxTokens = int32Value(payload, "max_tokens")
457
+ ?? int32Value(payload, "maxTokens")
458
+ ?? 256
459
+ let temperature = floatValue(payload, "temperature") ?? 0.7
460
+ let topP = floatValue(payload, "top_p") ?? floatValue(payload, "topP") ?? 0.95
461
+ let topK = int32Value(payload, "top_k") ?? int32Value(payload, "topK") ?? 40
462
+ let stopSequences = stringArrayValue(payload, "stop")
463
+ ?? stringArrayValue(payload, "stopSequences")
464
+ ?? []
465
+
466
+ let generate = {
467
+ LlamaBridgeImpl.shared.generate(
468
+ contextId: contextId,
469
+ prompt: prompt,
470
+ maxTokens: maxTokens,
471
+ temperature: temperature,
472
+ topP: topP,
473
+ topK: topK,
474
+ stopSequences: stopSequences
475
+ )
476
+ }
477
+ let result: LlamaGenerateResult
478
+ if let queue = LlamaBridgeImpl.shared.workQueue(for: contextId) {
479
+ result = queue.sync(execute: generate)
480
+ } else {
481
+ result = generate()
482
+ }
483
+ if let error = result.error {
484
+ return encodeHostEnvelope(ok: false, error: error)
485
+ }
486
+ return encodeHostEnvelope(ok: true, result: [
487
+ "text": result.text,
488
+ "promptTokens": NSNumber(value: result.promptTokens),
489
+ "outputTokens": NSNumber(value: result.outputTokens),
490
+ "durationMs": NSNumber(value: result.durationMs),
491
+ ])
492
+ }
493
+
494
+ private func handleFree(_ payload: [String: Any]) -> String {
495
+ guard let contextId = int64Value(payload, "context_id")
496
+ ?? int64Value(payload, "contextId") else {
497
+ return encodeHostEnvelope(ok: true, result: ["freed": false])
498
+ }
499
+ LlamaBridgeImpl.shared.free(contextId: contextId)
500
+ return encodeHostEnvelope(ok: true, result: [
501
+ "freed": true,
502
+ "context_id": NSNumber(value: contextId),
503
+ ])
504
+ }
505
+
506
+ private func handleCancel(_ payload: [String: Any]) -> String {
507
+ guard let contextId = int64Value(payload, "context_id")
508
+ ?? int64Value(payload, "contextId") else {
509
+ return encodeHostEnvelope(ok: true, result: ["cancelled": false])
510
+ }
511
+ LlamaBridgeImpl.shared.cancel(contextId: contextId)
512
+ return encodeHostEnvelope(ok: true, result: [
513
+ "cancelled": true,
514
+ "context_id": NSNumber(value: contextId),
515
+ ])
516
+ }
517
+
518
+ private func handleTtsSynthesize(_ payload: [String: Any]) -> String {
519
+ guard let bundleDir = stringValue(payload, "bundleDir")
520
+ ?? stringValue(payload, "bundle_dir"),
521
+ !bundleDir.isEmpty else {
522
+ return encodeHostEnvelope(ok: false, error: "eliza_tts_synthesize requires bundleDir")
523
+ }
524
+ guard let text = stringValue(payload, "text"), !text.isEmpty else {
525
+ return encodeHostEnvelope(ok: false, error: "eliza_tts_synthesize requires text")
526
+ }
527
+ let speakerPresetId = stringValue(payload, "speakerPresetId")
528
+ ?? stringValue(payload, "speaker_preset_id")
529
+ ?? stringValue(payload, "voice")
530
+ ?? stringValue(payload, "voiceId")
531
+ let maxSamples = intValue(payload, "maxSamples")
532
+ ?? intValue(payload, "max_samples")
533
+ ?? 24_000 * 60
534
+ let result = LlamaBridgeImpl.shared.synthesizeSpeech(
535
+ bundleDir: bundleDir,
536
+ text: text,
537
+ speakerPresetId: speakerPresetId,
538
+ maxSamples: maxSamples
539
+ )
540
+ if let error = result.error {
541
+ return encodeHostEnvelope(ok: false, error: error)
542
+ }
543
+ var payload: [String: Any] = [
544
+ "contentType": result.contentType,
545
+ "sampleRate": NSNumber(value: result.sampleRate),
546
+ "samples": NSNumber(value: result.samples),
547
+ "durationMs": NSNumber(value: result.durationMs),
548
+ ]
549
+ if let audioFilePath = result.audioFilePath, !audioFilePath.isEmpty {
550
+ payload["audioFilePath"] = audioFilePath
551
+ } else {
552
+ payload["audioBase64"] = result.audioBase64
553
+ }
554
+ return encodeHostEnvelope(ok: true, result: payload)
555
+ }
556
+
557
+ /// Wire format: `pcm` is mono fp32 audio in [-1, 1] carried as a JSON number
558
+ /// array (no base64). `bridge.ts` encodes the same way. `sampleRate` is the
559
+ /// source rate in Hz; the inference slice resamples internally as needed.
560
+ private func handleAsrTranscribe(_ payload: [String: Any]) -> String {
561
+ guard let bundleDir = stringValue(payload, "bundleDir")
562
+ ?? stringValue(payload, "bundle_dir"),
563
+ !bundleDir.isEmpty else {
564
+ return encodeHostEnvelope(ok: false, error: "eliza_asr_transcribe requires bundleDir")
565
+ }
566
+ guard let pcm = floatArrayValue(payload, "pcm"), !pcm.isEmpty else {
567
+ return encodeHostEnvelope(ok: false, error: "eliza_asr_transcribe requires pcm")
568
+ }
569
+ let sampleRate = intValue(payload, "sampleRate")
570
+ ?? intValue(payload, "sample_rate")
571
+ ?? 16_000
572
+ let result = LlamaBridgeImpl.shared.transcribeSpeech(
573
+ bundleDir: bundleDir,
574
+ pcm: pcm,
575
+ sampleRate: sampleRate
576
+ )
577
+ if let error = result.error {
578
+ return encodeHostEnvelope(ok: false, error: error)
579
+ }
580
+ return encodeHostEnvelope(ok: true, result: [
581
+ "text": result.text,
582
+ "durationMs": NSNumber(value: result.durationMs),
583
+ ])
584
+ }
585
+
586
+ private func decodeHostPayload(_ json: String) throws -> [String: Any] {
587
+ guard let data = json.data(using: .utf8) else { return [:] }
588
+ let value = try JSONSerialization.jsonObject(with: data)
589
+ return value as? [String: Any] ?? [:]
590
+ }
591
+
592
+ private func encodeHostEnvelope(
593
+ ok: Bool,
594
+ result: Any? = nil,
595
+ error: String? = nil
596
+ ) -> String {
597
+ var object: [String: Any] = ["ok": ok]
598
+ if let result {
599
+ object["result"] = result
600
+ } else if ok {
601
+ object["result"] = NSNull()
602
+ }
603
+ if let error {
604
+ object["error"] = error
605
+ }
606
+ guard JSONSerialization.isValidJSONObject(object),
607
+ let data = try? JSONSerialization.data(withJSONObject: object),
608
+ let json = String(data: data, encoding: .utf8) else {
609
+ let fallback = (error ?? "Failed to encode native host response")
610
+ .replacingOccurrences(of: "\"", with: "\\\"")
611
+ return "{\"ok\":false,\"error\":\"\(fallback)\"}"
612
+ }
613
+ return json
614
+ }
615
+
616
+ private func stringValue(_ payload: [String: Any], _ key: String) -> String? {
617
+ payload[key] as? String
618
+ }
619
+
620
+ private func boolValue(_ payload: [String: Any], _ key: String) -> Bool? {
621
+ if let value = payload[key] as? Bool { return value }
622
+ if let value = payload[key] as? NSNumber { return value.boolValue }
623
+ if let value = payload[key] as? String {
624
+ if value == "true" || value == "1" { return true }
625
+ if value == "false" || value == "0" { return false }
626
+ }
627
+ return nil
628
+ }
629
+
630
+ private func int32Value(_ payload: [String: Any], _ key: String) -> Int32? {
631
+ if let value = payload[key] as? NSNumber { return value.int32Value }
632
+ if let value = payload[key] as? String, let parsed = Int32(value) { return parsed }
633
+ return nil
634
+ }
635
+
636
+ private func intValue(_ payload: [String: Any], _ key: String) -> Int? {
637
+ if let value = payload[key] as? NSNumber { return value.intValue }
638
+ if let value = payload[key] as? String, let parsed = Int(value) { return parsed }
639
+ return nil
640
+ }
641
+
642
+ private func int64Value(_ payload: [String: Any], _ key: String) -> Int64? {
643
+ if let value = payload[key] as? NSNumber { return value.int64Value }
644
+ if let value = payload[key] as? String, let parsed = Int64(value) { return parsed }
645
+ return nil
646
+ }
647
+
648
+ private func uint32Value(_ payload: [String: Any], _ key: String) -> UInt32? {
649
+ if let value = payload[key] as? NSNumber { return value.uint32Value }
650
+ if let value = payload[key] as? String, let parsed = UInt32(value) { return parsed }
651
+ return nil
652
+ }
653
+
654
+ private func floatValue(_ payload: [String: Any], _ key: String) -> Float? {
655
+ if let value = payload[key] as? NSNumber { return value.floatValue }
656
+ if let value = payload[key] as? String, let parsed = Float(value) { return parsed }
657
+ return nil
658
+ }
659
+
660
+ private func stringArrayValue(_ payload: [String: Any], _ key: String) -> [String]? {
661
+ if let values = payload[key] as? [String] { return values }
662
+ if let values = payload[key] as? [Any] {
663
+ return values.compactMap { $0 as? String }
664
+ }
665
+ return nil
666
+ }
667
+
668
+ private func floatArrayValue(_ payload: [String: Any], _ key: String) -> [Float]? {
669
+ if let values = payload[key] as? [NSNumber] {
670
+ return values.map { $0.floatValue }
671
+ }
672
+ if let values = payload[key] as? [Any] {
673
+ return values.compactMap { ($0 as? NSNumber)?.floatValue }
674
+ }
675
+ return nil
676
+ }
677
+ }