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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ out/

# Auto-generated Gradle daemon JVM criteria (machine-specific)
gradle/gradle-daemon-jvm.properties

# Internal scratch / tooling / working docs — kept local, not part of the SDK.
# (iOS→Android parity audit artifacts, multi-agent workflow scripts, implementation notes.)
/scripts/
/docs/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ is pre-1.0, breaking changes bump the **minor** version.

## [Unreleased]

## [0.9.0] - 2026-06-30

Adds the `ai.poly:voice` WebRTC voice-calling SDK (first publish) alongside `ai.poly:messaging:0.9.0`.

### Added
- **Voice calling (`ai.poly:voice`):** live, two-way WebRTC voice calls to a PolyAI agent — call
lifecycle (`CallState`), mute, accessory-aware audio-output routing with mid-call switching
(speaker / earpiece / wired / Bluetooth), audio-focus interruption handling, signaling reconnect,
and a foreground-service recipe for background calls. Separate artifact; reuses the messaging
`Configuration`. See [`polyvoice/README.md`](polyvoice/README.md).
- **`PolyError.Voice`** error cases (e.g. `Disconnected`, `Interrupted`) on the shared error type.

## [0.8.0] - 2026-06-16

First public release on Maven Central (`ai.poly:messaging:0.8.0`).
Expand Down
123 changes: 94 additions & 29 deletions README.md

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ plugins {
alias(libs.plugins.bcv)
}

// binary-compatibility-validator: only the published SDK has a tracked ABI; ignore samples/tests.
// binary-compatibility-validator: only the published SDKs have a tracked ABI; ignore samples/tests.
apiValidation {
// Only :polymessaging has a tracked public ABI; ignore the example apps + container projects.
ignoredProjects.addAll(subprojects.map { it.name }.filter { it != "polymessaging" })
// The published SDKs (:polymessaging, :polyvoice) have a tracked public ABI; ignore the
// example apps + container projects.
val tracked = setOf("polymessaging", "polyvoice")
ignoredProjects.addAll(subprojects.map { it.name }.filter { it !in tracked })
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Set your API key in `HelloApplication.kt` (currently `"YOUR_API_KEY"`); the comm
- Auto-scroll as the agent's reply streams in
- Send with `session.send(text)` from a coroutine (`scope.launch { runCatching { session.send(text) } }`)

The SDK invariants behind each pattern are in the root README's [Integration guide](../../../README.md#integration-guide); this example shows them as one concrete file.
The SDK invariants behind each pattern are in the root README's [Integration guide](../../../../README.md#integration-guide); this example shows them as one concrete file.

## How it works

Expand Down Expand Up @@ -66,7 +66,7 @@ After this, `PolyMessaging.chat()` works from any `Activity` / `@Composable`.

**Under the hood:** `initialize` just stashes your config (API key + environment) process-wide — no network happens yet. The work starts when you call `chat()`.

*See [Quick start](../../../README.md#quick-start).*
*See [Quick start](../../../../README.md#quick-start).*

### Get a session and render messages — `MainActivity.kt`

Expand Down Expand Up @@ -101,11 +101,11 @@ private fun ChatScreen() {

`remember { … }` keeps **one** session per composition lifecycle. `session.messages` is a `StateFlow`, so `collectAsStateWithLifecycle()` re-renders any read of `messages` when the SDK updates the transcript.

**Streaming is on by default** — `Configuration.streamingEnabled` defaults to `true`, so agent replies grow token-by-token (ChatGPT-style). To switch to complete-message bubbles instead, pass `PolyMessaging.chat(streamingEnabled = false)`. See the root README's [Streaming](../../../README.md#streaming) section.
**Streaming is on by default** — `Configuration.streamingEnabled` defaults to `true`, so agent replies grow token-by-token (ChatGPT-style). To switch to complete-message bubbles instead, pass `PolyMessaging.chat(streamingEnabled = false)`. See the root README's [Streaming](../../../../README.md#streaming) section.

**Under the hood:** `chat()` runs the whole REST + WebSocket handshake, agent-join, and resume-or-create for you; `isReady` flips true once it's connected. `messages` is the SDK-maintained transcript (`User` / `Agent` / `System`) that re-emits on every change, so your list just re-renders.

*See [Integration guide › The core pattern](../../../README.md#the-core-pattern-render-messages-yourself).*
*See [Integration guide › The core pattern](../../../../README.md#the-core-pattern-render-messages-yourself).*

### Render each message — `MainActivity.kt`

Expand Down Expand Up @@ -134,7 +134,7 @@ A user bubble is drawn faded while `delivery == Delivery.PENDING`, then settles

**Under the hood:** with `streamingEnabled = true` (the default), `ChatSession` extends the last `Agent` message's `text` on every chunk and re-emits `messages`. The `StateFlow` collection recomposes `MessageRow` and the text grows as the reply streams.

*See [Integration guide › Streaming](../../../README.md#streaming).*
*See [Integration guide › Streaming](../../../../README.md#streaming).*

### Scroll as the agent types — `MainActivity.kt`

Expand Down Expand Up @@ -165,7 +165,7 @@ Streaming grows the last agent message's `text` in place — `messages.size` doe

**Under the hood:** with `streamingEnabled = true` (the default), `ChatSession` extends the last `Agent` message's `text` on every chunk and re-emits `messages`. The `StateFlow` collection recomposes, the `LaunchedEffect` keys change, and the scroll tracks the reply as it grows.

*See [Integration guide › Streaming](../../../README.md#streaming).*
*See [Integration guide › Streaming](../../../../README.md#streaming).*

### Send a message — `MainActivity.kt`

Expand Down Expand Up @@ -202,7 +202,7 @@ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {

**Under the hood:** `send(text)` is optimistic — the bubble appears in `messages` immediately while the SDK manages delivery and the server echo behind the scenes.

*See [Integration guide › The core pattern](../../../README.md#the-core-pattern-render-messages-yourself).*
*See [Integration guide › The core pattern](../../../../README.md#the-core-pattern-render-messages-yourself).*

### Keyboard & insets — `MainActivity.kt`

Expand All @@ -225,7 +225,7 @@ Scaffold(

`WindowInsets.ime` follows the keyboard, `navigationBars` keeps the composer off the gesture bar, and `union` takes the larger of the two so the field never jumps.

*See [Integration guide › Avatars & keyboard](../../../README.md#avatars--keyboard).*
*See [Integration guide › Avatars & keyboard](../../../../README.md#avatars--keyboard).*

## What this example skips

Expand All @@ -234,10 +234,10 @@ Scaffold(
- offline detection, full-screen terminal error → 04-Resilience (see [`../04-resilience/`](../04-resilience/))
- live agent handoff → 05-Handoff (see [`../05-handoff/`](../05-handoff/))

It also doesn't surface a **terminal-error dialog** for a bad API key — `session.failureReason` (and `session.client.resume()` for retry) is wired up in [`../02-standard/`](../02-standard/). See [Terminal errors](../../../README.md#terminal-errors) for the pattern.
It also doesn't surface a **terminal-error dialog** for a bad API key — `session.failureReason` (and `session.client.resume()` for retry) is wired up in [`../02-standard/`](../02-standard/). See [Terminal errors](../../../../README.md#terminal-errors) for the pattern.

---

- **Views counterpart:** [`../../views/01-hello/`](../../views/01-hello/)
- **SDK reference:** root [README → Integration guide](../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../README.md#install)
- **SDK reference:** root [README → Integration guide](../../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../../README.md#install)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Set your API key in `src/main/kotlin/ai/poly/examples/standard/compose/StandardA
- Failure overlay — `session.failureReason`, `session.client.resume()`
- Keyboard handling — `WindowInsets.ime` + `windowInsetsPadding` (no manual layout math)

The SDK invariants behind each pattern are in the root README's [Integration guide](../../../README.md#integration-guide); this example shows them composed into one chat screen.
The SDK invariants behind each pattern are in the root README's [Integration guide](../../../../README.md#integration-guide); this example shows them composed into one chat screen.

## How it works

Expand Down Expand Up @@ -85,7 +85,7 @@ scope.launch { runCatching { session.sendTyping() } }

**Under the hood:** `isAgentTyping` is SDK-managed — true while the agent composes (driven by its thinking/streaming signals), auto-cleared on the next agent message or after the typing timeout (~10s), so you never run a timer. `sendTyping()` throttles outgoing STARTED frames to ≤1 per 3s and auto-emits STOPPED ~5s after your last call, so it's safe to fire on every keystroke.

*See [Integration guide › Typing](../../../README.md#typing).*
*See [Integration guide › Typing](../../../../README.md#typing).*

### Connection banner — `components/ConnectionBanner.kt`

Expand Down Expand Up @@ -124,7 +124,7 @@ It sits at the top of the `Column`, above the message list, so it pushes the cha

**Under the hood:** `session.connection` is SDK-driven — a transient drop surfaces as `Open → Reconnecting(n) → Open` (auto-reconnect with backoff and jitter, no `Closed` flash), so you only need a banner on `Reconnecting`. `Failed` arrives only after the reconnect budget is exhausted (handled by the failure overlay below).

*See [Integration guide › Connection & reconnect](../../../README.md#connection--reconnect).*
*See [Integration guide › Connection & reconnect](../../../../README.md#connection--reconnect).*

### Suggestion pills — under the last agent message

Expand Down Expand Up @@ -167,7 +167,7 @@ if (showSuggestions && m.suggestions.isNotEmpty()) {

**Under the hood:** `AgentMessage.suggestions` are quick replies the agent attached to *that* message (agent messages only). `clearSuggestions(id)` empties them in the model so the pills vanish before `send(...)` resolves — feels instant.

*See [Integration guide › Suggestions](../../../README.md#suggestions-quick-replies).*
*See [Integration guide › Suggestions](../../../../README.md#suggestions-quick-replies).*

### End chat + Start new chat — `ChatScreen.kt`

Expand Down Expand Up @@ -210,7 +210,7 @@ if (hasEnded) {

**Under the hood:** `session.end()` flips `hasEnded`. `startNewSession()` creates a fresh session — when the session id changes, `ChatSession` clears `messages` and resets the latched flags for you, so no view bookkeeping needed.

*See [Integration guide › Starting, resuming & ending a session](../../../README.md#starting-resuming--ending-a-session).*
*See [Integration guide › Starting, resuming & ending a session](../../../../README.md#starting-resuming--ending-a-session).*

### Delivery state + retry — inside the user bubble (`components/MessageBubbleView.kt`)

Expand Down Expand Up @@ -265,7 +265,7 @@ onRetry = { draftId, text ->

**Under the hood:** `UserMessage.delivery` is optimistic — `PENDING` immediately, then the SDK matches the server echo (via a local id) → `SENT`; if no echo arrives after retries (up to 3×) it settles on `FAILED`. You only render it; `removeMessage(draftId)` drops the failed draft so a retry doesn't leave a duplicate bubble.

*See [Integration guide › Delivery state & retry](../../../README.md#delivery-state--retry).*
*See [Integration guide › Delivery state & retry](../../../../README.md#delivery-state--retry).*

### Failure overlay — `ChatScreen.kt`

Expand Down Expand Up @@ -299,7 +299,7 @@ Button(onClick = onReconnect) { Text("Reconnect") }

**Under the hood:** `failureReason` is set whenever the chat can't auto-recover — an invalid `apiKey` rejected at the initial connect, the auto-reconnect budget exhausted, or the session expiring. Recovery is consumer-driven — call `session.client.resume()` to retry.

*See [Integration guide › Terminal errors](../../../README.md#terminal-errors).*
*See [Integration guide › Terminal errors](../../../../README.md#terminal-errors).*

### Keyboard handling — `ChatScreen.kt`

Expand All @@ -318,7 +318,7 @@ Row(

The `Scaffold` sets `contentWindowInsets = WindowInsets(0, 0, 0, 0)` so the content owns the bottom inset itself. The send button is a blue circle with an up-arrow vector (`R.drawable.ic_arrow_upward`).

*See [Integration guide › Avatars & keyboard](../../../README.md#avatars--keyboard).*
*See [Integration guide › Avatars & keyboard](../../../../README.md#avatars--keyboard).*

## What this example skips

Expand All @@ -329,5 +329,5 @@ The `Scaffold` sets `contentWindowInsets = WindowInsets(0, 0, 0, 0)` so the cont
---

- **Views counterpart:** [`examples/views/02-standard/`](../../views/02-standard/)
- **SDK reference:** root [README → Integration guide](../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../README.md#install)
- **SDK reference:** root [README → Integration guide](../../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../../README.md#install)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Then launch the installed app (the launcher `MainActivity`, which hosts `ChatScr

**The SDK decodes the data; it never fetches bytes or dials phones.** You own image loading, caching, retry, link-opening, and the `tel:` URI. This example does all of that with Coil + `Intent`s.

The SDK invariants behind each pattern are in the root README's [Integration guide](../../../README.md#integration-guide).
The SDK invariants behind each pattern are in the root README's [Integration guide](../../../../README.md#integration-guide).

## How it works

Expand Down Expand Up @@ -57,7 +57,7 @@ if (images.isNotEmpty()) {

**Under the hood:** the SDK decodes the agent's `attachments` array and groups them onto the same `AgentMessage` as the text. No background fetch happens — you load `contentUrl` / `previewImageUrl` yourself.

*See [Integration guide › Attachments, link cards & call buttons](../../../README.md#attachments-link-cards--call-buttons).*
*See [Integration guide › Attachments, link cards & call buttons](../../../../README.md#attachments-link-cards--call-buttons).*

### Render URL link cards — `components/UrlCard.kt`

Expand Down Expand Up @@ -86,7 +86,7 @@ if (urls.isNotEmpty()) {

**Under the hood:** same decoded `Attachment` data — the SDK hands you the URL + preview + title, and leaves the card layout and link-opening entirely to your code. The example puts `clickable(enabled = contentUrl != null)` on the whole card (rather than a link inside it) so the tap target covers the full card and you can intercept (e.g. route in-app) before launching the `Intent`.

*See [Integration guide › Attachments, link cards & call buttons](../../../README.md#attachments-link-cards--call-buttons).*
*See [Integration guide › Attachments, link cards & call buttons](../../../../README.md#attachments-link-cards--call-buttons).*

### Render `tel:` call buttons — `components/CallActionButton.kt`

Expand All @@ -109,7 +109,7 @@ if (m.callActions.isNotEmpty()) {

`CallActionButton` sanitizes the number (digits + leading `+`), builds `tel:<digits>`, and hands it to the dialer via `Intent(ACTION_DIAL)` (no `CALL_PHONE` permission — the dialer pre-fills).

*See [Integration guide › Attachments, link cards & call buttons](../../../README.md#attachments-link-cards--call-buttons).*
*See [Integration guide › Attachments, link cards & call buttons](../../../../README.md#attachments-link-cards--call-buttons).*

### Render Markdown — `components/RichText.kt`

Expand Down Expand Up @@ -141,7 +141,7 @@ if (m.text.isNotEmpty()) {

> **Why normalize HTML?** Agent content is authored once and rendered on both web and mobile. The web widget pipes it through `marked` + DOMPurify, so a reply can reach mobile with literal `<br>` tags. `normalizeAgentHtml` mirrors that allow-list so the bubble matches the web. The minimal [`01-Hello`](../01-hello/) / [`02-Standard`](../02-standard/) examples skip this (plain `Text`), so they show `<br>` raw.

*See [Integration guide › Rich text & links](../../../README.md#rich-text--links).*
*See [Integration guide › Rich text & links](../../../../README.md#rich-text--links).*

### Bubble layout — compose everything

Expand Down Expand Up @@ -194,7 +194,7 @@ NewMessageNotifier(session, NotificationPolicy.WHEN_BACKGROUNDED) // component

It requests `POST_NOTIFICATIONS` (API 33+), creates a notification channel, and observes `client.events` for completed `AgentMessage` / `LiveAgentMessage` events (full text + stable `messageId`, not chunks). Collection is gated on the **`CREATED`** lifecycle (not `STARTED`) so a reply that lands while the chat is backgrounded can still raise a banner; on each event it checks whether the chat is currently on screen (`lifecycle.currentState.isAtLeast(STARTED)`) and — for `WHEN_BACKGROUNDED` — stays quiet if it is. Already-shown ids are skipped (persisted in `SharedPreferences`, so resume/relaunch replays don't re-fire), and a `NotificationCompat` banner is posted otherwise. On Android 13+ (API 33) the banner only appears if the user grants the `POST_NOTIFICATIONS` prompt — if it's denied, `post()` silently no-ops and no banner shows.

*See [Integration guide › In-app new-message alerts (local-only workaround)](../../../README.md#in-app-new-message-alerts-local-only-workaround).*
*See [Integration guide › In-app new-message alerts (local-only workaround)](../../../../README.md#in-app-new-message-alerts-local-only-workaround).*

## What this example skips

Expand All @@ -204,5 +204,5 @@ It requests `POST_NOTIFICATIONS` (API 33+), creates a notification channel, and
---

- **Views counterpart:** [`../../views/03-richcontent/`](../../views/03-richcontent/)
- **SDK reference:** root [README → Integration guide](../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../README.md#install)
- **SDK reference:** root [README → Integration guide](../../../../README.md#integration-guide)
- **Install the package:** root [README → Install](../../../../README.md#install)
Loading