DEV Community

StareBrain
StareBrain

Posted on

Not Every Backend Tells You What Happened: Provider Capability Tiers in an AI Agent

Introduction

When an AI agent dispatches an action — send a message, book a slot, toggle a setting — it needs to know whether the action succeeded. But not every backend gives you the same quality of answer.

Some tell you exactly what happened. Some give you enough to figure it out. Some give you nothing at all.

If your agent treats all backends the same way, it will make wrong decisions when a provider goes quiet. The fix is to model capability explicitly before you dispatch — not after.

The Four Tiers

`enum class ProviderCapabilityTier {
    FULL,           // Confirms delivery + acceptance
    RECONCILABLE,   // No direct confirmation; can query state after dispatch
    PARTIAL,        // Confirms delivery only; acceptance unknown
    BLIND           // No confirmation, no queryable state
}`
Enter fullscreen mode Exit fullscreen mode

FULL — The provider sends back a confirmed result. You know delivery happened and the action was accepted. Calendar providers with a write API, messaging platforms with delivery receipts.

RECONCILABLE — No direct confirmation on dispatch, but you can query the provider’s state afterward and compare against a pre-dispatch snapshot. The diff tells you what changed.

PARTIAL — The provider acknowledges receipt but can’t tell you whether the action was actually carried out. You know it arrived; you don’t know if it landed.

BLIND — Fire and forget. No receipt, no queryable state. You dispatched. That’s all you know.

Why It Matters Before Dispatch

The tier determines what your reconciliation strategy needs to be — and that decision has to be made before you dispatch, not after you’re already waiting for a response that isn’t coming.

`data class DispatchStrategy(
    val tier: ProviderCapabilityTier,
    val reconciliationKey: String?,
    val eligibleForReconciliationAt: Instant,
    val requiresManualVerification: Boolean
)

fun buildDispatchStrategy(
    tier: ProviderCapabilityTier,
    now: Instant
): DispatchStrategy {
    return when (tier) {
        ProviderCapabilityTier.FULL -> DispatchStrategy(
            tier = tier,
            reconciliationKey = null,
            eligibleForReconciliationAt = now,
            requiresManualVerification = false
        )
        ProviderCapabilityTier.RECONCILABLE -> DispatchStrategy(
            tier = tier,
            reconciliationKey = generateReconciliationKey(),
            eligibleForReconciliationAt = now.plusSeconds(CONSISTENCY_WINDOW_SECONDS),
            requiresManualVerification = false
        )
        ProviderCapabilityTier.PARTIAL -> DispatchStrategy(
            tier = tier,
            reconciliationKey = generateReconciliationKey(),
            eligibleForReconciliationAt = now.plusSeconds(CONSISTENCY_WINDOW_SECONDS),
            requiresManualVerification = true
        )
        ProviderCapabilityTier.BLIND -> DispatchStrategy(
            tier = tier,
            reconciliationKey = null,
            eligibleForReconciliationAt = Instant.MAX,
            requiresManualVerification = true
        )
    }
}

private const val CONSISTENCY_WINDOW_SECONDS = 30L`
Enter fullscreen mode Exit fullscreen mode

Wiring It Into the Action Record

From the Oct 22 post, the ActionRecord carries delivery and acceptance state. The tier slots in alongside it:

`data class ActionRecord(
    val actionId: String,
    val dispatchId: String,
    val providerObjectId: String?,
    val tier: ProviderCapabilityTier,
    val dispatchStrategy: DispatchStrategy,
    val delivery: DeliveryState,
    val acceptance: AcceptanceState,
    val dispatchedAt: Instant,
    val confirmedAt: Instant?,
    val eligibleForReconciliationAt: Instant
)`
Enter fullscreen mode Exit fullscreen mode

For a BLIND provider, eligibleForReconciliationAt is Instant.MAX — the record never becomes eligible for automated reconciliation. It stays DENIED_UNRESOLVED until a human or a higher-level signal resolves it.

For a RECONCILABLE provider, the consistency window enforces a minimum age before the reconciliation job touches the record — the same replication lag protection from the Oct 22 post.

The Rule

Model the tier at registration time, not at failure time. By the time dispatch has failed silently, it’s too late to decide how you were going to handle it.

Every provider your agent talks to gets a declared tier. Every action record carries the strategy that tier implies. Reconciliation and confirmation handling downstream just read the record — they don’t need to know which provider they’re dealing with.

Series so far:

  • Part 1: Two-axis state model + DENIED_UNRESOLVED
  • Part 2: Context propagation + confidence thresholds
  • Part 3: Late confirmation race + terminal state check
  • Part 4: Provider capability tiers ← today

Top comments (0)