feat(agent): a setup step that prepares fast Lab builds for the installed version, kept alive while it runs

This commit is contained in:
Kurt Himebauch committed 2026-11-10 00:40:36 -05:00
1 parent eb3da7595f
commit 58c83968de
5 files changed
+47 -7

No files matched your search

@@ -378,7 +378,7 @@ object AgentHost {
now = SystemClock.elapsedRealtime()
appContext?.let { app ->
lab = LabProgress.read(LinuxRuntime.rootDir(app))
if (state == State.WORKING) AgentHostService.update(app)
AgentHostService.update(app)
}
main.postDelayed(::tick, if (state == State.WORKING || lab != null) TICK_MS else IDLE_TICK_MS)
}
@@ -18,6 +18,7 @@ class AgentHostService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_STOP) {
if (AgentHost.lab?.running == true) AgentHost.cancelLabBuild(this)
AgentHost.stop(this)
return START_NOT_STICKY
}
@@ -30,19 +31,35 @@ class AgentHostService : Service() {
private const val NOTIFICATION_ID = 4107
private const val ACTION_STOP = "com.droiddeck.launcher.agent.STOP"
private var lastText: String? = null
@Volatile private var started = false
/** Setup steps that keep the app alive while they run, besides the agent's turn. */
private var setupSteps = 0
fun start(context: Context) {
lastText = null
started = true
runCatching { context.startForegroundService(Intent(context, AgentHostService::class.java)) }
}
fun stop(context: Context) {
if (setupSteps > 0) return
started = false
context.stopService(Intent(context, AgentHostService::class.java))
}
fun setupStarted(context: Context) {
setupSteps++
start(context)
}
fun setupEnded(context: Context) {
setupSteps = (setupSteps - 1).coerceAtLeast(0)
if (AgentHost.state != AgentHost.State.WORKING) stop(context)
}
/** Refreshes the notification with the current step; unchanged text is not re-posted. */
fun update(context: Context) {
if (AgentHost.state != AgentHost.State.WORKING) return
if (!started) return
val text = describe(context)
if (text == lastText) return
lastText = text
@@ -52,6 +69,7 @@ class AgentHostService : Service() {
private fun describe(context: Context): String {
val lab = AgentHost.lab?.takeIf { it.running }
if (lab != null) return context.getString(R.string.agent_lab_progress, lab.permille / 10, minutes(lab.etaSeconds.toLong()))
if (AgentHost.state != AgentHost.State.WORKING) return context.getString(R.string.agent_setting_up)
val tool = AgentHost.running()
val elapsed = minutes((SystemClock.elapsedRealtime() - AgentHost.turnStarted) / 1000)
return if (tool != null) "${tool.tool} · ${tool.detail}".take(120) + " · $elapsed" else context.getString(R.string.agent_thinking_for, elapsed)
@@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit
* and to make Lab builds on the device.
*/
object AgentSetup {
enum class Step(val required: Boolean) { CONNECTION(true), INSTALL(true), SIGN_IN(true), GITHUB(false), BUILD_TOOLS(false) }
enum class Step(val required: Boolean) { CONNECTION(true), INSTALL(true), SIGN_IN(true), GITHUB(false), BUILD_TOOLS(false), PREPARE(false) }
enum class Status { UNKNOWN, DONE, TODO, WORKING, FAILED }
val status = mutableStateMapOf<Step, Status>()
@@ -48,8 +48,8 @@ object AgentSetup {
for (step in Step.entries) if (status[step] != Status.WORKING) {
when (step) {
Step.INSTALL -> set(step, if (AgentHost.guestFileExists(app, harness.binary)) Status.DONE else Status.TODO)
Step.BUILD_TOOLS -> set(step, if (AgentHost.guestFileExists(app, "/root/aapt2/aapt2") &&
AgentHost.guestFileExists(app, "$CHECKOUT/.git")) Status.DONE else Status.TODO)
Step.BUILD_TOOLS -> set(step, if (buildToolsInstalled(app)) Status.DONE else Status.TODO)
Step.PREPARE -> set(step, if (prepared(app)) Status.DONE else Status.TODO)
Step.CONNECTION -> probes += step to listOf("sh", "-c",
"for i in 1 2 3; do adb connect \"\$ADB_SERIAL\" >/dev/null 2>&1; " +
"adb -s \"\$ADB_SERIAL\" get-state 2>&1 | tee /dev/stderr | grep -q device && exit 0; sleep 1; done; exit 1")
@@ -74,9 +74,11 @@ object AgentSetup {
Step.GITHUB -> listOf("dd-gh-login")
Step.BUILD_TOOLS -> listOf("sh", "-c", "set -e; [ -d $CHECKOUT/.git ] || git clone --filter=blob:none $REPO $CHECKOUT; " +
"cd $CHECKOUT; [ -x tools/agent/setup ] || { echo 'This DroidDeck version has no tools/agent/setup yet'; exit 1; }; tools/agent/setup")
Step.PREPARE -> listOf("sh", "-c", "cd $CHECKOUT && dd-lab-build --prepare")
}
set(step, Status.WORKING)
detail.remove(step)
AgentHostService.setupStarted(app)
Thread({
val result = runCatching {
if (step == Step.INSTALL) installHelpers(app)
@@ -89,6 +91,7 @@ object AgentSetup {
child.waitFor()
}
main.post {
AgentHostService.setupEnded(app)
if (result.getOrNull() == 0) {
detail.remove(step)
status[step] = Status.UNKNOWN
@@ -101,6 +104,14 @@ object AgentSetup {
}, "agent-setup-${step.name.lowercase()}").start()
}
fun buildToolsInstalled(app: Context) = AgentHost.guestFileExists(app, "/root/aapt2/aapt2") && AgentHost.guestFileExists(app, "$CHECKOUT/.git")
/** dd-lab-build compiled this install's commit, so a Lab build compiles only what changed. */
private fun prepared(app: Context) = runCatching {
File(LinuxRuntime.rootDir(app), "root/.cache/dd-lab-build-base").readText().trim()
.startsWith(com.droiddeck.launcher.lab.LabOverlay.apkCommit.take(7))
}.getOrDefault(false)
/** Drops what the checks found, which belonged to the previous harness. */
fun forget() {
status.clear()
@@ -65,7 +65,10 @@ internal fun AgentSetupView(modifier: Modifier, onDone: () -> Unit = {}) {
steps.forEachIndexed { index, step ->
val status = AgentSetup.status[step] ?: Status.UNKNOWN
val (title, hint) = labels(step, AgentHost.harness.name)
val detail = AgentSetup.detail[step] ?: hint
val lab = AgentHost.lab?.takeIf { it.prepare && it.running }
val detail = if (step == Step.PREPARE && lab != null)
stringResource(R.string.agent_prepare_progress, lab.permille / 10, clock(lab.etaSeconds.toLong().coerceAtLeast(0)))
else AgentSetup.detail[step] ?: hint
CheckRow(
when (status) {
Status.DONE -> CheckState.OK
@@ -74,7 +77,8 @@ internal fun AgentSetupView(modifier: Modifier, onDone: () -> Unit = {}) {
},
title, detail, divider = index < steps.lastIndex,
) {
val blocked = step == Step.SIGN_IN && AgentSetup.status[Step.INSTALL] != Status.DONE
val blocked = (step == Step.SIGN_IN && AgentSetup.status[Step.INSTALL] != Status.DONE) ||
(step == Step.PREPARE && AgentSetup.status[Step.BUILD_TOOLS] != Status.DONE)
if ((status == Status.TODO || status == Status.FAILED) && !blocked) {
val label = stringResource(if (status == Status.FAILED) R.string.agent_setup_retry else action(step))
val run = { if (step == Step.CONNECTION) pair(context) else AgentSetup.run(context, step) }
@@ -151,6 +155,7 @@ private fun labels(step: Step, harness: String): Pair<String, String> = when (st
Step.SIGN_IN -> stringResource(R.string.agent_step_sign_in, harness) to stringResource(R.string.agent_step_sign_in_hint)
Step.GITHUB -> stringResource(R.string.agent_step_github) to stringResource(R.string.agent_step_github_hint)
Step.BUILD_TOOLS -> stringResource(R.string.agent_step_build) to stringResource(R.string.agent_step_build_hint)
Step.PREPARE -> stringResource(R.string.agent_step_prepare) to stringResource(R.string.agent_step_prepare_hint)
}
private fun action(step: Step): Int = when (step) {
@@ -158,6 +163,7 @@ private fun action(step: Step): Int = when (step) {
Step.INSTALL -> R.string.agent_action_install
Step.SIGN_IN, Step.GITHUB -> R.string.agent_action_sign_in
Step.BUILD_TOOLS -> R.string.agent_action_install
Step.PREPARE -> R.string.agent_action_prepare
}
/** The pairing DroidDeck already uses for the phantom process fix; the agent reuses its key. */
+5
View File
@@ -1164,6 +1164,7 @@
<string name="agent_failed">Failed: %1$s</string>
<string name="agent_exited">%1$s ended (exit %2$d)</string>
<string name="agent_stopped">Stopped</string>
<string name="agent_setting_up">Setting up the agent</string>
<string name="agent_rebuild_lab">Rebuild my Lab build %1$s for the installed version of DroidDeck and restart on it.</string>
<string name="agent_tokens">%1$s in · %2$s out</string>
<string name="agent_thinking">Thinking</string>
@@ -1220,6 +1221,10 @@
<string name="agent_step_github_hint">Optional, to open pull requests</string>
<string name="agent_step_build">Build tools</string>
<string name="agent_step_build_hint">Optional, for Lab builds on this device · about 5 GB</string>
<string name="agent_step_prepare">Fast Lab builds</string>
<string name="agent_step_prepare_hint">Compiles this version ahead, so Lab builds take minutes · about 20 minutes once</string>
<string name="agent_prepare_progress">Compiling · %1$d%% · about %2$s left</string>
<string name="agent_action_prepare">Prepare</string>
<string name="agent_action_pair">Pair</string>
<string name="agent_action_install">Install</string>
<string name="agent_action_sign_in">Sign in</string>