Skip to content

Android Demo Architecture

Overview

The ClawSeed Android Demo is a complete on-device AI agent application. The entire agent stack runs on the Android device — the Rust-compiled Gateway binary runs as a foreground service process, while the Android client connects via WebSocket and registers device-side tools.

Download the Android App

Download the latest Android APK

Choose the .apk file under Assets on the latest release page. Previous builds and their release notes remain available in all ClawSeed releases.

User-Facing Agent Features

  • Personas bind each new session to a focused assistant. A persona can override its Soul, model, thinking mode, memory namespace, allowed tools, and enabled or blocked skills.
  • User profiles preserve durable preferences and context as structured items. Items may be added manually or inferred after successful replies, reviewed in the app, rejected so they are not learned again, and included in backups.
  • Speech output uses Android Text-to-Speech to read assistant replies aloud. Users can enable automatic playback globally and play or stop individual replies from the chat screen.
  • Scheduled tasks run prompts in the background and retain their own session so device and CETP tools remain available during execution.
  • Cross-app tools are discovered through CETP and bridged into every active tool registry. The Jiucaihua integration demonstrates the complete flow with real investment data.

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    Android Device                        │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  ClawseedService (Foreground Service)             │  │
│  │                                                   │  │
│  │  ┌────────────────────────────────────────────┐  │  │
│  │  │  libclawseed.so (Rust Gateway Process)     │  │  │
│  │  │  - Axum HTTP/WS server (port 42617)        │  │  │
│  │  │  - Agent loop + LLM calls                  │  │  │
│  │  │  - Built-in tool execution                 │  │  │
│  │  │  - Remote tool bridge                      │  │  │
│  │  └────────────────────────────────────────────┘  │  │
│  │         ↑ ProcessBuilder launch                   │  │
│  │         ↓ /health polling for readiness           │  │
│  └──────────────────────────────────────────────────┘  │
│                      ↕ WebSocket                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │  MainActivity (Compose UI)                        │  │
│  │  ┌────────────────────────────────────────────┐  │  │
│  │  │  ClawseedClient (SDK Library)              │  │  │
│  │  │  - OkHttp WebSocket connection             │  │  │
│  │  │  - Tool registration (device_info, etc.)   │  │  │
│  │  │  - Tool call handling                      │  │  │
│  │  │  - Streaming response callbacks            │  │  │
│  │  └────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│                    ↕ Network                             │
│              LLM Provider (Anthropic, etc.)             │
└─────────────────────────────────────────────────────────┘

Key design: The entire agent stack runs on-device; LLM inference is called over the network to a cloud provider.

User Profile Backup

The Settings data-transfer screen can export the structured user profile into user_profile/profile.json inside the backup ZIP. Imports support merge, append, and replace strategies. Profile imports are applied through the authenticated Gateway API so the live SQLite database is updated atomically without restarting the app.

Module Structure

clients/android/
├── lib/                          # ClawSeed SDK Library
│   ├── build.gradle.kts
│   └── src/main/kotlin/dev/clawseed/client/
│       ├── ClawseedClient.kt     # WebSocket client
│       └── ClawseedMessages.kt   # Message protocol types
├── app/                          # Demo Application
│   ├── build.gradle.kts
│   └── src/main/kotlin/dev/clawseed/demo/
│       ├── MainActivity.kt       # Compose UI
│       └── ClawseedService.kt    # Foreground service (Gateway process manager)
└── settings.gradle.kts

lib — SDK Library

Class Responsibility
ClawseedClient WebSocket connection management, tool registration, message send/receive
ToolSpec Tool specification (name, description, JSON Schema parameters)
ToolCallRequest Tool invocation request from the server
ToolCallResult Tool call result (Success / Failure)
IncomingMessage Sealed class for all server-to-client message types
ToolCallHandler Tool call handler interface (functional interface)

app — Demo Application

Class Responsibility
MainActivity Compose UI, connection/message/tool registration entry point
ClawseedService Foreground service, manages Gateway process lifecycle, scheduled task execution

ClawseedClient — WebSocket Client

Builder Pattern

val client = ClawseedClient.builder("ws://127.0.0.1:42617/ws/chat")
    .authToken("optional-token")
    .registerTool(ToolSpec(
        name = "device_info",
        description = "Get Android device information",
        parameters = """{"type":"object","properties":{},"required":[]}"""
    ))
    .toolCallHandler { request ->
        when (request.name) {
            "device_info" -> ToolCallResult.Success(queryDeviceInfo())
            else -> ToolCallResult.Failure("unknown tool")
        }
    }
    .onConnected { /* Connected */ }
    .onDisconnected { /* Disconnected */ }
    .onChunk { text -> /* Streaming text chunk */ }
    .onThinking { text -> /* Thinking process */ }
    .onDone { finalText -> /* Turn complete */ }
    .onToolCall { id, name, args -> /* Tool call notification */ }
    .onToolResult { id, name, output -> /* Tool result notification */ }
    .onAborted { /* Turn aborted */ }
    .onError { message -> /* Error */ }
    .build()

client.connect()           // Establish WebSocket connection
client.sendMessage("Hello") // Send user message
client.disconnect()         // Disconnect

Connection Flow

  1. OkHttp establishes WebSocket connection (readTimeout=0 for streaming)
  2. onOpen callback automatically sends register_tools message
  3. Server confirms tool registration (tools_registered)
  4. Connection is ready for messages

Tool Call Handling

// When a tool_call_request message is received
private fun dispatchToolCall(request: ToolCallRequest) {
    val handler = toolCallHandler ?: run {
        webSocket?.send(ToolCallResult.Failure("No handler").toJson(request.id).toString())
        return
    }
    executor.execute {
        val result = runCatching { handler.handleToolCall(request) }
            .getOrElse { ToolCallResult.Failure(it.message ?: "Exception") }
        webSocket?.send(result.toJson(request.id).toString())
    }
}

Key points: - Single-threaded executor for tool call handling to avoid race conditions - Exceptions are caught and wrapped as ToolCallResult.Failure - Results are sent back to the server immediately via WebSocket

Message Protocol

Client → Server

Type Format Description
User message {"type":"message","content":"..."} Send a chat message
Tool registration {"type":"register_tools","tools":[...]} Register tool list
Tool result {"type":"tool_result","id":"...","output":"...","success":true} Return success result
Tool error {"type":"tool_error","id":"...","error":"...","success":false} Return execution error
Regenerate {"type":"regenerate"} Regenerate last assistant response

Server → Client

Type Description
session_start Session started (sessionId, name, resumed, messageCount)
connected Connection confirmed
chunk Streaming text chunk
thinking Agent thinking process
done Turn completed (full_response)
tool_call Tool call notification (informational)
tool_result Tool result notification (informational)
tool_call_request Request client to execute a tool (requires response)
tools_registered Tool registration confirmed (count, registered)
result_acknowledged Result acknowledged
chunk_reset Reset streaming output
aborted Turn aborted
error Error message

Complete Interaction Example

Client                                  Server
  │                                       │
  │ ──── WebSocket connect ────────────→  │
  │ ──── register_tools ──────────────→  │
  │ ←─── tools_registered ────────────  │
  │                                       │
  │ ──── message: "Tell me about device" │
  │ ←─── chunk: "Let me check" ────────  │
  │ ←─── tool_call_request ────────────  │
  │      {id:"tc1", name:"device_info"}  │
  │                                       │
  │ ──── tool_result ─────────────────→  │
  │      {id:"tc1", output:"..."}        │
  │                                       │
  │ ←─── chunk: "Your device is..." ───  │
  │ ←─── done ─────────────────────────  │

ClawseedService — Foreground Service

Lifecycle

onCreate()
  ├── Create notification channel
  └── startForeground("Starting clawseed gateway...")

onStartCommand()
  └── scope.launch { startGateway() }
        ├── Extract libclawseed.so binary
        ├── ensureConfig() — configuration initialization
        ├── ProcessBuilder launches Gateway process
        │     Env: HOME, XDG_CONFIG_HOME, XDG_DATA_HOME
        │     Args: gateway --port 42617
        │     API Key: loaded from .clawseed/api_key
        └── waitUntilReady()
              └── Poll http://127.0.0.1:42617/health
                    Every 500ms, max 40 attempts (20 seconds)

onDestroy()
  ├── Cancel coroutines
  ├── Destroy Gateway process
  └── Cleanup resources

Binary Extraction and Execution

// useLegacyPackaging = true in build.gradle.kts
// libclawseed.so is extracted to nativeLibraryDir
val binary = File(applicationInfo.nativeLibraryDir, "libclawseed.so")

// Launch as subprocess
process = ProcessBuilder(binary.absolutePath, "gateway", "--port", "42617")
    .redirectErrorStream(true)
    .also { pb ->
        pb.environment()["HOME"] = filesDir.absolutePath
        pb.environment()["CLAWSEED_API_KEY"] = apiKey
    }
    .start()

Why the .so naming: Android APKs only allow .so files to be packaged in jniLibs/, but this is actually an executable Rust binary, executed via ProcessBuilder rather than System.loadLibrary().

Configuration Management

ensureConfig() handles initialization and patching:

  1. Creates ~/.clawseed/ and workspace/ directories
  2. Generates initial config if clawseed.toml doesn't exist
  3. Patches missing fields if it does (workspace_dir, web feature enablement, etc.)
  4. Auto-enables web_fetch, http_request, web_search
  5. Adds allowed_domains = ["*"] for network tools

Initial config template:

workspace_dir = "{WORKSPACE_DIR}"

[gateway]

[web_fetch]
enabled = true
allowed_domains = ["*"]

[http_request]
enabled = true
allowed_domains = ["*"]

[web_search]
enabled = true
provider = "duckduckgo"

Readiness Detection

private suspend fun waitUntilReady() {
    val healthUrl = "http://127.0.0.1:42617/health"
    repeat(MAX_HEALTH_ATTEMPTS) {  // 40 attempts
        val code = // HTTP GET healthUrl
        if (code in 200..299) {
            isReady = true
            readyCallbacks.forEach { it() }  // Notify MainActivity
            return
        }
        delay(500)  // 500ms interval
    }
    // Timeout → stop service
    stopSelf()
}

Network Security Configuration

<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">127.0.0.1</domain>
        <domain includeSubdomains="false">localhost</domain>
    </domain-config>
</network-security-config>

Only localhost cleartext connections are allowed (Gateway runs locally on port 42617).

Permissions

Permission Purpose
INTERNET Network access (LLM API, WebSocket)
FOREGROUND_SERVICE Run foreground service
FOREGROUND_SERVICE_SPECIAL_USE Android 14+ foreground service type declaration
POST_NOTIFICATIONS Android 13+ notification permission
SCHEDULE_EXACT_ALARM Schedule exact alarms for scheduled tasks (Android 12+; falls back to inexact alarms if not granted)
RECEIVE_BOOT_COMPLETED Re-schedule enabled tasks after device reboot

Build Configuration

Item Value
minSdk 26 (Android 8.0)
targetSdk / compileSdk 36 (Android 15)
Java version 17
Compose BOM 2026.04.01
OkHttp 4.12.0
Kotlin Coroutines 1.9.0
useLegacyPackaging true (binary extraction)

Steps to Customize the Demo

  1. Add tools: Define new ToolSpec and corresponding handler logic in MainActivity.kt
  2. Modify UI: Adjust Compose layout
  3. Add permissions: Declare device capabilities (camera, location, etc.) in AndroidManifest.xml
  4. Configure Gateway: Modify ClawseedService.INITIAL_CONFIG to adjust defaults
  5. API Key: Place in filesDir/.clawseed/api_key file

Scheduled Background Tasks

The task list distinguishes the next planned run from the previous execution. Open a task or its details icon to inspect the full prompt, stored result, and error. Failure details link to settings without automatically rerunning the task. The time picker opens on demand, keeping repeat modes and selected weekdays in a compact editor.

The app supports AlarmManager-based scheduled tasks that wake the device at specified times, execute AI prompts via WebSocket, and notify the user of results.

Architecture

  • ScheduledTaskManager — Manages alarm scheduling via AlarmManager.setExactAndAllowWhileIdle()
  • ScheduledTaskStore — DataStore persistence with atomic merge updates
  • BootReceiver — Re-schedules all enabled tasks after device reboot (RECEIVE_BOOT_COMPLETED)
  • ClawseedService task mode — Executes tasks via independent WebSocket session to avoid conflicts with chat UI

Task Model

Field Type Description
id UUID Unique task ID
name String Display name
message String AI prompt to send
hour / minute Int Scheduled time (24-hour format)
repeat Enum ONCE, DAILY, WEEKDAY, or CUSTOM
repeatDays List Selected weekdays for CUSTOM: 1 = Monday through 7 = Sunday
enabled Boolean Enable/disable without deletion
sessionId String? Optional session target

Repeat Modes

  • ONCE — Fires once, then auto-disables
  • DAILY — Fires every day at the specified time
  • WEEKDAY — Fires Monday–Friday only
  • CUSTOM: Fires only on the selected weekdays; selections persist across edits and restarts.

Execution Flow

  1. AlarmManager fires at scheduled time
  2. onTaskFired() wakes ClawseedService via Channel queue
  3. Service opens an independent WebSocket session (not the chat UI's session)
  4. AI prompt is sent and tools (device_info, get_location, CETP) are available
  5. High-priority notification with sound/vibration shows the result
  6. Tapping the notification navigates to the task's session in chat
  7. For recurring tasks, the next alarm is rescheduled automatically

Appearance Settings

The chat composer only takes focus on user interaction, so opening history does not open the keyboard. The history drawer searches titles, personas, and session IDs, groups conversations into collapsible persona sections, and shows each conversation’s local activity date. Conversation actions include pin/unpin: pinned conversations from all personas appear once in a dedicated section at the top, with their persona labels. Each persona menu can delete all its conversations, including pins and conversations hidden by search or collapse. The confirmation shows the total count; the confirmed snapshot is deleted sequentially, with failed conversations retained and reported. Persona headers remain available even when all their conversations are pinned. Pin and collapse preferences persist locally across app restarts. Search expands matching groups without changing the saved collapse preference. Loading failures are distinguished from an empty list. LLM configuration uses a dedicated view with a persistent save bar; service diagnostics are collapsed by default. Persona tool permissions are grouped and expandable without changing authorization rules.

The app supports light/dark/system theme selection with an OLED mode option:

Colors are centralized in ui/theme/AppColors.kt: neutral gray surfaces, warm gold actions and selections, green success states, and red errors and destructive actions. The composer uses a neutral outline that turns gold on focus. Persona colors remain in avatars and labels with only a subtle tint on large surfaces; label contrast follows the selected app theme.

  • System — Follows Android system setting (default)
  • Light — Always light theme
  • Dark — Always dark theme
  • OLED — Dark theme with true black backgrounds for OLED displays

Soul Customization

The settings UI includes a dedicated Soul editor that reads and writes workspace personality files (SOUL.md, etc.) via the /api/personality API endpoint. Only files in the allowlist can be edited. The Gateway restarts automatically after saving to apply personality changes.

Profile and Memory Management

Settings opens a unified "What the assistant remembers" screen with About me and Memory tabs. About me supports category, inferred-source, and rejected-status filters; multi-select delete/reject; delete-all-inferred; expiring change previews; conflict refresh; and one-tap undo after apply. Pending plan identity is retained across activity recreation, but a plan is never applied automatically. Memory entries show category and namespace so persona scope remains visible.

Profile tool results in chat use the same structured preview and action controls instead of parsing model text. Destructive operations require confirmation.

Profile and memory data are personal data. Privacy-filtered exports exclude both USER_PROFILE and MEMORY. Full archives may include both in plaintext and must be stored and shared accordingly; an in-place app upgrade never requires clearing app data or restoring an archive.

On Android 16, the app requests NEARBY_WIFI_DEVICES before starting the embedded loopback gateway. Android 16 uses this permission when its opt-in local-network protection is active; without it, opening the local listener can fail with EPERM.

Image conversations

Configure DeepSeek with model deepseek-v4-flash-vision-exp. In a connected chat, use the attachment icon inside the input field to choose up to four gallery images, inspect thumbnails, remove an image, or retry a failed upload. Send images alone or with text. Tap a sent image to open it and pinch to zoom. Older gateways report unsupported image uploads.

Draft copies are stored in app files and scoped to the gateway URL and session. Orientation is corrected; large images are downscaled and encoded as PNG to preserve screenshot text where possible. GIF selection uses a still frame. Failed turns retain image drafts for retry, including after process restart. Sent image history is fetched with the session's gateway authentication and survives gateway/app restarts. When earlier images leave the model context, a chat notice asks you to attach them again if needed; they remain viewable.

Vision configuration

LLM settings expose auto, enabled, and disabled vision modes. Existing profiles default to auto; the legacy DeepSeek vision model remains recognized. Other unknown models require an explicit setting in this first phase (provider metadata discovery and image probes are not yet implemented).

Personas can inherit or override vision independently. Inheritance applies only to the same model; changing the model resets capability to automatic. The chat attachment icon is disabled unless the effective session provider supports images. The gateway sends this decision in session_start, including on resume, and enforces it when processing images. Persona changes apply on new connections.

See Android file attachments for document metadata, budgets and client-side reading.