Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ResolveKit Android SDK — native runtime, UI, and tool function integration for

ResolveKit Android SDK provides runtime orchestration, tool calling, and chat UI surfaces for Kotlin apps.

**Tech Stack**: Kotlin 1.9.22, Compose, KSP, OkHttp, kotlinx.serialization
**Tech Stack**: Kotlin 1.9.25, Compose, KSP, OkHttp, kotlinx.serialization
**Min SDK**: 26 | **Compile SDK**: 36 | **JDK**: 17
**Packages**: `app.resolvekit:sdk`, `app.resolvekit:authoring`, `app.resolvekit:ksp`

Expand All @@ -33,6 +33,11 @@ curl -sL https://raw.githubusercontent.com/resolve-kit/resolvekit-skills/master/

When a user asks to integrate ResolveKit into their Android project, load `resolvekit-android-integration` and follow its steps.

## Documentation

- [Documentation Index](docs/INDEX.md) — full map of durable docs, execution plans, and tech debt tracking.
- [Agent-First Harness Notes](docs/agent-first/README.md) — how this repo's agent-first doc structure is enforced in CI.

## First Read

1. `build.gradle.kts` for project structure and plugins.
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.9.25"
ksp = "1.9.22-1.0.17"
ksp = "1.9.25-1.0.20"
coroutines = "1.7.3"
serialization = "1.6.3"
okhttp = "4.12.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ class ResolveKitAPIClient(
request: ToolResultRequest
): ToolResultResponse = post("/v1/sessions/$sessionId/tool-results", request, token)

/** POST /v1/sessions/{id}/feedback — submit a CSAT rating (1-5). */
suspend fun submitFeedback(
sessionId: String,
token: String,
request: FeedbackRequest
): FeedbackResponse = post("/v1/sessions/$sessionId/feedback", request, token)

// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ class ResolveKitEventStreamClient(
ResolveKitEvent.ServerError(envelope.eventId, p.code, p.message, p.recoverable)
}.getOrNull()

"session_escalated" -> runCatching {
val p = json.decodeFromJsonElement<SessionEscalatedPayload>(envelope.payload)
ResolveKitEvent.SessionEscalated(envelope.eventId, p.reason)
}.getOrNull()

"human_message" -> runCatching {
val p = json.decodeFromJsonElement<HumanMessagePayload>(envelope.payload)
ResolveKitEvent.HumanMessage(envelope.eventId, p.messageId, p.text)
}.getOrNull()

"feedback_requested" -> runCatching {
val p = json.decodeFromJsonElement<FeedbackRequestedPayload>(envelope.payload)
ResolveKitEvent.FeedbackRequested(envelope.eventId, p.immediate)
}.getOrDefault(ResolveKitEvent.FeedbackRequested(envelope.eventId, immediate = false))

else -> ResolveKitEvent.Unknown(envelope.eventId, envelope.type)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ data class ServerErrorPayload(
val recoverable: Boolean = true
)

@Serializable
data class SessionEscalatedPayload(
val reason: String
)

@Serializable
data class HumanMessagePayload(
@SerialName("message_id") val messageId: String,
val text: String
)

@Serializable
data class FeedbackRequestedPayload(
val immediate: Boolean = false
)

sealed class ResolveKitEvent {
data class TextDelta(
val eventId: String,
Expand Down Expand Up @@ -69,6 +85,22 @@ sealed class ResolveKitEvent {
val recoverable: Boolean
) : ResolveKitEvent()

data class SessionEscalated(
val eventId: String,
val reason: String
) : ResolveKitEvent()

data class HumanMessage(
val eventId: String,
val messageId: String,
val text: String
) : ResolveKitEvent()

data class FeedbackRequested(
val eventId: String,
val immediate: Boolean
) : ResolveKitEvent()

data class Unknown(
val eventId: String,
val type: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ data class ToolResultResponse(
val deduplicated: Boolean = false
)

@Serializable
data class FeedbackRequest(
val rating: Int,
val comment: String? = null
)

@Serializable
data class FeedbackResponse(
val id: String,
@SerialName("session_id") val sessionId: String,
val rating: Int,
val comment: String? = null
)

@Serializable
data class SessionLocalization(
val locale: String,
Expand Down
5 changes: 4 additions & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ android {
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
// Must track the Compose-Kotlin compatibility map for the Kotlin
// version pinned in gradle/libs.versions.toml (currently 1.9.25).
// https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion = "1.5.15"
}

compileOptions {
Expand Down
5 changes: 4 additions & 1 deletion sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
<application
android:name=".SampleApplication"
android:label="ResolveKit Sample"
android:theme="@style/Theme.Material3.Light.NoActionBar">
android:theme="@style/Theme.Material3.Light.NoActionBar"
android:usesCleartextTraffic="true">
<!-- Allows plain HTTP for staging/local dev environments, matching the
iOS sample's NSAllowsArbitraryLoads exception for the same reason. -->

<!-- Launcher — redirects to Dashboard -->
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import app.resolvekit.ui.ResolveKitChatActivity

/**
* Dashboard activity showcasing all integration patterns available in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package app.resolvekit.sample

import android.app.Application
import app.resolvekit.core.AnyResolveKitFunction
import app.resolvekit.core.JSONValue
import app.resolvekit.core.ResolveKitFunctionPack
import app.resolvekit.core.ResolveKitPlatform
import app.resolvekit.ui.ResolveKitConfiguration

/**
Expand Down Expand Up @@ -65,7 +67,8 @@ class SampleApplication : Application() {
* This pack bundles utility functions for the sample app.
*/
object SampleFunctionPack : ResolveKitFunctionPack {
override val id = "sample_utilities"
override val packName = "sample_utilities"
override val supportedPlatforms = listOf(ResolveKitPlatform.ANDROID)
override val functions = listOf(EchoMessageAdapterHolder)
}

Expand All @@ -81,7 +84,7 @@ object EchoMessageAdapterHolder : AnyResolveKitFunction {
// See SampleFunctions.kt for the @ResolveKit annotation on EchoMessage.
override val resolveKitName = "echo_message"
override val resolveKitDescription = "Echoes back the provided message"
override val resolveKitParametersSchema: Map<String, Any> = mapOf(
override val resolveKitParametersSchema: app.resolvekit.core.JSONObject = mapOf(
"type" to JSONValue.String("object"),
"properties" to JSONValue.Object(mapOf(
"message" to JSONValue.Object(mapOf("type" to JSONValue.String("string")))
Expand Down
5 changes: 4 additions & 1 deletion ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ android {
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
// Must track the Compose-Kotlin compatibility map for the Kotlin
// version pinned in gradle/libs.versions.toml (currently 1.9.25).
// https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion = "1.5.15"
}

compileOptions {
Expand Down
71 changes: 67 additions & 4 deletions ui/src/main/kotlin/app/resolvekit/ui/ResolveKitChatView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ fun ResolveKitChatView(
val messagePlaceholder by runtime.messagePlaceholder.collectAsState()
val appearanceMode by runtime.appearanceMode.collectAsState()
val chatTheme by runtime.chatTheme.collectAsState()
val isEscalated by runtime.isEscalated.collectAsState()
val pendingFeedbackRequest by runtime.pendingFeedbackRequest.collectAsState()

val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
Expand Down Expand Up @@ -117,6 +119,10 @@ fun ResolveKitChatView(
else -> Unit
}

if (isEscalated) {
ConnectionBanner("Connecting you with a human agent…", palette)
}

// ----- Message list -----
LazyColumn(
state = listState,
Expand Down Expand Up @@ -157,6 +163,14 @@ fun ResolveKitChatView(
)
}

if (pendingFeedbackRequest) {
FeedbackPromptCard(
palette = palette,
onRate = { rating -> scope.launch { runtime.submitFeedback(rating) } },
onDismiss = { runtime.dismissFeedbackRequest() }
)
}

var inputText by remember { mutableStateOf("") }
val canSend = inputText.isNotBlank() && !isTurnInProgress &&
connectionState == ResolveKitConnectionState.ACTIVE
Expand Down Expand Up @@ -252,12 +266,61 @@ private fun ChatBubble(message: ResolveKitChatMessage, palette: ResolveKitPalett
color = if (isUser) palette.userBubbleBackground else palette.assistantBubbleBackground,
modifier = Modifier.widthIn(max = 300.dp)
) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)) {
if (message.role == ChatMessageRole.HUMAN_AGENT) {
Text(
text = "Support Agent",
color = palette.statusText,
style = MaterialTheme.typography.labelSmall
)
Spacer(Modifier.height(2.dp))
}
Text(
text = message.text,
color = if (isUser) palette.userBubbleText else palette.assistantBubbleText,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}

@Composable
private fun FeedbackPromptCard(
palette: ResolveKitPaletteColors,
onRate: (Int) -> Unit,
onDismiss: () -> Unit
) {
Surface(
color = palette.toolCardBackground,
shape = RoundedCornerShape(14.dp),
border = BorderStroke(1.dp, palette.toolCardBorder),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)) {
Text(
text = message.text,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
color = if (isUser) palette.userBubbleText else palette.assistantBubbleText,
style = MaterialTheme.typography.bodyMedium
text = "How did we do?",
color = palette.titleText,
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
for (rating in 1..5) {
TextButton(onClick = { onRate(rating) }) {
Text(
text = "★",
color = palette.statusText,
style = MaterialTheme.typography.titleMedium
)
}
}
Spacer(Modifier.weight(1f))
TextButton(onClick = onDismiss) {
Text("Dismiss", color = palette.statusText)
}
}
}
}
}
Expand Down
Loading