Practical engineering with agents · · 15 min read

API compatibility when clients and servers update separately

Test the supported version pairs, unknown states, queued data, capabilities and rollout order instead of trusting the newest local pair.

Editorial illustrationAI-generated editorial illustration of the story's ideas, not an application screenshot.

Updating the server does not update every client that talks to it. A mobile app may remain installed for weeks, a local agent runtime may still be running an older process, and a background worker may complete a job prepared before the deployment. A change can therefore pass tests with the newest code on both sides and still break the real workflow. Compatibility work asks a different question: which combinations of versions and capabilities must remain useful while the system changes one part at a time?

This workshop follows a hypothetical task-status API adding a richer progress object while retaining an older status field. The API is an example, not a description of a released GrantTap wire contract or a proposal to add another provider. GrantTap offers a relevant real-world lesson: the app, local MCP and coding provider have independent lifecycles. The current updates guide explains those product boundaries. We will use a concrete compatibility matrix, failure fixtures and staged verification to prepare an agent-assisted change that works beyond the newest local checkout.

Define the supported combinations first

List the client and server versions that the product actually supports. Include deployment constraints: mobile users cannot all update at once, a machine may be offline during a runtime upgrade, and a rollback may temporarily restore an older server. Do not turn every historical version into a permanent obligation, but make the support window explicit. The agent needs to know which combinations must work, which may degrade with an explanation and which are intentionally rejected. Without that decision, tests tend to cover only whichever versions happen to be installed on the developer's machine.

For the workshop, define old client with old server, old client with new server, new client with old server and new client with new server. Add mixed server instances if the deployment can serve both versions during a rollout. Give each combination an expected behavior. This matrix is a product and engineering contract, not a decorative table. It determines the order of implementation, fixtures and release. A single green integration run proves its tested pair; it does not establish the other combinations merely because the request and response use familiar names.

Original AI-generated conceptual illustration. It explains the workshop, not a product screen or a measured result.
Editorial illustrationOriginal AI-generated conceptual illustration. It explains the workshop, not a product screen or a measured result.

Preserve meaning as well as parsing

A response can remain valid JSON while changing its meaning. The old status complete might previously mean the operation finished, while a new server uses it when the operation was merely accepted. That is a semantic break even if every field keeps the same type. The Google AIP-180 compatibility guidance distinguishes source, wire and semantic compatibility. Use that distinction as a review aid, not as a claim that its exact rules automatically govern every API or transport in your project.

Write the meaning of each important state beside the schema. In the example, accepted, running and finished must remain distinguishable. If the new progress object adds detail, the old status should continue to carry the old supported meaning during the transition. Do not fix a new screen by weakening the server's old guarantee. Ask the agent to explain which user decision each field supports. That often exposes a hidden breaking change more clearly than a type diff: an older client may parse the response successfully and still enable the wrong next action.

Use additions carefully rather than assuming they are harmless

Adding an optional field is often easier to introduce than removing a required one, but clients still need to handle unknown data appropriately. Some decoders reject unexpected fields, some preserve them and some ignore them. Test the actual client implementation rather than relying on a general belief about JSON flexibility. A new response member can also change payload size or expose information that should not be sent to an older consumer. Compatibility includes the application's real reading and decision behavior, not only whether a schema validator accepts the object.

For the workshop, supply an old-client fixture with the new progress member present. Verify that it still displays the established status and can perform the supported next action. Then test the new client against a response without progress. Define the intended fallback explicitly. It may show limited detail while preserving the correct state, or it may explain that an upgrade is required for that particular feature. It should not invent percentages or reinterpret missing detail as zero progress simply to keep every visual element populated.

Distinguish absent, null and empty

An omitted field, an explicit null and an empty object can have different meanings. Decide what each means for the proposed progress data. An old server omitting progress may mean the capability is unavailable, while an empty object from a new server might be malformed. A null could mean no current run if the contract defines it that way. Do not let the agent collapse these states through a convenient default that erases information needed for the user to choose a correct action.

Create separate fixtures and expectations for those representations. Exercise the public decoding and display path, not only a helper that accepts all three. If the client shows unknown progress, the status and action controls must remain consistent with that uncertainty. The GrantTap capability guide is a useful product reference for separating availability from confirmed usage. Apply the same discipline to your own API: missing capability detail is not evidence that an operation did not run, and a present field is not independent proof that the underlying action succeeded.

Plan for new enum values

A server may introduce a state that an older client does not recognize. Decide how the client should behave before that state is deployed. A safe unknown presentation can preserve identity and offer a supported status refresh without pretending the operation is complete or failed. The right fallback depends on the action's meaning. Avoid mapping every unknown value to a terminal success, because an older UI might then allow a conflicting operation while the server is still performing the original one.

Test a synthetic future state against the old-client fixture and inspect the resulting user journey. The client should not crash or silently choose an unrelated action if continued support requires graceful handling. Also test whether the client sends enum values back to the server, because read compatibility and write compatibility are separate questions. If an unknown state cannot be handled safely, return a clear supported-version requirement for that operation. Honest refusal can be a correct contract; fabricated compatibility is not an improvement simply because it avoids displaying an error.

Keep request evolution separate from response evolution

A new server accepting an older request does not establish that an older server accepts a new request. Review each direction. If the new client adds a parameter, determine how the oldest supported server treats it. If a field becomes required, identify what older clients send and whether the server can preserve the old behavior. A default that changes the target or scope is particularly important to review. The agent should not use a guessed default to make an incompatible request appear valid while performing a different operation from the user's intent.

For the fixture, run the new client against the old server with the proposed request shape. Verify both the response and the actual side effect in the controlled backend. A successful HTTP status can conceal that an old implementation ignored an unsupported parameter and performed its older operation. The acceptance condition is the agreed behavior, not merely the absence of a transport error. If the feature requires a new server capability, check that capability before offering the action and provide a specific upgrade path rather than silently degrading the semantics of the request.

Negotiate capabilities where versions are insufficient

A version number is useful evidence about an installation, but it may not describe configuration, permissions or provider-specific support. Where the architecture provides capability discovery, use its actual contract to determine availability. Do not invent a negotiation protocol inside a client screen without implementing and testing the corresponding server behavior. A cached capability response also has a scope and lifetime. It should identify the source it describes so a response from one computer cannot accidentally authorize a feature on another selected route.

In GrantTap, device management distinguishes running and installed MCP versions and the confirmation of maintenance actions. That is a concrete reminder to check the process that is actually serving the Task. An updated package on disk does not establish that an older process has restarted. For your API workshop, verify capability state after a supported restart or reconnect. Keep the old observation visible as stale where appropriate instead of treating a package-manager success as proof that every active client-server connection now has the new semantics.

Make unsupported behavior explicit

If the old server cannot support the new progress feature, decide what remains useful. The new client might show the existing coarse status and omit richer detail, while explaining the requirement for additional information. A feature that changes side effects may need a clear refusal instead. The important point is that the user understands what will happen. Do not ignore an unsupported parameter and display success for another operation. A polished fallback that performs the wrong action is a correctness defect, not a compatibility feature.

Prepare a fixture where capability discovery reports the feature unavailable and another where discovery itself fails. Those states should not necessarily produce the same conclusion. Unavailable is a known limit; unknown means the source did not establish support. Give each a bounded next step. Through GrantTap, the providers guide explains that integrations have different implemented coverage. The same honesty belongs in your own application: support must be tied to observed implementation and contract, not inferred from the presence of an agent name or a generic connected indicator.

A separate ImageGen illustration of the scenario. It is not evidence that the proposed checks or actions have been performed.
Editorial illustrationA separate ImageGen illustration of the scenario. It is not evidence that the proposed checks or actions have been performed.

Test stored and queued data across the transition

Compatibility affects more than live requests. Cached responses, queued operations and persisted documents may survive a client upgrade. A new decoder can therefore encounter old stored data even after every server instance is updated. Decide how the application reads, migrates or invalidates that data. Keep the user's unsent work separate from disposable cache. Clearing everything can hide a migration bug while destroying useful state. An agent should not solve a decoder failure by deleting all local data without understanding what the data represents and what the user expects to retain.

For the workshop, save an old response and a queued request as fixtures, then open them with the new client path. Verify the correct identity, target and supported action. If conversion is required, test interruption and repetition of that conversion under the application's actual storage model. Keep invalid data handling explicit. A malformed record may need a recoverable error, while a legitimate older record needs a supported path. The GrantTap offline-storage guide is useful context for why cached history and current computer state should not be treated as interchangeable evidence.

Keep operation identity stable across retries

A rollout can change which server instance receives a follow-up request. If the client is inspecting an existing operation, its identity should continue to refer to that operation under the supported deployment model. Do not solve a missing status response by creating a new operation with a new ID. Define how routing, storage and compatibility preserve inspection during the transition. This may be an infrastructure concern as well as a schema concern, so the agent should identify the owning layer rather than patch only the visible client error.

Test a controlled sequence where one version accepts the request and another supported version receives the status query. Verify the intended behavior and preserve unknown outcomes where the source cannot establish them. Also test a duplicate request according to the service's actual idempotency contract. Do not assume that a newly introduced idempotency key protects an older server that never implemented it. The GrantTap delivery guide separates transport receipts from agent activity; your service similarly needs explicit semantics for acceptance, recovery and the eventual outcome of an operation spanning a deployment boundary.

Keep contract fixtures independent from both implementations

A compatibility test can become meaningless if the newest server generates every fixture and the newest client defines every expectation. Preserve representative inputs and expected observable behavior for the older supported contract. Use synthetic data with explicit provenance and avoid private production payloads. The fixture should establish what the consumer relied on, including error cases and missing fields. If an upstream SDK generates types, distinguish those generated types from the application's actual runtime decoding and user decisions. Compilation alone cannot establish wire or semantic compatibility.

Run the version pairs that the support matrix promises. A lightweight controlled server can help narrow client behavior, while an integration run with the actual supported server establishes the pair's real behavior. State which layer each test covers. If an old binary or environment is unavailable, mark that combination unverified and explain the consequence. Do not label a mock of the old response as a completed live test of the old application. The final packet should let a reviewer see which combinations have direct evidence and which remain assumptions or proposed checks.

Roll out in an order supported by the matrix

Choose the sequence from the observed compatibility relationships. Often a server can first add a compatible response before clients begin using it, but that is not a universal rule for every change. If the new server breaks the old client, deploying it first is not safe under a support promise that includes that client. If the new client cannot function with the old server, its release needs an appropriate gate or deployment order. Let the actual matrix determine the plan instead of using a familiar slogan about always updating one side first.

Account for mixed instances and the normal delay between installing and running a component. A local runtime may need a supported restart; a mobile update may take time to reach users. Keep the previous compatible artifact available according to the release process, and identify what a rollback would expose to already updated clients. The source revert is not the whole recovery story when persisted data or new requests have changed. Ask the agent to describe the target combinations after recovery as carefully as the combinations intended during the forward rollout.

Verify the affected journey after activation

Once the authorized rollout occurs, inspect the serving version and exercise the user journey that depends on the change. For the example, create a safe fixture operation, observe its coarse and rich status through the supported clients and verify that the action controls match the real state. A health endpoint can establish basic service availability without proving this contract. Keep deployment acceptance, serving identity and application behavior as separate observations connected to the same release. This prevents a successful activation message from becoming a substitute for the compatibility check the Task actually required.

Through GrantTap, preserve the correct execution and computer route when reporting results. A test against a local candidate is useful but does not establish what an external environment serves. If the rollout cannot be completed, finish the concrete reviewable change and report the remaining dependency. The evidence guide explains why a reported result is narrower than an independently observed effect. Apply that precision to the release summary: name the version pair, environment and behavior that actually passed instead of claiming that all clients everywhere are now synchronized and current.

Retire old support deliberately

A support window can end, but the decision should be explicit and reflected in the product's behavior and documentation. Determine how unsupported clients learn that they need an update and what local work they can still preserve. Do not make a server change that leaves them indefinitely waiting for a response they can no longer interpret. If an operation cannot proceed, return a bounded explanation through the supported error path. The goal is a clear transition, not permanent compatibility at any cost or a silent break disguised as an ordinary transient network problem.

Keep the tests that describe the remaining supported set and remove obsolete fixtures only with a clear reason. The next agent should find a current contract rather than a pile of contradictory historical expectations. A precise GrantTap Task can now request an API change with its client-server matrix, stored-data fixtures and staged verification. The benefit is practical: the newest local test no longer stands in for the whole deployed system. The team can explain which combinations work, which degrade honestly, which require an upgrade and what evidence supports each of those conclusions.

Sources

Next storyDatabase migrations with an agent: rehearse the transition and recovery →