One of the hardest trust problems in an AI agent isnât getting the action right. Itâs convincing the user itâs going to get the action right before it runs.
StareBrain operates on a userâs phone â real apps, real data, actions that canât always be undone. Booking a cab, sending a message, setting a reminder. The agent needs to earn the userâs confidence on every action, not just the first time.
The way we approached this: every action goes through a dry-run pass before it executes. The agent builds a full action plan, estimates the side effects, and presents a human-readable summary for confirmation. Only after the user approves does the real execution begin.
Hereâs how that works in code.
The action plan
Before any execution, the planner produces an ActionPlan â a complete description of what the agent intends to do:
`data class ActionPlan(
val actionId: String,
val intent: String,
val steps: List<PlannedStep>,
val estimatedSideEffects: List<SideEffect>,
val requiresConfirmation: Boolean
)
data class PlannedStep(
val stepId: String,
val appTarget: String,
val operation: String,
val parameters: Map<String, String>,
val isReversible: Boolean
)
sealed class SideEffect {
data class DataWrite(val app: String, val description: String) : SideEffect()
data class NetworkRequest(val destination: String, val description: String) : SideEffect()
data class Notification(val description: String) : SideEffect()
object NoSideEffect : SideEffect()
}`
The plan is built entirely from the intent and the resolved device context â no app is opened, no tap is made. Itâs a description of what would happen, not what is happening.
Dry-run execution
The dry-run pass walks every step in the plan and validates it without executing:
`class DryRunExecutor(
private val appRegistry: InstalledAppRegistry,
private val permissionChecker: PermissionChecker
) {
fun validate(plan: ActionPlan): DryRunResult {
val stepResults = plan.steps.map { step ->
validateStep(step)
}
return DryRunResult(
planId = plan.actionId,
stepResults = stepResults,
isExecutable = stepResults.all { it.canExecute },
blockers = stepResults.flatMap { it.blockers }
)
}
private fun validateStep(step: PlannedStep): StepValidation {
val appInstalled = appRegistry.isInstalled(step.appTarget)
val hasPermission = permissionChecker.hasRequired(step.operation)
val blockers = buildList {
if (!appInstalled) add(Blocker.AppNotInstalled(step.appTarget))
if (!hasPermission) add(Blocker.PermissionMissing(step.operation))
}
return StepValidation(
stepId = step.stepId,
canExecute = blockers.isEmpty(),
blockers = blockers
)
}
}
data class DryRunResult(
val planId: String,
val stepResults: List<StepValidation>,
val isExecutable: Boolean,
val blockers: List<Blocker>
)
sealed class Blocker {
data class AppNotInstalled(val appName: String) : Blocker()
data class PermissionMissing(val permission: String) : Blocker()
data class ParameterUnresolved(val paramName: String) : Blocker()
}`
If the dry run finds blockers, the agent surfaces them before asking for confirmation â not after attempting execution and failing.
The confirmation surface
The dry-run result feeds directly into the confirmation UI. The user sees a plain-language summary of whatâs about to happen:
`fun ActionPlan.toConfirmationSummary(): ConfirmationSummary {
return ConfirmationSummary(
title = intent,
steps = steps.map { step ->
"${step.appTarget}: ${step.operation} " +
step.parameters.entries.joinToString(", ") { "${it.key} = ${it.value}" }
},
sideEffects = estimatedSideEffects
.filterNot { it is SideEffect.NoSideEffect }
.map { it.toHumanReadable() },
hasIrreversibleSteps = steps.any { !it.isReversible }
)
}
data class ConfirmationSummary(
val title: String,
val steps: List<String>,
val sideEffects: List<String>,
val hasIrreversibleSteps: Boolean
)`
If any step is irreversible, the confirmation dialog uses stronger language and requires an explicit tap â not just a dismiss. A booking canât be unbooked automatically. A sent message canât be unsent. The user should know that before they confirm.
Why dry-run before confirm, not confirm before dry-run
The tempting shortcut is to ask for confirmation first, then validate. Itâs simpler to implement.
The problem: if validation fails after confirmation, youâve already set an expectation you canât fulfill. The user said yes, and then the agent came back with âactually, the app isnât installed.â Thatâs a worse experience than not asking yet.
Dry-run first means the confirmation is always about a plan the agent knows it can execute. The âyesâ is to something real, not to a plan that might fail on step two.
The gap this closes
On-device agents earn trust by being predictable. The user doesnât need the agent to be perfect â they need to know what itâs about to do before it does it, and to have a moment to say no.
Dry-run mode is how we make that moment meaningful. Not a generic âthe agent will now perform actions on your behalfâ disclaimer, but a specific, checkable list of exactly whatâs going to happen.
If the user can read the plan and say âyes, thatâs what I meantâ â thatâs the confirmation worth having.
Top comments (0)