bleam 0.0.10 → 0.0.12

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 (90) hide show
  1. package/dist/ai.cjs +1872 -1182
  2. package/dist/ai.d.cts +267 -234
  3. package/dist/ai.d.ts +267 -234
  4. package/dist/ai.js +1859 -1167
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +3 -47
  7. package/dist/cli.d.ts +3 -47
  8. package/dist/cli.js +3 -183
  9. package/dist/config.d.cts +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
  12. package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
  13. package/dist/files.cjs +1 -1
  14. package/dist/files.js +1 -1
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/platform.cjs +1 -13
  18. package/dist/platform.d.cts +1 -28
  19. package/dist/platform.d.ts +1 -28
  20. package/dist/platform.js +1 -10
  21. package/dist/schema-B5BfdswF.js +226 -0
  22. package/dist/schema-BnVZOXfu.cjs +286 -0
  23. package/dist/schema-D5eImHxu.d.cts +125 -0
  24. package/dist/schema-SSjokbtw.d.ts +125 -0
  25. package/dist/schema.cjs +10 -2
  26. package/dist/schema.d.cts +2 -2
  27. package/dist/schema.d.ts +2 -2
  28. package/dist/schema.js +2 -2
  29. package/dist/state-Dh3HLixb.js +874 -0
  30. package/dist/state-LssDgpff.cjs +973 -0
  31. package/dist/state.cjs +16 -12
  32. package/dist/state.d.cts +144 -140
  33. package/dist/state.d.ts +145 -140
  34. package/dist/state.js +3 -3
  35. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  36. package/dist/{ui-Dd7SXdbg.d.cts → ui-D7bRLYee.d.ts} +7 -7
  37. package/dist/ui.d.cts +1 -1
  38. package/dist/ui.d.ts +1 -1
  39. package/dist/window.d.cts +1 -1
  40. package/dist/window.d.ts +1 -1
  41. package/package.json +3 -1
  42. package/templates/foundation-models/app/index.tsx +77 -15
  43. package/templates/image-generation/app/index.tsx +4 -2
  44. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  45. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  46. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  49. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  50. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  51. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  52. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  53. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  54. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  55. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  56. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  57. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  58. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  59. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  60. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  61. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  62. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  63. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  64. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  65. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  66. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  67. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +135 -129
  68. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  69. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  70. package/templates/native/ios/GenerationService/main.swift +174 -48
  71. package/templates/native/ios/Podfile.lock +173 -173
  72. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  73. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  74. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +239 -341
  75. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  76. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  77. package/templates/text-generation/app/index.tsx +124 -0
  78. package/templates/text-generation/app.config.ts +5 -0
  79. package/templates/updates/README.md +43 -199
  80. package/templates/updates/src/index.ts +5 -98
  81. package/templates/updates/src/schema.ts +18 -173
  82. package/dist/schema-Bo5Jvqus.js +0 -90
  83. package/dist/schema-DOOjfXvs.d.ts +0 -58
  84. package/dist/schema-ENSMX_1t.d.cts +0 -58
  85. package/dist/schema-rQ13mrpD.cjs +0 -102
  86. package/dist/state-Bx0VlTlO.cjs +0 -852
  87. package/dist/state-CAwe-Vw1.js +0 -767
  88. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  89. /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
  90. /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
@@ -15,90 +15,7 @@ public final class AIModule: Module {
15
15
 
16
16
  public func definition() -> ModuleDefinition {
17
17
  Name("AI")
18
- Events("imagegenProgress", "textGenerationUpdate", "toolCall")
19
-
20
- Function("isAvailable") { () -> Bool in
21
- #if canImport(FoundationModels)
22
- if #available(iOS 26, *) {
23
- return SystemLanguageModel.default.availability == .available
24
- }
25
- #endif
26
- return false
27
- }
28
-
29
- Function("availability") { () -> [String: Any] in
30
- #if canImport(FoundationModels)
31
- if #available(iOS 26, *) {
32
- switch SystemLanguageModel.default.availability {
33
- case .available:
34
- return ["available": true]
35
- case .unavailable(.deviceNotEligible):
36
- return ["available": false, "reason": "device_not_eligible"]
37
- case .unavailable(.appleIntelligenceNotEnabled):
38
- return ["available": false, "reason": "apple_intelligence_not_enabled"]
39
- case .unavailable(.modelNotReady):
40
- return ["available": false, "reason": "model_not_ready"]
41
- case .unavailable:
42
- return ["available": false, "reason": "unknown"]
43
- }
44
- }
45
- #endif
46
- return ["available": false, "reason": "unknown"]
47
- }
48
-
49
- AsyncFunction("generate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
50
- #if canImport(FoundationModels)
51
- if #available(iOS 26, *) {
52
- guard SystemLanguageModel.default.availability == .available else {
53
- promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
54
- return
55
- }
56
-
57
- guard !request.prompt.isEmpty else {
58
- promise.reject("INVALID_MESSAGE", "Expected prompt")
59
- return
60
- }
61
-
62
- let generationID = request.id ?? UUID().uuidString
63
- let task = Task { @MainActor [weak self] in
64
- guard let self else { return }
65
- do {
66
- let tools = try Self.tools(
67
- from: request.tools,
68
- generationID: generationID,
69
- coordinator: self.textGeneration,
70
- emit: { [weak self] event in self?.sendEvent("toolCall", event) }
71
- )
72
- let session = Self.session(system: request.system, tools: tools)
73
- let options = try Self.generationOptions(from: request)
74
-
75
- if let schema = request.outputSchema {
76
- let generationSchema = try Self.generationSchema(from: schema)
77
- let response = try await session.respond(
78
- to: request.prompt,
79
- schema: generationSchema,
80
- includeSchemaInPrompt: true,
81
- options: options
82
- )
83
- promise.resolve(try Self.value(from: response.content, schema: schema))
84
- return
85
- }
86
-
87
- let response = try await session.respond(to: request.prompt, options: options)
88
- promise.resolve(response.content)
89
- } catch {
90
- Self.reject(error, promise: promise)
91
- }
92
- self.textGeneration.finish(generationID)
93
- }
94
- Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
95
- } else {
96
- promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
97
- }
98
- #else
99
- promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
100
- #endif
101
- }
18
+ Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate")
102
19
 
103
20
  AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
104
21
  #if canImport(FoundationModels)
@@ -116,13 +33,7 @@ public final class AIModule: Module {
116
33
  let task = Task { @MainActor [weak self] in
117
34
  guard let self else { return }
118
35
  do {
119
- let tools = try Self.tools(
120
- from: request.tools,
121
- generationID: generationID,
122
- coordinator: self.textGeneration,
123
- emit: { [weak self] event in self?.sendEvent("toolCall", event) }
124
- )
125
- let session = Self.session(system: request.system, tools: tools)
36
+ let session = try Self.session(system: request.system, messages: request.messages)
126
37
  let options = try Self.generationOptions(from: request)
127
38
  let stream = session.streamResponse(to: request.prompt, options: options)
128
39
  var content = ""
@@ -147,11 +58,28 @@ public final class AIModule: Module {
147
58
  }
148
59
 
149
60
  AsyncFunction("cancelTextGeneration") { [weak self] (id: String) -> Bool in
150
- await self?.textGeneration.cancel(id) ?? false
61
+ let foundationCanceled = await self?.textGeneration.cancel(id) ?? false
62
+ let localCanceled = (try? await ImageGenerationServiceClient.shared.cancelText(id: id)) ?? false
63
+ return foundationCanceled || localCanceled
151
64
  }
152
65
 
153
- AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
154
- await self?.textGeneration.resolve(request) ?? false
66
+ AsyncFunction("streamLocalText") { [weak self] (request: LocalTextGenerationRequest, promise: Promise) in
67
+ Task { @MainActor in
68
+ do {
69
+ let event = try await ImageGenerationServiceClient.shared.runText(
70
+ request: Self.bonsaiTextRequest(from: request),
71
+ onUpdate: { [weak self] event in
72
+ self?.sendEvent("localTextGenerationUpdate", [
73
+ "id": event.id,
74
+ "tokenIds": event.tokenIDs,
75
+ ])
76
+ }
77
+ )
78
+ promise.resolve(event.tokenIDs)
79
+ } catch {
80
+ Self.rejectLocalTextGeneration(error, promise: promise)
81
+ }
82
+ }
155
83
  }
156
84
 
157
85
  AsyncFunction("generateImage") { (request: ImageGenerationRequest, promise: Promise) in
@@ -272,12 +200,23 @@ public final class AIModule: Module {
272
200
  guard !request.model.isEmpty else {
273
201
  throw ModuleError.invalidModel("Missing image model")
274
202
  }
275
- guard request.model == "flux2-klein-4b-8bit-abliterated" else {
203
+ guard request.model == "flux2-klein-4b-8bit-abliterated"
204
+ || request.model == "ternary-bonsai-image-4b"
205
+ else {
276
206
  throw ModuleError.invalidModel("Unsupported image model: \(request.model)")
277
207
  }
278
208
  guard !request.prompt.isEmpty else {
279
209
  throw ModuleError.invalidMessage("Expected prompt")
280
210
  }
211
+ if request.model == "ternary-bonsai-image-4b",
212
+ request.sourceImagePath != nil
213
+ || request.maskImagePath != nil
214
+ || request.outpaintWidth != nil
215
+ || !request.referenceImagePaths.isEmpty
216
+ || !request.loraPaths.isEmpty
217
+ {
218
+ throw ModuleError.invalidModel("Ternary Bonsai Image 4B currently supports text-to-image generation")
219
+ }
281
220
  if let sourceImagePath = request.sourceImagePath, sourceImagePath.isEmpty {
282
221
  throw ModuleError.invalidMessage("Source image path must not be empty")
283
222
  }
@@ -493,6 +432,40 @@ public final class AIModule: Module {
493
432
  )
494
433
  }
495
434
 
435
+ private static func bonsaiTextRequest(
436
+ from request: LocalTextGenerationRequest
437
+ ) throws -> BonsaiTextGenerationRequest {
438
+ guard !request.id.isEmpty else {
439
+ throw ModuleError.invalidMessage("Expected generation id")
440
+ }
441
+ guard !request.model.isEmpty else {
442
+ throw ModuleError.invalidModel("Expected Bonsai model id")
443
+ }
444
+ var isDirectory: ObjCBool = false
445
+ guard FileManager.default.fileExists(atPath: request.modelPath, isDirectory: &isDirectory),
446
+ isDirectory.boolValue
447
+ else {
448
+ throw ModuleError.invalidModel("Missing Bonsai model directory: \(request.modelPath)")
449
+ }
450
+ guard !request.inputIds.isEmpty else {
451
+ throw ModuleError.invalidMessage("Expected tokenized prompt")
452
+ }
453
+ return BonsaiTextGenerationRequest(
454
+ id: request.id,
455
+ model: request.model,
456
+ modelPath: request.modelPath,
457
+ inputIDs: request.inputIds,
458
+ endTokenIDs: request.endTokenIds,
459
+ maximumNewTokens: request.maxTokens,
460
+ temperature: request.temperature,
461
+ topP: request.topP,
462
+ topK: request.topK,
463
+ repetitionPenalty: request.repetitionPenalty,
464
+ repetitionContextSize: request.repetitionContextSize,
465
+ seed: request.seed
466
+ )
467
+ }
468
+
496
469
  private static func metadataDictionary(_ metadata: GenerationWorkerMetadata?) -> [String: Any]? {
497
470
  guard let metadata else { return nil }
498
471
  var value: [String: Any] = [
@@ -530,33 +503,53 @@ public final class AIModule: Module {
530
503
  promise.reject(error)
531
504
  }
532
505
 
533
- #if canImport(FoundationModels)
534
- @available(iOS 26, *)
535
- private static func session(system: String?, tools: [any Tool]) -> LanguageModelSession {
536
- if let system, !system.isEmpty {
537
- return LanguageModelSession(tools: tools, instructions: system)
506
+ private static func rejectLocalTextGeneration(_ error: Error, promise: Promise) {
507
+ if error is CancellationError {
508
+ promise.reject("GENERATION_CANCELED", "Generation canceled")
509
+ return
510
+ }
511
+ if error as? GenerationServiceClientError == .concurrentRequest {
512
+ promise.reject("CONCURRENT_REQUEST", error.localizedDescription)
513
+ return
538
514
  }
539
- return LanguageModelSession(tools: tools)
515
+ if let failure = error as? GenerationWorkerFailure {
516
+ promise.reject("GENERATION_FAILED", failure.localizedDescription)
517
+ return
518
+ }
519
+ promise.reject(error)
540
520
  }
541
521
 
522
+ #if canImport(FoundationModels)
542
523
  @available(iOS 26, *)
543
- private static func tools(
544
- from definitions: [ToolDefinitionRequest],
545
- generationID: String,
546
- coordinator: TextGenerationCoordinator,
547
- emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
548
- ) throws -> [any Tool] {
549
- try definitions.map { definition in
550
- let parameters = try generationSchema(from: definition.inputSchema)
551
- return DynamicTool(
552
- name: definition.name,
553
- description: definition.description,
554
- parameters: parameters,
555
- generationID: generationID,
556
- coordinator: coordinator,
557
- emit: emit
558
- )
524
+ private static func session(
525
+ system: String?,
526
+ messages: [TextMessageRequest]
527
+ ) throws -> LanguageModelSession {
528
+ if !messages.isEmpty {
529
+ var entries: [Transcript.Entry] = []
530
+ if system?.isEmpty == false {
531
+ entries.append(.instructions(Transcript.Instructions(
532
+ segments: system.map { [.text(Transcript.TextSegment(content: $0))] } ?? [],
533
+ toolDefinitions: []
534
+ )))
535
+ }
536
+ for message in messages {
537
+ let segments: [Transcript.Segment] = [.text(Transcript.TextSegment(content: message.content))]
538
+ switch message.role {
539
+ case "user":
540
+ entries.append(.prompt(Transcript.Prompt(segments: segments)))
541
+ case "assistant":
542
+ entries.append(.response(Transcript.Response(assetIDs: [], segments: segments)))
543
+ default:
544
+ throw ModuleError.invalidMessage("Unsupported transcript role: \(message.role)")
545
+ }
546
+ }
547
+ return LanguageModelSession(transcript: Transcript(entries: entries))
548
+ }
549
+ if let system, !system.isEmpty {
550
+ return LanguageModelSession(instructions: system)
559
551
  }
552
+ return LanguageModelSession()
560
553
  }
561
554
 
562
555
  @available(iOS 26, *)
@@ -568,131 +561,6 @@ public final class AIModule: Module {
568
561
  )
569
562
  }
570
563
 
571
- @available(iOS 26, *)
572
- private static func generationSchema(from schema: [String: Any]) throws -> GenerationSchema {
573
- do {
574
- return try GenerationSchema(root: dynamicSchema(from: schema), dependencies: [])
575
- } catch {
576
- throw ModuleError.invalidSchema(error.localizedDescription)
577
- }
578
- }
579
-
580
- @available(iOS 26, *)
581
- private static func dynamicSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
582
- guard let type = schema["type"] as? String else {
583
- throw ModuleError.invalidSchema("Missing schema type")
584
- }
585
-
586
- switch type {
587
- case "object":
588
- return try objectSchema(from: schema)
589
- case "array":
590
- guard let itemSchema = schema["items"] as? [String: Any] else {
591
- throw ModuleError.invalidSchema("Array schema must include items")
592
- }
593
- return try DynamicGenerationSchema(arrayOf: dynamicSchema(from: itemSchema))
594
- case "string":
595
- return DynamicGenerationSchema(type: String.self)
596
- case "number":
597
- return DynamicGenerationSchema(type: Double.self)
598
- case "boolean":
599
- return DynamicGenerationSchema(type: Bool.self)
600
- default:
601
- throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
602
- }
603
- }
604
-
605
- @available(iOS 26, *)
606
- private static func objectSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
607
- let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
608
- let required = Set(schema["required"] as? [String] ?? [])
609
- var properties: [DynamicGenerationSchema.Property] = []
610
-
611
- for key in propertiesDictionary.keys.sorted() {
612
- guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
613
- throw ModuleError.invalidSchema("Property \(key) schema must be an object")
614
- }
615
-
616
- properties.append(
617
- DynamicGenerationSchema.Property(
618
- name: key,
619
- description: nil,
620
- schema: try dynamicSchema(from: propertySchema),
621
- isOptional: !required.contains(key)
622
- )
623
- )
624
- }
625
-
626
- return DynamicGenerationSchema(name: schema["title"] as? String ?? "Output", properties: properties)
627
- }
628
-
629
- @available(iOS 26, *)
630
- private static func value(from content: GeneratedContent, schema: [String: Any]) throws -> Any {
631
- guard let type = schema["type"] as? String else {
632
- throw ModuleError.invalidSchema("Missing schema type")
633
- }
634
-
635
- switch type {
636
- case "object":
637
- let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
638
- let output = NSMutableDictionary()
639
-
640
- for key in propertiesDictionary.keys.sorted() {
641
- guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
642
- throw ModuleError.invalidSchema("Property \(key) schema must be an object")
643
- }
644
- output[key] = try propertyValue(from: content, property: key, schema: propertySchema)
645
- }
646
-
647
- return output
648
- default:
649
- throw ModuleError.invalidSchema("Root schema must be an object")
650
- }
651
- }
652
-
653
- @available(iOS 26, *)
654
- private static func propertyValue(
655
- from content: GeneratedContent,
656
- property: String,
657
- schema: [String: Any]
658
- ) throws -> Any {
659
- guard let type = schema["type"] as? String else {
660
- throw ModuleError.invalidSchema("Missing schema type")
661
- }
662
-
663
- switch type {
664
- case "string":
665
- return try content.value(String.self, forProperty: property)
666
- case "number":
667
- return try content.value(Double.self, forProperty: property)
668
- case "boolean":
669
- return try content.value(Bool.self, forProperty: property)
670
- case "object":
671
- let nestedContent = try content.value(GeneratedContent.self, forProperty: property)
672
- return try value(from: nestedContent, schema: schema)
673
- case "array":
674
- guard let itemSchema = schema["items"] as? [String: Any], let itemType = itemSchema["type"] as? String else {
675
- throw ModuleError.invalidSchema("Array schema must include items")
676
- }
677
-
678
- switch itemType {
679
- case "string":
680
- return try content.value([String].self, forProperty: property)
681
- case "number":
682
- return try content.value([Double].self, forProperty: property)
683
- case "boolean":
684
- return try content.value([Bool].self, forProperty: property)
685
- case "object":
686
- let values = try content.value([GeneratedContent].self, forProperty: property)
687
- return try values.map { try value(from: $0, schema: itemSchema) }
688
- default:
689
- throw ModuleError.invalidSchema("Unsupported array item type: \(itemType)")
690
- }
691
- default:
692
- throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
693
- }
694
- }
695
-
696
564
  @available(iOS 26, *)
697
565
  private static func reject(_ error: Error, promise: Promise) {
698
566
  if error is CancellationError {
@@ -716,55 +584,14 @@ public final class AIModule: Module {
716
584
  }
717
585
  return
718
586
  }
719
- if let toolError = error as? LanguageModelSession.ToolCallError {
720
- promise.reject("TOOL_ERROR", toolError.localizedDescription)
721
- return
722
- }
723
-
724
587
  promise.reject(error)
725
588
  }
726
589
  #endif
727
590
  }
728
591
 
729
- #if canImport(FoundationModels)
730
- @available(iOS 26, *)
731
- private struct DynamicTool: Tool {
732
- typealias Arguments = GeneratedContent
733
-
734
- let name: String
735
- let description: String
736
- let parameters: GenerationSchema
737
- let generationID: String
738
- let coordinator: TextGenerationCoordinator
739
- let emit: @MainActor @Sendable ([String: Any]) -> Void
740
-
741
- func call(arguments: GeneratedContent) async throws -> String {
742
- let data = Data(arguments.jsonString.utf8)
743
- let value = try JSONSerialization.jsonObject(with: data)
744
- return try await coordinator.call(
745
- generationID: generationID,
746
- name: name,
747
- arguments: value,
748
- emit: emit
749
- )
750
- }
751
- }
752
- #endif
753
-
754
- private struct ToolExecutionError: LocalizedError {
755
- let message: String
756
- var errorDescription: String? { message }
757
- }
758
-
759
592
  @MainActor
760
593
  private final class TextGenerationCoordinator {
761
- private struct PendingToolCall {
762
- let generationID: String
763
- let continuation: CheckedContinuation<String, Error>
764
- }
765
-
766
594
  private var tasks: [String: Task<Void, Never>] = [:]
767
- private var toolCalls: [String: PendingToolCall] = [:]
768
595
 
769
596
  func start(_ id: String, task: Task<Void, Never>) {
770
597
  tasks[id]?.cancel()
@@ -773,67 +600,22 @@ private final class TextGenerationCoordinator {
773
600
 
774
601
  func finish(_ id: String) {
775
602
  tasks[id] = nil
776
- failToolCalls(for: id, error: CancellationError())
777
603
  }
778
604
 
779
605
  func cancel(_ id: String) -> Bool {
780
606
  guard let task = tasks.removeValue(forKey: id) else { return false }
781
607
  task.cancel()
782
- failToolCalls(for: id, error: CancellationError())
783
608
  return true
784
609
  }
785
-
786
- func call(
787
- generationID: String,
788
- name: String,
789
- arguments: Any,
790
- emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
791
- ) async throws -> String {
792
- try Task.checkCancellation()
793
- let callID = UUID().uuidString
794
- return try await withCheckedThrowingContinuation { continuation in
795
- toolCalls[callID] = PendingToolCall(generationID: generationID, continuation: continuation)
796
- emit([
797
- "generationId": generationID,
798
- "callId": callID,
799
- "name": name,
800
- "arguments": arguments,
801
- ])
802
- }
803
- }
804
-
805
- func resolve(_ request: ToolResultRequest) -> Bool {
806
- guard let pending = toolCalls.removeValue(forKey: request.callId),
807
- pending.generationID == request.generationId
808
- else { return false }
809
-
810
- if let error = request.error {
811
- pending.continuation.resume(throwing: ToolExecutionError(message: error))
812
- } else {
813
- pending.continuation.resume(returning: request.output ?? "")
814
- }
815
- return true
816
- }
817
-
818
- private func failToolCalls(for generationID: String, error: Error) {
819
- let matches = toolCalls.filter { $0.value.generationID == generationID }
820
- for (id, pending) in matches {
821
- toolCalls[id] = nil
822
- pending.continuation.resume(throwing: error)
823
- }
824
- }
825
610
  }
826
611
 
827
612
  private enum ModuleError: Error, LocalizedError {
828
- case invalidSchema(String)
829
613
  case invalidMessage(String)
830
614
  case invalidModel(String)
831
615
  case imageGeneration(String)
832
616
 
833
617
  var errorDescription: String? {
834
618
  switch self {
835
- case .invalidSchema(let message):
836
- return "Invalid schema: \(message)"
837
619
  case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
838
620
  return message
839
621
  }
@@ -849,6 +631,7 @@ private final class ImageGenerationServiceClient {
849
631
  }
850
632
  private var connection: NSXPCConnection?
851
633
  private var pendingRun: PendingRun?
634
+ private var pendingTextRun: PendingTextRun?
852
635
  private let eventReceiver = GenerationServiceEventReceiver()
853
636
 
854
637
  private final class PendingRun {
@@ -864,13 +647,29 @@ private final class ImageGenerationServiceClient {
864
647
  }
865
648
  }
866
649
 
650
+ private final class PendingTextRun {
651
+ let id: String
652
+ let continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>
653
+ let onUpdate: (BonsaiTextGenerationEvent) -> Void
654
+
655
+ init(
656
+ id: String,
657
+ continuation: CheckedContinuation<BonsaiTextGenerationEvent, Error>,
658
+ onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void
659
+ ) {
660
+ self.id = id
661
+ self.continuation = continuation
662
+ self.onUpdate = onUpdate
663
+ }
664
+ }
665
+
867
666
  func run(
868
667
  request: GenerationWorkerRequest,
869
668
  onProgress: @escaping (GenerationWorkerEvent) -> Void = { _ in }
870
669
  ) async throws -> GenerationWorkerEvent {
871
670
  let terminalEvent: GenerationWorkerEvent = try await withCheckedThrowingContinuation { continuation in
872
671
  do {
873
- guard pendingRun == nil else {
672
+ guard pendingRun == nil, pendingTextRun == nil else {
874
673
  continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
875
674
  return
876
675
  }
@@ -905,6 +704,71 @@ private final class ImageGenerationServiceClient {
905
704
  return terminalEvent
906
705
  }
907
706
 
707
+ func runText(
708
+ request: BonsaiTextGenerationRequest,
709
+ onUpdate: @escaping (BonsaiTextGenerationEvent) -> Void = { _ in }
710
+ ) async throws -> BonsaiTextGenerationEvent {
711
+ let terminalEvent: BonsaiTextGenerationEvent = try await withCheckedThrowingContinuation { continuation in
712
+ do {
713
+ guard pendingRun == nil, pendingTextRun == nil else {
714
+ continuation.resume(throwing: GenerationServiceClientError.concurrentRequest)
715
+ return
716
+ }
717
+ eventReceiver.onTextEvent = { [weak self] event in
718
+ self?.deliverText(event)
719
+ }
720
+ pendingTextRun = PendingTextRun(
721
+ id: request.id,
722
+ continuation: continuation,
723
+ onUpdate: onUpdate
724
+ )
725
+ let requestData = try JSONEncoder().encode(request)
726
+ let service = try remoteProxy(errorHandler: { [weak self] error in
727
+ Task { @MainActor in
728
+ self?.failPendingTextRun(Self.mapConnectionError(error))
729
+ }
730
+ })
731
+ service.startTextGeneration(requestData: requestData) { [weak self] accepted in
732
+ Task { @MainActor in
733
+ guard accepted else {
734
+ self?.failPendingTextRun(GenerationServiceClientError.concurrentRequest)
735
+ return
736
+ }
737
+ }
738
+ }
739
+ } catch {
740
+ pendingTextRun = nil
741
+ eventReceiver.onTextEvent = nil
742
+ continuation.resume(throwing: error)
743
+ }
744
+ }
745
+
746
+ switch terminalEvent.type {
747
+ case .completed:
748
+ return terminalEvent
749
+ case .canceled:
750
+ throw CancellationError()
751
+ case .failed:
752
+ throw GenerationWorkerFailure(message: terminalEvent.message ?? "Text generation failed.")
753
+ case .update:
754
+ throw GenerationServiceClientError.invalidResponse
755
+ }
756
+ }
757
+
758
+ func cancelText(id: String) async throws -> Bool {
759
+ guard pendingTextRun?.id == id else { return false }
760
+ let service = try remoteProxy(errorHandler: { [weak self] error in
761
+ Task { @MainActor in
762
+ self?.failPendingTextRun(Self.mapConnectionError(error))
763
+ }
764
+ })
765
+ return try await withCheckedThrowingContinuation { continuation in
766
+ service.cancelTextGeneration(id: id) { canceled in
767
+ continuation.resume(returning: canceled)
768
+ }
769
+ }
770
+ }
771
+
908
772
  func cancel() async throws -> Bool {
909
773
  guard pendingRun != nil else {
910
774
  return false
@@ -940,7 +804,7 @@ private final class ImageGenerationServiceClient {
940
804
  }
941
805
 
942
806
  func unload() async throws -> Bool {
943
- guard pendingRun == nil else { return false }
807
+ guard pendingRun == nil, pendingTextRun == nil else { return false }
944
808
  let service = try remoteProxy(errorHandler: { _ in })
945
809
  let unloaded = try await withCheckedThrowingContinuation { continuation in
946
810
  service.shutdown { accepted in
@@ -1025,6 +889,17 @@ private final class ImageGenerationServiceClient {
1025
889
  }
1026
890
  }
1027
891
 
892
+ private func deliverText(_ event: BonsaiTextGenerationEvent) {
893
+ guard let pendingTextRun, event.id == pendingTextRun.id else { return }
894
+ if event.type == .update {
895
+ pendingTextRun.onUpdate(event)
896
+ return
897
+ }
898
+ self.pendingTextRun = nil
899
+ eventReceiver.onTextEvent = nil
900
+ pendingTextRun.continuation.resume(returning: event)
901
+ }
902
+
1028
903
  private func failPendingRun(_ error: Error) {
1029
904
  guard let pendingRun else { return }
1030
905
  self.pendingRun = nil
@@ -1035,9 +910,17 @@ private final class ImageGenerationServiceClient {
1035
910
  pendingRun.continuation.resume(throwing: error)
1036
911
  }
1037
912
 
913
+ private func failPendingTextRun(_ error: Error) {
914
+ guard let pendingTextRun else { return }
915
+ self.pendingTextRun = nil
916
+ eventReceiver.onTextEvent = nil
917
+ pendingTextRun.continuation.resume(throwing: error)
918
+ }
919
+
1038
920
  private func handleConnectionLost() {
1039
921
  connection = nil
1040
922
  failPendingRun(GenerationServiceClientError.serviceInterrupted)
923
+ failPendingTextRun(GenerationServiceClientError.serviceInterrupted)
1041
924
  }
1042
925
 
1043
926
  private func invalidate() {
@@ -1150,6 +1033,7 @@ private enum GenerationServiceClientError: LocalizedError, Equatable {
1150
1033
 
1151
1034
  private final class GenerationServiceEventReceiver: NSObject, GenerationServiceClientProtocol {
1152
1035
  @MainActor var onEvent: ((GenerationWorkerEvent) -> Void)?
1036
+ @MainActor var onTextEvent: ((BonsaiTextGenerationEvent) -> Void)?
1153
1037
 
1154
1038
  func generationWorkerDidEmit(eventData: Data) {
1155
1039
  guard let event = try? JSONDecoder().decode(GenerationWorkerEvent.self, from: eventData) else { return }
@@ -1157,29 +1041,43 @@ private final class GenerationServiceEventReceiver: NSObject, GenerationServiceC
1157
1041
  onEvent?(event)
1158
1042
  }
1159
1043
  }
1044
+
1045
+
1046
+ func textGenerationWorkerDidEmit(eventData: Data) {
1047
+ guard let event = try? JSONDecoder().decode(BonsaiTextGenerationEvent.self, from: eventData) else { return }
1048
+ Task { @MainActor in
1049
+ onTextEvent?(event)
1050
+ }
1051
+ }
1160
1052
  }
1161
1053
 
1162
1054
  private struct TextGenerationRequest: Record {
1163
1055
  @Field var id: String?
1164
1056
  @Field var system: String?
1165
1057
  @Field var prompt: String = ""
1166
- @Field var outputSchema: [String: Any]?
1058
+ @Field var messages: [TextMessageRequest] = []
1167
1059
  @Field var temperature: Double?
1168
1060
  @Field var maxTokens: Int?
1169
- @Field var tools: [ToolDefinitionRequest] = []
1170
1061
  }
1171
1062
 
1172
- private struct ToolDefinitionRequest: Record {
1173
- @Field var name: String = ""
1174
- @Field var description: String = ""
1175
- @Field var inputSchema: [String: Any] = [:]
1063
+ private struct TextMessageRequest: Record {
1064
+ @Field var role: String = ""
1065
+ @Field var content: String = ""
1176
1066
  }
1177
1067
 
1178
- private struct ToolResultRequest: Record {
1179
- @Field var generationId: String = ""
1180
- @Field var callId: String = ""
1181
- @Field var output: String?
1182
- @Field var error: String?
1068
+ private struct LocalTextGenerationRequest: Record {
1069
+ @Field var id: String = ""
1070
+ @Field var model: String = ""
1071
+ @Field var modelPath: String = ""
1072
+ @Field var inputIds: [Int] = []
1073
+ @Field var endTokenIds: [Int] = []
1074
+ @Field var maxTokens: Int?
1075
+ @Field var temperature: Double?
1076
+ @Field var topP: Double?
1077
+ @Field var topK: Int?
1078
+ @Field var repetitionPenalty: Double?
1079
+ @Field var repetitionContextSize: Int?
1080
+ @Field var seed: Int?
1183
1081
  }
1184
1082
 
1185
1083
  private struct ImageGenerationRequest: Record {