Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ba5e013
Kernel: add native embedding adapter capability
jsilvanus Sep 4, 2026
75df0af
Engine: implement native llama embedding adapter
jsilvanus Sep 4, 2026
28e3e65
Engine: reconcile Android model runtime with persistent catalog
jsilvanus Sep 4, 2026
f2b7214
Engine: wire persistent model catalog into native backend
jsilvanus Sep 4, 2026
e62097b
Engine: parse optional model context metadata safely
jsilvanus Sep 4, 2026
cee9fda
Engine: route embeddings through native adapter
jsilvanus Sep 5, 2026
e32740c
Test Engine native embedding HTTP path
jsilvanus Sep 5, 2026
580b450
Engine: verify installed model digest before load
jsilvanus Sep 5, 2026
e8842eb
Engine: make model descriptor metadata explicit
jsilvanus Sep 5, 2026
4983a45
Engine: expose authoritative model format and quantization
jsilvanus Sep 5, 2026
75a21f3
Engine: enforce catalog authority when installing models
jsilvanus Sep 5, 2026
b4be031
Engine: test catalog authority and digest pinning
jsilvanus Sep 5, 2026
d0682a6
Engine: expose catalog metadata and integrity errors
jsilvanus Sep 5, 2026
c7d3189
Engine: reconcile installed model catalog state
jsilvanus Sep 5, 2026
fb581ae
Engine: reconcile model catalog before listing
jsilvanus Sep 5, 2026
0825ec1
test(engine): verify shutdown cancels admitted inference
jsilvanus Sep 5, 2026
8f49a2e
test(engine): cover model integrity API errors
jsilvanus Sep 5, 2026
395d0e7
test(engine): remove unused integrity test imports
jsilvanus Sep 5, 2026
3bc5ac1
feat(storage): add Android SQLDelight driver support
jsilvanus Sep 5, 2026
e31ea47
feat(storage): provide Android app-private database factory
jsilvanus Sep 5, 2026
062fe8e
feat(android): add persistent storage dependency
jsilvanus Sep 5, 2026
27bbebd
feat(android): wire persistent RuntimeClient factory
jsilvanus Sep 5, 2026
74a154c
feat(android): use persistent RuntimeClient in MainActivity
jsilvanus Sep 5, 2026
6653e60
feat(android): share persistent RuntimeClient with service
jsilvanus Sep 5, 2026
3227bd1
fix(storage): expose Android drivers directly
jsilvanus Sep 5, 2026
21853e2
fix(android): use Android storage driver API
jsilvanus Sep 5, 2026
2424c49
feat(engine): reconcile model catalog during service startup
jsilvanus Sep 5, 2026
14aa92d
fix(engine): preserve service wiring and reconcile catalog at startup
jsilvanus Sep 5, 2026
84a0d50
Engine: gate model deletion on inference admission
jsilvanus Sep 5, 2026
31afcf3
Engine: expose inference-gated model deletion
jsilvanus Sep 5, 2026
c4675d1
Engine: route UI model deletion through inference gate
jsilvanus Sep 5, 2026
cb465a0
Engine: use safe deletion from model UI
jsilvanus Sep 5, 2026
798c303
Engine: test safe model deletion gate
jsilvanus Sep 5, 2026
57e0648
Engine: add cancellable model adapter capability
jsilvanus Sep 5, 2026
f202efb
Engine: interrupt active llama.cpp generation on model close
jsilvanus Sep 5, 2026
1ddee0d
Engine: close models by cancelling native inference first
jsilvanus Sep 5, 2026
e2fbd94
Engine: scope close cancellation to the target model
jsilvanus Sep 5, 2026
5d82f82
Engine: test interrupting inference before model unload
jsilvanus Sep 5, 2026
add825e
Engine: make cancellation test model unload ordering deterministic
jsilvanus Sep 5, 2026
72861ff
Engine: expose model close through HTTP server
jsilvanus Sep 5, 2026
693b753
Engine: expose model close through service API
jsilvanus Sep 5, 2026
aea488a
Engine: add explicit model open lifecycle operation
jsilvanus Sep 5, 2026
09d9b7e
Engine: expose explicit model open through service API
jsilvanus Sep 5, 2026
95092da
Engine: wire service open directly to runtime load
jsilvanus Sep 5, 2026
90cdbd2
Engine: test explicit model open
jsilvanus Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions agent/androidapp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ kotlin {
androidTarget()

sourceSets {
// M28/M31 platform-neutral logic lives in commonMain so that when androidTarget()
// is wired (needs AGP from dl.google.com), no file moves are required.
commonMain.dependencies {
implementation(project(":kernel"))
implementation(project(":api"))
Expand All @@ -31,10 +29,6 @@ kotlin {
implementation("app.cash.sqldelight:runtime:2.0.2")
implementation("app.cash.sqldelight:sqlite-driver:2.0.2")
implementation("org.xerial:sqlite-jdbc:3.45.2.0")
// jvmMain has no Compose UI of its own, but the Compose Compiler Gradle plugin
// (kotlin.plugin.compose) runs its version check against every compilation in
// this module, androidTarget included — it fails hard without the runtime on
// the classpath even here. Unused at runtime, only satisfies that check.
implementation("androidx.compose.runtime:runtime:1.6.0")
}
}
Expand All @@ -50,34 +44,31 @@ kotlin {
}
}

// androidMain sourceSet — uncomment when androidTarget() is wired.
val androidMain by getting {
dependencies {
implementation(project(":kernel"))
implementation(project(":api"))
implementation(project(":storage"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")

// Compose and Material Design 3
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.1.0")
implementation("androidx.compose.foundation:foundation:1.6.0")
implementation("androidx.activity:activity-compose:1.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")

// Navigation
implementation("androidx.navigation:navigation-compose:2.7.0")

// Android core
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
// AidosService : LifecycleService (RFC-0050) — the foreground service hosting
// RuntimeServiceHost.
implementation("androidx.lifecycle:lifecycle-service:2.6.1")

// For scoped storage and file access

implementation("androidx.documentfile:documentfile:1.0.1")
}
}
Expand All @@ -93,29 +84,23 @@ sqldelight {
}
}

// android { } — uncomment when androidTarget() is wired.
android {
namespace = "fi.italeino.aidos"
compileSdk = 34

defaultConfig {
applicationId = "fi.italeino.aidos"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

// A release build type may only reference a signingConfig that is actually
// signing-ready (AGP's packageRelease requires storeFile once one is attached) - so the
// keystore's presence gates both populating the config below and attaching it to the
// release build type. Absent (e.g. no KEYSTORE_BASE64 secret in CI), the release build
// proceeds unsigned, matching the workflow's own documented fallback.
val releaseKeystorePath = System.getenv("KEYSTORE_FILE")
?: System.getenv("HOME")?.let { "$it/.android/aidos-keystore.jks" }
val hasReleaseKeystore = releaseKeystorePath != null && File(releaseKeystorePath).exists()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package fi.italeino.aidos

import android.content.Context
import dev.aidos.api.RealRuntimeClient
import dev.aidos.storage.AndroidAidosStorage
import kotlinx.datetime.Clock
import java.io.File

/**
* Process-scoped Android RuntimeClient composition root.
*
* The activity and foreground service deliberately share this instance: recreating a
* RealRuntimeClient in either component would recreate the in-memory session/event state and would
* make the persistence seams appear wired while the UI was still talking to a different client.
* Project locking stays unset until Android file-lock semantics have an instrumentation test.
*/
object AndroidRuntimeClientFactory {
@Volatile
private var instance: RealRuntimeClient? = null

fun get(context: Context): RealRuntimeClient {
instance?.let { return it }
return synchronized(this) {
instance ?: create(context.applicationContext).also { instance = it }
}
}

private fun create(context: Context): RealRuntimeClient {
val nowIso = { Clock.System.now().toString() }
val userDriver = AndroidAidosStorage.openUser(context, nowIso)
val projectsRoot = File(context.filesDir, "projects").apply { mkdirs() }

return RealRuntimeClient().apply {
this.userDriver = userDriver
projectDbFactory = { projectRoot ->
AndroidAidosStorage.openProject(context, projectRoot, nowIso)
}
runtimeManagedProjectsRoot = projectsRoot.absolutePath
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.rememberNavController
import dev.aidos.api.RealRuntimeClient
import dev.aidos.androidapp.ui.diff.CommitPresenter
import dev.aidos.androidapp.ui.projects.ProjectsPresenter
import dev.aidos.androidapp.ui.runs.RunListPresenter
import dev.aidos.androidapp.ui.sessions.SessionListPresenter
import dev.aidos.androidapp.ui.diff.CommitPresenter
import fi.italeino.aidos.navigation.AidosNavHost
import fi.italeino.aidos.theme.AidosTheme
import kotlinx.coroutines.CoroutineScope
Expand All @@ -21,21 +19,13 @@ import kotlinx.coroutines.SupervisorJob
/**
* Main activity for Aidos Android app (M27, M28, RFC-0050).
*
* Hosts the Compose UI and wires the runtime presenters.
* The runtime itself runs in a foreground service (RuntimeServiceHost, M27).
*
* This activity only manages the UI — state, navigation, and presenter lifecycle.
* All business logic remains in presenters (commonMain), which are platform-neutral
* and testable on the JVM.
* Hosts the Compose UI and wires the runtime presenters. The runtime client is the same
* process-scoped, persistence-backed instance used by the foreground service.
*/
class MainActivity : ComponentActivity() {

private val applicationScope = CoroutineScope(SupervisorJob())

// RFC-0050 MVP item 2 puts the runtime in-process in a foreground service; binding this
// activity to that service (RuntimeServiceHost) and injecting its client is the next step.
// Until that binding exists, each activity instance owns its own RealRuntimeClient — real
// command/event semantics, but state doesn't yet survive the activity being destroyed.

private lateinit var projectsPresenter: ProjectsPresenter
private lateinit var sessionListPresenter: SessionListPresenter
private lateinit var runListPresenter: RunListPresenter
Expand All @@ -44,10 +34,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// TODO(RFC-0050 MVP item 2): replace with the RuntimeClient bound from the foreground
// service (RuntimeServiceHost) once the Service subclass exists.
val runtimeClient = RealRuntimeClient()

val runtimeClient = AndroidRuntimeClientFactory.get(this)

projectsPresenter = ProjectsPresenter(runtimeClient, applicationScope)
sessionListPresenter = SessionListPresenter(runtimeClient, applicationScope)
runListPresenter = RunListPresenter(runtimeClient, applicationScope)
Expand All @@ -70,7 +58,6 @@ class MainActivity : ComponentActivity() {
}
}

// Load projects on app start
projectsPresenter.loadProjects()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService
import dev.aidos.androidapp.notification.NotificationManager as AidosNotificationManager
import dev.aidos.androidapp.service.RuntimeServiceHost
import dev.aidos.api.RealRuntimeClient
import fi.italeino.aidos.AndroidRuntimeClientFactory
import fi.italeino.aidos.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -19,36 +19,19 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* Android foreground service hosting the runtime (RFC-0050's `AidosService : LifecycleService`,
* M27, D24).
*
* Wires the platform-neutral [RuntimeServiceHost] into the Android service lifecycle:
* [onStartCommand] starts hosting a run and calls `startForeground` with the ongoing
* notification (RFC-0050 "Notifications" item 1 — "what is running, and Cancel"); [onDestroy]
* shuts the host down. Notification *content* decisions (what text, whether a given
* notification should fire at all) stay in the platform-neutral
* `dev.aidos.androidapp.notification.NotificationManager` — this class only performs the actual
* Android `NotificationManager`/`NotificationChannel` calls RFC-0050 says belong to the service
* wrapper.
*
* Owns its own [RealRuntimeClient] for now, separate from [MainActivity]'s — binding the two so
* they share one client (and its state survives activity recreation) is the next step, tracked
* in PIPELINE.md's Group 2 checklist.
*
* The Cancel action and the wake lock RFC-0050's D24(a) calls for are not wired yet — this link
* covers the service lifecycle and the ongoing notification itself; both are flagged as
* follow-up in PIPELINE.md rather than guessed at here.
*/
/** Android foreground service hosting the runtime (RFC-0050's AidosService, M27, D24). */
class AidosService : LifecycleService() {

private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val runtimeClient = RealRuntimeClient()
private val runtimeServiceHost = RuntimeServiceHost(client = runtimeClient, scope = serviceScope)
private lateinit var runtimeServiceHost: RuntimeServiceHost
private val notificationDecisions = AidosNotificationManager()

override fun onCreate() {
super.onCreate()
runtimeServiceHost = RuntimeServiceHost(
client = AndroidRuntimeClientFactory.get(this),
scope = serviceScope,
)
createNotificationChannel()

serviceScope.launch {
Expand All @@ -67,26 +50,19 @@ class AidosService : LifecycleService() {
runtimeServiceHost.startRun(runId, runDescription)
}

// A Run reaching a model call needs the foreground window regardless of what started
// this call (D24(a)) — startForeground on every onStartCommand keeps that window open.
startForeground(NOTIFICATION_ID, buildNotification())

return START_NOT_STICKY
}

override fun onDestroy() {
// RuntimeServiceHost.shutdown() cancels the active run's job and joins it so the
// recovery checkpoint (RFC-0009) is the one actually written, not whatever was in
// flight — serviceScope is cancelled separately below rather than racing this suspend
// call against Service teardown.
runBlocking { runtimeServiceHost.shutdown() }
serviceScope.cancel()
super.onDestroy()
}

private fun updateNotification() {
val manager = getSystemService(AndroidNotificationManager::class.java)
manager.notify(NOTIFICATION_ID, buildNotification())
getSystemService(AndroidNotificationManager::class.java)
.notify(NOTIFICATION_ID, buildNotification())
}

private fun buildNotification(): Notification {
Expand All @@ -100,8 +76,6 @@ class AidosService : LifecycleService() {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(content.title)
.setContentText(content.body)
// TODO: real app icon (no drawable resources exist in this module yet — tracked
// separately from the service wiring, not blocking it).
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(openAppIntent)
.setOngoing(true)
Expand All @@ -116,7 +90,8 @@ class AidosService : LifecycleService() {
).apply {
description = "Shows what Aidos is doing while a run is in progress"
}
getSystemService(AndroidNotificationManager::class.java).createNotificationChannel(channel)
getSystemService(AndroidNotificationManager::class.java)
.createNotificationChannel(channel)
}

companion object {
Expand Down
19 changes: 10 additions & 9 deletions agent/storage/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ kotlin {
implementation(project(":kernel"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// D35: the driver only, not SQLDelight's .sq schema codegen. schema/ stays the one
// canonical DDL (RFC-0040); this is the KMP SqlDriver interface gitsema-kotlin also
// uses, so both libraries share one SQLite build per process on Android.
implementation("app.cash.sqldelight:runtime:2.0.2")
}

Expand All @@ -23,16 +20,10 @@ kotlin {
}

val jvmMain by getting {
// schema/ is read directly from its one canonical location (RFC-0040) rather than
// copied into this module, so there is exactly one file a change to the DDL touches.
resources.srcDir(rootProject.projectDir.resolve("../schema"))
resources.include("*.sql")
dependencies {
implementation("app.cash.sqldelight:sqlite-driver:2.0.2")
// sqlite-driver depends on this at runtime only; SQLiteConfig (used to bake WAL /
// synchronous / foreign_keys / busy_timeout into every connection the driver
// opens, since PRAGMA after the fact only reaches one connection) needs it at
// compile time too.
implementation("org.xerial:sqlite-jdbc:3.45.2.0")
}
}
Expand All @@ -42,6 +33,12 @@ kotlin {
implementation(kotlin("test-junit"))
}
}

val androidMain by getting {
dependencies {
implementation("app.cash.sqldelight:android-driver:2.0.2")
}
}
}

compilerOptions {
Expand All @@ -57,6 +54,10 @@ android {
minSdk = 26
}

// The canonical DDL remains in the repository-level schema/ directory. Package those files
// as Android assets so AndroidStorage can feed the same MigrationRunner as the JVM target.
sourceSets["main"].assets.srcDir(rootProject.projectDir.resolve("../schema"))

compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
Expand Down
Loading
Loading