@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineScope
1212import kotlinx.coroutines.Dispatchers
1313import kotlinx.coroutines.SupervisorJob
1414import kotlinx.coroutines.launch
15+ import kotlinx.coroutines.withContext
1516import kotlinx.coroutines.cancel
1617import kotlinx.coroutines.delay
1718import kotlinx.coroutines.Job
@@ -44,9 +45,23 @@ import com.gemma.api.mcp.MCPServer
4445 */
4546class GemmaService : Service (), AgentPlatformCallbacks {
4647
48+ inner class LocalBinder : android.os.Binder () {
49+ fun getService (): GemmaService = this @GemmaService
50+ }
51+
52+ // UI streaming interface for the native chat activity
53+ interface UiCallback {
54+ fun onMessageAdded (message : String , isUser : Boolean , isComplete : Boolean = true)
55+ fun onThinkingStateChanged (isThinking : Boolean )
56+ }
57+
58+ var uiCallback: UiCallback ? = null
59+
60+ private val serviceScope = CoroutineScope (Dispatchers .Default + Job ())
61+
4762 // Mandatory Service implementation
4863 override fun onBind (intent : Intent ? ): IBinder ? {
49- return null
64+ return LocalBinder ()
5065 }
5166
5267 // IO dispatcher: inference + DB work must not compete with the UI render thread.
@@ -698,20 +713,57 @@ class GemmaService : Service(), AgentPlatformCallbacks {
698713 }
699714 }
700715
701- suspend fun processQuery (userPrompt : String , sessionId : String , isDream : Boolean = false): String {
716+ /* *
717+ * Called directly by MainActivity to process native chat interface queries.
718+ * It uses the same backend engine but avoids spinning up unnecessary Overlay/Audio managers.
719+ */
720+ fun processQueryFromUi (prompt : String ) {
721+ if (! ::koogAgent.isInitialized || ! koogAgent.isReady) {
722+ uiCallback?.onMessageAdded(" Agent is still initializing. Please wait a moment and try again." , isUser = false )
723+ return
724+ }
725+
726+ // Let UI know we accepted it
727+ uiCallback?.onMessageAdded(prompt, isUser = true )
728+ uiCallback?.onThinkingStateChanged(true )
729+
730+ serviceScope.launch {
731+ try {
732+ // Pass it through the core pipeline without triggering TTS audio unless explicitly asked
733+ // processQuery signature: suspend fun processQuery(userPrompt: String, sessionId: String? = null): String
734+ val response = processQuery(prompt, null , false )
735+
736+ withContext(Dispatchers .Main ) {
737+ uiCallback?.onThinkingStateChanged(false )
738+ uiCallback?.onMessageAdded(response ? : " Error: Did not generate response." , isUser = false )
739+ }
740+ } catch (e: Exception ) {
741+ Timber .e(e, " UI processing failure" )
742+ withContext(Dispatchers .Main ) {
743+ uiCallback?.onThinkingStateChanged(false )
744+ uiCallback?.onMessageAdded(" Error: ${e.message} " , isUser = false )
745+ }
746+ }
747+ }
748+ }
749+
750+ /* *
751+ * Core orchestrator: Context gathering + LLM reasoning + Tool execution
752+ */
753+ suspend fun processQuery (userPrompt : String , sessionId : String? = null, isDream : Boolean = false): String? {
702754 if (! ::koogAgent.isInitialized || ! koogAgent.isReady) {
703755 responseNotificationManager.showResponse(" ⚠️ Agent still starting up... try again in a moment" )
704756 return " Agent is still initializing. Please wait a moment and try again."
705757 }
706758
707759 if (! isDream) markActivity()
708760
709- return kotlinx.coroutines.withTimeout (120000 ) {
761+ return kotlinx.coroutines.withTimeoutOrNull (120000 ) {
710762 koogAgent.processUserMessage(
711763 message = userPrompt,
712- sessionId = sessionId,
764+ sessionId = sessionId ? : java.util. UUID .randomUUID().toString() ,
713765 isDream = isDream
714- )
766+ ) ? : " Error: Agent returned null. "
715767 }
716768 }
717769
@@ -861,6 +913,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
861913 private var currentFrame = 0
862914 private var animationJob: Job ? = null
863915 private var lastActivityTime = System .currentTimeMillis()
916+ private var lastKvFlushTime = System .currentTimeMillis()
864917
865918 private fun startAnimationLoop () {
866919 animationJob?.cancel()
@@ -870,6 +923,16 @@ class GemmaService : Service(), AgentPlatformCallbacks {
870923 val frame = getNextAnimationFrame()
871924 updateNotification(" ✧💭 $frame " )
872925 }
926+
927+ val now = System .currentTimeMillis()
928+ if (now - lastActivityTime > 15 * 60 * 1000 && now - lastKvFlushTime > 15 * 60 * 1000 ) {
929+ lastKvFlushTime = now
930+ if (::koogAgent.isInitialized) {
931+ Timber .d(" GemmaService: Triggering 15-min inactivity KV Cache Flush" )
932+ koogAgent.sendSystemEvent(KoogAgent .SystemEventType .KV_CACHE_FLUSH )
933+ }
934+ }
935+
873936 delay(500 ) // 500ms per frame
874937 }
875938 }
@@ -927,7 +990,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
927990 // Checkpoint agent state in case process is killed next
928991 if (::koogAgent.isInitialized) {
929992 try {
930- kotlinx.coroutines.runBlocking {
993+ kotlinx.coroutines.runBlocking( Dispatchers . IO ) {
931994 kotlinx.coroutines.withTimeoutOrNull(3000L ) {
932995 koogAgent.checkpoint()
933996 }
@@ -960,7 +1023,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
9601023 private fun performCriticalCleanup () {
9611024 try {
9621025 // Use runBlocking to ensure cleanup completes (Kimi's fix for GlobalScope leak)
963- kotlinx.coroutines.runBlocking {
1026+ kotlinx.coroutines.runBlocking( Dispatchers . IO ) {
9641027 kotlinx.coroutines.withTimeoutOrNull(5000L ) {
9651028 // Shutdown agent (closes event queue and checkpoints)
9661029 if (::koogAgent.isInitialized) {
0 commit comments