cd /news/ai-agents/context-propagation-in-android-ai-ag… · home › topics › ai-agents › article
[ARTICLE · art-155777] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Context Propagation in Android AI Agents: Resolving Device State Before Action Dispatch

A developer built StareBrain's context propagation layer for Android AI agents, resolving device state (calendar events, saved locations, recent contacts, notifications) into action parameters at intent-parse time rather than dispatch time. Each parameter is assigned a confidence level — HIGH auto-fills silently, MEDIUM surfaces a suggestion, and LOW or UNRESOLVABLE prompts the user — so the agent avoids both asking for information the phone already has and silently filling in details it only partially knows. The snapshot approach is designed to prevent contacts or events changed after the user spoke from producing unapproved parameters.

by read5 min views1 publishedOct 23, 2026

An AI agent on a phone has access to something a server-side agent doesn’t: the device itself. Calendar events, saved locations, recent contacts, active notifications — all of it is locally available before any network call is made.

The question isn’t whether to use that context. It’s how to resolve it into action parameters without asking the user for information the phone already has, and without silently filling in details the phone only partially knows.

This post covers the context propagation layer in StareBrain — how device state gets resolved into action parameters, how confidence thresholds decide when to auto-fill vs. suggest vs. ask, and what happens when resolution fails.

When a user says “book a slot for my dentist appointment,” the agent needs to resolve:

Some of these can be resolved from device state with high confidence. Others can be suggested. Others need to be asked explicitly. Collapsing all of them into a single “ask the user for everything” flow is safe but slow. Auto-filling everything without surfacing uncertainty is fast but fragile.

The resolution layer sits between intent parsing and action dispatch, and its job is to decide which parameters fall into which bucket.

`data class DeviceContext(
    val upcomingEvents: List<CalendarEvent>,
    val savedLocations: List<SavedLocation>,
    val recentContacts: List<Contact>,
    val recentApps: List<String>,
    val activeNotifications: List<NotificationSummary>
)

data class CalendarEvent(
    val eventId: String,
    val title: String,
    val startTime: Instant,
    val location: String?,
    val attendees: List<String>
)

data class SavedLocation(
    val label: String,
    val address: String,
    val coordinates: Pair<Double, Double>
)`

The context is captured at intent-parse time — a snapshot of device state at the moment the user expressed the intent. This matters: if resolution runs against live state at dispatch time, a contact added or an event modified after the user spoke could produce parameters the user never approved.

Each parameter resolution produces a confidence level that determines what happens next:

`enum class ResolutionConfidence { HIGH, MEDIUM, LOW, UNRESOLVABLE }

data class ParameterValue(
    val value: Any?,
    val confidence: ResolutionConfidence,
    val source: ContextSource
)

sealed class ContextSource {
    object DeviceCalendar : ContextSource()
    object SavedLocations : ContextSource()
    object RecentContacts : ContextSource()
    object UserProvided : ContextSource()
    object Inferred : ContextSource()
}

fun ParameterValue.shouldAutoFill(): Boolean =
    confidence == ResolutionConfidence.HIGH

fun ParameterValue.shouldSuggest(): Boolean =
    confidence == ResolutionConfidence.MEDIUM

fun ParameterValue.shouldAsk(): Boolean =
    confidence == ResolutionConfidence.LOW ||
    confidence == ResolutionConfidence.UNRESOLVABLE`

HIGH → auto-fill silently. The device has this unambiguously (default calendar, a location saved under an exact label match).

MEDIUM → surface as a suggestion. The device has a candidate but it’s not certain (a location match on partial label, a contact with a common name).

LOW / UNRESOLVABLE → ask the user explicitly before proceeding.

`class ContextResolver(
    private val calendarProvider: CalendarProvider,
    private val locationStore: LocationStore,
    private val contactStore: ContactStore
) {
    fun resolve(
        intent: ActionIntent,
        context: DeviceContext
    ): Map<String, ParameterValue> {
        return intent.requiredParameters.associate { param ->
            param.name to resolveParameter(param, context)
        }
    }

    private fun resolveParameter(
        param: Parameter,
        context: DeviceContext
    ): ParameterValue {
        return when (param) {
            is Parameter.Location -> resolveLocation(param, context)
            is Parameter.Contact -> resolveContact(param, context)
            is Parameter.TimeSlot -> resolveTimeSlot(param, context)
            is Parameter.Calendar -> resolveCalendar(param, context)
        }
    }

    private fun resolveLocation(
        param: Parameter.Location,
        context: DeviceContext
    ): ParameterValue {
        val exactMatch = context.savedLocations
            .firstOrNull { it.label.equals(param.hint, ignoreCase = true) }

        if (exactMatch != null) {
            return ParameterValue(
                value = exactMatch,
                confidence = ResolutionConfidence.HIGH,
                source = ContextSource.SavedLocations
            )
        }

        val partialMatch = context.savedLocations
            .firstOrNull { it.label.contains(param.hint, ignoreCase = true) }

        if (partialMatch != null) {
            return ParameterValue(
                value = partialMatch,
                confidence = ResolutionConfidence.MEDIUM,
                source = ContextSource.SavedLocations
            )
        }

        return ParameterValue(
            value = null,
            confidence = ResolutionConfidence.UNRESOLVABLE,
            source = ContextSource.SavedLocations
        )
    }
}`

The resolved parameters get packaged into an AgentContext that travels with the action through to dispatch:

`data class AgentContext(
    val actionIntent: ActionIntent,
    val deviceContext: DeviceContext,
    val resolvedParameters: Map<String, ParameterValue>,
    val capturedAt: Instant
)

fun AgentContext.pendingParameters(): List<String> {
    return resolvedParameters
        .filter { (_, value) -> value.shouldAsk() }
        .map { (name, _) -> name }
}

fun AgentContext.suggestedParameters(): Map<String, ParameterValue> {
    return resolvedParameters
        .filter { (_, value) -> value.shouldSuggest() }
}

fun AgentContext.autoFilledParameters(): Map<String, Any?> {
    return resolvedParameters
        .filter { (_, value) -> value.shouldAutoFill() }
        .mapValues { (_, value) -> value.value }
}`

Before the action proceeds to the confirmation step, the agent checks pendingParameters(). If anything is unresolvable, the user is asked. If there are suggestions, they’re surfaced with the option to confirm or override. Only HIGH confidence parameters skip the user entirely.

The capturedAt timestamp on AgentContext is the snapshot boundary. Resolution runs once, against the device state at intent time. If the user takes three minutes to confirm the action, the context doesn’t re-resolve — the parameters they’re approving are the ones they were shown.

This is intentional. Re-resolving at confirmation time could change the suggested contact or location between when the user read the suggestion and when they tapped confirm, which is a different version of the stale-plan problem at the resolution layer rather than the dispatch layer.

The resolution snapshot is the contract. The dispatch layer’s job (covered in earlier posts) is to verify that the external world still matches that contract at execution time.

The resolver separates three categories of knowledge: what the device knows for certain, what it can reasonably guess, and what it genuinely doesn’t have. Surfacing that distinction explicitly — rather than flattening it into “fill what you can and ask for the rest” — keeps the user in the loop on the decisions that affect them and out of the loop on the ones that don’t.

── more in #ai-agents 4 stories · sorted by recency
── more on @starebrain 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/context-propagation-…] indexed:0 read:5min 2026-10-23 · —