Kotlin Multiplatform
The in-app agent in a Kotlin Multiplatform app — one API in shared code for Android and iOS, with the chat (its window size, conversations that continue, a new conversation), calls, scheduled calls, several agents, and the chat, the call screen and the voice bar for Compose Multiplatform.
ai.graine:inapp-kmp puts the agent into a Kotlin Multiplatform app from
shared code. On each platform Graine's native SDK does the work — the same
engine, the same design from the dashboard, the same calls — and this library
is one API over both. ai.graine:inapp-kmp-compose adds the SDK's own chat,
call screen and voice bar as Compose Multiplatform composables.
Install
Kotlin 2.1.20 or newer. Two dashboard steps, the same as every platform: give
the app its own agent, and add
app:// to its Allowed domains under Embed & Widgets.
Android
Android 7.0 (API 24) and up. The Android SDK comes with the library, and its manifest brings the permissions, the call screen, the call service and a Firebase messaging service of its own. The library finds your application by itself, so configuring from shared code just works.
For calls, add Firebase to your Android app; the SDK registers for pushes
itself. With Firebase Cloud Messaging in the app (google-services.json, its
Gradle plugin and com.google.firebase:firebase-messaging in the Android
target's dependencies), the SDK's own messaging service receives the token and
Graine's calls, and the SDK reads the current token when it starts. The SDK does
not depend on Firebase: an app without it builds and runs as before.
Already have your own FirebaseMessagingService? It outranks the SDK's and
receives every message, so hand Graine's over — two lines:
Ask for POST_NOTIFICATIONS (Android 13+) during onboarding, and forward
permission answers from an activity that shows the voice bar or places calls:
iOS
iOS 15 and up. Add Graine's iOS SDK to the app — the library calls it, and never carries a second copy:
- Swift Package Manager — Xcode → File → Add Package Dependencies…,
https://github.com/GraineAI/inapp-iosfrom0.10.3, on the app target. Build the shared framework as a static framework (isStatic = true, the Compose Multiplatform default). - CocoaPods — in the
Podfile:pod 'GraineInApp', :git => 'https://github.com/GraineAI/inapp-ios.git', :tag => '0.10.3'. With Kotlin's CocoaPods plugin and a dynamic framework, declarepod("GraineInApp") { version = "0.10.3"; linkOnly = true }in the shared module instead.
In Info.plist the microphone description, and for calls the Push
Notifications capability and the Audio and Voice over IP background modes:
Call Graine.calls.enableVoIPPushes() at every launch: iOS wakes a closed app
for a call and hands the push over once it has run.
Calls over WebRTC (optional)
Calls — the user calling the agent, and a call Graine rang — carry their audio over a WebSocket unless the app includes each platform's WebRTC module. With it, they carry it over WebRTC: a jitter buffer, loss concealment and a connection that holds up better on mobile data. Nothing in shared code changes.
- Android — the module beside the Android SDK, at the same version, in the
Android app (or
androidMain):implementation("ai.graine:inapp-android-rtc:0.10.3"). Its manifest addsACCESS_NETWORK_STATE(a normal permission: nothing is asked) and its R8 rules ship inside it. - iOS — the
GraineInAppRTCpackage (https://github.com/GraineAI/inapp-ios-rtc), at the same version asGraineInApp, its library added to the app target besideGraineInApp; or, with CocoaPods,pod 'GraineInAppRTC', :git => 'https://github.com/GraineAI/inapp-ios-rtc.git', :tag => '0.10.3'besidepod 'GraineInApp', the same way and at the same version. The calls find it by themselves.
A call WebRTC cannot set up continues on the WebSocket inside the same call — no second ring, no error. The agent's In-app call audio setting (Calls & scheduling) — WebRTC (recommended) or Standard — can move every install's calls to the WebSocket from their next start. Over WebRTC the agent's voice arrives at telephone bandwidth today. Size, per phone: about 12 MB installed on an iPhone, about 7.4 MB on a 64-bit Android phone. WebRTC is BSD 3-Clause with Google's patent grant — list it in your app's open-source licences.
Start
Once, at launch: Application.onCreate on Android, the app's init on iOS.
Every method may be called from any thread; the suspending ones never block
the main thread, and every state is a flow that holds the latest value.
Who the user is, and memory
The agent's Identity setting names the trait that says who the user is —
user_id unless you chose email, phone or a custom trait. Its value goes
only to Graine, never into the prompt, and a placeholder value is nobody. A
forgetMemory() that succeeds also forgets the conversation kept for the user
and clears the chat on screen; one that throws (unavailable, device_limit,
network) changes nothing.
What is on the screen
Report real values; they are masked on the device on the way out.
Graine.setVariables(mapOf("plan" to user.plan)) adds prompt variables from your
app's own data, and Graine.currentVariables() returns every variable the agent
will receive — the dashboard's defaults, the config's variables, setVariables,
then who is signed in, merged in that order. 0.9.0
Let the agent act
The handler starts on the main thread and answers within six seconds; it may suspend. Arguments arrive as the same plain values on both platforms.
The chat and the voice bar
Both are the SDK's own screens, in the brand's design: GraineChat() wraps the
platform SDK's chat window, so everything below comes with it. For your own UI,
draw from Graine.chat (turns, status, typing, upcomingCalls; open,
send, close, retry, newConversation, clear — the screen only —
answerWidget(id, data, summary) for a component the agent drew,
refreshUpcomingCalls, cancelUpcomingCall(id)), Graine.voice (state, with
captionsOn and captions; start, end, setMuted, setCaptionsOn(on),
clearCaptions()) and Graine.theme — the brand's design tokens (colours, type,
corners, depth, motion, icons, the chat window's and the call screen's), for the
device's light or dark appearance, null before the session. 0.9.0
How big the chat opens. The Garage's Chat window → Window size sets it per agent; your app can choose otherwise, for every open or for one. The open's size wins, then the app's, then the dashboard's.
COMPACT is the window above the voice bar, LARGE a sheet over most of a
phone (a panel in a tablet's corner), FULL the whole screen. A voice bar on
screen moves to the chat and draws it at that size; moving between the call and
the chat keeps it.
Conversations continue. Where the agent remembers its users, a signed-in
user who comes back — later today, tomorrow, on their other phone — finds the
chat where they left it, and the agent picks up from there. The chat opens with
the earlier messages, oldest first, each restored = true, and what was said on
an earlier call as channel = GraineChatChannel.VOICE. GraineChat() shows
them under day separators, an earlier call as one Voice call row, with no
starters and New conversation in its header. A chat of your own lays the
turns out the same way:
Graine.chat.newConversation() starts a fresh thread: Graine lets go of the
kept conversation, the chat clears and opens again with the welcome, and what the
agent remembers is untouched. It throws a GraineException when Graine refuses
(device_limit, network…), and the chat is then as it was.
Calls
With the WebRTC modules in the app,
Graine.calls.configure(GraineCallsConfig(callTransport = GraineCallTransport.AUTO))
— the default — carries calls over WebRTC (WEBSOCKET keeps the WebSocket,
WEBRTC asks for WebRTC wherever it can carry the call), and the call's state says
which one ran: transport (WEBRTC or WEBSOCKET) and transportFallbackReason
(why a call that tried WebRTC is on the WebSocket). 0.9.0
Each outcome has the call's id, what happened (RINGING, ANSWERED,
DECLINED, MISSED, FAILED, ENDED), why, how long a connected call lasted,
whether Graine rang it, and which agent it was with (agent, the name it was
configured under — on Android null for a ring refused because another call was
live). A ring (Graine.calls.incoming) and the call's state carry agent
too. With your push credentials uploaded (Workspace
settings → In-app calling), Graine rings your users itself: the calls the
agent books in a conversation, the ones users book in your app, and the ones
booked from the dashboard.
Scheduled calls
The agent's rules are checked before anything is sent (scheduling_off,
too_soon, too_far), then the server's (outside_window,
too_many_pending). Every refusal is a GraineException with a stable
code, the server's sentence as its message, and retryAfterSeconds when the
server asked to wait.
Several agents
Each agent has its own session, chat, voice bar (GraineAgentBar(agent = "support")),
actions, scheduling and memory (Graine.agent("support")); who the user is, the
install id, the memory choice and the screen are shared. Each keeps its own
conversation and opens its chat at its own design's size. One conversation at a
time across all of them; a call's agent says which one it is with.
Every function and type, with each platform's beside it: the SDK reference and Types.

