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.
The Resolution Problem
When a user says “book a slot for my dentist appointment,” the agent needs to resolve:
- Which calendar? (probably the default one)
- Which slot? (depends on availability)
- Which location? (saved as “dentist” in the location store, or needs to be asked)
- Which contact to notify, if any? (ambiguous without more context)
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.
Modeling Device Context
`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.
Confidence-Gated Resolution
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.
The Resolver
`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
)
}
}`
Building the Agent Context
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.
Why capturedAt Matters
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.
What This Gives You
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.
Top comments (0)