@dr33m/react-native-litert-lm 0.6.2 → 0.6.4

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.
@@ -749,14 +749,36 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
749
749
  // for it while holding the lock would block loadModel behind it.
750
750
  cancelInFlightGeneration()
751
751
  synchronized(initLock) {
752
+ /*
753
+ * Dropped first, closed second.
754
+ *
755
+ * These used to be nulled after their `close()` calls, inside one
756
+ * try. A close that threw — and closing a conversation that has
757
+ * just been cancelled is exactly when one does — skipped the rest
758
+ * of the block and left a retired engine still referenced. Nothing
759
+ * downstream could tell: `isReady()` reads `engine != null`, so the
760
+ * instance went on reporting itself loaded, and the next
761
+ * `resetConversation()` called `createConversation` on a closed
762
+ * engine and threw "Engine is not initialized" from deep in the SDK.
763
+ *
764
+ * Letting go of the references first makes that impossible: after
765
+ * this block the instance is unloaded whatever the native side did.
766
+ */
767
+ val retiredConversation = conversation
768
+ val retiredEngine = engine
769
+ conversation = null
770
+ engine = null
771
+ loadedModelPath = null
772
+
773
+ try {
774
+ retiredConversation?.close()
775
+ } catch (e: Exception) {
776
+ Log.w(TAG, "Error closing conversation: ${e.message}")
777
+ }
752
778
  try {
753
- conversation?.close()
754
- conversation = null
755
- engine?.close() // Direct call
756
- engine = null
757
- loadedModelPath = null
779
+ retiredEngine?.close()
758
780
  } catch (e: Exception) {
759
- Log.e(TAG, "Error closing resources", e)
781
+ Log.w(TAG, "Error closing engine: ${e.message}")
760
782
  }
761
783
  }
762
784
  }
@@ -778,7 +800,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
778
800
  }
779
801
  conversation = null
780
802
  }
781
- // Map tools capture tool calls for JS instead of executing natively
803
+ // Map tools. The engine only parses calls; JS runs them (see `automaticToolCalling`).
782
804
  val lmTools: List<ToolProvider>? = tools?.map { toolDef ->
783
805
  val apiTool = object : OpenApiTool {
784
806
  override fun getToolDescriptionJsonString(): String {
@@ -790,12 +812,9 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
790
812
  return fullDesc.toString()
791
813
  }
792
814
  override fun execute(paramsJsonString: String): String {
793
- Log.d(TAG, "Tool called: ${toolDef.name} with args: $paramsJsonString")
794
- pendingToolCalls.add(ToolCall(
795
- name = toolDef.name,
796
- argumentsJson = paramsJsonString
797
- ))
798
- return "{\"status\": \"pending\", \"message\": \"Tool execution delegated to application\"}"
815
+ // Never invoked: automatic tool calling is off, so tool
816
+ // execution happens on the JS side.
817
+ return "{}"
799
818
  }
800
819
  }
801
820
  tool(apiTool)
@@ -812,6 +831,17 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
812
831
  systemInstruction = systemPrompt?.let { Contents.of(Content.Text(it)) },
813
832
  initialMessages = initialMessages,
814
833
  tools = lmTools ?: emptyList(),
834
+ /*
835
+ * Off, ported from upstream (hung-yueh 2cc938a). The SDK default runs
836
+ * each parsed call against `execute()` above, which was a stub, and
837
+ * feeds that stub's reply straight back to the model in the same
838
+ * turn. The model then answered the stub: prose claiming a result it
839
+ * never had ("I have removed it…"), streamed to the reader, then
840
+ * discarded once JS ran the real tool and generated again. A wasted
841
+ * generation per call, and its tokens stayed in the KV cache.
842
+ * With it off, calls arrive on `Message.toolCalls` instead.
843
+ */
844
+ automaticToolCalling = false,
815
845
  // Available since LiteRT-LM 0.15.0; before that the Kotlin SDK had no
816
846
  // way to express it and every Android reply ran to the engine default.
817
847
  maxOutputToken = maxOutputTokens,
@@ -825,7 +855,12 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
825
855
  // - Kotlin: Not yet available — track at https://github.com/google-ai-edge/LiteRT-LM
826
856
  //
827
857
  // Once the Kotlin SDK exposes this, wire it via ConversationConfig here.
828
- conversation = engine!!.createConversation(convConfig)
858
+ // Checked rather than forced: this runs from `resetConversation`, which
859
+ // JS calls on paths that do not first ask whether anything is loaded.
860
+ // A named error beats a null-pointer dereference from inside the SDK.
861
+ val activeEngine = engine
862
+ ?: throw RuntimeException("Cannot create a conversation: no model is loaded.")
863
+ conversation = activeEngine.createConversation(convConfig)
829
864
  }
830
865
 
831
866
 
@@ -996,7 +1031,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
996
1031
  history = history,
997
1032
  userMessage = userTextRepresentation,
998
1033
  onStatsReady = { stats -> lastStats = stats },
999
- onFailure = { e -> errorRef.set(e) }
1034
+ onFailure = { e -> errorRef.set(e) },
1035
+ onToolCalls = { calls -> captureToolCalls(calls) }
1000
1036
  )
1001
1037
 
1002
1038
  try {
@@ -1034,6 +1070,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
1034
1070
  .joinToString("") { it.text }
1035
1071
 
1036
1072
  val thinkingText = responseMsg.channels["thought"] ?: ""
1073
+ captureToolCalls(responseMsg.toolCalls)
1037
1074
 
1038
1075
  history.add(Message(Role.MODEL, response))
1039
1076
 
@@ -1067,6 +1104,15 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
1067
1104
  }
1068
1105
  }
1069
1106
 
1107
+ /** Records SDK-parsed calls in the shape JS receives on `ExecuteResult.toolCalls`. */
1108
+ private fun captureToolCalls(calls: List<com.google.ai.edge.litertlm.ToolCall>?) {
1109
+ for (call in calls.orEmpty()) {
1110
+ val argumentsJson = org.json.JSONObject(call.arguments).toString()
1111
+ Log.d(TAG, "Tool called: ${call.name} with args: $argumentsJson")
1112
+ pendingToolCalls.add(ToolCall(name = call.name, argumentsJson = argumentsJson))
1113
+ }
1114
+ }
1115
+
1070
1116
  override fun sendToolResponse(
1071
1117
  responses: Array<ToolResponse>,
1072
1118
  onToken: ((token: String, done: Boolean) -> Unit)?
@@ -18,6 +18,8 @@ internal class StreamingCallbackListener(
18
18
  private val onStatsReady: (GenerationStats) -> Unit,
19
19
  private val onFailure: ((Throwable) -> Unit)? = null,
20
20
  private val onThinkingToken: ((String) -> Unit)? = null,
21
+ /** Tool calls the engine parsed out of this reply. They arrive on the final message. */
22
+ private val onToolCalls: ((List<com.google.ai.edge.litertlm.ToolCall>) -> Unit)? = null,
21
23
  ) : com.google.ai.edge.litertlm.MessageCallback {
22
24
 
23
25
  private val startTime = System.nanoTime()
@@ -29,6 +31,8 @@ internal class StreamingCallbackListener(
29
31
  .filterIsInstance<Content.Text>()
30
32
  .joinToString("") { it.text }
31
33
 
34
+ if (message.toolCalls.isNotEmpty()) onToolCalls?.invoke(message.toolCalls)
35
+
32
36
  // Capture thinking from the "thought" channel
33
37
  val thinkingChunk = message.channels["thought"]
34
38
  if (!thinkingChunk.isNullOrEmpty()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr33m/react-native-litert-lm",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "litertLm": {
5
5
  "version": "0.15.0",
6
6
  "androidMavenVersion": "0.15.0",