@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,705 @@
1
+ import Foundation
2
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
3
+ import JavaScriptCore
4
+ #endif
5
+ import Capacitor
6
+
7
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
8
+ // Disambiguate `JSValue` — both JavaScriptCore (class) and Capacitor (marker
9
+ // protocol) export a type called `JSValue`. Inside this file we always mean
10
+ // the JSC class.
11
+ private typealias JSValue = JavaScriptCore.JSValue
12
+ #endif
13
+
14
+ #if ELIZA_IOS_FULL_BUN_ENGINE
15
+ private enum RuntimeQueue {
16
+ static let label = "ai.eliza.bun.runtime"
17
+ static var current: DispatchQueue?
18
+ }
19
+ #endif
20
+
21
+ /// Core runtime that hosts a JavaScriptCore JSContext on a dedicated serial
22
+ /// queue. The plugin shell (`ElizaBunRuntimePlugin`) talks to this class to
23
+ /// start/stop the agent, send chat messages, and route React UI calls into
24
+ /// JS-registered handlers via the UI bridge.
25
+ public final class ElizaBunRuntime {
26
+ // MARK: - Public state
27
+
28
+ public private(set) var isRunning: Bool = false
29
+ public private(set) var bridgeVersion: String?
30
+ public private(set) var loadedModelPath: String?
31
+ public private(set) var tokensPerSecond: Double?
32
+ public private(set) var engineMode: String = "compat"
33
+
34
+ // MARK: - Private state
35
+
36
+ private let queue = DispatchQueue(label: RuntimeQueue.label, qos: .userInitiated)
37
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
38
+ private let virtualMachine = JSVirtualMachine()!
39
+ private var context: JSContext?
40
+ private var bridges: BridgeKit?
41
+ #endif
42
+ private var fullBunEngine: FullBunEngineHost?
43
+ private weak var plugin: CAPPlugin?
44
+
45
+ private static var defaultBridgeVersion: String {
46
+ #if ELIZA_IOS_FULL_BUN_ENGINE
47
+ return "v1"
48
+ #else
49
+ return BridgeInstaller.version
50
+ #endif
51
+ }
52
+
53
+ public typealias RuntimeStatus = (
54
+ ready: Bool,
55
+ engine: String,
56
+ bridgeVersion: String?,
57
+ model: String?,
58
+ tokensPerSecond: Double?
59
+ )
60
+
61
+ // MARK: - Init
62
+
63
+ public init(plugin: CAPPlugin?) {
64
+ self.plugin = plugin
65
+ }
66
+
67
+ // MARK: - Lifecycle
68
+
69
+ /// Starts the runtime. Loads the polyfill prefix, installs the bridge,
70
+ /// then evaluates the agent bundle. Calls `startEliza()` if exported.
71
+ public func start(
72
+ bundlePath: String?,
73
+ polyfillPath: String?,
74
+ engine: String,
75
+ argv: [String],
76
+ env: [String: String],
77
+ completion: @escaping (Result<StartOutcome, Error>) -> Void
78
+ ) {
79
+ queue.async { [weak self] in
80
+ guard let self = self else { return }
81
+ RuntimeQueue.current = self.queue
82
+ NSLog("[ElizaBunRuntime] start queued engine=\(engine) argv=\(argv) envKeys=\(env.keys.sorted())")
83
+ if self.isRunning {
84
+ if let fullBunEngine = self.fullBunEngine, !fullBunEngine.isRunning {
85
+ NSLog("[ElizaBunRuntime] start found stale full Bun host")
86
+ self.isRunning = false
87
+ self.fullBunEngine = nil
88
+ } else {
89
+ NSLog("[ElizaBunRuntime] start reused running runtime engineMode=\(self.engineMode)")
90
+ completion(.success(StartOutcome(bridgeVersion: self.bridgeVersion ?? Self.defaultBridgeVersion)))
91
+ return
92
+ }
93
+ }
94
+ let startedAt = Date()
95
+ do {
96
+ try self.bootstrap(
97
+ bundlePath: bundlePath,
98
+ polyfillPath: polyfillPath,
99
+ engine: engine,
100
+ argv: argv,
101
+ env: env
102
+ )
103
+ let outcome = StartOutcome(bridgeVersion: self.bridgeVersion ?? Self.defaultBridgeVersion)
104
+ let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
105
+ NSLog("[ElizaBunRuntime] start completed engineMode=\(self.engineMode) bridgeVersion=\(outcome.bridgeVersion) durationMs=\(durationMs)")
106
+ completion(.success(outcome))
107
+ } catch {
108
+ let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000)
109
+ NSLog("[ElizaBunRuntime] start failed engine=\(engine) durationMs=\(durationMs) error=\(error)")
110
+ completion(.failure(error))
111
+ }
112
+ }
113
+ }
114
+
115
+ public func stop(completion: @escaping () -> Void) {
116
+ queue.async { [weak self] in
117
+ self?.teardown()
118
+ completion()
119
+ }
120
+ }
121
+
122
+ public func currentStatus(completion: @escaping (RuntimeStatus) -> Void) {
123
+ queue.async { [weak self] in
124
+ guard let self = self else {
125
+ completion((false, "compat", nil, nil, nil))
126
+ return
127
+ }
128
+ completion((
129
+ self.isRunning,
130
+ self.engineMode,
131
+ self.bridgeVersion,
132
+ self.loadedModelPath,
133
+ self.tokensPerSecond
134
+ ))
135
+ }
136
+ }
137
+
138
+ public struct StartOutcome {
139
+ public let bridgeVersion: String
140
+ }
141
+
142
+ // MARK: - Bridge-facing hooks
143
+
144
+ /// Called by `ProcessBridge` when the agent calls `exit(code)`. Tears
145
+ /// down the runtime and posts a UI event so the React shell can refresh.
146
+ public func handleAgentExit(code: Int) {
147
+ queue.async { [weak self] in
148
+ guard let self = self else { return }
149
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
150
+ self.bridges?.ui.handler(for: "__internal_on_exit__")?.callSync(args: [code])
151
+ #endif
152
+ self.teardown()
153
+ DispatchQueue.main.async {
154
+ self.plugin?.notifyListeners("eliza:runtime-exit", data: ["code": code])
155
+ }
156
+ }
157
+ }
158
+
159
+ // MARK: - Public RPC surface used by the plugin shell
160
+
161
+ public func sendMessage(
162
+ text: String,
163
+ conversationId: String?,
164
+ completion: @escaping (Result<String, Error>) -> Void
165
+ ) {
166
+ queue.async { [weak self] in
167
+ guard let self = self else {
168
+ completion(.failure(Self.runtimeStaleError()))
169
+ return
170
+ }
171
+ if let fullBunEngine = self.fullBunEngine {
172
+ do {
173
+ let payload: [String: Any] = [
174
+ "message": text,
175
+ "conversationId": conversationId ?? NSNull(),
176
+ ]
177
+ let result = try fullBunEngine.call(method: "send_message", payload: payload)
178
+ completion(.success(self.extractReply(from: result)))
179
+ } catch {
180
+ completion(.failure(error))
181
+ }
182
+ return
183
+ }
184
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
185
+ guard let ctx = self.context else {
186
+ completion(.failure(self.makeError("Runtime is not started")))
187
+ return
188
+ }
189
+ guard let handler = self.bridges?.ui.handler(for: "send_message") else {
190
+ completion(.failure(self.makeError("Agent has not registered a send_message handler")))
191
+ return
192
+ }
193
+ let payload: [String: Any] = [
194
+ "message": text,
195
+ "conversationId": conversationId ?? NSNull(),
196
+ ]
197
+ guard let result = handler.callSync(args: [payload]) else {
198
+ completion(.failure(self.makeError("send_message handler returned undefined")))
199
+ return
200
+ }
201
+ if let err = ctx.takeException() {
202
+ completion(.failure(err))
203
+ return
204
+ }
205
+ self.unwrapReply(result: result, ctx: ctx, completion: completion)
206
+ #else
207
+ completion(.failure(self.makeError("Full Bun runtime is not started")))
208
+ #endif
209
+ }
210
+ }
211
+
212
+ public func dispatchHandler(
213
+ method: String,
214
+ args: Any?,
215
+ completion: @escaping (Result<Any?, Error>) -> Void
216
+ ) {
217
+ queue.async { [weak self] in
218
+ guard let self = self else {
219
+ completion(.failure(Self.runtimeStaleError()))
220
+ return
221
+ }
222
+ if let fullBunEngine = self.fullBunEngine {
223
+ do {
224
+ completion(.success(try fullBunEngine.call(method: method, payload: args ?? NSNull())))
225
+ } catch {
226
+ completion(.failure(error))
227
+ }
228
+ return
229
+ }
230
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
231
+ guard let ctx = self.context else {
232
+ completion(.failure(self.makeError("Runtime is not started")))
233
+ return
234
+ }
235
+ guard let handler = self.bridges?.ui.handler(for: method) else {
236
+ completion(.failure(self.makeError("No handler registered for \(method)")))
237
+ return
238
+ }
239
+ let callArgs: [Any] = args == nil ? [] : [args!]
240
+ guard let result = handler.callSync(args: callArgs) else {
241
+ completion(.failure(self.makeError("\(method) handler returned undefined")))
242
+ return
243
+ }
244
+ if let err = ctx.takeException() {
245
+ completion(.failure(err))
246
+ return
247
+ }
248
+ self.unwrapAny(result: result, ctx: ctx, completion: completion)
249
+ #else
250
+ completion(.failure(self.makeError("Full Bun runtime is not started")))
251
+ #endif
252
+ }
253
+ }
254
+
255
+ // MARK: - Bootstrap
256
+
257
+ private func bootstrap(
258
+ bundlePath: String?,
259
+ polyfillPath: String?,
260
+ engine: String,
261
+ argv: [String],
262
+ env: [String: String]
263
+ ) throws {
264
+ let requestedEngine = IosRuntimePolicy.normalizeEngine(engine)
265
+ let runtimeEnv = IosRuntimePolicy.sanitizeEnvironment(env)
266
+ #if ELIZA_IOS_FULL_BUN_ENGINE
267
+ let compiledEngine = "full-bun"
268
+ #else
269
+ let compiledEngine = "compat"
270
+ #endif
271
+ NSLog("[ElizaBunRuntime] bootstrap requestedEngine=\(requestedEngine) compiledEngine=\(compiledEngine)")
272
+ if requestedEngine == "bun" || requestedEngine == "auto" || requestedEngine.isEmpty {
273
+ let host = FullBunEngineHost.shared
274
+ do {
275
+ let paths = SandboxPaths()
276
+ let appSupportDir = paths.appSupport.path
277
+ let workspaceDir = paths.appSupport.appendingPathComponent("workspace").path
278
+ let pgliteDir = paths.appSupport.appendingPathComponent(".elizadb").path
279
+ let resolvedBundlePath = try resolveFullBunAgentBundlePath(override: bundlePath)
280
+ let assetDir = URL(fileURLWithPath: resolvedBundlePath).deletingLastPathComponent().path
281
+ let publicDir = URL(fileURLWithPath: assetDir).deletingLastPathComponent().path
282
+ let stateLocalInferenceModelsDir = paths.appSupport
283
+ .appendingPathComponent("local-inference", isDirectory: true)
284
+ .appendingPathComponent("models", isDirectory: true)
285
+ try? FileManager.default.createDirectory(
286
+ atPath: workspaceDir,
287
+ withIntermediateDirectories: true
288
+ )
289
+ try? FileManager.default.createDirectory(
290
+ atPath: pgliteDir,
291
+ withIntermediateDirectories: true
292
+ )
293
+ try? FileManager.default.createDirectory(
294
+ at: stateLocalInferenceModelsDir,
295
+ withIntermediateDirectories: true
296
+ )
297
+ var fullBunEnv = runtimeEnv
298
+ fullBunEnv["HOME"] = appSupportDir
299
+ fullBunEnv["ELIZA_HOME"] = appSupportDir
300
+ fullBunEnv["ELIZA_STATE_DIR"] = appSupportDir
301
+ fullBunEnv["ELIZA_IOS_APP_SUPPORT_DIR"] = appSupportDir
302
+ fullBunEnv["ELIZA_WORKSPACE_DIR"] = workspaceDir
303
+ fullBunEnv["MOBILE_WORKSPACE_ROOT"] = appSupportDir
304
+ fullBunEnv["PGLITE_DATA_DIR"] = pgliteDir
305
+ fullBunEnv["ELIZA_IOS_AGENT_BUNDLE"] = resolvedBundlePath
306
+ fullBunEnv["ELIZA_IOS_AGENT_ASSET_DIR"] = assetDir
307
+ fullBunEnv["ELIZA_IOS_AGENT_PUBLIC_DIR"] = publicDir
308
+ fullBunEnv["ELIZA_IOS_BRIDGE_TRANSPORT"] = "bun-host-ipc"
309
+ NSLog("[ElizaBunRuntime] full Bun bootstrap bundle=\(resolvedBundlePath) appSupport=\(appSupportDir) pglite=\(pgliteDir) assetDir=\(assetDir)")
310
+ try host.start(
311
+ bundlePath: resolvedBundlePath,
312
+ argv: argv,
313
+ env: fullBunEnv,
314
+ appSupportDir: appSupportDir
315
+ )
316
+ self.fullBunEngine = host
317
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
318
+ self.context = nil
319
+ self.bridges = nil
320
+ #endif
321
+ self.engineMode = "bun"
322
+ self.bridgeVersion = "bun-ios:\(host.abiVersion)"
323
+ self.isRunning = true
324
+ NSLog("[ElizaBunRuntime] full Bun bootstrap ready bridgeVersion=\(self.bridgeVersion ?? "unknown")")
325
+ return
326
+ } catch {
327
+ #if ELIZA_IOS_FULL_BUN_ENGINE
328
+ throw error
329
+ #else
330
+ if requestedEngine == "bun" {
331
+ throw error
332
+ }
333
+ NSLog("[ElizaBunRuntime] Full Bun engine unavailable; falling back to JSContext: \(error)")
334
+ #endif
335
+ }
336
+ }
337
+
338
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
339
+ guard IosRuntimePolicy.allowsJSContextCompatibilityFallback else {
340
+ throw makeError(
341
+ "JSContext compatibility fallback is disabled outside iOS DEBUG/development builds; request engine=bun"
342
+ )
343
+ }
344
+
345
+ let ctx = JSContext(virtualMachine: virtualMachine)!
346
+ ctx.name = "ElizaBunRuntime"
347
+ ctx.exceptionHandler = { _, exception in
348
+ let msg = exception?.toString() ?? "<unknown exception>"
349
+ let stack = exception?.objectForKeyedSubscript("stack")?.toString() ?? ""
350
+ NSLog("[ElizaBunRuntime] JS exception: \(msg)\n\(stack)")
351
+ }
352
+ self.context = ctx
353
+
354
+ // Surface `console.log` into NSLog before any user code runs so polyfill
355
+ // load errors are visible.
356
+ installMinimalConsole(into: ctx)
357
+
358
+ // Build the bridges.
359
+ let pluginRef = CAPPluginRef(plugin)
360
+ let kit = BridgeInstaller.install(
361
+ into: ctx,
362
+ paths: SandboxPaths(),
363
+ plugin: pluginRef,
364
+ argv: argv,
365
+ env: runtimeEnv,
366
+ runtime: self
367
+ )
368
+ self.bridges = kit
369
+ self.bridgeVersion = BridgeInstaller.version
370
+ self.engineMode = "compat"
371
+
372
+ // Load the polyfill prefix.
373
+ let polyfillSource = try loadPolyfillSource(override: polyfillPath)
374
+ ctx.evaluateScript(polyfillSource)
375
+ if let err = ctx.takeException() {
376
+ throw makeError("Polyfill load failed: \(err)")
377
+ }
378
+
379
+ // Load the agent bundle.
380
+ let agentSource = try loadAgentSource(override: bundlePath)
381
+ ctx.evaluateScript(agentSource)
382
+ if let err = ctx.takeException() {
383
+ throw makeError("Agent bundle load failed: \(err)")
384
+ }
385
+
386
+ // Invoke `globalThis.startEliza()` if exported.
387
+ if let startEliza = ctx.objectForKeyedSubscript("startEliza"), startEliza.isObject {
388
+ _ = startEliza.call(withArguments: [])
389
+ if let err = ctx.takeException() {
390
+ throw makeError("startEliza threw: \(err)")
391
+ }
392
+ }
393
+
394
+ self.isRunning = true
395
+ #else
396
+ throw makeError("JSContext compatibility fallback is not compiled into full Bun builds; request engine=bun")
397
+ #endif
398
+ }
399
+
400
+ private func teardown() {
401
+ fullBunEngine?.stop()
402
+ fullBunEngine = nil
403
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
404
+ bridges?.httpServer.shutdown()
405
+ bridges?.ui.clear()
406
+ bridges = nil
407
+ context = nil
408
+ #endif
409
+ isRunning = false
410
+ loadedModelPath = nil
411
+ tokensPerSecond = nil
412
+ engineMode = "compat"
413
+ RuntimeQueue.current = nil
414
+ }
415
+
416
+ // MARK: - Source loading
417
+
418
+ private func resolveFullBunAgentBundlePath(override: String?) throws -> String {
419
+ if let url = Bundle.main.url(
420
+ forResource: "agent-bundle",
421
+ withExtension: "js",
422
+ subdirectory: "public/agent"
423
+ ) {
424
+ #if DEBUG
425
+ if let override = override, !override.isEmpty {
426
+ let overrideURL = URL(fileURLWithPath: override).resolvingSymlinksInPath()
427
+ let bundleURL = Bundle.main.bundleURL.resolvingSymlinksInPath()
428
+ if overrideURL.path == url.resolvingSymlinksInPath().path {
429
+ return overrideURL.path
430
+ }
431
+ if overrideURL.path.hasPrefix(bundleURL.path + "/") {
432
+ return overrideURL.path
433
+ }
434
+ throw makeError(
435
+ "full Bun bundlePath override must stay inside the signed app bundle resources"
436
+ )
437
+ }
438
+ #endif
439
+ return url.path
440
+ }
441
+ throw makeError(
442
+ "public/agent/agent-bundle.js not found in app bundle resources for full Bun engine"
443
+ )
444
+ }
445
+
446
+ #if !ELIZA_IOS_FULL_BUN_ENGINE
447
+ private func loadAgentSource(override: String?) throws -> String {
448
+ return try String(contentsOfFile: resolveAgentBundlePath(override: override), encoding: .utf8)
449
+ }
450
+
451
+ private func resolveAgentBundlePath(override: String?) throws -> String {
452
+ if let override = override, !override.isEmpty {
453
+ return override
454
+ }
455
+ let candidates: [(String, String?, String?)] = [
456
+ ("agent-bundle-ios", "js", nil),
457
+ ("agent-bundle", "js", nil),
458
+ ("agent-bundle-ios", "js", "public/agent"),
459
+ ("agent-bundle", "js", "public/agent"),
460
+ ]
461
+ for (name, ext, subdir) in candidates {
462
+ if let url = Bundle.main.url(
463
+ forResource: name,
464
+ withExtension: ext,
465
+ subdirectory: subdir
466
+ ) {
467
+ return url.path
468
+ }
469
+ }
470
+ throw makeError(
471
+ "agent-bundle.js not found in app bundle resources (searched app root and public/agent)"
472
+ )
473
+ }
474
+
475
+ private func loadPolyfillSource(override: String?) throws -> String {
476
+ if let override = override, !override.isEmpty {
477
+ return try String(contentsOfFile: override, encoding: .utf8)
478
+ }
479
+ if let url = Bundle.main.url(forResource: "eliza-polyfill-prefix", withExtension: "js") {
480
+ return try String(contentsOf: url, encoding: .utf8)
481
+ }
482
+ // Minimal embedded fallback. Just exposes the bridge version + globals
483
+ // so the agent code can detect the runtime even when the full
484
+ // polyfill bundle isn't shipped yet.
485
+ return """
486
+ if (typeof globalThis.__ELIZA_BRIDGE__ !== "object") {
487
+ throw new Error("__ELIZA_BRIDGE__ host not installed");
488
+ }
489
+ if (globalThis.__ELIZA_BRIDGE_VERSION__ !== "v1") {
490
+ throw new Error("Bridge version mismatch: expected v1, got " + globalThis.__ELIZA_BRIDGE_VERSION__);
491
+ }
492
+ """
493
+ }
494
+
495
+ private func installMinimalConsole(into ctx: JSContext) {
496
+ let levels: [(String, String)] = [
497
+ ("log", "info"),
498
+ ("info", "info"),
499
+ ("debug", "debug"),
500
+ ("warn", "warn"),
501
+ ("error", "error"),
502
+ ]
503
+ ctx.evaluateScript("globalThis.console = globalThis.console || {};")
504
+ guard let console = ctx.objectForKeyedSubscript("console") else { return }
505
+ for (method, level) in levels {
506
+ let block: @convention(block) () -> Void = {
507
+ let args = JSContext.currentArguments() as? [JSValue] ?? []
508
+ let message = args.map { $0.toString() ?? "" }.joined(separator: " ")
509
+ NSLog("[ElizaBunRuntime console.\(level)] \(message)")
510
+ }
511
+ console.setObject(unsafeBitCast(block, to: AnyObject.self), forKeyedSubscript: method as NSString)
512
+ }
513
+ }
514
+
515
+ // MARK: - Promise / response unwrapping
516
+
517
+ private func unwrapReply(
518
+ result: JSValue,
519
+ ctx: JSContext,
520
+ completion: @escaping (Result<String, Error>) -> Void
521
+ ) {
522
+ // The reply is expected to be `{ reply: string }` or a Promise of it.
523
+ let isThenable = result.objectForKeyedSubscript("then")?.isObject == true
524
+ if isThenable {
525
+ let onResolve: @convention(block) (JSValue) -> Void = { [weak self] resolved in
526
+ guard let self = self else { return }
527
+ if let err = ctx.takeException() {
528
+ completion(.failure(err))
529
+ return
530
+ }
531
+ completion(.success(self.extractReply(from: resolved)))
532
+ }
533
+ let onReject: @convention(block) (JSValue) -> Void = { rejected in
534
+ let msg = rejected.toString() ?? "Promise rejected"
535
+ completion(.failure(NSError(
536
+ domain: "ElizaBunRuntime",
537
+ code: -1,
538
+ userInfo: [NSLocalizedDescriptionKey: msg]
539
+ )))
540
+ }
541
+ _ = result.objectForKeyedSubscript("then")?.call(withArguments: [
542
+ JSValue(object: unsafeBitCast(onResolve, to: AnyObject.self), in: ctx) as Any,
543
+ JSValue(object: unsafeBitCast(onReject, to: AnyObject.self), in: ctx) as Any,
544
+ ])
545
+ return
546
+ }
547
+ completion(.success(extractReply(from: result)))
548
+ }
549
+
550
+ private func extractReply(from value: JSValue) -> String {
551
+ if value.isString {
552
+ return value.toString() ?? ""
553
+ }
554
+ if let s = value.objectForKeyedSubscript("reply")?.toString(), !s.isEmpty {
555
+ return s
556
+ }
557
+ if let s = value.objectForKeyedSubscript("text")?.toString(), !s.isEmpty {
558
+ return s
559
+ }
560
+ return value.toString() ?? ""
561
+ }
562
+
563
+ private func extractReply(from value: Any?) -> String {
564
+ if let s = value as? String { return s }
565
+ if let dict = value as? [String: Any] {
566
+ if let s = dict["reply"] as? String { return s }
567
+ if let s = dict["text"] as? String { return s }
568
+ if let result = dict["result"] { return extractReply(from: result) }
569
+ }
570
+ return String(describing: value ?? "")
571
+ }
572
+
573
+ private func unwrapAny(
574
+ result: JSValue,
575
+ ctx: JSContext,
576
+ completion: @escaping (Result<Any?, Error>) -> Void
577
+ ) {
578
+ let isThenable = result.objectForKeyedSubscript("then")?.isObject == true
579
+ if isThenable {
580
+ let onResolve: @convention(block) (JSValue) -> Void = { resolved in
581
+ if let err = ctx.takeException() {
582
+ completion(.failure(err))
583
+ return
584
+ }
585
+ completion(.success(resolved.toObject()))
586
+ }
587
+ let onReject: @convention(block) (JSValue) -> Void = { rejected in
588
+ let msg = rejected.toString() ?? "Promise rejected"
589
+ completion(.failure(NSError(
590
+ domain: "ElizaBunRuntime",
591
+ code: -1,
592
+ userInfo: [NSLocalizedDescriptionKey: msg]
593
+ )))
594
+ }
595
+ _ = result.objectForKeyedSubscript("then")?.call(withArguments: [
596
+ JSValue(object: unsafeBitCast(onResolve, to: AnyObject.self), in: ctx) as Any,
597
+ JSValue(object: unsafeBitCast(onReject, to: AnyObject.self), in: ctx) as Any,
598
+ ])
599
+ return
600
+ }
601
+ completion(.success(result.toObject()))
602
+ }
603
+ #endif
604
+
605
+ #if ELIZA_IOS_FULL_BUN_ENGINE
606
+ private func extractReply(from value: Any?) -> String {
607
+ if let s = value as? String { return s }
608
+ if let dict = value as? [String: Any] {
609
+ if let s = dict["reply"] as? String { return s }
610
+ if let s = dict["text"] as? String { return s }
611
+ if let result = dict["result"] { return extractReply(from: result) }
612
+ }
613
+ return String(describing: value ?? "")
614
+ }
615
+ #endif
616
+
617
+ // MARK: - Errors
618
+
619
+ private func makeError(_ message: String) -> Error {
620
+ return NSError(
621
+ domain: "ElizaBunRuntime",
622
+ code: -1,
623
+ userInfo: [NSLocalizedDescriptionKey: message]
624
+ )
625
+ }
626
+
627
+ private static func runtimeStaleError() -> Error {
628
+ return NSError(
629
+ domain: "ElizaBunRuntime",
630
+ code: -1,
631
+ userInfo: [NSLocalizedDescriptionKey: "Runtime has been deallocated"]
632
+ )
633
+ }
634
+ }
635
+
636
+ enum IosRuntimePolicy {
637
+ static let defaultEngine = "auto"
638
+ static let safeLocalExecutionMode = "local-safe"
639
+ #if ELIZA_IOS_FULL_BUN_ENGINE
640
+ static let allowsJSContextCompatibilityFallback = false
641
+ #elseif DEBUG
642
+ static let allowsJSContextCompatibilityFallback = true
643
+ #else
644
+ static let allowsJSContextCompatibilityFallback = false
645
+ #endif
646
+
647
+ private static let executionModeKeys = [
648
+ "ELIZA_RUNTIME_MODE",
649
+ "RUNTIME_MODE",
650
+ "LOCAL_RUNTIME_MODE",
651
+ ]
652
+
653
+ static func normalizeEngine(_ value: String) -> String {
654
+ switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
655
+ case "bun":
656
+ return "bun"
657
+ case "auto", "":
658
+ return "auto"
659
+ case "compat":
660
+ return "compat"
661
+ default:
662
+ return defaultEngine
663
+ }
664
+ }
665
+
666
+ static func sanitizeEnvironment(_ env: [String: String]) -> [String: String] {
667
+ var sanitized = env.filter { key, _ in
668
+ !key.uppercased().hasPrefix("DYLD_")
669
+ }
670
+
671
+ sanitized["ELIZA_PLATFORM"] = "ios"
672
+ sanitized["ELIZA_MOBILE_PLATFORM"] = "ios"
673
+
674
+ let resolvedMode = executionModeKeys
675
+ .compactMap { normalizeExecutionMode(sanitized[$0]) }
676
+ .first ?? safeLocalExecutionMode
677
+ let clampedMode = resolvedMode == "local-yolo" ? safeLocalExecutionMode : resolvedMode
678
+ for key in executionModeKeys {
679
+ sanitized[key] = clampedMode
680
+ }
681
+
682
+ sanitized["ELIZA_IOS_RUNTIME_POLICY"] = safeLocalExecutionMode
683
+ #if ELIZA_IOS_FULL_BUN_ENGINE
684
+ sanitized["ELIZA_IOS_JAVASCRIPT_ENGINE"] = "bun"
685
+ #else
686
+ sanitized["ELIZA_IOS_JAVASCRIPT_ENGINE"] = "javascriptcore"
687
+ #endif
688
+ sanitized["ELIZA_IOS_JIT"] = "0"
689
+ sanitized["ELIZA_IOS_DYNAMIC_CODE_SIGNING"] = "0"
690
+ return sanitized
691
+ }
692
+
693
+ private static func normalizeExecutionMode(_ value: String?) -> String? {
694
+ switch value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
695
+ case "cloud":
696
+ return "cloud"
697
+ case "local-safe":
698
+ return safeLocalExecutionMode
699
+ case "local-yolo":
700
+ return "local-yolo"
701
+ default:
702
+ return nil
703
+ }
704
+ }
705
+ }