@dr33m/react-native-litert-lm 0.5.5 → 0.6.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 (24) hide show
  1. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +38 -2
  2. package/ios/HybridLiteRTLM+Execute.swift +12 -0
  3. package/ios/HybridLiteRTLM.swift +53 -6
  4. package/lib/__mocks__/react-native-nitro-modules.d.ts +2 -0
  5. package/lib/__mocks__/react-native-nitro-modules.js +1 -0
  6. package/lib/__tests__/modelFactory.test.js +19 -0
  7. package/lib/modelFactory.js +13 -0
  8. package/lib/specs/LiteRTLM.nitro.d.ts +17 -0
  9. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +13 -0
  10. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +1 -0
  11. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +0 -2
  12. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +0 -2
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +6 -0
  14. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +2 -0
  15. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +6 -0
  16. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +1 -0
  17. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +11 -0
  18. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +1 -0
  19. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +1 -0
  20. package/package.json +1 -1
  21. package/src/__mocks__/react-native-nitro-modules.ts +1 -0
  22. package/src/__tests__/modelFactory.test.ts +21 -0
  23. package/src/modelFactory.ts +14 -0
  24. package/src/specs/LiteRTLM.nitro.ts +18 -0
@@ -563,6 +563,26 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
563
563
  createNewConversation()
564
564
  }
565
565
 
566
+ override fun resetConversationWith(messages: Array<Message>) {
567
+ val seed = messages.map { msg ->
568
+ LiteRTMessage(
569
+ role = when (msg.role) {
570
+ Role.MODEL -> com.google.ai.edge.litertlm.Role.MODEL
571
+ Role.SYSTEM -> com.google.ai.edge.litertlm.Role.SYSTEM
572
+ else -> com.google.ai.edge.litertlm.Role.USER
573
+ },
574
+ contents = Contents.of(Content.Text(msg.content))
575
+ )
576
+ }
577
+ synchronized(history) {
578
+ history.clear()
579
+ history.addAll(messages.toList())
580
+ }
581
+ // Recreating the conversation is what actually frees the old KV cache;
582
+ // the seed is replayed into the new one without triggering generation.
583
+ createNewConversation(seed)
584
+ }
585
+
566
586
  override fun isReady(): Boolean {
567
587
  return isLoaded_
568
588
  }
@@ -575,7 +595,10 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
575
595
  return lastStats
576
596
  }
577
597
 
578
- override fun getMemoryUsage(): MemoryUsage {
598
+ /** Shared by the public [getMemoryUsage] override and the pre-flight guard
599
+ * in [execute] — both need the same real, OS-level reading, not an
600
+ * estimate. */
601
+ private fun currentMemoryUsage(): MemoryUsage {
579
602
  // Native heap: allocated bytes from Debug APIs (most accurate for native allocations)
580
603
  val nativeHeapBytes = Debug.getNativeHeapAllocatedSize().toDouble()
581
604
 
@@ -619,6 +642,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
619
642
  )
620
643
  }
621
644
 
645
+ override fun getMemoryUsage(): MemoryUsage = currentMemoryUsage()
646
+
622
647
  override fun checkModelCapabilities(modelPath: String): ModelCapabilities {
623
648
  var supportsSpeculativeDecoding = false
624
649
  try {
@@ -668,7 +693,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
668
693
  }
669
694
  }
670
695
 
671
- private fun createNewConversation() {
696
+ private fun createNewConversation(initialMessages: List<LiteRTMessage> = emptyList()) {
672
697
  ensureLoaded()
673
698
  // v0.10.2 enforces single-session: close existing conversation first
674
699
  conversation?.let { oldConv ->
@@ -711,6 +736,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
711
736
  temperature = temperature.toDouble(),
712
737
  ),
713
738
  systemInstruction = systemPrompt?.let { Contents.of(Content.Text(it)) },
739
+ initialMessages = initialMessages,
714
740
  tools = lmTools ?: emptyList()
715
741
  )
716
742
  // TODO: maxOutputTokens is not configurable on Android — the Kotlin SDK's
@@ -784,6 +810,16 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
784
810
 
785
811
  return Promise.parallel {
786
812
  ensureLoaded()
813
+
814
+ // Refuse rather than risk it: growing the KV-cache under real memory
815
+ // pressure can fail at the native engine level in a way that never
816
+ // surfaces as a catchable Kotlin exception — it takes the whole
817
+ // process down. A plain RuntimeException here is a normal rejected
818
+ // Promise for callers, same contract as any other execute() failure.
819
+ if (currentMemoryUsage().isLowMemory) {
820
+ throw RuntimeException("LiteRTLM: Device memory is critically low; refusing to continue generation.")
821
+ }
822
+
787
823
  // Clear any previous tool calls before new inference
788
824
  pendingToolCalls.clear()
789
825
 
@@ -80,6 +80,18 @@ extension HybridLiteRTLM {
80
80
  return
81
81
  }
82
82
 
83
+ // Refuse rather than risk it: growing the KV-cache under real memory
84
+ // pressure can fail inside the underlying C engine in a way that
85
+ // never surfaces as a catchable Swift error — it takes the whole
86
+ // process down (Jetsam or a hard native failure). An NSError here
87
+ // is a normal rejected Promise for callers, same contract as any
88
+ // other execute() failure.
89
+ if Self.currentMemoryUsage().isLowMemory {
90
+ promise.reject(withError: NSError(domain: "LiteRTLM", code: 507,
91
+ userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: Device memory is critically low; refusing to continue generation."]))
92
+ return
93
+ }
94
+
83
95
  let payload: (json: String, tempFiles: [String])
84
96
  do { payload = try self.buildExecutePayload(preprocessed) }
85
97
  catch { promise.reject(withError: error); return }
@@ -92,6 +92,26 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
92
92
  }
93
93
  }
94
94
 
95
+ public func resetConversationWith(messages: [Message]) throws {
96
+ queue.sync {
97
+ history = messages
98
+ lastStats = GenerationStats(
99
+ promptTokens: 0.0,
100
+ completionTokens: 0.0,
101
+ totalTokens: 0.0,
102
+ timeToFirstToken: 0.0,
103
+ totalTime: 0.0,
104
+ tokensPerSecond: 0.0
105
+ )
106
+ if isLoaded && engine != nil {
107
+ // Recreating the conversation is what actually frees the old KV
108
+ // cache; the seed is replayed into the new one without
109
+ // triggering generation.
110
+ createNewConversation(initialMessages: messages)
111
+ }
112
+ }
113
+ }
114
+
95
115
  public func getStats() throws -> GenerationStats {
96
116
  return queue.sync { lastStats }
97
117
  }
@@ -111,9 +131,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
111
131
  }
112
132
 
113
133
  public func getMemoryUsage() throws -> MemoryUsage {
134
+ return Self.currentMemoryUsage()
135
+ }
136
+
137
+ /// Shared by the public `getMemoryUsage()` override and the pre-flight
138
+ /// guard in `HybridLiteRTLM+Execute.swift` — both need the same real,
139
+ /// OS-level reading, not an estimate. `static` since it needs no
140
+ /// instance state, which also makes it trivially callable from the
141
+ /// extension file.
142
+ static func currentMemoryUsage() -> MemoryUsage {
114
143
  var residentBytes: Double = 0.0
115
144
  var nativeHeapBytes: Double = 0.0
116
-
145
+
117
146
  // Retrieve process resident set size (RSS) via Mach basic task info
118
147
  var info = mach_task_basic_info()
119
148
  var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
@@ -122,18 +151,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
122
151
  task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
123
152
  }
124
153
  }
125
-
154
+
126
155
  if kerr == KERN_SUCCESS {
127
156
  residentBytes = Double(info.resident_size)
128
157
  nativeHeapBytes = Double(info.resident_size)
129
158
  }
130
-
159
+
131
160
  // os_proc_available_memory reports actual headroom available before Jetsam termination (iOS 13+)
132
161
  let availableBytes = Double(os_proc_available_memory())
133
-
162
+
134
163
  // Flag memory warning at ~200MB remaining headroom
135
164
  let isLowMemory = availableBytes < 200.0 * 1024.0 * 1024.0
136
-
165
+
137
166
  return MemoryUsage(
138
167
  nativeHeapBytes: nativeHeapBytes,
139
168
  residentBytes: residentBytes,
@@ -384,7 +413,7 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
384
413
 
385
414
  // MARK: - Internal Engine Helpers
386
415
 
387
- private func createNewConversation() {
416
+ private func createNewConversation(initialMessages: [Message] = []) {
388
417
  guard let engine = self.engine else { return }
389
418
 
390
419
  if let oldConv = self.conversation {
@@ -437,6 +466,24 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
437
466
  }
438
467
  }
439
468
 
469
+ if !initialMessages.isEmpty {
470
+ let payload = initialMessages.map { msg -> [String: String] in
471
+ let role: String
472
+ switch msg.role {
473
+ case .model: role = "model"
474
+ case .system: role = "system"
475
+ default: role = "user"
476
+ }
477
+ return ["role": role, "content": msg.content]
478
+ }
479
+ if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
480
+ let jsonString = String(data: data, encoding: .utf8) {
481
+ jsonString.withCString { messagesC in
482
+ litert_lm_conversation_config_set_messages(convConfig, messagesC)
483
+ }
484
+ }
485
+ }
486
+
440
487
  self.conversation = litert_lm_conversation_create(engine, convConfig)
441
488
  }
442
489
 
@@ -14,6 +14,7 @@ export declare const mockLiteRTLM: {
14
14
  sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
15
15
  getHistory: jest.Mock<never[], [], any>;
16
16
  resetConversation: jest.Mock<any, any, any>;
17
+ resetConversationWith: jest.Mock<any, any, any>;
17
18
  getStats: jest.Mock<{
18
19
  promptTokens: number;
19
20
  completionTokens: number;
@@ -54,6 +55,7 @@ export declare const NitroModules: {
54
55
  sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
55
56
  getHistory: jest.Mock<never[], [], any>;
56
57
  resetConversation: jest.Mock<any, any, any>;
58
+ resetConversationWith: jest.Mock<any, any, any>;
57
59
  getStats: jest.Mock<{
58
60
  promptTokens: number;
59
61
  completionTokens: number;
@@ -56,6 +56,7 @@ exports.mockLiteRTLM = {
56
56
  sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "audio", path: audioPath }], onToken).then(() => { })),
57
57
  getHistory: jest.fn(() => []),
58
58
  resetConversation: jest.fn(),
59
+ resetConversationWith: jest.fn(),
59
60
  getStats: jest.fn(() => ({
60
61
  promptTokens: 10,
61
62
  completionTokens: 20,
@@ -33,6 +33,25 @@ describe('modelFactory Security & Proxy Unit Tests', () => {
33
33
  expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversation).toHaveBeenCalled();
34
34
  expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
35
35
  });
36
+ it('should successfully proxy resetConversationWith and record memory metrics', async () => {
37
+ const seed = [{ role: 'user', content: 'earlier turn' }];
38
+ await llm.resetConversationWith(seed);
39
+ expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith).toHaveBeenCalledWith(seed);
40
+ expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
41
+ });
42
+ it('should report resetConversationWith as absent on older native builds', () => {
43
+ // The app feature-detects this method, so the proxy must not hand back a
44
+ // wrapper the native side cannot service.
45
+ const original = react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith;
46
+ // @ts-expect-error deliberately removing the method to simulate old native
47
+ delete react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith;
48
+ try {
49
+ expect((0, modelFactory_1.createLLM)().resetConversationWith).toBeUndefined();
50
+ }
51
+ finally {
52
+ react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith = original;
53
+ }
54
+ });
36
55
  it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
37
56
  const onToken = jest.fn();
38
57
  await llm.sendMessageAsync("Async prompt", onToken);
@@ -95,6 +95,19 @@ function createLLM(options) {
95
95
  return result;
96
96
  };
97
97
  }
98
+ if (prop === "resetConversationWith") {
99
+ // Absent on native builds older than this JS wrapper, and callers
100
+ // feature-detect it — so fall through rather than handing back a
101
+ // wrapper that would fail at the bridge.
102
+ if (typeof target.resetConversationWith !== "function") {
103
+ return undefined;
104
+ }
105
+ return (messages) => {
106
+ const result = target.resetConversationWith(messages);
107
+ recordMemorySnapshot();
108
+ return result;
109
+ };
110
+ }
98
111
  if (prop === "sendToolResponse") {
99
112
  return (responses, onToken) => {
100
113
  if (onToken) {
@@ -313,6 +313,23 @@ export interface LiteRTLM extends HybridObject<{
313
313
  * Clear the conversation context and start fresh.
314
314
  */
315
315
  resetConversation(): void;
316
+ /**
317
+ * Clear the conversation and re-seed it with prior turns.
318
+ *
319
+ * The engine allocates one KV cache per conversation, sized by
320
+ * `maxContextTokens`. Long tool-calling sessions fill it, and overflowing it
321
+ * aborts the process rather than raising a catchable error. Recreating the
322
+ * conversation frees the cache, but `resetConversation()` alone also throws
323
+ * away the thread.
324
+ *
325
+ * This seeds the fresh conversation with `messages` as prior context, so a
326
+ * caller can drop the oldest turns and keep the recent ones. The system
327
+ * prompt and tools configured at `loadModel` are reapplied automatically.
328
+ * No generation is triggered and no reply is produced.
329
+ *
330
+ * @param messages Prior turns, oldest first.
331
+ */
332
+ resetConversationWith(messages: Message[]): void;
316
333
  /**
317
334
  * Check if a model is loaded and ready for inference.
318
335
  */
@@ -288,6 +288,19 @@ namespace margelo::nitro::litertlm {
288
288
  static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("resetConversation");
289
289
  method(_javaPart);
290
290
  }
291
+ void JHybridLiteRTLMSpec::resetConversationWith(const std::vector<Message>& messages) {
292
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JMessage>> /* messages */)>("resetConversationWith");
293
+ method(_javaPart, [&](auto&& __input) {
294
+ size_t __size = __input.size();
295
+ jni::local_ref<jni::JArrayClass<JMessage>> __array = jni::JArrayClass<JMessage>::newArray(__size);
296
+ for (size_t __i = 0; __i < __size; __i++) {
297
+ const auto& __element = __input[__i];
298
+ auto __elementJni = JMessage::fromCpp(__element);
299
+ __array->setElement(__i, *__elementJni);
300
+ }
301
+ return __array;
302
+ }(messages));
303
+ }
291
304
  bool JHybridLiteRTLMSpec::isReady() {
292
305
  static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("isReady");
293
306
  auto __result = method(_javaPart);
@@ -66,6 +66,7 @@ namespace margelo::nitro::litertlm {
66
66
  std::shared_ptr<Promise<void>> sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) override;
67
67
  std::vector<Message> getHistory() override;
68
68
  void resetConversation() override;
69
+ void resetConversationWith(const std::vector<Message>& messages) override;
69
70
  bool isReady() override;
70
71
  GenerationStats getStats() override;
71
72
  double countTokens(const std::string& text) override;
@@ -10,7 +10,6 @@ package com.margelo.nitro.dev.litert.litertlm
10
10
  import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
- import dalvik.annotation.optimization.FastNative
14
13
 
15
14
 
16
15
  /**
@@ -59,7 +58,6 @@ class Func_void_double_cxx: Func_void_double {
59
58
  override fun invoke(progress: Double): Unit
60
59
  = invoke_cxx(progress)
61
60
 
62
- @FastNative
63
61
  private external fun invoke_cxx(progress: Double): Unit
64
62
  }
65
63
 
@@ -10,7 +10,6 @@ package com.margelo.nitro.dev.litert.litertlm
10
10
  import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
- import dalvik.annotation.optimization.FastNative
14
13
 
15
14
 
16
15
  /**
@@ -59,7 +58,6 @@ class Func_void_std__string_bool_cxx: Func_void_std__string_bool {
59
58
  override fun invoke(token: String, done: Boolean): Unit
60
59
  = invoke_cxx(token,done)
61
60
 
62
- @FastNative
63
61
  private external fun invoke_cxx(token: String, done: Boolean): Unit
64
62
  }
65
63
 
@@ -10,6 +10,7 @@ package com.margelo.nitro.dev.litert.litertlm
10
10
  import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
+ import dalvik.annotation.optimization.FastNative
13
14
  import com.margelo.nitro.core.Promise
14
15
  import com.margelo.nitro.core.HybridObject
15
16
 
@@ -97,6 +98,10 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
97
98
  @Keep
98
99
  abstract fun resetConversation(): Unit
99
100
 
101
+ @DoNotStrip
102
+ @Keep
103
+ abstract fun resetConversationWith(messages: Array<Message>): Unit
104
+
100
105
  @DoNotStrip
101
106
  @Keep
102
107
  abstract fun isReady(): Boolean
@@ -157,6 +162,7 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
157
162
  @Keep
158
163
  protected open class CxxPart(javaPart: HybridLiteRTLMSpec): HybridObject.CxxPart(javaPart) {
159
164
  // C++ JHybridLiteRTLMSpec::CxxPart::initHybrid(...)
165
+ @FastNative
160
166
  external override fun initHybrid(): HybridData
161
167
  }
162
168
  override fun createCxxPart(): CxxPart {
@@ -10,6 +10,7 @@ package com.margelo.nitro.dev.litert.litertlm
10
10
  import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
+ import dalvik.annotation.optimization.FastNative
13
14
  import com.margelo.nitro.core.Promise
14
15
  import com.margelo.nitro.core.HybridObject
15
16
 
@@ -64,6 +65,7 @@ abstract class HybridModelStoreSpec: HybridObject() {
64
65
  @Keep
65
66
  protected open class CxxPart(javaPart: HybridModelStoreSpec): HybridObject.CxxPart(javaPart) {
66
67
  // C++ JHybridModelStoreSpec::CxxPart::initHybrid(...)
68
+ @FastNative
67
69
  external override fun initHybrid(): HybridData
68
70
  }
69
71
  override fun createCxxPart(): CxxPart {
@@ -206,6 +206,12 @@ namespace margelo::nitro::litertlm {
206
206
  std::rethrow_exception(__result.error());
207
207
  }
208
208
  }
209
+ inline void resetConversationWith(const std::vector<Message>& messages) override {
210
+ auto __result = _swiftPart.resetConversationWith(messages);
211
+ if (__result.hasError()) [[unlikely]] {
212
+ std::rethrow_exception(__result.error());
213
+ }
214
+ }
209
215
  inline bool isReady() override {
210
216
  auto __result = _swiftPart.isReady();
211
217
  if (__result.hasError()) [[unlikely]] {
@@ -25,6 +25,7 @@ public protocol HybridLiteRTLMSpec_protocol: HybridObject {
25
25
  func sendMessageAsync(message: String, onToken: @escaping (_ token: String, _ done: Bool) -> Void) throws -> Promise<Void>
26
26
  func getHistory() throws -> [Message]
27
27
  func resetConversation() throws -> Void
28
+ func resetConversationWith(messages: [Message]) throws -> Void
28
29
  func isReady() throws -> Bool
29
30
  func getStats() throws -> GenerationStats
30
31
  func countTokens(text: String) throws -> Double
@@ -370,6 +370,17 @@ open class HybridLiteRTLMSpec_cxx {
370
370
  }
371
371
  }
372
372
 
373
+ @inline(__always)
374
+ public final func resetConversationWith(messages: bridge.std__vector_Message_) -> bridge.Result_void_ {
375
+ do {
376
+ try self.__implementation.resetConversationWith(messages: messages.map({ __item in __item }))
377
+ return bridge.create_Result_void_()
378
+ } catch (let __error) {
379
+ let __exceptionPtr = __error.toCpp()
380
+ return bridge.create_Result_void_(__exceptionPtr)
381
+ }
382
+ }
383
+
373
384
  @inline(__always)
374
385
  public final func isReady() -> bridge.Result_bool_ {
375
386
  do {
@@ -26,6 +26,7 @@ namespace margelo::nitro::litertlm {
26
26
  prototype.registerHybridMethod("sendMessageAsync", &HybridLiteRTLMSpec::sendMessageAsync);
27
27
  prototype.registerHybridMethod("getHistory", &HybridLiteRTLMSpec::getHistory);
28
28
  prototype.registerHybridMethod("resetConversation", &HybridLiteRTLMSpec::resetConversation);
29
+ prototype.registerHybridMethod("resetConversationWith", &HybridLiteRTLMSpec::resetConversationWith);
29
30
  prototype.registerHybridMethod("isReady", &HybridLiteRTLMSpec::isReady);
30
31
  prototype.registerHybridMethod("getStats", &HybridLiteRTLMSpec::getStats);
31
32
  prototype.registerHybridMethod("countTokens", &HybridLiteRTLMSpec::countTokens);
@@ -90,6 +90,7 @@ namespace margelo::nitro::litertlm {
90
90
  virtual std::shared_ptr<Promise<void>> sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) = 0;
91
91
  virtual std::vector<Message> getHistory() = 0;
92
92
  virtual void resetConversation() = 0;
93
+ virtual void resetConversationWith(const std::vector<Message>& messages) = 0;
93
94
  virtual bool isReady() = 0;
94
95
  virtual GenerationStats getStats() = 0;
95
96
  virtual double countTokens(const std::string& text) = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr33m/react-native-litert-lm",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "litertLm": {
5
5
  "version": "0.12.0",
6
6
  "androidMavenVersion": "0.12.0",
@@ -81,6 +81,7 @@ export const mockLiteRTLM = {
81
81
  ),
82
82
  getHistory: jest.fn(() => []),
83
83
  resetConversation: jest.fn(),
84
+ resetConversationWith: jest.fn(),
84
85
  getStats: jest.fn(() => ({
85
86
  promptTokens: 10,
86
87
  completionTokens: 20,
@@ -49,6 +49,27 @@ describe('modelFactory Security & Proxy Unit Tests', () => {
49
49
  expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
50
50
  });
51
51
 
52
+ it('should successfully proxy resetConversationWith and record memory metrics', async () => {
53
+ const seed = [{ role: 'user', content: 'earlier turn' }];
54
+ await llm.resetConversationWith(seed as never);
55
+
56
+ expect(mockLiteRTLM.resetConversationWith).toHaveBeenCalledWith(seed);
57
+ expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
58
+ });
59
+
60
+ it('should report resetConversationWith as absent on older native builds', () => {
61
+ // The app feature-detects this method, so the proxy must not hand back a
62
+ // wrapper the native side cannot service.
63
+ const original = mockLiteRTLM.resetConversationWith;
64
+ // @ts-expect-error deliberately removing the method to simulate old native
65
+ delete mockLiteRTLM.resetConversationWith;
66
+ try {
67
+ expect(createLLM().resetConversationWith).toBeUndefined();
68
+ } finally {
69
+ mockLiteRTLM.resetConversationWith = original;
70
+ }
71
+ });
72
+
52
73
  it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
53
74
  const onToken = jest.fn();
54
75
  await llm.sendMessageAsync("Async prompt", onToken);
@@ -130,6 +130,20 @@ export function createLLM(options?: {
130
130
  };
131
131
  }
132
132
 
133
+ if (prop === "resetConversationWith") {
134
+ // Absent on native builds older than this JS wrapper, and callers
135
+ // feature-detect it — so fall through rather than handing back a
136
+ // wrapper that would fail at the bridge.
137
+ if (typeof (target as any).resetConversationWith !== "function") {
138
+ return undefined;
139
+ }
140
+ return (messages: unknown[]) => {
141
+ const result = (target as any).resetConversationWith(messages);
142
+ recordMemorySnapshot();
143
+ return result;
144
+ };
145
+ }
146
+
133
147
  if (prop === "sendToolResponse") {
134
148
  return (responses: any[], onToken?: TokenCallback) => {
135
149
  if (onToken) {
@@ -364,6 +364,24 @@ export interface LiteRTLM extends HybridObject<{
364
364
  */
365
365
  resetConversation(): void;
366
366
 
367
+ /**
368
+ * Clear the conversation and re-seed it with prior turns.
369
+ *
370
+ * The engine allocates one KV cache per conversation, sized by
371
+ * `maxContextTokens`. Long tool-calling sessions fill it, and overflowing it
372
+ * aborts the process rather than raising a catchable error. Recreating the
373
+ * conversation frees the cache, but `resetConversation()` alone also throws
374
+ * away the thread.
375
+ *
376
+ * This seeds the fresh conversation with `messages` as prior context, so a
377
+ * caller can drop the oldest turns and keep the recent ones. The system
378
+ * prompt and tools configured at `loadModel` are reapplied automatically.
379
+ * No generation is triggered and no reply is produced.
380
+ *
381
+ * @param messages Prior turns, oldest first.
382
+ */
383
+ resetConversationWith(messages: Message[]): void;
384
+
367
385
  /**
368
386
  * Check if a model is loaded and ready for inference.
369
387
  */