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.
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 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, or WEEKDAY |
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
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 app supports light/dark/system theme selection with an OLED mode option:
- 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.