{"slug": "context-propagation-in-android-ai-agents-resolving-device-state-before-action", "title": "Context Propagation in Android AI Agents: Resolving Device State Before Action Dispatch", "summary": "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.", "body_md": "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.\n\nThe 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.\n\nThis 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.\n\nWhen a user says “book a slot for my dentist appointment,” the agent needs to resolve:\n\nSome 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.\n\nThe resolution layer sits between intent parsing and action dispatch, and its job is to decide which parameters fall into which bucket.\n\n```\n`data class DeviceContext(\n    val upcomingEvents: List<CalendarEvent>,\n    val savedLocations: List<SavedLocation>,\n    val recentContacts: List<Contact>,\n    val recentApps: List<String>,\n    val activeNotifications: List<NotificationSummary>\n)\n\ndata class CalendarEvent(\n    val eventId: String,\n    val title: String,\n    val startTime: Instant,\n    val location: String?,\n    val attendees: List<String>\n)\n\ndata class SavedLocation(\n    val label: String,\n    val address: String,\n    val coordinates: Pair<Double, Double>\n)`\n```\n\nThe 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.\n\nEach parameter resolution produces a confidence level that determines what happens next:\n\n```\n`enum class ResolutionConfidence { HIGH, MEDIUM, LOW, UNRESOLVABLE }\n\ndata class ParameterValue(\n    val value: Any?,\n    val confidence: ResolutionConfidence,\n    val source: ContextSource\n)\n\nsealed class ContextSource {\n    object DeviceCalendar : ContextSource()\n    object SavedLocations : ContextSource()\n    object RecentContacts : ContextSource()\n    object UserProvided : ContextSource()\n    object Inferred : ContextSource()\n}\n\nfun ParameterValue.shouldAutoFill(): Boolean =\n    confidence == ResolutionConfidence.HIGH\n\nfun ParameterValue.shouldSuggest(): Boolean =\n    confidence == ResolutionConfidence.MEDIUM\n\nfun ParameterValue.shouldAsk(): Boolean =\n    confidence == ResolutionConfidence.LOW ||\n    confidence == ResolutionConfidence.UNRESOLVABLE`\n```\n\n**HIGH** → auto-fill silently. The device has this unambiguously (default calendar, a location saved under an exact label match).\n\n**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).\n\n**LOW / UNRESOLVABLE** → ask the user explicitly before proceeding.\n\n```\n`class ContextResolver(\n    private val calendarProvider: CalendarProvider,\n    private val locationStore: LocationStore,\n    private val contactStore: ContactStore\n) {\n    fun resolve(\n        intent: ActionIntent,\n        context: DeviceContext\n    ): Map<String, ParameterValue> {\n        return intent.requiredParameters.associate { param ->\n            param.name to resolveParameter(param, context)\n        }\n    }\n\n    private fun resolveParameter(\n        param: Parameter,\n        context: DeviceContext\n    ): ParameterValue {\n        return when (param) {\n            is Parameter.Location -> resolveLocation(param, context)\n            is Parameter.Contact -> resolveContact(param, context)\n            is Parameter.TimeSlot -> resolveTimeSlot(param, context)\n            is Parameter.Calendar -> resolveCalendar(param, context)\n        }\n    }\n\n    private fun resolveLocation(\n        param: Parameter.Location,\n        context: DeviceContext\n    ): ParameterValue {\n        val exactMatch = context.savedLocations\n            .firstOrNull { it.label.equals(param.hint, ignoreCase = true) }\n\n        if (exactMatch != null) {\n            return ParameterValue(\n                value = exactMatch,\n                confidence = ResolutionConfidence.HIGH,\n                source = ContextSource.SavedLocations\n            )\n        }\n\n        val partialMatch = context.savedLocations\n            .firstOrNull { it.label.contains(param.hint, ignoreCase = true) }\n\n        if (partialMatch != null) {\n            return ParameterValue(\n                value = partialMatch,\n                confidence = ResolutionConfidence.MEDIUM,\n                source = ContextSource.SavedLocations\n            )\n        }\n\n        return ParameterValue(\n            value = null,\n            confidence = ResolutionConfidence.UNRESOLVABLE,\n            source = ContextSource.SavedLocations\n        )\n    }\n}`\n```\n\nThe resolved parameters get packaged into an `AgentContext` that travels with the action through to dispatch:\n\n```\n`data class AgentContext(\n    val actionIntent: ActionIntent,\n    val deviceContext: DeviceContext,\n    val resolvedParameters: Map<String, ParameterValue>,\n    val capturedAt: Instant\n)\n\nfun AgentContext.pendingParameters(): List<String> {\n    return resolvedParameters\n        .filter { (_, value) -> value.shouldAsk() }\n        .map { (name, _) -> name }\n}\n\nfun AgentContext.suggestedParameters(): Map<String, ParameterValue> {\n    return resolvedParameters\n        .filter { (_, value) -> value.shouldSuggest() }\n}\n\nfun AgentContext.autoFilledParameters(): Map<String, Any?> {\n    return resolvedParameters\n        .filter { (_, value) -> value.shouldAutoFill() }\n        .mapValues { (_, value) -> value.value }\n}`\n```\n\nBefore 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.\n\nThe `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.\n\nThis 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.\n\nThe 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.\n\nThe 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.", "url": "https://wpnews.pro/news/context-propagation-in-android-ai-agents-resolving-device-state-before-action", "canonical_source": "https://dev.to/starebrain/context-propagation-in-android-ai-agents-resolving-device-state-before-action-dispatch-15dc", "published_at": "2026-10-23 03:30:01+00:00", "updated_at": "2026-10-23 03:41:12.304561+00:00", "lang": "en", "topics": ["ai-agents", "artificial-intelligence", "ai-tools"], "entities": ["StareBrain", "Android"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/context-propagation-in-android-ai-agents-resolving-device-state-before-action", "markdown": "https://wpnews.pro/news/context-propagation-in-android-ai-agents-resolving-device-state-before-action.md", "text": "https://wpnews.pro/news/context-propagation-in-android-ai-agents-resolving-device-state-before-action.txt", "jsonld": "https://wpnews.pro/news/context-propagation-in-android-ai-agents-resolving-device-state-before-action.jsonld"}}