kapability

Declare app capabilities once in shared code; generates native assistant glue, wrappers, entities and a runtime dispatch bridge, eliminating handwritten wiring and failing fast on unsupported types.

Android
iOS
GitHub stars0
Dependents0
LicenseApache License 2.0
Creation date3 months ago

Last activity3 months ago
Latest release0.2.0 (3 months ago)

⚡ Kapability

Declare an app capability once in Kotlin Multiplatform — ship it to Android AppFunctions and iOS App Intents.

Maven Central Gradle Plugin License

CI Kotlin Platforms KMP


Android (Gemini) and iOS (Siri / Apple Intelligence) are converging on the same idea: apps expose structured, self-describing functions that on-device AI agents can discover and invoke. Android calls it AppFunctions; Apple calls it App Intents. The two APIs are conceptually identical — annotated function + typed params + description + entity types — but completely incompatible in implementation. So a KMP team today writes and maintains two parallel capability layers.

Kapability collapses that into one. A Gradle plugin + KSP processor + thin runtime that turns a single @Capability declaration in commonMain into native glue for both platforms at build time.

[!NOTE] Status: early / pre-release. Proven end-to-end on both platforms; while pre-1.0 the public API may still change. (Current version: the Maven Central badge above.)

✨ Why Kapability

  • 🎯 Declare once, ship both — one annotated function in commonMain, native on Android and iOS.
  • 🔌 Zero hand-written glue — the @AppFunction wrapper, @AppFunctionSerializable types, the Swift AppIntent + AppEntity, and the dispatch bridge are all generated.
  • 🏗️ Official toolchains — feeds Google's androidx.appfunctions compiler and Xcode's App Intents extraction; no reimplemented metadata formats.
  • 🧭 Graceful asymmetry — @Capability(platforms = [ANDROID]) for platform-specific capabilities.
  • 🛡️ Fail fast — unsupported types stop the build with a clear message instead of mis-generating.
  • ⚙️ One line of setup — plugins { id("dev.kotforge.kapability") }.

🚀 Installation

Artifacts are on Maven Central and the plugin is on the Gradle Plugin Portal, so the default repositories are enough:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

Your KMP module (where capabilities live):

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("co.touchlab.skie")                       // required: Kotlin suspend -> Swift async bridge
    id("dev.kotforge.kapability") version "0.2.0"
}

Your Android app module:

// androidApp/build.gradle.kts
plugins {
    id("com.android.application")
    kotlin("android")
    id("dev.kotforge.kapability") version "0.2.0"
}

kapability {
    capabilityProject.set(project(":shared"))
}

That's it — the plugin adds the kapability-annotations / kapability-runtime dependencies, wires the KSP processor + the androidx.appfunctions two-pass, and manages the generated sources for you.

✍️ Usage

Write this once, in commonMain:

import dev.kotforge.kapability.annotations.*

@CapabilityEntity(description = "A note in the app")
data class Note(
    @CapabilityId val id: String,
    val title: String,
    val content: String,
    val priority: Int,
)

class NoteCapabilities(private val repo: NoteRepository) {

    @Capability(description = "Create a note with the given title, content and priority")
    suspend fun createNote(
        @CapabilityParam(description = "Title of the note") title: String,
        @CapabilityParam(description = "Body content of the note") content: String,
        @CapabilityParam(description = "Priority from 1 (low) to 5 (high)") priority: Int,
    ): Note = repo.create(title, content, priority)
}

…and Kapability generates, at build time:

Platform Generated Reaches
🤖 Android @AppFunction wrapper + @AppFunctionSerializable types → androidx.appfunctions Gemini (AppSearch-indexed)
🍎 iOS Swift struct CreateNoteIntent: AppIntent + AppEntity (via SKIE async) Siri / Shortcuts / Spotlight

Both dispatch through one generated KapabilityRuntime.invoke() entry point into your shared code. Wire your capability instance once at startup (KapabilityRuntime.install(NoteCapabilities(repo))), and you're done.

🧩 How it works

        commonMain: @Capability
                 │  Kapability KSP processor
                 ▼
   ┌──────────────────────────────────────────────┐
   │  generated KapabilityRuntime.invoke() (common) │
   └───────────────┬────────────────┬──────────────┘
      Android glue  │                │  iOS glue
    @AppFunction    │                │  Swift AppIntent
  @AppFunctionSerializable           │  AppEntity  (SKIE async)
                 ▼                    ▼
     androidx.appfunctions       App Intents runtime
        (Gemini)                 (Siri · Shortcuts · Spotlight)

📦 Modules

Artifact Purpose
dev.kotforge:kapability-annotations Public API — @Capability, @CapabilityEntity, @CapabilityParam, @CapabilityId, Platform
dev.kotforge:kapability-runtime The invoke() bridge types + error model
dev.kotforge:kapability-processor KSP2 processor (code generation)
dev.kotforge.kapability (Gradle plugin) Wires everything into your build

✅ Supported types

What each on-device platform supports natively, and what Kapability carries today — as parameters and @CapabilityEntity properties, returning a @CapabilityEntity. Values travel across a JSON bridge; anything Kapability doesn't support fails the build with a clear error.

Type Android AppFunctions iOS App Intents Kapability How Kapability carries it
String · Int · Double · Boolean ✅ ✅ ✅ JSON scalar → native type on both sides
Long · Float ✅ ✅ 🕓 planned
Enum ❌ 1 ✅ AppEnum ✅ enum name as a JSON string → Swift AppEnum on iOS, String on the Android surface
List<String> ✅ ✅ [String] ✅ JSON array → List<String> / [String]
List<Int> · List<Double> · List<Boolean> ⚠️ arrays 2 ✅ [T] 🕓 planned — needs IntArray/… mapping on Android
Nullable (T?) ✅ ✅ optional ✅ JSON null / absent → nullable type on both sides
Date ✅ java.time.* ✅ Date 🕓 planned — needs kotlinx-datetime + mapping
@CapabilityEntity (return type) ✅ @AppFunctionSerializable ✅ AppEntity ✅ generated @AppFunctionSerializable / AppEntity, one level deep
Nested @CapabilityEntity ✅ 3 ✅ 🕓 planned — recursive codegen
List<@CapabilityEntity> ⚠️ 3 ✅ [AppEntity] 🕓 planned — recursive codegen

Legend: ✅ supported · 🕓 planned (roadmap) · ⚠️ supported with caveats · ❌ not supported by the platform

1 androidx.appfunctions 1.0.0-alpha08 rejects arbitrary enums — Kapability bridges the gap by surfacing an enum as its String name on Android (a real AppEnum on iOS), so it works on both. 2 AppFunctions models numeric collections as primitive arrays (IntArray, DoubleArray, BooleanArray), not List<T>. 3 Native platform support expected but not yet verified against alpha08.

The Android column reflects androidx.appfunctions 1.0.0-alpha08.

🔧 Requirements

Version
Kotlin 2.2.20 (KSP2)
Android Gradle Plugin 8.11+, compileSdk 36 (Android 16 AppFunctions)
SKIE latest (you apply it — for the iOS suspend → async bridge)
Xcode 16+ (iOS App Intents build)

🧪 Sample

samples/notes is a full KMP app that consumes the published SDK from a separate Gradle build (proving the external-consumer path) and runs on both an Android 16+ emulator (adb shell cmd app_function execute-app-function) and the iOS simulator (App Intents test).

🗺️ Roadmap

  • Richer types: Date, non-String lists (via primitive-array mapping), nested @CapabilityEntity + List<CustomObject>
  • Reduced consumer wiring (generate the appfunctions Provider; register once in commonMain)
  • manifest.json + a verifyKapability drift-detection task
  • Local Swift Package emission for iOS distribution
  • v0.2+: schema conformance, entity queries / Spotlight, interaction donations

🤝 Contributing & releases

Kapability uses Conventional Commits + release-please. Commit / squash-merge titles drive the next version:

Prefix Bump Example
feat: minor (0.1.0 → 0.2.0) feat: support List<T> parameters
fix: patch (0.1.0 → 0.1.1) fix: correct nullable decoding
feat!: / BREAKING CHANGE: major (→ 1.0.0) feat!: rename @Capability.description
docs: chore: refactor: test: ci: build: none (changelog only) docs: expand README

On merge to main, release-please maintains a release PR (version + CHANGELOG.md). Merging it tags and publishes to Maven Central + the Gradle Plugin Portal automatically.

📄 License

Copyright 2026 Piyush Sinha

Licensed under the Apache License, Version 2.0 — see LICENSE.
Android
iOS
GitHub stars0
Dependents0
LicenseApache License 2.0
Creation date3 months ago

Last activity3 months ago
Latest release0.2.0 (3 months ago)

⚡ Kapability

Declare an app capability once in Kotlin Multiplatform — ship it to Android AppFunctions and iOS App Intents.

Maven Central Gradle Plugin License

CI Kotlin Platforms KMP


Android (Gemini) and iOS (Siri / Apple Intelligence) are converging on the same idea: apps expose structured, self-describing functions that on-device AI agents can discover and invoke. Android calls it AppFunctions; Apple calls it App Intents. The two APIs are conceptually identical — annotated function + typed params + description + entity types — but completely incompatible in implementation. So a KMP team today writes and maintains two parallel capability layers.

Kapability collapses that into one. A Gradle plugin + KSP processor + thin runtime that turns a single @Capability declaration in commonMain into native glue for both platforms at build time.

[!NOTE] Status: early / pre-release. Proven end-to-end on both platforms; while pre-1.0 the public API may still change. (Current version: the Maven Central badge above.)

✨ Why Kapability

  • 🎯 Declare once, ship both — one annotated function in commonMain, native on Android and iOS.
  • 🔌 Zero hand-written glue — the @AppFunction wrapper, @AppFunctionSerializable types, the Swift AppIntent + AppEntity, and the dispatch bridge are all generated.
  • 🏗️ Official toolchains — feeds Google's androidx.appfunctions compiler and Xcode's App Intents extraction; no reimplemented metadata formats.
  • 🧭 Graceful asymmetry — @Capability(platforms = [ANDROID]) for platform-specific capabilities.
  • 🛡️ Fail fast — unsupported types stop the build with a clear message instead of mis-generating.
  • ⚙️ One line of setup — plugins { id("dev.kotforge.kapability") }.

🚀 Installation

Artifacts are on Maven Central and the plugin is on the Gradle Plugin Portal, so the default repositories are enough:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

Your KMP module (where capabilities live):

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("co.touchlab.skie")                       // required: Kotlin suspend -> Swift async bridge
    id("dev.kotforge.kapability") version "0.2.0"
}

Your Android app module:

// androidApp/build.gradle.kts
plugins {
    id("com.android.application")
    kotlin("android")
    id("dev.kotforge.kapability") version "0.2.0"
}

kapability {
    capabilityProject.set(project(":shared"))
}

That's it — the plugin adds the kapability-annotations / kapability-runtime dependencies, wires the KSP processor + the androidx.appfunctions two-pass, and manages the generated sources for you.

✍️ Usage

Write this once, in commonMain:

import dev.kotforge.kapability.annotations.*

@CapabilityEntity(description = "A note in the app")
data class Note(
    @CapabilityId val id: String,
    val title: String,
    val content: String,
    val priority: Int,
)

class NoteCapabilities(private val repo: NoteRepository) {

    @Capability(description = "Create a note with the given title, content and priority")
    suspend fun createNote(
        @CapabilityParam(description = "Title of the note") title: String,
        @CapabilityParam(description = "Body content of the note") content: String,
        @CapabilityParam(description = "Priority from 1 (low) to 5 (high)") priority: Int,
    ): Note = repo.create(title, content, priority)
}

…and Kapability generates, at build time:

Platform Generated Reaches
🤖 Android @AppFunction wrapper + @AppFunctionSerializable types → androidx.appfunctions Gemini (AppSearch-indexed)
🍎 iOS Swift struct CreateNoteIntent: AppIntent + AppEntity (via SKIE async) Siri / Shortcuts / Spotlight

Both dispatch through one generated KapabilityRuntime.invoke() entry point into your shared code. Wire your capability instance once at startup (KapabilityRuntime.install(NoteCapabilities(repo))), and you're done.

🧩 How it works

        commonMain: @Capability
                 │  Kapability KSP processor
                 ▼
   ┌──────────────────────────────────────────────┐
   │  generated KapabilityRuntime.invoke() (common) │
   └───────────────┬────────────────┬──────────────┘
      Android glue  │                │  iOS glue
    @AppFunction    │                │  Swift AppIntent
  @AppFunctionSerializable           │  AppEntity  (SKIE async)
                 ▼                    ▼
     androidx.appfunctions       App Intents runtime
        (Gemini)                 (Siri · Shortcuts · Spotlight)

📦 Modules

Artifact Purpose
dev.kotforge:kapability-annotations Public API — @Capability, @CapabilityEntity, @CapabilityParam, @CapabilityId, Platform
dev.kotforge:kapability-runtime The invoke() bridge types + error model
dev.kotforge:kapability-processor KSP2 processor (code generation)
dev.kotforge.kapability (Gradle plugin) Wires everything into your build

✅ Supported types

What each on-device platform supports natively, and what Kapability carries today — as parameters and @CapabilityEntity properties, returning a @CapabilityEntity. Values travel across a JSON bridge; anything Kapability doesn't support fails the build with a clear error.

Type Android AppFunctions iOS App Intents Kapability How Kapability carries it
String · Int · Double · Boolean ✅ ✅ ✅ JSON scalar → native type on both sides
Long · Float ✅ ✅ 🕓 planned
Enum ❌ 1 ✅ AppEnum ✅ enum name as a JSON string → Swift AppEnum on iOS, String on the Android surface
List<String> ✅ ✅ [String] ✅ JSON array → List<String> / [String]
List<Int> · List<Double> · List<Boolean> ⚠️ arrays 2 ✅ [T] 🕓 planned — needs IntArray/… mapping on Android
Nullable (T?) ✅ ✅ optional ✅ JSON null / absent → nullable type on both sides
Date ✅ java.time.* ✅ Date 🕓 planned — needs kotlinx-datetime + mapping
@CapabilityEntity (return type) ✅ @AppFunctionSerializable ✅ AppEntity ✅ generated @AppFunctionSerializable / AppEntity, one level deep
Nested @CapabilityEntity ✅ 3 ✅ 🕓 planned — recursive codegen
List<@CapabilityEntity> ⚠️ 3 ✅ [AppEntity] 🕓 planned — recursive codegen

Legend: ✅ supported · 🕓 planned (roadmap) · ⚠️ supported with caveats · ❌ not supported by the platform

1 androidx.appfunctions 1.0.0-alpha08 rejects arbitrary enums — Kapability bridges the gap by surfacing an enum as its String name on Android (a real AppEnum on iOS), so it works on both. 2 AppFunctions models numeric collections as primitive arrays (IntArray, DoubleArray, BooleanArray), not List<T>. 3 Native platform support expected but not yet verified against alpha08.

The Android column reflects androidx.appfunctions 1.0.0-alpha08.

🔧 Requirements

Version
Kotlin 2.2.20 (KSP2)
Android Gradle Plugin 8.11+, compileSdk 36 (Android 16 AppFunctions)
SKIE latest (you apply it — for the iOS suspend → async bridge)
Xcode 16+ (iOS App Intents build)

🧪 Sample

samples/notes is a full KMP app that consumes the published SDK from a separate Gradle build (proving the external-consumer path) and runs on both an Android 16+ emulator (adb shell cmd app_function execute-app-function) and the iOS simulator (App Intents test).

🗺️ Roadmap

  • Richer types: Date, non-String lists (via primitive-array mapping), nested @CapabilityEntity + List<CustomObject>
  • Reduced consumer wiring (generate the appfunctions Provider; register once in commonMain)
  • manifest.json + a verifyKapability drift-detection task
  • Local Swift Package emission for iOS distribution
  • v0.2+: schema conformance, entity queries / Spotlight, interaction donations

🤝 Contributing & releases

Kapability uses Conventional Commits + release-please. Commit / squash-merge titles drive the next version:

Prefix Bump Example
feat: minor (0.1.0 → 0.2.0) feat: support List<T> parameters
fix: patch (0.1.0 → 0.1.1) fix: correct nullable decoding
feat!: / BREAKING CHANGE: major (→ 1.0.0) feat!: rename @Capability.description
docs: chore: refactor: test: ci: build: none (changelog only) docs: expand README

On merge to main, release-please maintains a release PR (version + CHANGELOG.md). Merging it tags and publishes to Maven Central + the Gradle Plugin Portal automatically.

📄 License

Copyright 2026 Piyush Sinha

Licensed under the Apache License, Version 2.0 — see LICENSE.