@swiftbrowser/native 0.18.2 → 0.19.0

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 (73) hide show
  1. package/README.md +19 -0
  2. package/bin/swiftbrowser-native.mjs +47 -0
  3. package/dist/bridge.d.ts +34 -7
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js.map +1 -1
  6. package/dist/encoding.d.ts +13 -0
  7. package/dist/encoding.d.ts.map +1 -1
  8. package/dist/encoding.js +24 -0
  9. package/dist/encoding.js.map +1 -1
  10. package/dist/fetch.d.ts +10 -4
  11. package/dist/fetch.d.ts.map +1 -1
  12. package/dist/fetch.js +19 -11
  13. package/dist/fetch.js.map +1 -1
  14. package/dist/hostRequests.d.ts +22 -6
  15. package/dist/hostRequests.d.ts.map +1 -1
  16. package/dist/hostRequests.js +65 -13
  17. package/dist/hostRequests.js.map +1 -1
  18. package/dist/ios.d.ts +79 -0
  19. package/dist/ios.d.ts.map +1 -0
  20. package/dist/ios.js +181 -0
  21. package/dist/ios.js.map +1 -0
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +16 -7
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/subtle.d.ts +1 -1
  26. package/dist/subtle.d.ts.map +1 -1
  27. package/dist/subtle.js +9 -5
  28. package/dist/subtle.js.map +1 -1
  29. package/dist/webSocket.d.ts +12 -5
  30. package/dist/webSocket.d.ts.map +1 -1
  31. package/dist/webSocket.js +24 -10
  32. package/dist/webSocket.js.map +1 -1
  33. package/package.json +7 -3
  34. package/src/bridge.ts +30 -7
  35. package/src/encoding.ts +22 -0
  36. package/src/fetch.ts +18 -13
  37. package/src/hostRequests.ts +72 -19
  38. package/src/ios.ts +253 -0
  39. package/src/runtime.ts +23 -8
  40. package/src/subtle.ts +10 -6
  41. package/src/webSocket.ts +27 -12
  42. package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
  43. package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
  44. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +203 -0
  45. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +523 -0
  46. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +188 -0
  47. package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
  48. package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
  49. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
  50. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
  51. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
  52. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
  53. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +3 -3
  54. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +106 -28
  55. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +43 -13
  56. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
  57. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
  58. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
  59. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
  60. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
  61. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
  62. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
  63. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
  64. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
  65. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
  66. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +81 -20
  67. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
  68. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +100 -0
  69. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
  70. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +4 -0
  71. package/swift/Sources/SwiftBrowserProtocol/Props.swift +26 -0
  72. package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
  73. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -25,8 +25,22 @@ public final class JavaScriptCoreHost: JSHost {
25
25
  installExceptionHandler()
26
26
  }
27
27
 
28
+ /// JavaScriptCore keeps a process-wide registry of its virtual machines
29
+ /// under a lock of its own, and making one on a thread while another is
30
+ /// being made or dropped elsewhere has crashed in that lock (`VM::VM` →
31
+ /// `unlockSlow`, "Invalid value for lock", the iOS 26.2 simulator under
32
+ /// the test suites running in parallel, each with contexts and workers
33
+ /// of its own). So every virtual machine this package makes or drops is
34
+ /// made or dropped one at a time, under this lock: the app's context
35
+ /// here and each worker's (WorkerHost.swift).
36
+ nonisolated private static let virtualMachineLock = NSLock()
37
+
38
+ nonisolated static func withVirtualMachineLock<T>(_ body: () throws -> T) rethrows -> T {
39
+ try virtualMachineLock.withLock(body)
40
+ }
41
+
28
42
  private static func makeContext() -> (JSContext, JSValue) {
29
- let context = JSContext()!
43
+ let context = withVirtualMachineLock { JSContext()! }
30
44
  context.name = "SwiftBrowser"
31
45
  let native = JSValue(newObjectIn: context)!
32
46
  context.setObject(native, forKeyedSubscript: "__sbNative" as NSString)
@@ -46,8 +60,10 @@ public final class JavaScriptCoreHost: JSHost {
46
60
 
47
61
  public func reset() {
48
62
  context.exceptionHandler = nil
49
- (context, native) = JavaScriptCoreHost.makeContext()
50
63
  pendingError = nil
64
+ let fresh = JavaScriptCoreHost.makeContext()
65
+ // The old context (and its virtual machine) goes under the lock too.
66
+ JavaScriptCoreHost.withVirtualMachineLock { (context, native) = fresh }
51
67
  installExceptionHandler()
52
68
  }
53
69
 
@@ -84,6 +100,42 @@ public final class JavaScriptCoreHost: JSHost {
84
100
  return result.map(JavaScriptCoreHost.json(from:)) ?? .null
85
101
  }
86
102
 
103
+ public var carriesBytes: Bool { true }
104
+
105
+ public func installBytes(_ name: String, _ body: @escaping ([HostValue]) -> HostValue?) {
106
+ let block: @convention(block) () -> Any? = {
107
+ let arguments = (JSContext.currentArguments() as? [JSValue]) ?? []
108
+ switch body(arguments.map(JavaScriptCoreHost.hostValue(from:))) {
109
+ case .json(let json)?: return json.anyObject
110
+ case .bytes(let bytes)?:
111
+ guard let context = JSContext.current() else { return nil }
112
+ return JavaScriptCoreHost.typedArray(bytes, in: context)
113
+ case nil: return nil
114
+ }
115
+ }
116
+ native.setObject(unsafeBitCast(block, to: AnyObject.self), forKeyedSubscript: name as NSString)
117
+ }
118
+
119
+ @discardableResult
120
+ public func call(_ function: String, values: [HostValue]) throws -> JSONValue {
121
+ guard let callee = context.objectForKeyedSubscript(function), !callee.isUndefined else {
122
+ throw JSError(message: "\(function) is not defined")
123
+ }
124
+ pendingError = nil
125
+ let arguments: [Any] = values.map { value in
126
+ switch value {
127
+ case .json(let json): return json.anyObject
128
+ case .bytes(let bytes): return JavaScriptCoreHost.typedArray(bytes, in: context)
129
+ }
130
+ }
131
+ let result = callee.call(withArguments: arguments)
132
+ if let error = pendingError {
133
+ pendingError = nil
134
+ throw error
135
+ }
136
+ return result.map(JavaScriptCoreHost.json(from:)) ?? .null
137
+ }
138
+
87
139
  public func hasGlobal(_ name: String) -> Bool {
88
140
  guard let value = context.objectForKeyedSubscript(name) else { return false }
89
141
  return !value.isUndefined
@@ -99,5 +151,92 @@ public final class JavaScriptCoreHost: JSHost {
99
151
  if value.isNumber { return .number(value.toDouble()) }
100
152
  return JSONValue(any: value.toObject())
101
153
  }
154
+
155
+ // MARK: Bytes
156
+
157
+ /// A JavaScript value as a `HostValue`: a typed array's or an
158
+ /// `ArrayBuffer`'s bytes copied out (docs/notes/bytes.md), else JSON. The
159
+ /// typed-array check comes first: `toObject` would turn a view into a
160
+ /// dictionary of its indices, slowly. The pointer the engine gives is
161
+ /// read inside this call, while no JavaScript runs.
162
+ nonisolated static func hostValue(from value: JSValue) -> HostValue {
163
+ guard !value.isUndefined, !value.isNull, value.isObject, let context = value.context?.jsGlobalContextRef else { return .json(json(from: value)) }
164
+ var exception: JSValueRef?
165
+ let type = JSValueGetTypedArrayType(context, value.jsValueRef, &exception)
166
+ guard type != kJSTypedArrayTypeNone, let kind = bytesKind(type), let object = JSValueToObject(context, value.jsValueRef, &exception) else {
167
+ return .json(json(from: value))
168
+ }
169
+ if kind == .arrayBuffer {
170
+ let length = JSObjectGetArrayBufferByteLength(context, object, &exception)
171
+ guard length > 0, let pointer = JSObjectGetArrayBufferBytesPtr(context, object, &exception) else { return .bytes(HostBytes(Data(), kind: kind)) }
172
+ return .bytes(HostBytes(Data(bytes: pointer, count: length), kind: kind))
173
+ }
174
+ // The pointer is the backing buffer's start; the view begins at its offset.
175
+ let length = JSObjectGetTypedArrayByteLength(context, object, &exception)
176
+ let offset = JSObjectGetTypedArrayByteOffset(context, object, &exception)
177
+ guard length > 0, let pointer = JSObjectGetTypedArrayBytesPtr(context, object, &exception) else { return .bytes(HostBytes(Data(), kind: kind)) }
178
+ return .bytes(HostBytes(Data(bytes: pointer + offset, count: length), kind: kind))
179
+ }
180
+
181
+ /// A typed array (or `ArrayBuffer`) the engine allocates, the bytes copied in.
182
+ nonisolated static func typedArray(_ bytes: HostBytes, in context: JSContext) -> JSValue {
183
+ let global = context.jsGlobalContextRef
184
+ let size = elementSize(bytes.kind)
185
+ let count = bytes.data.count / size
186
+ var exception: JSValueRef?
187
+ let type = bytes.kind == .arrayBuffer ? kJSTypedArrayTypeUint8Array : typedArrayType(bytes.kind)
188
+ guard let array = JSObjectMakeTypedArray(global, type, count, &exception) else { return JSValue(nullIn: context) }
189
+ if count > 0, let pointer = JSObjectGetTypedArrayBytesPtr(global, array, &exception) {
190
+ bytes.data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: count * size)
191
+ }
192
+ if bytes.kind == .arrayBuffer, let buffer = JSObjectGetTypedArrayBuffer(global, array, &exception) {
193
+ return JSValue(jsValueRef: buffer, in: context)
194
+ }
195
+ return JSValue(jsValueRef: array, in: context)
196
+ }
197
+
198
+ private nonisolated static func elementSize(_ kind: BytesKind) -> Int {
199
+ switch kind {
200
+ case .int8, .uint8, .uint8Clamped, .arrayBuffer: return 1
201
+ case .int16, .uint16: return 2
202
+ case .int32, .uint32, .float32: return 4
203
+ case .float64, .bigInt64, .bigUint64: return 8
204
+ }
205
+ }
206
+
207
+ private nonisolated static func bytesKind(_ type: JSTypedArrayType) -> BytesKind? {
208
+ switch type {
209
+ case kJSTypedArrayTypeInt8Array: return .int8
210
+ case kJSTypedArrayTypeUint8Array: return .uint8
211
+ case kJSTypedArrayTypeUint8ClampedArray: return .uint8Clamped
212
+ case kJSTypedArrayTypeInt16Array: return .int16
213
+ case kJSTypedArrayTypeUint16Array: return .uint16
214
+ case kJSTypedArrayTypeInt32Array: return .int32
215
+ case kJSTypedArrayTypeUint32Array: return .uint32
216
+ case kJSTypedArrayTypeFloat32Array: return .float32
217
+ case kJSTypedArrayTypeFloat64Array: return .float64
218
+ case kJSTypedArrayTypeBigInt64Array: return .bigInt64
219
+ case kJSTypedArrayTypeBigUint64Array: return .bigUint64
220
+ case kJSTypedArrayTypeArrayBuffer: return .arrayBuffer
221
+ default: return nil
222
+ }
223
+ }
224
+
225
+ private nonisolated static func typedArrayType(_ kind: BytesKind) -> JSTypedArrayType {
226
+ switch kind {
227
+ case .int8: return kJSTypedArrayTypeInt8Array
228
+ case .uint8: return kJSTypedArrayTypeUint8Array
229
+ case .uint8Clamped: return kJSTypedArrayTypeUint8ClampedArray
230
+ case .int16: return kJSTypedArrayTypeInt16Array
231
+ case .uint16: return kJSTypedArrayTypeUint16Array
232
+ case .int32: return kJSTypedArrayTypeInt32Array
233
+ case .uint32: return kJSTypedArrayTypeUint32Array
234
+ case .float32: return kJSTypedArrayTypeFloat32Array
235
+ case .float64: return kJSTypedArrayTypeFloat64Array
236
+ case .bigInt64: return kJSTypedArrayTypeBigInt64Array
237
+ case .bigUint64: return kJSTypedArrayTypeBigUint64Array
238
+ case .arrayBuffer: return kJSTypedArrayTypeArrayBuffer
239
+ }
240
+ }
102
241
  }
103
242
  #endif
@@ -0,0 +1,511 @@
1
+ #if canImport(SwiftUI) && !canImport(FoundationBridge)
2
+ import Foundation
3
+ import SwiftBrowserProtocol
4
+ #if canImport(FoundationModels)
5
+ import FoundationModels
6
+ #endif
7
+
8
+ /// Foundation Models for a JavaScript app (`LanguageModelSession` in
9
+ /// @swiftbrowser/react; docs/ops-protocol.md, "The on-device language
10
+ /// model"): `languageModel` requests, answered by Apple Intelligence's
11
+ /// on-device model where it can answer (iOS 26 and macOS 26), else by the
12
+ /// app's `LanguageModels.fallback` (LanguageModelProvider.swift), else not at
13
+ /// all. The commands are the page's (Web/src/languageModel.ts):
14
+ ///
15
+ /// - `availability` → `{ status: "available" }` or `{ status: "unavailable",
16
+ /// reason }`, the model that would answer, with `download: { size?,
17
+ /// downloaded, progress?, failure? }` where it is a model the app fetches;
18
+ /// and `models`, the choices (Apple Intelligence where it answers, then the
19
+ /// fallback's), each `{ id, name, summary?, selected, download? }`;
20
+ /// - `select` makes the choice `id` answer from the next session on
21
+ /// (remembered across launches);
22
+ /// - `download` starts fetching the app's model, `deleteDownload` removes it;
23
+ /// - `open` makes the app's session `session` with its `instructions` and
24
+ /// `tools` (`[{ name, description, parameters }]`, the arguments a JSON
25
+ /// schema) on that model, `prewarm` loads it, `close` lets it go;
26
+ /// - `toolCall` answers the session's next call of one of its tools, once
27
+ /// the model makes one, as `{ call, name, arguments }` (`{ closed: true }`
28
+ /// once the session is closed); the app runs it and answers with
29
+ /// `toolResult` (`call`, and `output` or `error`), which the model reads;
30
+ /// - `respond` → `{ content }`, the whole response;
31
+ /// - `stream` starts a response as stream `stream`; each `next` answers
32
+ /// `{ content, done }` once the response has grown or ended, the latest
33
+ /// snapshot (a reader slower than the model skips what it missed);
34
+ /// `cancel` stops it.
35
+ ///
36
+ /// A failure rejects as a tagged message (`context: …`, `guardrail: …`),
37
+ /// which the app reads back as its `GenerationError` case.
38
+ @MainActor
39
+ final class LanguageModelBridge {
40
+ /// A response under way, read by `next`.
41
+ @MainActor
42
+ private final class Stream {
43
+ var content = ""
44
+ var done = false
45
+ var error: Error?
46
+ /// Whether the app has had the content as it is now.
47
+ var delivered = true
48
+ var waiter: CheckedContinuation<Void, Never>?
49
+ var task: Task<Void, Never>?
50
+
51
+ func wake() {
52
+ waiter?.resume()
53
+ waiter = nil
54
+ }
55
+ }
56
+
57
+ private var sessions: [Int: any LanguageModelProviderSession] = [:]
58
+ /// Each session's tool calls, waiting for the app.
59
+ private var relays: [Int: ToolRelay] = [:]
60
+ private var streams: [Int: Stream] = [:]
61
+ /// Apple Intelligence's model, once asked for (iOS 26 and macOS 26).
62
+ private var system: (any LanguageModelProvider)?
63
+ /// The viewer's choice: `system`, `fallback`, or nil for the default
64
+ /// (Apple Intelligence where it answers, else the app's model).
65
+ private static let choiceKey = "SwiftBrowserNative.languageModelChoice"
66
+ static let systemChoice = "system"
67
+
68
+ func handle(_ params: JSONObject) async throws -> JSONValue {
69
+ switch params["action"]?.string {
70
+ case "availability":
71
+ let current = await choice()
72
+ var answer = Self.json(current.availability, download: current.provider?.downloadState)
73
+ let models = await modelsJSON(current)
74
+ if !models.isEmpty, case .object(var object) = answer {
75
+ object["models"] = .array(models)
76
+ answer = .object(object)
77
+ }
78
+ return answer
79
+ case "select":
80
+ guard let id = params["id"]?.string else { throw HostRequestFailure("languageModel: no id") }
81
+ if id == Self.systemChoice {
82
+ UserDefaults.standard.set(Self.systemChoice, forKey: Self.choiceKey)
83
+ } else {
84
+ UserDefaults.standard.set("fallback", forKey: Self.choiceKey)
85
+ LanguageModels.fallback?.select(id)
86
+ }
87
+ return .object([:])
88
+ case "download":
89
+ LanguageModels.fallback?.startDownload()
90
+ return .object([:])
91
+ case "deleteDownload":
92
+ try await LanguageModels.fallback?.deleteDownload()
93
+ return .object([:])
94
+ case "open":
95
+ let id = try Self.int(params, "session")
96
+ let current = await choice()
97
+ guard let provider = current.provider, current.availability == .available else {
98
+ throw LanguageModelError(.unavailable, "the on-device language model is not available")
99
+ }
100
+ let relay = ToolRelay()
101
+ let tools = (params["tools"]?.array ?? []).compactMap { tool -> LanguageModelTool? in
102
+ guard let object = tool.object, let name = object["name"]?.string else { return nil }
103
+ return LanguageModelTool(
104
+ name: name,
105
+ description: object["description"]?.string ?? "",
106
+ parameters: object["parameters"] ?? .object(["type": .string("object"), "properties": .object([:])]),
107
+ call: { arguments in try await relay.call(name: name, arguments: arguments) }
108
+ )
109
+ }
110
+ sessions[id] = try provider.makeSession(instructions: params["instructions"]?.string, tools: tools)
111
+ relays[id] = relay
112
+ return .object([:])
113
+ case "toolCall":
114
+ let id = try Self.int(params, "session")
115
+ guard let relay = relays[id] else { return .object(["closed": .bool(true)]) }
116
+ return await relay.next()
117
+ case "toolResult":
118
+ let id = try Self.int(params, "session")
119
+ relays[id]?.answer(try Self.int(params, "call"), output: params["output"]?.string, error: params["error"]?.string)
120
+ return .object([:])
121
+ case "prewarm":
122
+ try session(params).prewarm()
123
+ return .object([:])
124
+ case "respond":
125
+ let response = try session(params).streamResponse(to: params["prompt"]?.string ?? "", options: Self.options(params["options"]))
126
+ var content = ""
127
+ for try await snapshot in response {
128
+ content = snapshot
129
+ }
130
+ return .object(["content": .string(content)])
131
+ case "stream":
132
+ let response = try session(params).streamResponse(to: params["prompt"]?.string ?? "", options: Self.options(params["options"]))
133
+ stream(response, id: try Self.int(params, "stream"))
134
+ return .object([:])
135
+ case "next":
136
+ return try await next(Self.int(params, "stream"))
137
+ case "cancel":
138
+ if let stream = try streams[Self.int(params, "stream")], !stream.done {
139
+ stream.task?.cancel()
140
+ finish(stream)
141
+ }
142
+ return .object([:])
143
+ case "close":
144
+ let id = try Self.int(params, "session")
145
+ sessions.removeValue(forKey: id)?.close()
146
+ relays.removeValue(forKey: id)?.close()
147
+ return .object([:])
148
+ case let action:
149
+ throw HostRequestFailure("unsupported: languageModel action \(action ?? "none")")
150
+ }
151
+ }
152
+
153
+ /// Cancels every response and lets every session go (the app is going away).
154
+ func stop() {
155
+ for stream in streams.values {
156
+ stream.task?.cancel()
157
+ finish(stream)
158
+ }
159
+ streams = [:]
160
+ for session in sessions.values { session.close() }
161
+ sessions = [:]
162
+ for relay in relays.values { relay.close() }
163
+ relays = [:]
164
+ }
165
+
166
+ /// The model that answers now and its availability: Apple Intelligence's
167
+ /// where it can answer and the viewer has not chosen the app's model;
168
+ /// otherwise the app's fallback, unless the system model is only still
169
+ /// downloading and the viewer has not chosen; otherwise none, for the
170
+ /// system's reason.
171
+ private func choice() async -> Choice {
172
+ var reason = LanguageModelAvailability.Reason.deviceNotEligible
173
+ let chosen = UserDefaults.standard.string(forKey: Self.choiceKey)
174
+ var systemAnswers = false
175
+ #if canImport(FoundationModels)
176
+ if #available(iOS 26.0, macOS 26.0, *) {
177
+ let provider = system ?? FoundationModelsProvider()
178
+ system = provider
179
+ let availability = await provider.availability()
180
+ systemAnswers = availability == .available
181
+ if systemAnswers, chosen != "fallback" || LanguageModels.fallback == nil {
182
+ return Choice(provider: provider, availability: availability, systemAnswers: true, isFallback: false)
183
+ }
184
+ if case .unavailable(let systemReason) = availability { reason = systemReason }
185
+ }
186
+ #endif
187
+ if let fallback = LanguageModels.fallback, systemAnswers || reason != .modelNotReady || chosen == "fallback" {
188
+ return Choice(provider: fallback, availability: await fallback.availability(), systemAnswers: systemAnswers, isFallback: true)
189
+ }
190
+ return Choice(provider: nil, availability: .unavailable(reason), systemAnswers: systemAnswers, isFallback: false)
191
+ }
192
+
193
+ private struct Choice {
194
+ let provider: (any LanguageModelProvider)?
195
+ let availability: LanguageModelAvailability
196
+ /// Whether Apple Intelligence can answer, chosen or not.
197
+ let systemAnswers: Bool
198
+ let isFallback: Bool
199
+ }
200
+
201
+ /// The models the viewer can choose: Apple Intelligence where it answers,
202
+ /// then the fallback's; none when there is no choice to make.
203
+ private func modelsJSON(_ current: Choice) async -> [JSONValue] {
204
+ var models: [JSONValue] = []
205
+ if current.systemAnswers {
206
+ models.append(.object([
207
+ "id": .string(Self.systemChoice),
208
+ "name": .string("Apple Intelligence"),
209
+ "summary": .string("Built into this device"),
210
+ "selected": .bool(!current.isFallback),
211
+ ]))
212
+ }
213
+ if let fallback = LanguageModels.fallback {
214
+ let choices = fallback.choices
215
+ for choice in choices {
216
+ var object: JSONObject = [
217
+ "id": .string(choice.id),
218
+ "name": .string(choice.name),
219
+ "selected": .bool(current.isFallback && choice.id == fallback.selectedChoice),
220
+ ]
221
+ if let summary = choice.summary { object["summary"] = .string(summary) }
222
+ if let download = choice.download { object["download"] = Self.json(download) }
223
+ models.append(.object(object))
224
+ }
225
+ }
226
+ return models.count > 1 ? models : []
227
+ }
228
+
229
+ private func session(_ params: JSONObject) throws -> any LanguageModelProviderSession {
230
+ let id = try Self.int(params, "session")
231
+ guard let session = sessions[id] else { throw HostRequestFailure("languageModel: no session \(id)") }
232
+ return session
233
+ }
234
+
235
+ private func stream(_ response: AsyncThrowingStream<String, Error>, id: Int) {
236
+ let stream = Stream()
237
+ streams[id] = stream
238
+ stream.task = Task { @MainActor [weak self] in
239
+ do {
240
+ for try await snapshot in response {
241
+ if stream.done { break }
242
+ stream.content = snapshot
243
+ stream.delivered = false
244
+ stream.wake()
245
+ }
246
+ } catch {
247
+ if !Task.isCancelled { stream.error = error }
248
+ }
249
+ self?.finish(stream)
250
+ }
251
+ }
252
+
253
+ private func finish(_ stream: Stream) {
254
+ guard !stream.done else { return }
255
+ stream.done = true
256
+ stream.delivered = false
257
+ stream.wake()
258
+ }
259
+
260
+ private func next(_ id: Int) async throws -> JSONValue {
261
+ guard let stream = streams[id] else { throw HostRequestFailure("languageModel: no stream \(id)") }
262
+ while stream.delivered {
263
+ await withCheckedContinuation { stream.waiter = $0 }
264
+ }
265
+ stream.delivered = true
266
+ if stream.done { streams[id] = nil }
267
+ if let error = stream.error { throw error }
268
+ return .object(["content": .string(stream.content), "done": .bool(stream.done)])
269
+ }
270
+
271
+ // MARK: Conversions
272
+
273
+ static func json(_ availability: LanguageModelAvailability, download: LanguageModelDownload? = nil) -> JSONValue {
274
+ var object: JSONObject
275
+ switch availability {
276
+ case .available:
277
+ object = ["status": .string("available")]
278
+ case .unavailable(let reason):
279
+ object = ["status": .string("unavailable"), "reason": .string(reason.rawValue)]
280
+ }
281
+ if let download { object["download"] = json(download) }
282
+ return .object(object)
283
+ }
284
+
285
+ static func json(_ download: LanguageModelDownload) -> JSONValue {
286
+ var state: JSONObject = ["downloaded": .bool(download.isDownloaded)]
287
+ if let size = download.size { state["size"] = .number(Double(size)) }
288
+ if let progress = download.progress { state["progress"] = .number(progress) }
289
+ if let failure = download.failure { state["failure"] = .string(failure) }
290
+ return .object(state)
291
+ }
292
+
293
+ static func options(_ value: JSONValue?) -> LanguageModelOptions {
294
+ let options = value?.object ?? [:]
295
+ return LanguageModelOptions(
296
+ temperature: options["temperature"]?.double,
297
+ maximumResponseTokens: options["maximumResponseTokens"]?.int,
298
+ greedy: options["sampling"]?.string == "greedy"
299
+ )
300
+ }
301
+
302
+ private static func int(_ params: JSONObject, _ key: String) throws -> Int {
303
+ guard let value = params[key]?.int else { throw HostRequestFailure("languageModel: no \(key)") }
304
+ return value
305
+ }
306
+ }
307
+
308
+ /// A session's tool calls on their way to the app and back: the model's
309
+ /// call waits here until the app, asking with `toolCall`, has run it and
310
+ /// answered with `toolResult`.
311
+ @MainActor
312
+ final class ToolRelay {
313
+ private struct Call {
314
+ let id: Int
315
+ let name: String
316
+ let arguments: JSONValue
317
+ }
318
+
319
+ private var waiting: [Call] = []
320
+ private var answers: [Int: CheckedContinuation<String, Error>] = [:]
321
+ /// The app's `toolCall` request, when it asked before the model called.
322
+ private var asker: CheckedContinuation<JSONValue, Never>?
323
+ private var nextID = 1
324
+ private var closed = false
325
+
326
+ /// Hands the call to the app and waits for what it answers.
327
+ func call(name: String, arguments: JSONValue) async throws -> String {
328
+ guard !closed else { throw LanguageModelError(.unavailable, "the session is closed") }
329
+ let id = nextID
330
+ nextID += 1
331
+ return try await withCheckedThrowingContinuation { continuation in
332
+ answers[id] = continuation
333
+ let call = Call(id: id, name: name, arguments: arguments)
334
+ if let asker {
335
+ self.asker = nil
336
+ asker.resume(returning: Self.json(call))
337
+ } else {
338
+ waiting.append(call)
339
+ }
340
+ }
341
+ }
342
+
343
+ /// The next call for the app to run, once the model makes one.
344
+ func next() async -> JSONValue {
345
+ if !waiting.isEmpty { return Self.json(waiting.removeFirst()) }
346
+ if closed { return .object(["closed": .bool(true)]) }
347
+ return await withCheckedContinuation { asker = $0 }
348
+ }
349
+
350
+ func answer(_ id: Int, output: String?, error: String?) {
351
+ guard let continuation = answers.removeValue(forKey: id) else { return }
352
+ if let error {
353
+ continuation.resume(throwing: LanguageModelError(.unavailable, "the tool failed: \(error)"))
354
+ } else {
355
+ continuation.resume(returning: output ?? "")
356
+ }
357
+ }
358
+
359
+ /// The session is gone: the app stops asking, and calls under way fail.
360
+ func close() {
361
+ closed = true
362
+ asker?.resume(returning: .object(["closed": .bool(true)]))
363
+ asker = nil
364
+ for continuation in answers.values { continuation.resume(throwing: LanguageModelError(.unavailable, "the session is closed")) }
365
+ answers = [:]
366
+ waiting = []
367
+ }
368
+
369
+ private static func json(_ call: Call) -> JSONValue {
370
+ .object(["call": .number(Double(call.id)), "name": .string(call.name), "arguments": call.arguments])
371
+ }
372
+ }
373
+
374
+ #if canImport(FoundationModels)
375
+ /// Apple Intelligence's on-device model as a provider: FoundationModels'
376
+ /// `SystemLanguageModel.default` and `LanguageModelSession`.
377
+ @available(iOS 26.0, macOS 26.0, *)
378
+ @MainActor
379
+ final class FoundationModelsProvider: LanguageModelProvider {
380
+ func availability() async -> LanguageModelAvailability {
381
+ // Off the main thread: the first read can take a while (seconds in the Simulator).
382
+ Self.availability(await Task.detached { SystemLanguageModel.default.availability }.value)
383
+ }
384
+
385
+ func makeSession(instructions: String?, tools: [LanguageModelTool]) throws -> any LanguageModelProviderSession {
386
+ let bridged: [any Tool] = try tools.map { try BridgedTool($0) }
387
+ return Session(instructions.map { LanguageModelSession(tools: bridged, instructions: $0) } ?? LanguageModelSession(tools: bridged))
388
+ }
389
+
390
+ /// An app's tool as FoundationModels calls one: its arguments' schema made
391
+ /// at run time from the JSON schema, the arguments handed on as JSON.
392
+ struct BridgedTool: Tool {
393
+ let name: String
394
+ let description: String
395
+ let parameters: GenerationSchema
396
+ let run: @Sendable (JSONValue) async throws -> String
397
+
398
+ init(_ tool: LanguageModelTool) throws {
399
+ name = tool.name
400
+ description = tool.description
401
+ parameters = try GenerationSchema(root: Self.schema(tool.parameters, name: "\(tool.name)Arguments"), dependencies: [])
402
+ run = tool.call
403
+ }
404
+
405
+ func call(arguments: GeneratedContent) async throws -> String {
406
+ try await run((try? JSONValue.parse(arguments.jsonString)) ?? .object([:]))
407
+ }
408
+
409
+ /// A JSON schema as a generation schema: objects, strings (enums as
410
+ /// choices), numbers, integers, booleans and arrays.
411
+ static func schema(_ json: JSONValue, name: String) -> DynamicGenerationSchema {
412
+ let object = json.object ?? [:]
413
+ let description = object["description"]?.string
414
+ if let choices = object["enum"]?.array?.compactMap(\.string), !choices.isEmpty {
415
+ return DynamicGenerationSchema(name: name, description: description, anyOf: choices)
416
+ }
417
+ switch object["type"]?.string {
418
+ case "object":
419
+ let required = Set((object["required"]?.array ?? []).compactMap(\.string))
420
+ let properties = (object["properties"]?.object ?? [:]).sorted { $0.key < $1.key }.map { key, value in
421
+ DynamicGenerationSchema.Property(
422
+ name: key,
423
+ description: value.object?["description"]?.string,
424
+ schema: schema(value, name: "\(name)_\(key)"),
425
+ isOptional: !required.contains(key)
426
+ )
427
+ }
428
+ return DynamicGenerationSchema(name: name, description: description, properties: properties)
429
+ case "array":
430
+ return DynamicGenerationSchema(arrayOf: schema(object["items"] ?? .object([:]), name: "\(name)_item"))
431
+ case "integer":
432
+ return DynamicGenerationSchema(type: Int.self)
433
+ case "number":
434
+ return DynamicGenerationSchema(type: Double.self)
435
+ case "boolean":
436
+ return DynamicGenerationSchema(type: Bool.self)
437
+ default:
438
+ return DynamicGenerationSchema(type: String.self)
439
+ }
440
+ }
441
+ }
442
+
443
+ @MainActor
444
+ final class Session: LanguageModelProviderSession {
445
+ private let session: LanguageModelSession
446
+
447
+ init(_ session: LanguageModelSession) {
448
+ self.session = session
449
+ }
450
+
451
+ func prewarm() {
452
+ session.prewarm()
453
+ }
454
+
455
+ func streamResponse(to prompt: String, options: LanguageModelOptions) -> AsyncThrowingStream<String, Error> {
456
+ let session = session
457
+ return AsyncThrowingStream { continuation in
458
+ let task = Task { @MainActor in
459
+ do {
460
+ for try await snapshot in session.streamResponse(to: prompt, options: FoundationModelsProvider.options(options)) {
461
+ continuation.yield(snapshot.content)
462
+ }
463
+ continuation.finish()
464
+ } catch {
465
+ continuation.finish(throwing: FoundationModelsProvider.tagged(error))
466
+ }
467
+ }
468
+ continuation.onTermination = { _ in task.cancel() }
469
+ }
470
+ }
471
+
472
+ func close() {}
473
+ }
474
+
475
+ static func availability(_ availability: SystemLanguageModel.Availability) -> LanguageModelAvailability {
476
+ switch availability {
477
+ case .available:
478
+ return .available
479
+ case .unavailable(let reason):
480
+ switch reason {
481
+ case .deviceNotEligible: return .unavailable(.deviceNotEligible)
482
+ case .appleIntelligenceNotEnabled: return .unavailable(.appleIntelligenceNotEnabled)
483
+ case .modelNotReady: return .unavailable(.modelNotReady)
484
+ @unknown default: return .unavailable(.deviceNotEligible)
485
+ }
486
+ }
487
+ }
488
+
489
+ static func options(_ options: LanguageModelOptions) -> GenerationOptions {
490
+ GenerationOptions(sampling: options.greedy ? .greedy : nil, temperature: options.temperature, maximumResponseTokens: options.maximumResponseTokens)
491
+ }
492
+
493
+ /// A `GenerationError` as the error the app reads its case from.
494
+ static func tagged(_ error: Error) -> Error {
495
+ guard let error = error as? LanguageModelSession.GenerationError else { return error }
496
+ let kind: LanguageModelError.Kind
497
+ switch error {
498
+ case .exceededContextWindowSize: kind = .context
499
+ case .assetsUnavailable: kind = .unavailable
500
+ case .guardrailViolation: kind = .guardrail
501
+ case .unsupportedLanguageOrLocale: kind = .language
502
+ case .rateLimited: kind = .rateLimited
503
+ case .concurrentRequests: kind = .concurrent
504
+ case .refusal: kind = .refusal
505
+ default: return HostRequestFailure(error.localizedDescription)
506
+ }
507
+ return LanguageModelError(kind, error.localizedDescription)
508
+ }
509
+ }
510
+ #endif
511
+ #endif