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¶
- OkHttp establishes WebSocket connection (readTimeout=0 for streaming)
onOpencallback automatically sendsregister_toolsmessage- Server confirms tool registration (
tools_registered) - 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:
- Creates
~/.clawseed/andworkspace/directories - Generates initial config if
clawseed.tomldoesn't exist - Patches missing fields if it does (workspace_dir, web feature enablement, etc.)
- Auto-enables
web_fetch,http_request,web_search - 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¶
- Add tools: Define new
ToolSpecand corresponding handler logic inMainActivity.kt - Modify UI: Adjust Compose layout
- Add permissions: Declare device capabilities (camera, location, etc.) in
AndroidManifest.xml - Configure Gateway: Modify
ClawseedService.INITIAL_CONFIGto adjust defaults - API Key: Place in
filesDir/.clawseed/api_keyfile
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 viaAlarmManager.setExactAndAllowWhileIdle()ScheduledTaskStore— DataStore persistence with atomic merge updatesBootReceiver— Re-schedules all enabled tasks after device reboot (RECEIVE_BOOT_COMPLETED)ClawseedServicetask 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¶
AlarmManagerfires at scheduled timeonTaskFired()wakesClawseedServiceviaChannelqueue- Service opens an independent WebSocket session (not the chat UI's session)
- AI prompt is sent and tools (device_info, get_location, CETP) are available
- High-priority notification with sound/vibration shows the result
- Tapping the notification navigates to the task's session in chat
- 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.