RFC-000: Overall Architecture
RFC-000: Overall Architecture
Document Information
- RFC ID: RFC-000
- Related PRD: All (FEAT-001 through FEAT-009)
- Created: 2026-02-27
- Last Updated: 2026-02-27
- Status: Draft
- Author: TBD
Overview
Background
OneClaw is an Android app that serves as a mobile AI Agent runtime environment. Before implementing individual features, we need to establish the foundational technical decisions: technology stack, architecture pattern, module structure, core data models, and project directory layout. All subsequent feature RFCs will build on top of these decisions.
Goals
- Define the technology stack for the entire project
- Define the overall architecture pattern and layering
- Define the module structure and inter-module dependencies
- Define core data models shared across modules
- Define the project directory structure
- Provide enough detail for AI-assisted code generation to produce consistent, reproducible results
Non-Goals
- Detailed implementation of any specific feature (covered in RFC-001 through RFC-005)
- UI/UX design specifications (covered in UI Design Spec and PRDs)
- Testing implementation details (covered in testing strategy docs)
Technology Stack
Core Technologies
| Technology | Version | Purpose | Why This Choice |
|---|---|---|---|
| Kotlin | 2.0.x | Primary language | Android official language. Modern, expressive, null-safe. |
| Jetpack Compose | 1.6.x+ | UI framework | Modern declarative UI. Ideal for chat interfaces with dynamic content. Less boilerplate than XML layouts. |
| Kotlin Coroutines | 1.8.x | Asynchronous programming | Native Kotlin async. Clean syntax for sequential async operations (API calls, DB queries, tool execution). |
| Kotlin Flow | (part of Coroutines) | Reactive streams | Streaming API responses, real-time UI updates, state management. |
| Room | 2.6.x | Local database (SQLite) | Jetpack official ORM. Type-safe queries, migration support, Flow integration. |
| OkHttp | 4.12.x | HTTP client | Industry standard. SSE support for streaming. Interceptors for auth headers. |
| Retrofit | 2.9.x | REST API client | Built on OkHttp. Type-safe API definitions. Mature ecosystem. |
| Koin | 3.5.x | Dependency injection | Pure Kotlin DSL, simple to configure. No annotation processing, no compile-time overhead. Easy for AI to generate correct code. Runtime dependency resolution with test verification. |
| Jetpack Navigation Compose | 2.7.x | Screen navigation | Official Compose navigation. Type-safe routes. |
| Android Keystore + EncryptedSharedPreferences | (Android SDK) | Secrets storage | Encrypt API keys at rest. Hardware-backed security on supported devices. |
| Gson / Kotlinx Serialization | Latest | JSON serialization | Parse API responses, serialize tool parameters and results. Kotlinx Serialization preferred for Kotlin-native approach. |
| Coil | 3.x | Image loading | Compose-native image loading (for future multimodal support). Lightweight. |
| compose-markdown (Mikepenz) | Latest | Markdown rendering | Render AI responses as Markdown (code blocks, tables, links). Most active Compose Markdown library. See RFC-001. |
Build & Tooling
| Tool | Version | Purpose |
|---|---|---|
| Gradle | 8.x | Build system |
| Android Gradle Plugin | 8.x | Android build |
| Min SDK | 26 (Android 8.0) | Minimum supported version |
| Target SDK | 35 (Android 15) | Target version |
| Compile SDK | 35 | Compile version |
| Kotlin Compiler Plugin | Compose Compiler | Compose support |
Key Library Choices Explained
Why Koin over Hilt
- Simplicity: Pure Kotlin DSL, no annotations, no code generation. Configuration is plain Kotlin code.
- AI-friendly: Simpler code is easier for AI to generate correctly and for humans to review.
- Compile time: No annotation processing step, faster builds.
- Sufficient for project scale: Runtime DI resolution is fine for an app of this complexity.
- Testing: Koin provides
checkModules()to verify all dependencies resolve correctly at test time.
Why OkHttp + Retrofit over Ktor
- Ecosystem maturity: Most Android networking examples, tutorials, and StackOverflow answers use Retrofit.
- SSE support: OkHttp has solid SSE (Server-Sent Events) support for streaming AI responses.
- AI code generation: AI models have more training data on Retrofit patterns, resulting in more reliable generated code.
- Interceptor pattern: Clean way to inject API keys and handle auth across different providers.
Why Kotlinx Serialization over Gson
- Kotlin-native: Works with Kotlin data classes natively, including default values and nullable types.
- No reflection: Compile-time serialization, better performance.
- Multiplatform ready: If we ever consider KMP (Kotlin Multiplatform) in the future.
Architecture Pattern
Overall: Clean Architecture + MVVM
The app follows Clean Architecture with three main layers, combined with MVVM for the UI layer.
┌────────────────────────────────────────────────────┐
│ UI Layer │
│ (Compose Screens, ViewModels, UI State) │
│ │
│ Screen ←→ ViewModel ←→ UI State │
├────────────────────────────────────────────────────┤
│ Domain Layer │
│ (Use Cases, Domain Models, Repository Interfaces) │
│ │
│ UseCase → Repository Interface │
├────────────────────────────────────────────────────┤
│ Data Layer │
│ (Repository Impl, Local DB, Remote API, Tools) │
│ │
│ Repository Impl → DAO / API Service / Tool Engine │
└────────────────────────────────────────────────────┘
Layer Rules
UI Layer
- Contains: Compose screens, ViewModels, UI state data classes, UI-specific mappers
- Depends on: Domain layer only
- Never: Directly accesses data sources (DB, API, file system)
- Pattern: MVVM with unidirectional data flow
- ViewModel exposes
StateFlow<UiState>to the screen - Screen sends events/actions to ViewModel
- ViewModel calls UseCases, updates state
- ViewModel exposes
Domain Layer
- Contains: Use cases, domain model data classes, repository interfaces
- Depends on: Nothing (this is the innermost layer)
- Never: Contains Android-specific imports (no
Context, noActivity, no Android SDK) - Purpose: Pure business logic, testable without Android framework
Data Layer
- Contains: Repository implementations, Room DAOs and entities, Retrofit API services, data mappers
- Depends on: Domain layer (implements repository interfaces)
- Never: Exposes data-layer-specific types to the domain (e.g., Room entities stay in data layer; domain gets domain models)
Data Flow Example: User Sends a Message
1. ChatScreen: User taps send button
→ ChatViewModel.sendMessage(text)
2. ChatViewModel:
→ Updates UI state to show user message + loading indicator
→ Calls SendMessageUseCase(sessionId, text)
3. SendMessageUseCase:
→ Gets current Agent's config via AgentRepository
→ Gets provider/model config via ProviderRepository
→ Saves user message via MessageRepository
→ Calls ModelApiService.sendToModel(messages, tools, systemPrompt)
4. ModelApiService (implementation):
→ Formats request for the specific provider (OpenAI/Anthropic/Gemini adapter)
→ Sends HTTP request via Retrofit/OkHttp
→ Receives streaming response via SSE
→ Emits response chunks as Flow<StreamChunk>
5. SendMessageUseCase:
→ Collects streaming chunks
→ If chunk contains tool call → delegates to ToolExecutionEngine
→ ToolExecutionEngine runs tool, returns result
→ Result sent back to model (loop continues)
→ Saves AI response and tool call records via MessageRepository
6. ChatViewModel:
→ Collects updates from UseCase
→ Updates UI state progressively (streaming text, tool call status)
7. ChatScreen:
→ Recomposes based on UI state changes
→ User sees streaming response and tool call indicators
Module Structure
Feature Modules
The app is organized by feature modules. Each feature module contains its own UI, ViewModel, and feature-specific use cases. Shared domain models and repository interfaces live in a core module.
┌─────────────────────────────────────────────┐
│ App Module │
│ (Application class, Navigation, DI setup) │
├─────────────────────────────────────────────┤
│ Feature Modules │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Chat │ │ Agent │ │ Provider │ │
│ │ (001) │ │ (002) │ │ (003) │ │
│ └────┬────┘ └────┬────┘ └────┬─────┘ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴─────┐ │
│ │ Tool │ │ Session │ │ Settings │ │
│ │ (004) │ │ (005) │ │ (009) │ │
│ └────┬────┘ └────┬────┘ └────┬─────┘ │
│ │ │ │ │
├───────┴───────────┴───────────┴──────────────┤
│ Core Module │
│ (Domain models, Repository interfaces, │
│ shared utilities, base classes) │
├──────────────────────────────────────────────┤
│ Data Module │
│ (Room DB, Retrofit services, Repository │
│ implementations, Provider adapters) │
└──────────────────────────────────────────────┘
Module Dependencies
App Module
├── depends on: all Feature Modules, Core, Data
│
Feature Modules (chat, agent, provider, tool, session, settings)
├── depends on: Core Module
├── does NOT depend on: other Feature Modules directly
│ (communication goes through Core's domain layer)
│
Core Module
├── depends on: nothing (pure Kotlin)
│
Data Module
├── depends on: Core Module
Inter-Module Communication
Feature modules do not depend on each other directly. They communicate through:
- Shared domain models in Core (e.g.,
Session,Agent,Messageare defined in Core) - Repository interfaces in Core (e.g.,
AgentRepositoryinterface in Core, implementation in Data) - Navigation in App module (App module knows all feature modules and sets up navigation)
- Koin DI resolves cross-module dependencies at runtime
Core Data Models
These are the shared domain models defined in the Core module. They are used across multiple features.
Agent
data class Agent(
val id: String, // UUID
val name: String, // Display name
val description: String?, // Optional description
val systemPrompt: String, // System prompt text
val toolIds: List<String>, // IDs of tools this agent can use
val preferredProviderId: String?, // Optional preferred provider
val preferredModelId: String?, // Optional preferred model
val isBuiltIn: Boolean, // Whether this is a built-in agent
val createdAt: Long, // Timestamp millis
val updatedAt: Long // Timestamp millis
)
Provider
data class Provider(
val id: String, // UUID
val name: String, // Display name
val type: ProviderType, // OPENAI, ANTHROPIC, GEMINI (API protocol format)
val apiBaseUrl: String, // Base URL for API requests
val isPreConfigured: Boolean, // true = built-in template, false = user-created
val isActive: Boolean, // Whether this provider is enabled
val createdAt: Long,
val updatedAt: Long
)
// NOTE: API keys are NOT stored in the Provider domain model or in Room.
// They are stored separately in EncryptedSharedPreferences via ApiKeyStorage.
// To access the API key for a provider, use: ApiKeyStorage.getApiKey(providerId)
enum class ProviderType {
OPENAI, // OpenAI-compatible API format (also used for custom OpenAI-compatible endpoints)
ANTHROPIC, // Anthropic API format
GEMINI // Google Gemini API format
}
// NOTE: There is no CUSTOM type. The `type` field represents the API protocol format,
// not the service identity. User-created providers pick OPENAI, ANTHROPIC, or GEMINI
// based on which API format their endpoint is compatible with.
// The `isPreConfigured` field distinguishes built-in templates from user-created providers.
// See RFC-003 for full details.
Model
data class AiModel(
val id: String, // Model identifier (e.g., "gpt-4o")
val displayName: String?, // Human-friendly name
val providerId: String, // Which provider this belongs to
val isDefault: Boolean, // Whether this is the global default
val source: ModelSource // How this model was added
)
enum class ModelSource {
DYNAMIC, // Fetched from provider API
PRESET, // Pre-configured fallback
MANUAL // User-added
}
Session
data class Session(
val id: String, // UUID
val title: String, // Display title
val currentAgentId: String, // Current agent for this session
val messageCount: Int, // Number of messages
val lastMessagePreview: String?, // Truncated last message text for list display
val isActive: Boolean, // Whether a request is in-flight
val deletedAt: Long?, // Soft-delete timestamp (null = not deleted)
val createdAt: Long,
val updatedAt: Long // Last activity timestamp
)
ResolvedModel
/**
* Pairs an AiModel with its Provider for model resolution result.
* Used by ResolveModelUseCase (RFC-002) to return the resolved model + provider together.
*/
data class ResolvedModel(
val model: AiModel,
val provider: Provider
)
Message
data class Message(
val id: String, // UUID
val sessionId: String, // Which session this belongs to
val type: MessageType, // USER, AI_RESPONSE, TOOL_CALL, TOOL_RESULT, ERROR, SYSTEM
val content: String, // Message text content
val thinkingContent: String?, // AI thinking/reasoning content (for AI_RESPONSE type)
val toolCallId: String?, // Tool call ID (for TOOL_CALL and TOOL_RESULT types)
val toolName: String?, // Tool name (for TOOL_CALL type)
val toolInput: String?, // Tool input JSON (for TOOL_CALL type)
val toolOutput: String?, // Tool output JSON (for TOOL_RESULT type)
val toolStatus: ToolCallStatus?, // Tool execution status
val toolDurationMs: Long?, // Tool execution duration
val tokenCountInput: Int?, // Input tokens (if available)
val tokenCountOutput: Int?, // Output tokens (if available)
val modelId: String?, // Which model generated this (for AI_RESPONSE)
val providerId: String?, // Which provider was used
val createdAt: Long // Timestamp
)
enum class MessageType {
USER, // User's text message
AI_RESPONSE, // AI's text response (may include thinking content)
TOOL_CALL, // AI requested a tool call
TOOL_RESULT, // Result returned from tool execution
ERROR, // Error message (API failure, tool failure, etc.)
SYSTEM // System message (e.g., "Switched to Agent X")
}
enum class ToolCallStatus {
PENDING, // Tool call requested, not yet executed
EXECUTING, // Tool is currently running
SUCCESS, // Tool completed successfully
ERROR, // Tool failed
TIMEOUT // Tool timed out
}
ToolDefinition
data class ToolDefinition(
val name: String, // Unique tool name (snake_case)
val description: String, // Human-readable description
val parametersSchema: ToolParametersSchema, // Structured parameter schema (see RFC-004)
val requiredPermissions: List<String>, // Android permissions needed
val timeoutSeconds: Int // Max execution time
)
data class ToolParametersSchema(
val properties: Map<String, ToolParameter>, // Parameter name -> definition
val required: List<String> = emptyList() // Names of required parameters
)
data class ToolParameter(
val type: String, // "string", "integer", "number", "boolean", "object", "array"
val description: String, // Human-readable description
val enum: List<String>? = null, // Allowed values (if restricted)
val default: Any? = null // Default value (if optional)
)
ToolResult
data class ToolResult(
val status: ToolResultStatus, // SUCCESS or ERROR
val result: String?, // Result data (for success)
val errorType: String?, // Error type (for error)
val errorMessage: String? // Error message (for error)
)
enum class ToolResultStatus {
SUCCESS, ERROR
}
Database Schema
Room database with the following tables. Room entities are in the Data layer and map to/from domain models.
Tables
agents
CREATE TABLE agents (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
description TEXT,
system_prompt TEXT NOT NULL,
tool_ids TEXT NOT NULL, -- JSON array of tool ID strings
preferred_provider_id TEXT,
preferred_model_id TEXT,
is_built_in INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
providers
CREATE TABLE providers (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- "OPENAI", "ANTHROPIC", "GEMINI"
api_base_url TEXT NOT NULL,
is_pre_configured INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- NOTE: API keys are NOT stored in this table.
-- They are stored in EncryptedSharedPreferences (Android Keystore-backed)
-- via the ApiKeyStorage class, keyed by provider ID.
models
CREATE TABLE models (
id TEXT NOT NULL,
display_name TEXT,
provider_id TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL, -- "DYNAMIC", "PRESET", "MANUAL"
PRIMARY KEY (id, provider_id),
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
);
sessions
CREATE TABLE sessions (
id TEXT PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
current_agent_id TEXT NOT NULL,
message_count INTEGER NOT NULL DEFAULT 0,
last_message_preview TEXT,
is_active INTEGER NOT NULL DEFAULT 0,
deleted_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_sessions_updated_at ON sessions(updated_at);
CREATE INDEX idx_sessions_deleted_at ON sessions(deleted_at);
messages
CREATE TABLE messages (
id TEXT PRIMARY KEY NOT NULL,
session_id TEXT NOT NULL,
type TEXT NOT NULL, -- "USER", "AI_RESPONSE", "TOOL_CALL", "TOOL_RESULT", "ERROR", "SYSTEM"
content TEXT NOT NULL,
thinking_content TEXT,
tool_call_id TEXT,
tool_name TEXT,
tool_input TEXT, -- JSON string
tool_output TEXT, -- JSON string
tool_status TEXT, -- "PENDING", "EXECUTING", "SUCCESS", "ERROR", "TIMEOUT"
tool_duration_ms INTEGER,
token_count_input INTEGER,
token_count_output INTEGER,
model_id TEXT,
provider_id TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX idx_messages_session_id ON messages(session_id);
CREATE INDEX idx_messages_created_at ON messages(created_at);
app_settings
CREATE TABLE app_settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL
);
Repository Interfaces
Defined in the Core module, implemented in the Data module.
interface AgentRepository {
fun getAllAgents(): Flow<List<Agent>>
suspend fun getAgentById(id: String): Agent?
suspend fun createAgent(agent: Agent): Agent
suspend fun updateAgent(agent: Agent): AppResult<Unit>
suspend fun deleteAgent(id: String): AppResult<Unit>
suspend fun getBuiltInAgents(): List<Agent>
}
// See RFC-002 for full details on Agent management, including built-in agent seeding,
// clone operation, and model resolution logic.
interface ProviderRepository {
fun getAllProviders(): Flow<List<Provider>>
fun getActiveProviders(): Flow<List<Provider>>
suspend fun getProviderById(id: String): Provider?
suspend fun createProvider(provider: Provider)
suspend fun updateProvider(provider: Provider)
suspend fun deleteProvider(id: String): AppResult<Unit>
suspend fun setProviderActive(id: String, isActive: Boolean)
suspend fun getModelsForProvider(providerId: String): List<AiModel>
suspend fun fetchModelsFromApi(providerId: String): AppResult<List<AiModel>>
suspend fun addManualModel(providerId: String, modelId: String, displayName: String?): AppResult<Unit>
suspend fun deleteManualModel(providerId: String, modelId: String): AppResult<Unit>
suspend fun testConnection(providerId: String): AppResult<ConnectionTestResult>
fun getGlobalDefaultModel(): Flow<AiModel?>
suspend fun setGlobalDefaultModel(modelId: String, providerId: String)
}
// See RFC-003 for full details on provider management, including pre-configured
// provider seeding, model list fetching, manual model operations, and connection testing.
interface SessionRepository {
fun getAllSessions(): Flow<List<Session>>
suspend fun getSessionById(id: String): Session?
suspend fun createSession(session: Session): Session
suspend fun updateSession(session: Session)
suspend fun softDeleteSession(id: String)
suspend fun softDeleteSessions(ids: List<String>)
suspend fun restoreSession(id: String)
suspend fun restoreSessions(ids: List<String>)
suspend fun hardDeleteSession(id: String)
suspend fun hardDeleteAllSoftDeleted()
suspend fun updateAgentForSessions(oldAgentId: String, newAgentId: String)
suspend fun updateTitle(id: String, title: String)
suspend fun setGeneratedTitle(id: String, title: String)
suspend fun updateMessageStats(id: String, count: Int, preview: String?)
suspend fun setActive(id: String, isActive: Boolean)
suspend fun updateCurrentAgent(id: String, agentId: String)
}
// Soft-delete: Sessions have a deleted_at field. softDeleteSession sets it; restoreSession clears it.
// hardDeleteSession permanently removes the session and its messages (CASCADE).
// hardDeleteAllSoftDeleted: Called on app startup to clean up sessions left in soft-deleted state.
// updateAgentForSessions: Updates all sessions referencing oldAgentId to use newAgentId.
// Used by DeleteAgentUseCase (RFC-002) for fallback to General Assistant.
// See RFC-005 for full details on session management.
interface MessageRepository {
fun getMessagesForSession(sessionId: String): Flow<List<Message>>
suspend fun addMessage(message: Message): Message
suspend fun updateMessage(message: Message)
suspend fun deleteMessagesForSession(sessionId: String)
suspend fun getMessageCount(sessionId: String): Int
suspend fun getMessagesSnapshot(sessionId: String): List<Message> // Non-reactive snapshot for building API requests (RFC-001)
suspend fun deleteMessage(id: String) // Delete single message; used by regenerate and retry (RFC-001)
}
interface SettingsRepository {
suspend fun getString(key: String): String?
suspend fun setString(key: String, value: String)
suspend fun getBoolean(key: String, default: Boolean = false): Boolean
suspend fun setBoolean(key: String, value: Boolean)
}
Provider Adapter Pattern
Different AI providers have different API formats. We use an adapter pattern to abstract this.
// Core interface for all provider adapters
interface ModelApiAdapter {
// Send a chat completion request with streaming
fun sendMessageStream(
apiBaseUrl: String,
apiKey: String,
modelId: String,
messages: List<ApiMessage>,
tools: List<ToolDefinition>?,
systemPrompt: String?
): Flow<StreamEvent>
// Fetch available models
suspend fun listModels(
apiBaseUrl: String,
apiKey: String
): AppResult<List<AiModel>>
// Test connection
suspend fun testConnection(
apiBaseUrl: String,
apiKey: String
): AppResult<ConnectionTestResult>
// Convert tool definitions to provider-specific format
fun formatToolDefinitions(tools: List<ToolDefinition>): Any
// Send a simple (non-streaming) chat completion request.
// Used for lightweight tasks like title generation (RFC-005).
// Full streaming implementation in RFC-001.
suspend fun generateSimpleCompletion(
apiBaseUrl: String,
apiKey: String,
modelId: String,
prompt: String,
maxTokens: Int = 100
): AppResult<String>
}
// Stream events emitted during a streaming response
sealed class StreamEvent {
data class TextDelta(val text: String) : StreamEvent()
data class ThinkingDelta(val text: String) : StreamEvent()
data class ToolCallStart(val toolCallId: String, val toolName: String) : StreamEvent()
data class ToolCallDelta(val toolCallId: String, val argumentsDelta: String) : StreamEvent()
data class ToolCallEnd(val toolCallId: String) : StreamEvent()
data class Usage(val inputTokens: Int, val outputTokens: Int) : StreamEvent()
data class Error(val message: String, val code: String?) : StreamEvent()
object Done : StreamEvent()
}
// Implementations (each takes OkHttpClient for HTTP calls)
class OpenAiAdapter(client: OkHttpClient) : ModelApiAdapter { /* ... */ }
class AnthropicAdapter(client: OkHttpClient) : ModelApiAdapter { /* ... */ }
class GeminiAdapter(client: OkHttpClient) : ModelApiAdapter { /* ... */ }
// Factory to get the right adapter
class ModelApiAdapterFactory(private val okHttpClient: OkHttpClient) {
fun getAdapter(providerType: ProviderType): ModelApiAdapter {
return when (providerType) {
ProviderType.OPENAI -> OpenAiAdapter(okHttpClient)
ProviderType.ANTHROPIC -> AnthropicAdapter(okHttpClient)
ProviderType.GEMINI -> GeminiAdapter(okHttpClient)
}
}
}
Tool Execution Engine
// Tool interface that all tools must implement
interface Tool {
val definition: ToolDefinition
suspend fun execute(parameters: Map<String, Any?>): ToolResult
}
// Tool registry
class ToolRegistry {
private val tools = mutableMapOf<String, Tool>()
fun register(tool: Tool) {
tools[tool.definition.name] = tool
}
fun getTool(name: String): Tool? = tools[name]
fun getAllTools(): List<ToolDefinition> = tools.values.map { it.definition }
fun getToolsByIds(ids: List<String>): List<ToolDefinition> =
ids.mapNotNull { tools[it]?.definition }
}
// Tool execution engine
class ToolExecutionEngine(
private val registry: ToolRegistry,
private val permissionChecker: PermissionChecker
) {
suspend fun executeTool(
toolName: String,
parameters: Map<String, Any?>,
availableToolIds: List<String>
): ToolResult {
// 1. Check tool exists and is available
val tool = registry.getTool(toolName)
?: return ToolResult.error("tool_not_found", "Tool '$toolName' not found")
if (toolName !in availableToolIds) {
return ToolResult.error("tool_not_available", "Tool '$toolName' is not available for this agent")
}
// 2. Check permissions
val missingPermissions = permissionChecker.getMissingPermissions(tool.definition.requiredPermissions)
if (missingPermissions.isNotEmpty()) {
val granted = permissionChecker.requestPermissions(missingPermissions)
if (!granted) {
return ToolResult.error("permission_denied", "Required permissions were denied")
}
}
// 3. Execute with timeout
return try {
withTimeout(tool.definition.timeoutSeconds * 1000L) {
tool.execute(parameters)
}
} catch (e: TimeoutCancellationException) {
ToolResult.error("timeout", "Tool execution timed out after ${tool.definition.timeoutSeconds}s")
} catch (e: Exception) {
ToolResult.error("execution_error", "Tool execution failed: ${e.message}")
}
}
}
Project Directory Structure
app/
├── build.gradle.kts
├── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/com/oneclaw/shadow/
│ │ │ │
│ │ │ ├── OneclawApplication.kt # Application class, Koin initialization
│ │ │ ├── MainActivity.kt # Single activity (Compose)
│ │ │ │
│ │ │ ├── core/ # Core Module
│ │ │ │ ├── model/ # Domain models
│ │ │ │ │ ├── Agent.kt
│ │ │ │ │ ├── Provider.kt
│ │ │ │ │ ├── AiModel.kt
│ │ │ │ │ ├── Session.kt
│ │ │ │ │ ├── Message.kt
│ │ │ │ │ ├── ToolDefinition.kt
│ │ │ │ │ ├── ToolResult.kt
│ │ │ │ │ ├── ResolvedModel.kt
│ │ │ │ │ └── AgentConstants.kt # GENERAL_ASSISTANT_ID constant (RFC-002)
│ │ │ │ ├── repository/ # Repository interfaces
│ │ │ │ │ ├── AgentRepository.kt
│ │ │ │ │ ├── ProviderRepository.kt
│ │ │ │ │ ├── SessionRepository.kt
│ │ │ │ │ ├── MessageRepository.kt
│ │ │ │ │ └── SettingsRepository.kt
│ │ │ │ └── util/ # Shared utilities
│ │ │ │ ├── Result.kt # Result wrapper
│ │ │ │ └── DateTimeUtils.kt
│ │ │ │
│ │ │ ├── data/ # Data Module
│ │ │ │ ├── local/ # Local data sources
│ │ │ │ │ ├── db/
│ │ │ │ │ │ ├── AppDatabase.kt # Room database definition
│ │ │ │ │ │ └── Converters.kt # Type converters
│ │ │ │ │ ├── dao/ # Room DAOs
│ │ │ │ │ │ ├── AgentDao.kt
│ │ │ │ │ │ ├── ProviderDao.kt
│ │ │ │ │ │ ├── ModelDao.kt
│ │ │ │ │ │ ├── SessionDao.kt
│ │ │ │ │ │ ├── MessageDao.kt
│ │ │ │ │ │ └── SettingsDao.kt
│ │ │ │ │ ├── entity/ # Room entities
│ │ │ │ │ │ ├── AgentEntity.kt
│ │ │ │ │ │ ├── ProviderEntity.kt
│ │ │ │ │ │ ├── ModelEntity.kt
│ │ │ │ │ │ ├── SessionEntity.kt
│ │ │ │ │ │ ├── MessageEntity.kt
│ │ │ │ │ │ └── SettingsEntity.kt
│ │ │ │ │ └── mapper/ # Entity <-> Domain model mappers
│ │ │ │ │ ├── AgentMapper.kt
│ │ │ │ │ ├── ProviderMapper.kt
│ │ │ │ │ ├── SessionMapper.kt
│ │ │ │ │ └── MessageMapper.kt
│ │ │ │ ├── remote/ # Remote data sources
│ │ │ │ │ ├── adapter/ # Provider API adapters
│ │ │ │ │ │ ├── ModelApiAdapter.kt # Interface
│ │ │ │ │ │ ├── ModelApiAdapterFactory.kt
│ │ │ │ │ │ ├── OpenAiAdapter.kt
│ │ │ │ │ │ ├── AnthropicAdapter.kt
│ │ │ │ │ │ ├── GeminiAdapter.kt
│ │ │ │ │ │ └── ApiMessage.kt # ApiMessage sealed class, ApiToolCall (RFC-001)
│ │ │ │ │ ├── dto/ # Data transfer objects
│ │ │ │ │ │ ├── openai/
│ │ │ │ │ │ ├── anthropic/
│ │ │ │ │ │ └── gemini/
│ │ │ │ │ └── sse/ # SSE streaming support
│ │ │ │ │ └── SseParser.kt # ResponseBody.asSseFlow() utility (RFC-001)
│ │ │ │ ├── repository/ # Repository implementations
│ │ │ │ │ ├── AgentRepositoryImpl.kt
│ │ │ │ │ ├── ProviderRepositoryImpl.kt
│ │ │ │ │ ├── SessionRepositoryImpl.kt
│ │ │ │ │ ├── MessageRepositoryImpl.kt
│ │ │ │ │ └── SettingsRepositoryImpl.kt
│ │ │ │ └── security/ # API key storage
│ │ │ │ └── ApiKeyStorage.kt # EncryptedSharedPreferences wrapper
│ │ │ │
│ │ │ ├── tool/ # Tool Module
│ │ │ │ ├── engine/
│ │ │ │ │ ├── Tool.kt # Tool interface
│ │ │ │ │ ├── ToolRegistry.kt
│ │ │ │ │ ├── ToolExecutionEngine.kt
│ │ │ │ │ ├── ToolSchemaSerializer.kt # ToolParametersSchema -> JSON Schema (RFC-004)
│ │ │ │ │ └── PermissionChecker.kt
│ │ │ │ └── builtin/ # Built-in tool implementations
│ │ │ │ ├── GetCurrentTimeTool.kt
│ │ │ │ ├── ReadFileTool.kt
│ │ │ │ ├── WriteFileTool.kt
│ │ │ │ └── HttpRequestTool.kt
│ │ │ │
│ │ │ ├── feature/ # Feature Modules (UI + ViewModels)
│ │ │ │ ├── chat/ # FEAT-001
│ │ │ │ │ ├── ChatScreen.kt
│ │ │ │ │ ├── ChatViewModel.kt
│ │ │ │ │ ├── ChatUiState.kt # ChatUiState, ChatMessageItem, ActiveToolCall
│ │ │ │ │ ├── ChatEvent.kt # Sealed class: StreamingText, ThinkingText, ToolCallStarted, etc.
│ │ │ │ │ ├── components/ # Chat-specific UI components
│ │ │ │ │ │ ├── ChatTopBar.kt
│ │ │ │ │ │ ├── ChatInput.kt
│ │ │ │ │ │ ├── MessageList.kt
│ │ │ │ │ │ ├── UserMessageBubble.kt
│ │ │ │ │ │ ├── AiMessageBubble.kt
│ │ │ │ │ │ ├── ThinkingBlock.kt
│ │ │ │ │ │ ├── ToolCallCard.kt
│ │ │ │ │ │ ├── ToolResultCard.kt
│ │ │ │ │ │ ├── ErrorMessageCard.kt
│ │ │ │ │ │ ├── SystemMessageCard.kt
│ │ │ │ │ │ ├── EmptyChatState.kt
│ │ │ │ │ │ └── StreamingCursor.kt
│ │ │ │ │ └── usecase/ # Chat-specific use cases
│ │ │ │ │ ├── SendMessageUseCase.kt
│ │ │ │ │ └── MessageToApiMapper.kt # Message -> ApiMessage conversion
│ │ │ │ │
│ │ │ │ ├── agent/ # FEAT-002
│ │ │ │ │ ├── AgentListScreen.kt
│ │ │ │ │ ├── AgentDetailScreen.kt
│ │ │ │ │ ├── AgentSelectorSheet.kt # Bottom sheet for chat agent switching
│ │ │ │ │ ├── AgentListViewModel.kt
│ │ │ │ │ ├── AgentDetailViewModel.kt
│ │ │ │ │ ├── AgentUiState.kt
│ │ │ │ │ ├── AgentValidator.kt # Input validation for agent fields
│ │ │ │ │ └── usecase/
│ │ │ │ │ ├── CreateAgentUseCase.kt
│ │ │ │ │ ├── CloneAgentUseCase.kt
│ │ │ │ │ ├── DeleteAgentUseCase.kt
│ │ │ │ │ ├── GetAgentToolsUseCase.kt
│ │ │ │ │ └── ResolveModelUseCase.kt
│ │ │ │ │
│ │ │ │ ├── provider/ # FEAT-003
│ │ │ │ │ ├── ProviderListScreen.kt
│ │ │ │ │ ├── ProviderDetailScreen.kt
│ │ │ │ │ ├── SetupScreen.kt # First-time setup
│ │ │ │ │ ├── ProviderListViewModel.kt
│ │ │ │ │ ├── ProviderDetailViewModel.kt
│ │ │ │ │ ├── ProviderUiState.kt
│ │ │ │ │ └── usecase/
│ │ │ │ │ ├── TestConnectionUseCase.kt
│ │ │ │ │ ├── FetchModelsUseCase.kt
│ │ │ │ │ └── SetDefaultModelUseCase.kt
│ │ │ │ │
│ │ │ │ ├── session/ # FEAT-005
│ │ │ │ │ ├── SessionDrawerContent.kt # Drawer composable (session list)
│ │ │ │ │ ├── SessionListViewModel.kt
│ │ │ │ │ ├── SessionUiState.kt
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── SessionListItemRow.kt
│ │ │ │ │ │ └── RenameSessionDialog.kt
│ │ │ │ │ └── usecase/
│ │ │ │ │ ├── CreateSessionUseCase.kt
│ │ │ │ │ ├── DeleteSessionUseCase.kt
│ │ │ │ │ ├── BatchDeleteSessionsUseCase.kt
│ │ │ │ │ ├── RenameSessionUseCase.kt
│ │ │ │ │ ├── GenerateTitleUseCase.kt
│ │ │ │ │ └── CleanupSoftDeletedUseCase.kt
│ │ │ │ │
│ │ │ │ └── settings/ # FEAT-009
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ └── SettingsUiState.kt
│ │ │ │
│ │ │ ├── navigation/ # App navigation
│ │ │ │ ├── NavGraph.kt
│ │ │ │ └── Routes.kt
│ │ │ │
│ │ │ ├── di/ # Koin dependency injection modules
│ │ │ │ ├── AppModule.kt # Application-level dependencies
│ │ │ │ ├── DatabaseModule.kt # Room database, DAOs
│ │ │ │ ├── NetworkModule.kt # OkHttp, Retrofit
│ │ │ │ ├── RepositoryModule.kt # Repository bindings
│ │ │ │ ├── ToolModule.kt # Tool registry, tools
│ │ │ │ └── FeatureModule.kt # ViewModels, UseCases
│ │ │ │
│ │ │ └── ui/ # Shared UI components and theme
│ │ │ ├── theme/
│ │ │ │ ├── Theme.kt
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Typography.kt
│ │ │ │ └── Shape.kt
│ │ │ └── components/ # Shared/reusable UI components
│ │ │ ├── LoadingIndicator.kt
│ │ │ ├── ErrorMessage.kt
│ │ │ └── ConfirmationDialog.kt
│ │ │
│ │ └── res/
│ │ ├── values/
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── values-zh/
│ │ │ └── strings.xml # Chinese translations
│ │ ├── drawable/
│ │ └── mipmap/
│ │
│ ├── test/ # Unit tests
│ │ └── kotlin/com/oneclaw/shadow/
│ │ ├── core/
│ │ ├── data/
│ │ ├── tool/
│ │ └── feature/
│ │
│ └── androidTest/ # Instrumented tests
│ └── kotlin/com/oneclaw/shadow/
│ ├── data/
│ └── feature/
Koin Dependency Injection Setup
// AppModule.kt - Application-level dependencies
val appModule = module {
single { ApiKeyStorage(get()) } // EncryptedSharedPreferences for API keys
single { ModelApiAdapterFactory(get()) } // get() = OkHttpClient from NetworkModule
}
// DatabaseModule.kt
val databaseModule = module {
single { Room.databaseBuilder(get(), AppDatabase::class.java, "oneclaw.db").build() }
single { get<AppDatabase>().agentDao() }
single { get<AppDatabase>().providerDao() }
single { get<AppDatabase>().modelDao() }
single { get<AppDatabase>().sessionDao() }
single { get<AppDatabase>().messageDao() }
single { get<AppDatabase>().settingsDao() }
}
// NetworkModule.kt
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) // Longer for streaming
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
}
// RepositoryModule.kt
val repositoryModule = module {
single<AgentRepository> { AgentRepositoryImpl(get()) }
single<ProviderRepository> { ProviderRepositoryImpl(get(), get(), get(), get()) }
single<SessionRepository> { SessionRepositoryImpl(get()) }
single<MessageRepository> { MessageRepositoryImpl(get()) }
single<SettingsRepository> { SettingsRepositoryImpl(get()) }
}
// ToolModule.kt
val toolModule = module {
single { ToolRegistry().apply {
register(GetCurrentTimeTool())
register(ReadFileTool())
register(WriteFileTool())
register(HttpRequestTool(get())) // Needs OkHttpClient
}}
single { PermissionChecker(get()) }
single { ToolExecutionEngine(get(), get()) }
}
// FeatureModule.kt
val featureModule = module {
// Chat
factory { SendMessageUseCase(get(), get(), get(), get(), get(), get(), get(), get()) }
// SendMessageUseCase params: AgentRepository, SessionRepository, MessageRepository,
// ProviderRepository, ApiKeyStorage, ModelApiAdapterFactory, ToolExecutionEngine, ToolRegistry
viewModel { ChatViewModel(get(), get(), get(), get(), get(), get()) }
// ChatViewModel params: SendMessageUseCase, SessionRepository, MessageRepository,
// AgentRepository, CreateSessionUseCase, GenerateTitleUseCase
// Agent
factory { CreateAgentUseCase(get()) } // AgentRepository
factory { CloneAgentUseCase(get()) } // AgentRepository
factory { DeleteAgentUseCase(get(), get()) } // AgentRepository, SessionRepository
factory { GetAgentToolsUseCase(get(), get()) } // AgentRepository, ToolRegistry
factory { ResolveModelUseCase(get(), get()) } // AgentRepository, ProviderRepository
viewModel { AgentListViewModel(get(), get()) } // AgentRepository, ToolRegistry
viewModel { AgentDetailViewModel(get(), get(), get(), get(), get(), get(), get()) }
// AgentDetailViewModel params: AgentRepository, ProviderRepository, ToolRegistry,
// CreateAgentUseCase, CloneAgentUseCase, DeleteAgentUseCase, SavedStateHandle
// Provider
factory { TestConnectionUseCase(get()) }
factory { FetchModelsUseCase(get()) }
factory { SetDefaultModelUseCase(get()) }
viewModel { ProviderListViewModel(get(), get()) } // ProviderRepository, ApiKeyStorage
viewModel { ProviderDetailViewModel(get(), get(), get(), get(), get(), get()) }
// ProviderDetailViewModel params: ProviderRepository, ApiKeyStorage,
// TestConnectionUseCase, FetchModelsUseCase, SetDefaultModelUseCase, SavedStateHandle
// Session
factory { CreateSessionUseCase(get()) }
factory { DeleteSessionUseCase(get()) }
factory { BatchDeleteSessionsUseCase(get()) }
factory { RenameSessionUseCase(get()) }
factory { GenerateTitleUseCase(get(), get(), get(), get()) } // SessionRepository, ProviderRepository, ApiKeyStorage, ModelApiAdapterFactory
factory { CleanupSoftDeletedUseCase(get()) }
viewModel { SessionListViewModel(get(), get(), get(), get(), get()) } // SessionRepository, AgentRepository, DeleteSessionUseCase, BatchDeleteSessionsUseCase, RenameSessionUseCase
// Settings
viewModel { SettingsViewModel(get()) }
}
// Application class
class OneclawApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@OneclawApplication)
modules(
appModule,
databaseModule,
networkModule,
repositoryModule,
toolModule,
featureModule
)
}
// Cleanup any sessions that were soft-deleted but not hard-deleted
// (e.g., app was killed during undo window). See RFC-005.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
val cleanup: CleanupSoftDeletedUseCase = get()
cleanup()
}
}
}
Navigation Structure
// Routes.kt
sealed class Route(val path: String) {
object SessionList : Route("sessions")
object Chat : Route("chat/{sessionId}") {
fun create(sessionId: String) = "chat/$sessionId"
}
object NewChat : Route("chat/new")
object AgentList : Route("agents")
object AgentDetail : Route("agents/{agentId}") {
fun create(agentId: String) = "agents/$agentId"
}
object AgentCreate : Route("agents/create")
object ProviderList : Route("providers")
object ProviderDetail : Route("providers/{providerId}") {
fun create(providerId: String) = "providers/$providerId"
}
object Setup : Route("setup") // First-launch welcome, skippable
object Settings : Route("settings")
}
// Navigation flow:
// App first launch (no provider configured) → Welcome/Setup screen (skippable)
// - User can set up a provider OR tap "Skip for now" to go to Chat
// - Welcome screen is only shown once on first launch; after that, always Chat
// App subsequent launches → Chat/new (empty chat, home screen)
// When user tries to send a message with no provider configured:
// - Show inline error prompting to configure a provider, with link to Settings
// Chat screen has: Drawer (session list) on left, Settings on right
// Drawer → NewChat / Chat(sessionId)
// Chat → AgentSwitcher (bottom sheet via top bar agent name)
// Settings → ProviderList / ProviderDetail / AgentList / AgentDetail
// AgentList → AgentDetail / AgentCreate
// ProviderList → ProviderDetail
//
// See UI Design Spec (docs/design/ui-design-spec.md) for detailed screen layouts
Error Handling Strategy
Result Wrapper
// Used throughout the app for operations that can fail
sealed class AppResult<out T> {
data class Success<T>(val data: T) : AppResult<T>()
data class Error(
val exception: Exception? = null,
val message: String,
val code: ErrorCode = ErrorCode.UNKNOWN
) : AppResult<Nothing>()
}
enum class ErrorCode {
NETWORK_ERROR,
AUTH_ERROR,
TIMEOUT_ERROR,
VALIDATION_ERROR,
STORAGE_ERROR,
PERMISSION_ERROR,
PROVIDER_ERROR,
TOOL_ERROR,
UNKNOWN
}
Error Handling by Layer
- Data Layer: Catches exceptions, wraps in
AppResult.Errorwith appropriateErrorCode - Domain Layer: Passes
AppResultthrough, may add business logic validation errors - UI Layer: ViewModel translates
AppResult.Errorinto user-facing error messages in UI state
Implementation Phases
Phase 1: Project Foundation
- Create Android project with correct package name and SDK versions
- Set up Gradle dependencies (all libraries listed in tech stack)
- Set up Koin dependency injection modules (empty initially)
- Create Room database with all entity classes and DAOs
- Create all domain models in Core
- Create all repository interfaces in Core
- Create repository implementations in Data (basic CRUD)
- Set up Compose theme (colors, typography, shapes)
- Set up navigation graph with placeholder screens
- Verify the app builds and runs
Phase 2: Provider & Model (FEAT-003)
- Implement provider adapter pattern (OpenAI, Anthropic, Gemini adapters)
- Implement ApiKeyStorage (EncryptedSharedPreferences wrapper)
- Implement provider management screens (list, detail)
- Implement model list fetching (dynamic + preset fallback)
- Implement connection testing
- Implement skippable welcome/setup screen (shown once on first launch; “Skip for now” goes to chat)
- Verify: user can add a provider, test connection, see models
Phase 3: Tool System (FEAT-004)
- Implement Tool interface and ToolRegistry
- Implement ToolExecutionEngine with timeout and error handling
- Implement built-in tools (get_current_time, read_file, write_file, http_request)
- Implement permission checker
- Verify: tools can be registered, invoked, return results
Phase 4: Agent Management (FEAT-002)
- Implement built-in General Assistant agent
- Implement agent management screens (list, detail, create)
- Implement clone functionality
- Implement tool selection in agent config
- Verify: user can create/edit/clone/delete agents, select tools
Phase 5: Session Management (FEAT-005)
- Implement session list screen (home screen)
- Implement session creation, deletion (swipe + undo), batch delete
- Implement title generation (Phase 1: truncated, Phase 2: AI-generated)
- Implement manual title editing
- Verify: user can create/resume/delete sessions, titles auto-generate
Phase 6: Chat Interaction (FEAT-001)
- Implement chat screen with message list
- Implement message input and send
- Implement streaming response rendering
- Implement tool call display (compact and detailed modes)
- Implement thinking block display (collapsed/expandable)
- Implement Markdown rendering
- Implement stop generation
- Implement message copying
- Implement agent switching in chat
- Implement auto-scroll and scroll-to-bottom
- Verify: full conversation flow with streaming, tool calls, and agent switching
Phase 7: Polish & Integration
- End-to-end testing of complete flows
- Error handling refinement
- Performance optimization (lazy loading, list performance)
- Offline behavior verification
- UI polish (animations, transitions, empty states)
Performance Guidelines
Memory
- Use
LazyColumnfor all lists (session list, message list, agent list) - Lazy-load message history when opening a session (paginated, not all at once)
- Release large tool results from memory after they are persisted
Network
- Streaming responses: process chunks as they arrive, don’t buffer entire response
- Model list fetch: cache results, refresh only on explicit user action or periodic schedule
- Connection test: 10-second timeout
UI
- Use
rememberandderivedStateOfto minimize recompositions in Compose - Use
ImmutableorStableannotations on data classes passed to Compose - Message list: use
keyparameter inLazyColumnitems for efficient diffing
Database
- Index
messages.session_idandmessages.created_atfor fast queries - Use
Flowfor reactive queries (session list auto-updates when data changes) - Batch inserts for tool call results when multiple tools execute
Security Guidelines
- API keys: Stored in
EncryptedSharedPreferencesbacked by Android Keystore – NOT in the Room database. TheApiKeyStorageclass providesgetApiKey(providerId)andsetApiKey(providerId, key). API keys are never part of the Provider domain model or database entity. Never logged. Never sent anywhere except the configured provider endpoint. - Database: Use Room’s standard SQLite. The database contains NO sensitive secrets (API keys are in EncryptedSharedPreferences). For future consideration, SQLCipher can be added for full DB encryption.
- Network: All provider API calls over HTTPS. OkHttp handles certificate verification.
- Logs: Never log API keys, full API responses (may contain sensitive user data), or tool results that access local files.
UI Theme
The app uses a Material 3 theme generated from Material Theme Builder. The theme files are stored in docs/design/material-theme/ and should be copied into the project at build time.
Theme Summary
- Color scheme: Warm gold/amber primary (
#6D5E0F), with green tertiary accent - Variants: Light, Dark, Medium Contrast, High Contrast (all four included)
- Dynamic Color: Enabled on Android 12+ (follows user wallpaper); falls back to the defined color scheme on older devices
- Font: Roboto (via Google Fonts provider)
- Typography: Standard Material 3 typography scale with Roboto applied to all levels
Theme Files
docs/design/material-theme/ui/theme/Color.kt- All color definitionsdocs/design/material-theme/ui/theme/Theme.kt- Theme composition with dynamic color supportdocs/design/material-theme/ui/theme/Type.kt- Typography definitionsdocs/design/material-theme/res/values-v23/font_certs.xml- Google Fonts certificate
Integration
When creating the Android project:
- Copy theme files to
app/src/main/kotlin/com/oneclaw/shadow/ui/theme/ - Update package name from
com.example.composetocom.oneclaw.shadow.ui.theme - Copy
font_certs.xmltoapp/src/main/res/values-v23/
Open Questions
Should we use Gradle multi-module or single-module?Decision: Single Gradle module with package separation. Simpler build config, easier for AI code generation, sufficient for project scale. Layer boundaries enforced by package structure (core/,data/,feature/,tool/) and code review.- Exact versions of all libraries should be pinned when project is created.
- ProGuard/R8 rules for release builds need to be defined.
Change History
| Date | Version | Changes | Owner |
|---|---|---|---|
| 2026-02-27 | 0.1 | Initial version | - |
| 2026-02-27 | 0.2 | Updated navigation flow: chat is home screen, session list in drawer, settings on right; added UI Design Spec reference | - |
| 2026-02-27 | 0.3 | API keys moved from Room DB to EncryptedSharedPreferences (ApiKeyStorage); removed api_key field from Provider model and providers table; Setup screen is skippable (shown once on first launch, user can skip to chat) | |
| 2026-02-27 | 0.4 | Removed ProviderType.CUSTOM; type now represents API protocol format (OPENAI, ANTHROPIC, GEMINI); isPreConfigured distinguishes built-in vs user-created; see RFC-003 for details | - |
| 2026-02-27 | 0.5 | AgentRepository: updateAgent/deleteAgent now return AppResult |
- |
| 2026-02-27 | 0.6 | Session model: added lastMessagePreview, deletedAt fields; sessions table: added new columns + indexes; SessionRepository: expanded with soft-delete/restore/batch/rename/messageStats/active/agentSwitch methods; ModelApiAdapter: added generateSimpleCompletion(); added ResolvedModel data class; updated session feature directory structure and Koin config; added CleanupSoftDeletedUseCase on app startup; see RFC-005 for details | - |
| 2026-02-27 | 0.7 | MessageRepository: added getMessagesSnapshot() and deleteMessage(); chat feature directory structure expanded (ChatEvent, ChatTopBar, MessageList, UserMessageBubble, AiMessageBubble, ThinkingBlock, ToolCallCard, ToolResultCard, ErrorMessageCard, SystemMessageCard, EmptyChatState, StreamingCursor, MessageToApiMapper); added ApiMessage.kt and SseParser.kt to data/remote/; Koin config: updated SendMessageUseCase (8 deps) and ChatViewModel (6 deps); added compose-markdown to tech stack; see RFC-001 for details | - |
| 2026-02-27 | 0.8 | Cross-consistency fixes: ProviderRepository updated to match RFC-003 (Result->AppResult, added getActiveProviders/setProviderActive/addManualModel/deleteManualModel, deleteProvider returns AppResult |
- |