@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,270 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// Implements the `fs_*` host functions from `BRIDGE_CONTRACT.md`.
5
+ ///
6
+ /// All sync calls return `null` / `false` on failure and stash the error
7
+ /// string in a thread-local field readable via `fs_last_error()`.
8
+ public final class FSBridge {
9
+ private weak var context: JSContext?
10
+ private var lastError: String?
11
+
12
+ public init() {}
13
+
14
+ public func install(into ctx: JSContext) {
15
+ self.context = ctx
16
+ let fm = FileManager.default
17
+
18
+ ctx.installBridgeFunction(name: "fs_read_text") { args in
19
+ self.clearError()
20
+ guard let path = args.first?.toString() else {
21
+ self.setError("fs_read_text: missing path")
22
+ return NSNull()
23
+ }
24
+ do {
25
+ let s = try String(contentsOfFile: path, encoding: .utf8)
26
+ return s
27
+ } catch {
28
+ self.setError("fs_read_text: \(error.localizedDescription)")
29
+ return NSNull()
30
+ }
31
+ }
32
+
33
+ ctx.installBridgeFunction(name: "fs_read_bytes") { args in
34
+ self.clearError()
35
+ guard let path = args.first?.toString() else {
36
+ self.setError("fs_read_bytes: missing path")
37
+ return NSNull()
38
+ }
39
+ do {
40
+ let data = try Data(contentsOf: URL(fileURLWithPath: path))
41
+ return ctx.newUint8Array(data)
42
+ } catch {
43
+ self.setError("fs_read_bytes: \(error.localizedDescription)")
44
+ return NSNull()
45
+ }
46
+ }
47
+
48
+ ctx.installBridgeFunction(name: "fs_write_text") { args in
49
+ self.clearError()
50
+ guard args.count >= 2,
51
+ let path = args[0].toString(),
52
+ let data = args[1].toString() else {
53
+ self.setError("fs_write_text: missing args")
54
+ return false
55
+ }
56
+ do {
57
+ try data.write(toFile: path, atomically: true, encoding: .utf8)
58
+ return true
59
+ } catch {
60
+ self.setError("fs_write_text: \(error.localizedDescription)")
61
+ return false
62
+ }
63
+ }
64
+
65
+ ctx.installBridgeFunction(name: "fs_write_bytes") { args in
66
+ self.clearError()
67
+ guard args.count >= 2,
68
+ let path = args[0].toString(),
69
+ let bytes = args[1].toData() else {
70
+ self.setError("fs_write_bytes: missing args")
71
+ return false
72
+ }
73
+ do {
74
+ try bytes.write(to: URL(fileURLWithPath: path), options: .atomic)
75
+ return true
76
+ } catch {
77
+ self.setError("fs_write_bytes: \(error.localizedDescription)")
78
+ return false
79
+ }
80
+ }
81
+
82
+ ctx.installBridgeFunction(name: "fs_append_text") { args in
83
+ self.clearError()
84
+ guard args.count >= 2,
85
+ let path = args[0].toString(),
86
+ let data = args[1].toString() else {
87
+ self.setError("fs_append_text: missing args")
88
+ return false
89
+ }
90
+ return self.appendText(path: path, text: data)
91
+ }
92
+
93
+ ctx.installBridgeFunction(name: "fs_exists") { args in
94
+ self.clearError()
95
+ guard let path = args.first?.toString() else { return false }
96
+ return fm.fileExists(atPath: path)
97
+ }
98
+
99
+ ctx.installBridgeFunction(name: "fs_mkdir") { args in
100
+ self.clearError()
101
+ guard args.count >= 2,
102
+ let path = args[0].toString() else {
103
+ self.setError("fs_mkdir: missing args")
104
+ return false
105
+ }
106
+ let recursive = args[1].toBool()
107
+ do {
108
+ try fm.createDirectory(
109
+ atPath: path,
110
+ withIntermediateDirectories: recursive,
111
+ attributes: nil
112
+ )
113
+ return true
114
+ } catch {
115
+ self.setError("fs_mkdir: \(error.localizedDescription)")
116
+ return false
117
+ }
118
+ }
119
+
120
+ ctx.installBridgeFunction(name: "fs_readdir") { args in
121
+ self.clearError()
122
+ guard let path = args.first?.toString() else {
123
+ self.setError("fs_readdir: missing path")
124
+ return NSNull()
125
+ }
126
+ do {
127
+ let entries = try fm.contentsOfDirectory(atPath: path)
128
+ return entries
129
+ } catch {
130
+ self.setError("fs_readdir: \(error.localizedDescription)")
131
+ return NSNull()
132
+ }
133
+ }
134
+
135
+ ctx.installBridgeFunction(name: "fs_stat") { args in
136
+ self.clearError()
137
+ guard let path = args.first?.toString() else {
138
+ self.setError("fs_stat: missing path")
139
+ return NSNull()
140
+ }
141
+ do {
142
+ let attrs = try fm.attributesOfItem(atPath: path)
143
+ let size = (attrs[.size] as? NSNumber)?.intValue ?? 0
144
+ let mtimeDate = attrs[.modificationDate] as? Date
145
+ let mtimeMs = (mtimeDate?.timeIntervalSince1970 ?? 0) * 1000.0
146
+ let type = attrs[.type] as? FileAttributeType
147
+ let isDir = (type == .typeDirectory)
148
+ let isFile = (type == .typeRegular)
149
+ return [
150
+ "size": size,
151
+ "mtime_ms": mtimeMs,
152
+ "is_directory": isDir,
153
+ "is_file": isFile,
154
+ ] as [String: Any]
155
+ } catch {
156
+ self.setError("fs_stat: \(error.localizedDescription)")
157
+ return NSNull()
158
+ }
159
+ }
160
+
161
+ ctx.installBridgeFunction(name: "fs_remove") { args in
162
+ self.clearError()
163
+ guard let path = args.first?.toString() else {
164
+ self.setError("fs_remove: missing path")
165
+ return false
166
+ }
167
+ // `recursive` is an opt-in flag in the contract. FileManager
168
+ // removeItem already recurses for directories, but if the caller
169
+ // explicitly passes `false` for a directory we refuse so the
170
+ // semantics match POSIX rm-vs-rm-rf.
171
+ let recursive: Bool = args.count >= 2 ? args[1].toBool() : true
172
+ var isDir: ObjCBool = false
173
+ let exists = fm.fileExists(atPath: path, isDirectory: &isDir)
174
+ if !exists {
175
+ return true
176
+ }
177
+ if isDir.boolValue && !recursive {
178
+ self.setError("fs_remove: path is a directory and recursive=false")
179
+ return false
180
+ }
181
+ do {
182
+ try fm.removeItem(atPath: path)
183
+ return true
184
+ } catch {
185
+ self.setError("fs_remove: \(error.localizedDescription)")
186
+ return false
187
+ }
188
+ }
189
+
190
+ ctx.installBridgeFunction(name: "fs_rename") { args in
191
+ self.clearError()
192
+ guard args.count >= 2,
193
+ let from = args[0].toString(),
194
+ let to = args[1].toString() else {
195
+ self.setError("fs_rename: missing args")
196
+ return false
197
+ }
198
+ do {
199
+ try fm.moveItem(atPath: from, toPath: to)
200
+ return true
201
+ } catch {
202
+ self.setError("fs_rename: \(error.localizedDescription)")
203
+ return false
204
+ }
205
+ }
206
+
207
+ ctx.installBridgeFunction(name: "fs_copy") { args in
208
+ self.clearError()
209
+ guard args.count >= 2,
210
+ let from = args[0].toString(),
211
+ let to = args[1].toString() else {
212
+ self.setError("fs_copy: missing args")
213
+ return false
214
+ }
215
+ do {
216
+ if fm.fileExists(atPath: to) {
217
+ try fm.removeItem(atPath: to)
218
+ }
219
+ try fm.copyItem(atPath: from, toPath: to)
220
+ return true
221
+ } catch {
222
+ self.setError("fs_copy: \(error.localizedDescription)")
223
+ return false
224
+ }
225
+ }
226
+
227
+ ctx.installBridgeFunction(name: "fs_last_error") { _ in
228
+ return self.lastError ?? NSNull()
229
+ }
230
+ }
231
+
232
+ // MARK: - Internals
233
+
234
+ private func setError(_ message: String) {
235
+ self.lastError = message
236
+ }
237
+
238
+ private func clearError() {
239
+ self.lastError = nil
240
+ }
241
+
242
+ /// Appends UTF-8 text to a file, creating it if missing. Returns false
243
+ /// on failure and stashes a message in `lastError`.
244
+ private func appendText(path: String, text: String) -> Bool {
245
+ let fm = FileManager.default
246
+ if !fm.fileExists(atPath: path) {
247
+ do {
248
+ try text.write(toFile: path, atomically: true, encoding: .utf8)
249
+ return true
250
+ } catch {
251
+ setError("fs_append_text: \(error.localizedDescription)")
252
+ return false
253
+ }
254
+ }
255
+ guard let handle = FileHandle(forWritingAtPath: path),
256
+ let bytes = text.data(using: .utf8) else {
257
+ setError("fs_append_text: could not open file for writing")
258
+ return false
259
+ }
260
+ defer { try? handle.close() }
261
+ do {
262
+ try handle.seekToEnd()
263
+ try handle.write(contentsOf: bytes)
264
+ return true
265
+ } catch {
266
+ setError("fs_append_text: \(error.localizedDescription)")
267
+ return false
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,153 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// Implements `http_fetch` from `BRIDGE_CONTRACT.md`.
5
+ ///
6
+ /// Returns a JS Promise via the constructor pattern: the bridge captures the
7
+ /// resolve callback as a `ManagedCallback`, runs the URLSession request off
8
+ /// the JSContext queue, then re-enters the JSContext queue to fulfill the
9
+ /// promise.
10
+ public final class HTTPBridge {
11
+ private weak var context: JSContext?
12
+ private let urlSession: URLSession
13
+
14
+ public init(session: URLSession = .shared) {
15
+ self.urlSession = session
16
+ }
17
+
18
+ public func install(into ctx: JSContext) {
19
+ self.context = ctx
20
+
21
+ ctx.installBridgeFunction(name: "http_fetch") { args in
22
+ guard let ctx = self.context else { return NSNull() }
23
+ guard let opts = args.first, opts.isObject else {
24
+ return Self.rejectedPromise(in: ctx, error: "http_fetch: missing options")
25
+ }
26
+
27
+ let url = opts.objectForKeyedSubscript("url")?.toString() ?? ""
28
+ let method = (opts.objectForKeyedSubscript("method")?.toString() ?? "GET").uppercased()
29
+ let headers = opts.objectForKeyedSubscript("headers")?.toStringMap() ?? [:]
30
+ let bodyValue = opts.objectForKeyedSubscript("body")
31
+ let body: Data? = (bodyValue?.isNullish == false) ? bodyValue?.toData() : nil
32
+ let timeoutMs = opts.objectForKeyedSubscript("timeout_ms")?.toNumber()?.doubleValue
33
+
34
+ guard let target = URL(string: url) else {
35
+ return Self.rejectedPromise(in: ctx, error: "http_fetch: invalid url")
36
+ }
37
+ guard target.scheme == "http" || target.scheme == "https" else {
38
+ return Self.rejectedPromise(in: ctx, error: "http_fetch: unsupported URL scheme")
39
+ }
40
+ if Self.isLocalOrPrivateNetwork(target) {
41
+ return Self.rejectedPromise(in: ctx, error: "http_fetch: loopback/private URLs must use path-only http_request IPC")
42
+ }
43
+
44
+ var request = URLRequest(url: target)
45
+ request.httpMethod = method
46
+ for (k, v) in headers {
47
+ request.setValue(v, forHTTPHeaderField: k)
48
+ }
49
+ if let body = body, method != "GET", method != "HEAD" {
50
+ request.httpBody = body
51
+ }
52
+ if let ms = timeoutMs, ms > 0 {
53
+ request.timeoutInterval = ms / 1000.0
54
+ }
55
+
56
+ return self.runFetch(ctx: ctx, request: request)
57
+ }
58
+ }
59
+
60
+ // MARK: - Internals
61
+
62
+ private func runFetch(ctx: JSContext, request: URLRequest) -> Any? {
63
+ // Build the promise factory once.
64
+ let promiseScript = """
65
+ (function(){
66
+ let resolveFn;
67
+ const p = new Promise(function(res){ resolveFn = res; });
68
+ p.__eliza_resolve = resolveFn;
69
+ return p;
70
+ })
71
+ """
72
+ guard let promise = ctx.evaluateScript(promiseScript)?.call(withArguments: []) else {
73
+ return Self.rejectedPromise(in: ctx, error: "http_fetch: failed to construct promise")
74
+ }
75
+ let resolveValue = promise.forProperty("__eliza_resolve")
76
+ let managedResolve = resolveValue.flatMap { ManagedCallback(value: $0) }
77
+
78
+ let task = urlSession.dataTask(with: request) { data, response, error in
79
+ let result = Self.buildResultDict(data: data, response: response, error: error, ctx: ctx)
80
+ // Hop back onto the JS queue.
81
+ RuntimeQueue.dispatchOnJS {
82
+ guard let resolve = managedResolve else { return }
83
+ resolve.callSync(args: [result])
84
+ }
85
+ }
86
+ task.resume()
87
+
88
+ return promise
89
+ }
90
+
91
+ private static func isLocalOrPrivateNetwork(_ url: URL) -> Bool {
92
+ guard let host = url.host?.lowercased() else { return false }
93
+ return host == "localhost" ||
94
+ host == "127.0.0.1" ||
95
+ host == "0.0.0.0" ||
96
+ host == "::1" ||
97
+ host.hasPrefix("127.") ||
98
+ host.hasPrefix("10.") ||
99
+ host.hasPrefix("192.168.") ||
100
+ host.range(of: #"^172\.(1[6-9]|2[0-9]|3[0-1])\."#, options: .regularExpression) != nil ||
101
+ host.range(of: #"^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\."#, options: .regularExpression) != nil ||
102
+ host.hasPrefix("169.254.") ||
103
+ (host.contains(":") &&
104
+ (host.hasPrefix("fe80:") ||
105
+ host.hasPrefix("fc") ||
106
+ host.hasPrefix("fd"))) ||
107
+ host == "local" ||
108
+ host == "internal" ||
109
+ host == "lan" ||
110
+ host == "ts.net" ||
111
+ host.hasSuffix(".local") ||
112
+ host.hasSuffix(".internal") ||
113
+ host.hasSuffix(".lan") ||
114
+ host.hasSuffix(".ts.net")
115
+ }
116
+
117
+ private static func buildResultDict(data: Data?, response: URLResponse?, error: Error?, ctx: JSContext) -> [String: Any] {
118
+ if let error = error {
119
+ return [
120
+ "status": 0,
121
+ "headers": [:] as [String: String],
122
+ "body": ctx.newUint8Array(Data()),
123
+ "error": error.localizedDescription,
124
+ ]
125
+ }
126
+ guard let http = response as? HTTPURLResponse else {
127
+ return [
128
+ "status": 0,
129
+ "headers": [:] as [String: String],
130
+ "body": ctx.newUint8Array(Data()),
131
+ "error": "Non-HTTP response",
132
+ ]
133
+ }
134
+ var headerDict: [String: String] = [:]
135
+ for (k, v) in http.allHeaderFields {
136
+ guard let key = k as? String else { continue }
137
+ headerDict[key.lowercased()] = String(describing: v)
138
+ }
139
+ let payload = data ?? Data()
140
+ return [
141
+ "status": http.statusCode,
142
+ "headers": headerDict,
143
+ "body": ctx.newUint8Array(payload),
144
+ ]
145
+ }
146
+
147
+ private static func rejectedPromise(in ctx: JSContext, error: String) -> Any? {
148
+ // Promise.resolve({ error }) per contract — async errors are returned,
149
+ // not thrown.
150
+ let script = "(function(msg){return Promise.resolve({status:0,headers:{},body:new Uint8Array(),error:msg});})"
151
+ return ctx.evaluateScript(script)?.call(withArguments: [error])
152
+ }
153
+ }
@@ -0,0 +1,32 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// Compatibility registration for the old `http_serve_*` bridge surface.
5
+ ///
6
+ /// iOS local mode must route foreground and backend calls through Capacitor /
7
+ /// engine IPC (`ElizaBunRuntime.call("http_request", ...)`) instead of opening
8
+ /// a localhost listener inside the app. Keeping these symbols registered gives
9
+ /// older JSContext bundles a deterministic error without ever binding a port.
10
+ public final class HTTPServerBridge {
11
+ public init() {}
12
+
13
+ public func install(into ctx: JSContext) {
14
+ ctx.installBridgeFunction(name: "http_serve_start") { _ in
15
+ [
16
+ "ok": false,
17
+ "port": 0,
18
+ "error": "http_serve_start is disabled on iOS; use ElizaBunRuntime.call(http_request) IPC",
19
+ ]
20
+ }
21
+
22
+ ctx.installBridgeFunction(name: "http_serve_register_handler") { _ in
23
+ NSNull()
24
+ }
25
+
26
+ ctx.installBridgeFunction(name: "http_serve_stop") { _ in
27
+ NSNull()
28
+ }
29
+ }
30
+
31
+ public func shutdown() {}
32
+ }
@@ -0,0 +1,233 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ #if canImport(LlamaCppCapacitor)
5
+ import LlamaCppCapacitor
6
+ #endif
7
+
8
+ /// Implements `llama_*` from `BRIDGE_CONTRACT.md`.
9
+ ///
10
+ /// `LlamaBridgeImpl` is JSContext-agnostic by design: this file owns JSValue
11
+ /// parsing, promise wiring, and ManagedCallback streaming; the impl owns the
12
+ /// llama.cpp C API calls. Bridge failures resolve as `{ error }` values
13
+ /// because the JS polyfill layer treats bridge results as native response
14
+ /// payloads rather than exception channels.
15
+ public final class LlamaBridge {
16
+ private weak var context: JSContext?
17
+ private var nextContextId: Int = 1
18
+ private var contexts: [Int: LlamaContextState] = [:]
19
+ private var streamCallbacks: [String: ManagedCallback] = [:]
20
+ private let inferenceQueue = DispatchQueue(label: "ai.eliza.bun.runtime.llama", qos: .userInitiated)
21
+
22
+ public init() {}
23
+
24
+ public func install(into ctx: JSContext) {
25
+ self.context = ctx
26
+
27
+ ctx.installBridgeFunction(name: "llama_load_model") { args in
28
+ guard let ctx = self.context else { return NSNull() }
29
+ return self.loadModel(args: args, ctx: ctx)
30
+ }
31
+
32
+ ctx.installBridgeFunction(name: "llama_generate") { args in
33
+ guard let ctx = self.context else { return NSNull() }
34
+ return self.generate(args: args, ctx: ctx)
35
+ }
36
+
37
+ ctx.installBridgeFunction(name: "llama_register_stream_callback") { args in
38
+ guard args.count >= 2,
39
+ let token = args[0].toString() else { return NSNull() }
40
+ let handlerValue = args[1]
41
+ if let mc = ManagedCallback(value: handlerValue) {
42
+ self.streamCallbacks[token] = mc
43
+ }
44
+ return NSNull()
45
+ }
46
+
47
+ ctx.installBridgeFunction(name: "llama_cancel") { args in
48
+ guard let id = args.first?.toNumber()?.intValue else { return NSNull() }
49
+ if var state = self.contexts[id] {
50
+ state.cancelled = true
51
+ self.contexts[id] = state
52
+ }
53
+ LlamaBridgeImpl.shared.cancel(contextId: Int64(id))
54
+ return NSNull()
55
+ }
56
+
57
+ ctx.installBridgeFunction(name: "llama_free") { args in
58
+ guard let id = args.first?.toNumber()?.intValue else { return NSNull() }
59
+ self.contexts.removeValue(forKey: id)
60
+ LlamaBridgeImpl.shared.free(contextId: Int64(id))
61
+ return NSNull()
62
+ }
63
+
64
+ ctx.installBridgeFunction(name: "llama_hardware_info") { _ in
65
+ return self.hardwareInfo()
66
+ }
67
+ }
68
+
69
+ // MARK: - Context state
70
+
71
+ private struct LlamaContextState {
72
+ let id: Int
73
+ let modelPath: String
74
+ var contextSize: Int
75
+ var useGpu: Bool
76
+ var threads: Int
77
+ var cancelled: Bool
78
+ }
79
+
80
+ // MARK: - Implementations
81
+
82
+ private func loadModel(args: [JSValue], ctx: JSContext) -> Any? {
83
+ guard let opts = args.first, opts.isObject else {
84
+ return Self.rejectedAsync(in: ctx, error: "llama_load_model: missing options")
85
+ }
86
+ let path = opts.objectForKeyedSubscript("path")?.toString() ?? ""
87
+ if path.isEmpty {
88
+ return Self.rejectedAsync(in: ctx, error: "llama_load_model: missing path")
89
+ }
90
+ let contextSize = opts.objectForKeyedSubscript("context_size")?.toNumber()?.intValue ?? 4096
91
+ let useGpu = opts.objectForKeyedSubscript("use_gpu")?.toBool() ?? true
92
+ let threads = opts.objectForKeyedSubscript("threads")?.toNumber()?.intValue
93
+ ?? min(4, ProcessInfo.processInfo.activeProcessorCount)
94
+
95
+ // Build the promise + resolver pair on the JS side.
96
+ let (promise, resolver) = Self.makeAsyncPromise(in: ctx)
97
+ let managedResolve = resolver.flatMap { ManagedCallback(value: $0) }
98
+
99
+ inferenceQueue.async { [weak self] in
100
+ guard let self = self else { return }
101
+
102
+ if !FileManager.default.fileExists(atPath: path) {
103
+ RuntimeQueue.dispatchOnJS {
104
+ managedResolve?.callSync(args: [["error": "model file not found: \(path)"]])
105
+ }
106
+ return
107
+ }
108
+
109
+ let result = LlamaBridgeImpl.shared.loadModel(
110
+ path: path,
111
+ contextSize: UInt32(max(1, contextSize)),
112
+ useGPU: useGpu,
113
+ threads: Int32(max(1, threads))
114
+ )
115
+ if let error = result.error {
116
+ RuntimeQueue.dispatchOnJS {
117
+ managedResolve?.callSync(args: [["error": error]])
118
+ }
119
+ return
120
+ }
121
+ guard let contextId = result.contextId else {
122
+ RuntimeQueue.dispatchOnJS {
123
+ managedResolve?.callSync(args: [["error": "llama_load_model: backend returned no context_id"]])
124
+ }
125
+ return
126
+ }
127
+
128
+ let id = Int(contextId)
129
+ self.nextContextId = max(self.nextContextId, id + 1)
130
+ self.contexts[id] = LlamaContextState(
131
+ id: id,
132
+ modelPath: path,
133
+ contextSize: contextSize,
134
+ useGpu: useGpu,
135
+ threads: threads,
136
+ cancelled: false
137
+ )
138
+
139
+ RuntimeQueue.dispatchOnJS {
140
+ managedResolve?.callSync(args: [["context_id": id]])
141
+ }
142
+ }
143
+
144
+ return promise
145
+ }
146
+
147
+ private func generate(args: [JSValue], ctx: JSContext) -> Any? {
148
+ guard let opts = args.first, opts.isObject else {
149
+ return Self.rejectedAsync(in: ctx, error: "llama_generate: missing options")
150
+ }
151
+ let contextId = opts.objectForKeyedSubscript("context_id")?.toNumber()?.intValue ?? -1
152
+ let prompt = opts.objectForKeyedSubscript("prompt")?.toString() ?? ""
153
+ let maxTokens = opts.objectForKeyedSubscript("max_tokens")?.toNumber()?.intValue ?? 256
154
+ let temperature = opts.objectForKeyedSubscript("temperature")?.toNumber()?.doubleValue ?? 0.7
155
+ let topP = opts.objectForKeyedSubscript("top_p")?.toNumber()?.doubleValue ?? 0.95
156
+ let stop = opts.objectForKeyedSubscript("stop")?.toStringArray() ?? []
157
+ let streamToken = opts.objectForKeyedSubscript("stream_callback_token")?.toString()
158
+
159
+ guard let state = contexts[contextId] else {
160
+ return Self.rejectedAsync(in: ctx, error: "llama_generate: unknown context_id \(contextId)")
161
+ }
162
+
163
+ let (promise, resolver) = Self.makeAsyncPromise(in: ctx)
164
+ let managedResolve = resolver.flatMap { ManagedCallback(value: $0) }
165
+ let streamCallback = streamToken.flatMap { self.streamCallbacks[$0] }
166
+
167
+ let queue = LlamaBridgeImpl.shared.workQueue(for: Int64(contextId)) ?? inferenceQueue
168
+ queue.async {
169
+ let started = Date()
170
+ let result = LlamaBridgeImpl.shared.generate(
171
+ contextId: Int64(state.id),
172
+ prompt: prompt,
173
+ maxTokens: Int32(max(1, maxTokens)),
174
+ temperature: Float(temperature),
175
+ topP: Float(topP),
176
+ stopSequences: stop,
177
+ onToken: { token, isLast in
178
+ guard let cb = streamCallback else { return }
179
+ RuntimeQueue.dispatchOnJS {
180
+ cb.callSync(args: [token, isLast])
181
+ }
182
+ }
183
+ )
184
+
185
+ let durationMs = Int(Date().timeIntervalSince(started) * 1000)
186
+ let promptTokens = max(1, result.promptTokens)
187
+ let outputTokens = max(1, result.outputTokens)
188
+
189
+ RuntimeQueue.dispatchOnJS {
190
+ if let error = result.error {
191
+ managedResolve?.callSync(args: [["error": error]])
192
+ return
193
+ }
194
+ managedResolve?.callSync(args: [[
195
+ "text": result.text,
196
+ "prompt_tokens": promptTokens,
197
+ "output_tokens": outputTokens,
198
+ "duration_ms": Int(result.durationMs > 0 ? result.durationMs : Double(durationMs)),
199
+ ]])
200
+ }
201
+ }
202
+
203
+ return promise
204
+ }
205
+
206
+ private func hardwareInfo() -> [String: Any] {
207
+ return LlamaBridgeImpl.shared.hardwareInfo().asDict()
208
+ }
209
+
210
+ // MARK: - Promise builders
211
+
212
+ /// Returns `(promise, resolver)`. The resolver is a JS function value.
213
+ static func makeAsyncPromise(in ctx: JSContext) -> (Any, JSValue?) {
214
+ let script = """
215
+ (function(){
216
+ let resolveFn;
217
+ const p = new Promise(function(res){ resolveFn = res; });
218
+ p.__eliza_resolve = resolveFn;
219
+ return p;
220
+ })
221
+ """
222
+ guard let promise = ctx.evaluateScript(script)?.call(withArguments: []) else {
223
+ return (NSNull(), nil)
224
+ }
225
+ let resolver = promise.forProperty("__eliza_resolve")
226
+ return (promise, resolver)
227
+ }
228
+
229
+ static func rejectedAsync(in ctx: JSContext, error: String) -> Any? {
230
+ let script = "(function(msg){return Promise.resolve({error:msg});})"
231
+ return ctx.evaluateScript(script)?.call(withArguments: [error])
232
+ }
233
+ }