Android SDK for embedding ResolveKit agent chat, tool calling, and host UI surfaces into Android apps.
Support is moving into the product. ResolveKit is where it lands.
| Chat View | Tool Call Approval | Activity Integration |
|---|---|---|
![]() |
![]() |
![]() |
| Live agent chat with streaming responses | User approval for destructive tool calls | Full-screen and embedded host surfaces |
Note: Screenshots are placeholders. Replace
docs/assets/screenshots/with actual captures from the sample app.
End-to-end flow: SDK init → chat session → tool call → approval → result
┌─────────────────────────────────────────────────────────────┐
│ Your Android App │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Compose UI │ │ Activity │ │ Fragment │ │
│ │ChatView │ │ ChatActivity│ │ ChatFragment │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ ResolveKitRuntime │ │
│ │ (Session lifecycle, │ │
│ │ event handling, │ │
│ │ tool call mgmt) │ │
│ └─────────┬───────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ core/ │ │ networking/ │ │ authoring/ │ │
│ │ Types, │ │ API client, │ │ @ResolveKit │ │
│ │ Registry, │ │ Event stream │ │ annotation, │ │
│ │ Functions │ │ (WebSocket) │ │ KSP gen │ │
│ └───────────────┘ └───────────────┘ └───────┬───────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ ksp/ │ │
│ │ Code generation │ │
│ │ for tool adapters │ │
│ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ sdk/ (Facade) │
│ Single dependency that bundles ui + core + networking │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ ResolveKit Backend │
│ (Session management, │
│ LLM routing, agent │
│ orchestration) │
└───────────────────────┘
The repository contains:
- a Kotlin-first runtime for ResolveKit sessions
- a Jetpack Compose chat UI
- Activity and Fragment host surfaces for non-Compose apps
- a KSP-based authoring path for defining tools with
@ResolveKit - a sample Android app
| Module | Purpose |
|---|---|
sdk |
Public facade artifact that pulls in the default ResolveKit runtime and chat UI stack |
core |
Shared JSON types, tool definitions, registry, and errors |
networking |
ResolveKit API and event stream clients |
ui |
Runtime, Compose chat view, ResolveKitChatActivity, and ResolveKitChatFragment |
authoring |
@ResolveKit annotation and authoring interfaces |
ksp |
KSP processor that generates tool adapters |
sample |
Reference Android application |
| Requirement | Value |
|---|---|
| Min SDK | 26 (Android 8.0) |
| Target SDK | 36 |
| JDK | 17 |
| Android Studio | Ladybug+ |
| Gradle | 8.9+ (wrapper included) |
| Compose | BOM 2024.02.00 |
| Kotlin | 1.9.22 |
ResolveKit is set up to publish these coordinates:
app.resolvekit:sdk:<version>
app.resolvekit:authoring:<version>
app.resolvekit:ksp:<version>
core, networking, and ui remain published for advanced/internal use, but sdk is the default dependency for app consumers.
For most apps, depend on sdk. It brings in the default ResolveKit runtime and UI stack.
dependencies {
implementation("app.resolvekit:sdk:1.0.1")
}If you want the annotation-based tool authoring path:
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation("app.resolvekit:sdk:1.0.1")
implementation("app.resolvekit:authoring:1.0.1")
ksp("app.resolvekit:ksp:1.0.1")
}Use this when you need preview artifacts before a Maven Central release.
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/resolve-kit/resolvekit-android-sdk")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}Then use the same coordinates shown above.
If you are working on the SDK locally and want to consume it from another app without publishing externally:
./gradlew publishToMavenLocalThen add mavenLocal() to the consuming app:
repositories {
mavenLocal()
google()
mavenCentral()
}For active SDK development, composite builds are usually the best Android Studio workflow.
In the host app settings.gradle.kts:
includeBuild("../resolvekit-android-sdk")Then depend on the project coordinates normally:
dependencies {
implementation("app.resolvekit:sdk:1.0.1")
implementation("app.resolvekit:authoring:1.0.1")
ksp("app.resolvekit:ksp:1.0.1")
}Gradle will substitute the local modules automatically.
val runtime = ResolveKitRuntime(
configuration = ResolveKitConfiguration(
apiKeyProvider = { "rk_your_key_here" },
functions = listOf(GetCurrentTime)
),
context = applicationContext
)
setContent {
MaterialTheme {
ResolveKitChatView(runtime = runtime)
}
}startActivity(
ResolveKitChatActivity.createIntent(
context = this,
configuration = ResolveKitConfiguration(
apiKeyProvider = { "rk_your_key_here" },
functions = listOf(GetCurrentTime)
)
)
)supportFragmentManager.beginTransaction()
.replace(
R.id.container,
ResolveKitChatFragment.newInstance(
ResolveKitConfiguration(
apiKeyProvider = { "rk_your_key_here" },
functions = listOf(GetCurrentTime)
)
)
)
.commit()ResolveKitConfiguration is passed to ResolveKitRuntime at initialization and is immutable after that point.
ResolveKitConfiguration(
baseUrl: String = "https://agent.example.com",
apiKeyProvider: () -> String?,
deviceIdProvider: () -> String? = { null },
llmContextProvider: () -> JSONObject = { emptyMap() },
availableFunctionNamesProvider: (() -> List<String>)? = null,
localeProvider: (() -> String)? = null,
preferredLocalesProvider: (() -> List<String>)? = null,
functions: List<AnyResolveKitFunction> = emptyList(),
functionPacks: List<ResolveKitFunctionPack> = emptyList(),
context: Context
)Type: String | Required: No | Default: https://agent.example.com
Base URL of the ResolveKit backend. Override only when self-hosting:
baseUrl = "https://your-backend.example.com"Type: () -> String? | Required: Yes
Called at the start of each session. Return null or an empty string to block connection.
apiKeyProvider = { SecureConfig.getApiKey() }Type: () -> String? | Required: No | Default: { null }
Stable device or user identifier used to correlate sessions across app launches. If null is returned, the SDK generates and persists a UUID automatically.
deviceIdProvider = {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString("device_id", null) ?: run {
val id = UUID.randomUUID().toString()
prefs.edit().putString("device_id", id).apply()
id
}
}Type: () -> JSONObject | Required: No | Default: { emptyMap() }
Custom JSON context sent as llm_context during session creation. Use it to pass user preferences, location, app state, or any signal the agent needs at routing time.
Type: (() -> String)? | Required: No | Default: null
Provides the preferred locale for the chat session as a BCP 47 language tag (e.g. "en", "lt", "fr-CA"). If null, the SDK resolves locale from system settings.
Type: List<AnyResolveKitFunction> | Required: No | Default: emptyList()
List of tool functions available to the agent.
ResolveKitRuntime exposes the following state via StateFlow:
| Property | Type | Description |
|---|---|---|
messages |
StateFlow<List<ResolveKitChatMessage>> |
Chat transcript in chronological order |
connectionState |
StateFlow<ResolveKitConnectionState> |
Current session-stream connection phase |
isTurnInProgress |
StateFlow<Boolean> |
True while the agent is processing a turn |
toolCallChecklist |
StateFlow<List<ToolCallChecklistItem>> |
Live checklist of tool calls in current batch |
toolCallBatchState |
StateFlow<ResolveKitToolCallBatchState> |
Aggregate approval state of current batch |
toolCallBatches |
StateFlow<List<ToolCallChecklistBatch>> |
Historical tool call batches |
lastError |
StateFlow<String?> |
Last error message |
chatTitle |
StateFlow<String> |
Chat window title |
messagePlaceholder |
StateFlow<String> |
Input placeholder text |
appearanceMode |
StateFlow<ResolveKitAppearanceMode> |
Appearance mode |
currentLocale |
StateFlow<String> |
Current session locale |
chatTheme |
StateFlow<ChatTheme?> |
Active chat theme |
executionLog |
StateFlow<List<String>> |
Debug log of runtime lifecycle events |
| State | Description |
|---|---|
IDLE |
Not connected (initial state) |
REGISTERING |
Registering device with backend |
CONNECTING |
Establishing WebSocket connection |
ACTIVE |
Session established, ready for chat |
RECONNECTING |
Connection lost, attempting reconnect |
RECONNECTED |
Successfully reconnected after loss |
FAILED |
Connection failed with error |
BLOCKED |
Connection blocked (e.g., invalid API key) |
object GetCurrentTime : AnyResolveKitFunction {
override val resolveKitName = "get_current_time"
override val resolveKitDescription = "Returns the current local time"
override val resolveKitParametersSchema = mapOf(
"type" to JSONValue.String("object"),
"properties" to JSONValue.Object(emptyMap())
)
override val resolveKitTimeoutSeconds = 5
override val resolveKitRequiresApproval = false
override suspend fun invoke(
arguments: JSONObject,
context: ResolveKitFunctionContext
): JSONValue = JSONValue.String("12:00:00 UTC")
}@ResolveKit(
name = "echo_message",
description = "Echoes back the provided message",
requiresApproval = false
)
class EchoMessage(private val message: String) : ResolveKitFunction {
override suspend fun perform(): Any? = message
}The processor generates EchoMessageResolveKitAdapter, which can be passed into ResolveKitConfiguration.functions.
| Kotlin type | JSON Schema type | LLM coercion |
|---|---|---|
String |
"string" |
Tolerates numbers/bools |
Boolean |
"boolean" |
Tolerates 1/0/"true"/"false" |
Int, Long |
"integer" |
Truncates 3.0 → 3 |
Float, Double |
"number" |
— |
T? (any of above) |
Same as T, not in required |
null if key absent |
List<T> |
"array" with "items" schema |
— |
Map<String, V> |
"object" |
— |
Nested data class |
"object" |
— |
Add these rules to your app's proguard-rules.pro to prevent obfuscation of ResolveKit classes:
# ResolveKit
-keep class app.resolvekit.** { *; }
-keep class app.resolvekit.core.** { *; }
-keep class app.resolvekit.ui.** { *; }
-keep class app.resolvekit.networking.** { *; }
# KSP-generated adapters
-keep class **ResolveKitAdapter { *; }
# JSON serialization
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Keep function names for tool dispatch
-keepnames class app.resolvekit.authoring.** { *; }
- Open this repository in Android Studio.
- Ensure
local.propertiespoints at your Android SDK:
sdk.dir=/path/to/Android/sdk- If you want to run the sample app, also add:
resolvekit.apiKey=rk_your_key_here
resolvekit.baseUrl=https://agent.resolvekit.appYou can start from local.properties.example and copy it to local.properties.
- Select the
samplerun configuration or run:
./gradlew :sample:installDebugThe sample app now has a clear two-step test flow:
- Step 1: configuration screen (host + API key are required before continue)
- Step 2: capabilities screen with supported tool calls, prompt examples, and one
Open ChatCTA
After tool calls run in chat, return to the capabilities screen to see updated app state (vibe, mascot, confetti counter, laser state). This demonstrates SDK tool calls driving visible host-app behavior.
Recommended options:
includeBuild("../resolvekit-android-sdk")for day-to-day local development.publishToMavenLocalwhen you want artifact-style consumption without an external registry.
Avoid manually copying AARs between projects unless you specifically need a binary handoff.
./gradlew test
./gradlew :sample:assembleDebug
./gradlew publishToMavenLocalShared publishing logic lives in gradle/publish.gradle.kts.
Configured targets:
- Maven Central
- GitHub Packages
- local Maven via
publishToMavenLocal
Expected release credentials are read from Gradle properties or environment variables:
MAVEN_CENTRAL_USERNAME/MAVEN_CENTRAL_PASSWORDGITHUB_PACKAGES_USERNAME/GITHUB_PACKAGES_PASSWORDSIGNING_KEY_BASE64/SIGNING_PASSWORD
Legacy OSSRH_USERNAME and OSSRH_PASSWORD are also accepted for compatibility.
For local setup, copy .env.example to .env and fill in the values. .env is loaded by the publishing script and is gitignored.
For Maven Central releases with Gradle's built-in maven-publish, the artifact upload and the Portal release handoff are separate steps. The GitHub Actions workflow performs both automatically. For a local release, run:
./gradlew publishAllPublicationsToMavenCentralRepository
auth="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')"
curl --fail --silent --show-error \
-X POST \
-H "Authorization: Bearer $auth" \
"https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/app.resolvekit?publishing_type=automatic"After the handoff call succeeds, Sonatype may keep the deployment in PUBLISHING state for several minutes before the modules appear in Maven Central search.
GitHub Actions publishing is defined in .github/workflows/publish.yml. GitHub Packages uses GITHUB_TOKEN; Maven Central publishing runs only when these repository secrets are configured:
MAVEN_CENTRAL_USERNAMEMAVEN_CENTRAL_PASSWORDSIGNING_KEY_BASE64SIGNING_PASSWORD
- Verify your API key starts with
rk_ - Check that
baseUrlpoints to a running ResolveKit backend - Ensure the backend has an app configured for your API key
- Verify functions are registered in
ResolveKitConfiguration.functions - Check that the function name matches exactly (snake_case)
- Ensure the KSP processor ran:
./gradlew buildshould generate*ResolveKitAdapterclasses
- Ensure
ResolveKitRuntimeis created with a validContext - Check that
MaterialThemewrapsResolveKitChatView - Verify Compose BOM version compatibility
- Add the ProGuard rules from the ProGuard / R8 Rules section above
- Run
./gradlew :sample:assembleReleaseto test with minification enabled
This repo includes:
LICENSE(MIT)CONTRIBUTING.mdSECURITY.md- GitHub issue templates
- CI workflow for build, test, and local publication validation
local.propertiesis intentionally ignored.- Do not commit API keys or publishing credentials.
- Public API changes should be reflected in this README.
RESOLVEKIT_BASE_URL(orresolvekit.baseUrlinlocal.properties) controls the backend URL used by the sample app.



