DEV Community

StareBrain
StareBrain

Posted on

The Provider Capability Checklist Every Agent Builder Needs Before Writing a Single Line of Integration Code

Most agent integration bugs aren’t logic errors. They’re assumption errors — specifically, assumptions about what your provider will give you when something goes wrong.

Write the retry logic first, discover the provider gaps in production, add workarounds per-provider as bugs surface. That’s the default path. The workarounds are usually correct but undocumented, so the next provider gets the same treatment.

Three questions asked before integration starts costs ten minutes. The alternative costs more.

The Three Questions

Before wiring any provider into an agent’s action path:

  1. Does a dispatch return a stable, queryable artifact ID?

This is the reconciliation anchor. If a dispatch times out before returning, you need something to look up later. If the provider returns nothing — or returns an ID that isn’t queryable — you have no anchor on timeout.

  1. Does the provider accept an idempotency key or a reference ID you control?

This is what makes retries safe inside the consistency window. If the provider accepts a key you generate before dispatch, a retry with the same key returns the original result instead of creating a duplicate. Without it, a retry inside the window is a gamble.

  1. Is there a queryable endpoint that returns current state by that ID?

This is what makes reconciliation possible after the fact. If you can query by the artifact ID and get current state, you can confirm delivery without re-dispatching. Without it, you’re reconciling blind.

*The Four Integration Tiers
*

`enum class ProviderCapabilityTier {
    FULL,           // Returns artifact ID + accepts idempotency key + queryable by ID
    RECONCILABLE,   // Returns artifact ID + queryable, no idempotency key support  
    PARTIAL,        // Returns artifact ID, no query endpoint
    BLIND           // None of the above
}

data class ProviderCapabilities(
    val returnsArtifactId: Boolean,
    val supportsIdempotencyKey: Boolean,
    val hasQueryEndpoint: Boolean
) {
    fun tier(): ProviderCapabilityTier = when {
        returnsArtifactId && supportsIdempotencyKey && hasQueryEndpoint -> 
            ProviderCapabilityTier.FULL
        returnsArtifactId && hasQueryEndpoint -> 
            ProviderCapabilityTier.RECONCILABLE
        returnsArtifactId -> 
            ProviderCapabilityTier.PARTIAL
        else -> 
            ProviderCapabilityTier.BLIND
    }
}`
Enter fullscreen mode Exit fullscreen mode

What Each Tier Means for Your Integration

FULL — Safe by default

`class FullCapabilityDispatcher(val provider: FullProvider) {
    suspend fun dispatch(action: Action): ActionRecord {
        val idempotencyKey = generateIdempotencyKey(action)

        // Store before dispatch — key exists even if call times out
        val record = ActionRecord(
            actionId = action.id,
            dispatchId = idempotencyKey,
            delivery = DeliveryState.PENDING,
            eligibleForReconciliationAt = Instant.now().plus(provider.consistencyWindow)
        )
        store(record)

        val result = provider.dispatch(action, idempotencyKey = idempotencyKey)

        return record.copy(
            providerObjectId = result.artifactId,
            delivery = DeliveryState.PENDING // Still pending until lookup confirms
        )
    }
}`
Enter fullscreen mode Exit fullscreen mode

A retry with the same idempotency key returns the original result. “Not found” inside the consistency window costs a wait, never a duplicate.

RECONCILABLE — Safe after the window

`class ReconcilableDispatcher(val provider: ReconcilableProvider) {
    suspend fun dispatch(action: Action): ActionRecord {
        val dispatchId = generateDispatchId(action)

        val record = ActionRecord(
            actionId = action.id,
            dispatchId = dispatchId,
            delivery = DeliveryState.PENDING,
            eligibleForReconciliationAt = Instant.now().plus(provider.consistencyWindow)
        )
        store(record)

        return try {
            val result = provider.dispatch(action)
            record.copy(providerObjectId = result.artifactId)
        } catch (e: TimeoutException) {
            // dispatchId is the reconciliation anchor
            // Can query by it after the consistency window closes
            record // Stays PENDING
        }
    }

    suspend fun reconcile(record: ActionRecord): ActionRecord {
        if (!record.isEligibleForReconciliation()) return record

        // Query by the dispatch ID we generated
        val artifact = provider.lookupByDispatchId(record.dispatchId)

        return if (artifact != null) {
            record.copy(
                providerObjectId = artifact.id,
                delivery = DeliveryState.CONFIRMED
            )
        } else {
            record.copy(delivery = DeliveryState.FAILED)
        }
    }
}`
Enter fullscreen mode Exit fullscreen mode

No idempotency key support means retries inside the window are risky. The min-age gate matters more here — don’t retry until outside the consistency window.

PARTIAL — Proceed with documented risk

`class PartialCapabilityDispatcher(val provider: PartialProvider) {
    suspend fun dispatch(action: Action): ActionRecord {
        val record = ActionRecord(
            actionId = action.id,
            dispatchId = generateDispatchId(action),
            delivery = DeliveryState.PENDING,
            // Note: longer window because we can't query to confirm
            eligibleForReconciliationAt = Instant.now().plus(CONSERVATIVE_WINDOW)
        )
        store(record)

        return try {
            val result = provider.dispatch(action)
            // Has artifact ID but no query endpoint
            // Can only confirm by checking artifact indirectly
            record.copy(
                providerObjectId = result.artifactId,
                delivery = DeliveryState.PENDING
            )
        } catch (e: TimeoutException) {
            // No artifact ID returned, no query endpoint
            // Cannot reconcile programmatically
            record.copy(delivery = DeliveryState.DENIED_UNRESOLVED)
        }
    }
}`
Enter fullscreen mode Exit fullscreen mode

Timeout with no artifact ID returned = DENIED_UNRESOLVED. Document this as a constraint on the integration, not a bug to fix later.

BLIND — Human review on timeout

`class BlindProviderDispatcher(val provider: BlindProvider) {
    suspend fun dispatch(action: Action): ActionRecord {
        // Document the constraint before the first line of integration code
        require(action.requiresHumanReviewOnTimeout) {
            "BlindProvider integrations must have human review configured for timeout cases"
        }

        val record = ActionRecord(
            actionId = action.id,
            dispatchId = generateDispatchId(action),
            delivery = DeliveryState.PENDING,
            humanReviewRequired = true
        )
        store(record)

        return try {
            provider.dispatch(action)
            // Even on success, no artifact ID to anchor reconciliation
            record.copy(delivery = DeliveryState.PENDING)
        } catch (e: TimeoutException) {
            record.copy(
                delivery = DeliveryState.DENIED_UNRESOLVED,
                humanReviewFlag = HumanReviewFlag(
                    reason = "Blind provider timeout — action may have succeeded",
                    requiredAction = "Manual verification required before retry"
                )
            )
        }
    }
}`
Enter fullscreen mode Exit fullscreen mode

BLIND providers aren’t necessarily wrong to integrate — but the timeout path is manual by definition. Surface that to a human; don’t let DENIED_UNRESOLVED sit in a queue that auto-retries.

The Checklist in Practice

`fun evaluateProvider(provider: Provider): ProviderEvaluation {
    return ProviderEvaluation(
        provider = provider,
        capabilities = ProviderCapabilities(
            returnsArtifactId = provider.dispatchReturnSchema.contains("id"),
            supportsIdempotencyKey = provider.requestSchema.contains("idempotency_key") 
                || provider.requestSchema.contains("reference_id"),
            hasQueryEndpoint = provider.endpoints.any { it.queryableById }
        ),
        documentedConstraints = buildConstraintList(provider),
        recommendedTimeoutHandling = tier.timeoutStrategy()
    )
}`
Enter fullscreen mode Exit fullscreen mode

Run this before writing integration code. The output is documentation that lives next to the integration, not assumptions that live in someone’s head.

What This Looks Like for StareBrain

For on-device agent actions on Android, the accessibility layer confirms the UI action happened — the tap fired, the button was pressed. Whether the downstream provider recorded the result is a different question answered by a different clock.

A calendar booking confirmed by the accessibility service is one verification. The calendar provider returning a queryable event ID is a second, independent verification. They answer different questions. The provider capability tier determines which of those two verifications you can actually perform.

Ten minutes upfront. Document the tier. Design the timeout path before you need it.

Top comments (1)

Collapse
 
m_montazeri profile image
Mohammad Montazeri •

In the RECONCILABLE example, how does the provider learn dispatchId? The code generates it locally, calls provider.dispatch(action) without passing it, then queries lookupByDispatchId after a timeout. Looking up a returned artifact ID only helps if that ID was actually received.

I would separate “can look up a returned ID” from “can reconcile using a client reference persisted before dispatch,” and require the latter reference to be transmitted and queryable. Otherwise the timed-out call remains unresolved even after the consistency window; elapsed time alone does not prove it failed. Is that client-reference capability intended to be part of this tier?

This is a review of the example code, not a runtime test. AI-assisted reply; I work on Lisar Connect.