Skip to content

feat(ai-components): native AIAgent module for install id and tool-call sign-in - #3926

Open
isekovanic wants to merge 1 commit into
feat/ai-reply-stepsfrom
feat/ai-agent-native
Open

isekovanic wants to merge 1 commit into
feat/ai-reply-stepsfrom
feat/ai-agent-native

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

Ticket: https://linear.app/stream/issue/RN-485

Stacked on #3924.

🎯 Goal

Let a person sign in when an agent's tool needs a connector account (Linear, GitHub, …), and give each install a stable id so the router can ask only this device. This needs a small native module, AIAgent, which works the same way as Swift's and Android's AI SDKs.

🛠 Implementation details

Native AIAgent TurboModule (in both wrappers, sources in shared-native/)

  • installId() is synchronous and creates an id on first use, then keeps it.

    • iOS stores ios-<UUID> in UserDefaults.
    • Android writes android-<UUID> to a file in noBackupFilesDir, so a device restored from a backup gets its own id. The file name and format match the Android AI SDK.
  • openAuthSession(url, callbackScheme) resolves with the redirect URL, or '' when the person closes the sign-in.

    • iOS uses ASWebAuthenticationSession, ported from Swift's AIWebAuthentication. It needs no usage description, so it's always compiled; the podspecs only add AuthenticationServices.
    • Android uses an Auth Tab where the app and the browser support it (Chrome 137+). Otherwise it opens a Custom Tab and catches the redirect through the app's scheme in onNewIntent. Coming back to the app without a redirect counts as closed.
  • Auth Tab needs androidx.browser 1.9.0, and 1.9.0 needs compileSdk 36 and AGP 8.9.1. Our minimum, RN 0.76, ships with 35. The wrappers' Gradle files check both:

    • when both are met, the wrappers build with 1.9.0 and the Auth Tab sources;
    • otherwise they use 1.8.0 and the Custom Tab-only sources.

    sync-shared-native.sh copies both variants over.

  • No new permission. The manifest adds a <queries> entry for Custom Tabs services, which Android 11+ needs in order to find the browser.

  • The codegen spec is copied into each wrapper, and validate-entry-isolation keeps the copies identical.

JS (ai-components)

  • AIClientIdentity.installId() reads the native id. If the module isn't there, it falls back to an id kept in memory.
  • createAuthSessionAuthorizer({ callbackScheme, decline }) opens the tool's sign-in at url#handoff_token=… and reads status from the return. "Not now" calls decline.
  • AIToolAuthorization and AIToolAuthorizationCard, shown under a tool call that's waiting for a sign-in, have these states:
    • Connect, then Connecting... while the sign-in is open, which stays after a connected return until the step changes;
    • Not now;
    • connected or not connected;
    • expired;
    • didn't complete, or couldn't be opened.
  • The theme gets a new messageParts.authorizationCard section.
  • New strings are in all 13 locales. As before, ar and he could use a native speaker's look.

ExpoChatGPTSample

  • It sends the install id as the session's client_id, and its sessions set a return_url on io.getstream.expochatgptsample://sign-in. The router only accepts https or reverse-domain schemes there.
  • +native-intent.tsx keeps Expo Router from opening that return URL as a screen.
  • A reply's row reads its message from the channel's state. The stand-in MessageList doesn't re-render a row when only an attachment changes, such as a tool call starting to wait for a sign-in.
  • It hides the router's duplicate connector_authorization attachment.

🎨 UI Changes

The sign-in card under a tool call: Connect / Not now, Connecting..., and its outcomes.

🧪 Testing

  • Unit tests cover the launch URL, reading the return, the authorizer, and the card's states.

  • On an Android emulator (API 36), against a local Vision-Agents router with the Linear connector:

    • the card shows;
    • Connect opens the sign-in;
    • closing it resets the card;
    • a connected return shows Connecting...;
    • Not now declines.

    The return was sent with adb. I didn't sign into a real Linear account. I ran both Gradle variants, the Auth Tab one and a forced Custom Tab one.

  • On an API 34 emulator (Chrome 113, no Auth Tab), the same flow works through the Custom Tab.

  • On the iOS simulator: the card, the ASWebAuthenticationSession prompt, Cancel resetting the card, and Not now.

  • On the minimum supported setup, a bare RN 0.76.9 app (compileSdk 35, AGP 8.6) that uses the packed stream-chat-react-native builds against androidx.browser 1.8.0. On Android 7.0 (API 24) and Android 14, installId(), an app-scheme return and closing the browser all work.

  • The RN CLI wrapper also builds in SampleApp (compileSdk 37, so 1.9.0). I didn't run SampleApp itself.

  • Not tested: RTL and screen readers.

  • yarn lint, yarn typecheck, yarn build and yarn test:unit pass.

☑️ Checklist

@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2037 KB 2038 KB +213 B 🟢
ai_components_size 163 KB 207 KB +44849 B 🔴

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants