@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.
- package/ElizaosCapacitorBunRuntime.podspec +54 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/esm/definitions.d.ts +136 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +19 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +44 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +63 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +66 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
- package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
- package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
- package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
- package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
- package/package.json +68 -0
- package/src/bridge-contract.test.ts +127 -0
- package/src/definitions.d.ts +136 -0
- package/src/definitions.d.ts.map +1 -0
- package/src/definitions.ts +152 -0
- package/src/index.d.ts +9 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +16 -0
- package/src/web.d.ts +19 -0
- package/src/web.d.ts.map +1 -0
- 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
|
+
}
|