Yesterday’s Client Still Sends the Old Request
Two Programs Rarely Upgrade at the Same Instant
BOTIE
The ML technical debt paper published in 2015 discusses dependencies and consumers that can be difficult to track. An agent calling a retrieval tool is another such consumer. We will keep two explicitly supported message versions and write a small translator for each. These translators are adapters. They let the rest of the search code receive one agreed record while older callers finish migrating.
A Rename Broke an Agent That Had Not Changed
Botie’s tool searches maintenance manuals. It used to accept a text field and a limit on the number of results. A server update renames them query and count. Yesterday’s agent still sends the old names, so its searches fail even though neither the agent nor the manuals changed. Updating the server did not update every program that calls it.
We need an explicit way to recognise the message we received and translate supported older messages into one internal form. The question is which old behaviour we are promising to preserve, and for how long.
The adapters recognise only these two specified request versions. Supporting another client requires checking its actual message and behaviour, then adding the relevant adapter and fixtures.
Step 1 of 1
Different public shapes, one internal request
The version tells the service which interpretation is permitted.
| Caller | Public keys | Internal keys |
|---|---|---|
| Version 1 | version, text, optional limit | query, count |
| Version 2 | version, query, optional count | query, count |
| Version 3 | Not supported here | Unsupported-version error |
Version 1
- Public keys
- version, text, optional limit
- Internal keys
- query, count
Version 2
- Public keys
- version, query, optional count
- Internal keys
- query, count
Version 3
- Public keys
- Not supported here
- Internal keys
- Unsupported-version error
Separate the message version from the model version
Only the public edge depends on the old names.
Keep the old client working through an explicit adapter
The server needs to accept query/count while an existing agent still sends text/limit. Both supported messages should reach the intended search.
BOTIE
Canonical search request or a stable contract error
{
"query": "pump",
"count": 2
}Follow the operations
1. Versioned search request
{ "version": 1, "text": " pump ", "limit": 2 }2. Adapt version 1 search requests
{ "query": "pump", "count": 2 }3. Adapt version 2 search requests
null
4. Select the declared version adapter
{ "query": "pump", "count": 2 }5. Result
{ "query": "pump", "count": 2 }
The adapters recognise only these two specified request versions. Supporting another client requires checking its actual message and behaviour, then adding the relevant adapter and fixtures.
1. Separate the message version from the model version
A request version tells the server which names and rules the caller is using. A model version identifies the trained model being used to answer it. Changing the model does not necessarily require changing the message, and renaming a message field does not necessarily require retraining the model. Give those changes separate identifiers.
JSON Schema has its own versions too, called dialects. A dialect identifies the rules used to interpret the schema document. It is separate from our decision to call the old search message version 1 and the new one version 2.
Give each identifier one job. Recording all of them is more useful than calling an entire deployment version 2.
2. Translate at the edge, then use one internal form
Read the declared version before choosing field names. For version 1, copy text into query and limit into count. Version 2 already has those names. In both cases, remove surrounding query whitespace, require remaining text, and apply the same count rules.
The result is one common internal record. Documentation may call this the canonical form; here that simply means the search code can always read query and count. It no longer needs to know which public message produced them.
version 1 input = {version:1, text:" pump seal ", limit:2}
canonical input = {query:"pump seal", count:2}
version 2 input = {version:2, query:"pump seal", count:2}
canonical input = {query:"pump seal", count:2}Both public messages now describe the same query and requested result count. Retrieval can consume that common form.
Adapt a known historical fixture
A one-off fixture makes the rename visible. The challenge extends it into validation and version dispatch.
old = {"version":1,"text":" valve ","limit":4}
canonical = {"query":old["text"].strip(),"count":old["limit"]}
print(canonical){'query': 'valve', 'count': 4}Only the boundary knows these legacy names. This short demonstration assumes a valid version 1 fixture.
3. A new optional field can still break a reader
Compatibility has a direction. Can the new server read an old request? Can an old client read a new response? A strict reader that rejects unknown fields may reject an added optional field. A reader that ignores them might continue normally.
Changing a default is also observable. An omitted count can suddenly return a different number of passages even though every key still validates. Test behaviour as well as schema acceptance.
Check the actual reader before promising compatibility. A version label alone cannot tell us whether that reader accepts a new field.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Where should the rename be handled?
Use the information below.
Version 1 accepts text and optional limit; version 2 accepts query and optional count. Both default to 3.4. Do not guess a version from a convenient key
A message containing both text and query is ambiguous unless a declared version and its schema resolve it. Our contract rejects keys outside that version’s allowed set. An unsupported version returns an explicit error rather than falling through to the latest parser.
This strictness is a design choice for a small public request. Other systems allow extensions deliberately. Whichever policy you choose, test it in the direction your callers depend on.
The workshop keeps both adapter branches visible. Version selection chooses a branch; it does not merge their outputs.
5. Plan a migration that existing callers can actually make
Publish the new shape, identify consumers and keep the agreed legacy adapter while they migrate. Record use by contract version without logging private queries. State a retirement date through the normal support channel and make unsupported-version errors actionable.
A rollback needs to understand requests already accepted by the new release. Returning to a server that recognises only version 1 can strand a client upgraded to version 2.
A rollback plan therefore needs to name the client contracts it will still accept.
6. Save examples from both sides of the boundary
Keep representative request and response fixtures for supported versions. Verify that each accepted request becomes the intended canonical record and that ambiguous or unsupported forms fail. Add a previously broken client request as a regression test.
When responses evolve, define which extra fields clients may ignore and which values have changed meaning. Schema compatibility cannot establish that a new score still means the old thing; that is the next lesson’s problem.
Fixtures from each supported client let us review a server change before coordinating every caller’s deployment.
7. Review success and error responses together
A client depends on the error contract too. Renaming a validation code or changing whether an unsupported version is retried can break recovery even if successful responses stay identical. Keep error types stable, and include the supported versions in safe public documentation.
Treat an invalid count as an input error. Quietly clamping it changes the operation the caller requested and can hide a bug in an agent’s tool arguments.
The diagnostic workshop returns an error object. A real HTTP service also chooses a documented status and content type.
Check yourself
Questions on Parts 1 to 7
Question 1 of 4
What should this contract do?
Use the information below.
A request says version 3, but the service supports only 1 and 2.Try it with Botie
Build with Botie
Write and run Python