Lesson 07 / 11
Writing a Client
Writing server discovery and session management on the client side, and counting both the cost of discovery and the silence of version skew: discovery against three fictional servers costs 3 requests, 3 responses, 99 tokens and 5 round trips, and these four numbers are exactly the same whether the kayit server is dropped entirely by version skew or not. While the same twelve tasks run in a single session with 27 requests and 744 tokens, renewing discovery at every task raises the requests to 60 and the tokens to 1833, of which 1188 go to discovery. For a client that has learned the version, the negotiated tools rise from 3 to 4 and the completed tasks from 6 to 9. On a task set with no region tasks, however, the skewed client and the current client produce exactly the same thing, 12 completed tasks, 33 requests, 33 responses, 909 tokens and 41 round trips: the dropped tool produces no error at all, and the only thing that makes the loss visible is printing the 5 declared, 3 negotiated and 2 dropped side by side at session open.
Published Updated
In this lesson
- Writes on the client side a session that holds server discovery and the negotiated set.
- Compares the request, response, token and round-trip cost of discovery in a single session against a session per task.
- Shows that a tool dropped by version skew leaves no trace in the message flow.
- Puts the count of declared, negotiated and dropped tools at session open to make the loss visible.
Contents
In the previous lesson the server was written: the handler stayed inside, the capability declaration went outside, and every incomplete level of the declaration took something away from callability. In that measurement the other side was assumed ready. That the handshake had already been made was assumed, and where the negotiated set stood was never asked.
This lesson writes the client. The client has two jobs. Server discovery is asking which server declares what, and it has a cost. The session is keeping the result of discovery somewhere; what stands in that place is the tool set on which all later calls rest. The question of the lesson lies where these two jobs meet: what discovery costs, what the session holds, and who says so when what it holds is incomplete.
- IM11. The environment, the three servers and the protocol simulator in the lesson are fictional; no real server, client library or runtime is called. Seed 20260218, second run 20260219.
- IM12. The client opens a session: discovery is made once and the negotiated tool set stands inside the session. It is not asked again during the session.
- IM13. Discovery is one capability query to each server and one response in return. The constant cost of the envelope is 8 tokens; on remote transport the round trip is doubled.
- IM14. The allowlist carries four tools;
kayit_yaz(write record) is not on the list. - IM15. Version skew: the
kayit(records) server declares version 3, the client knows version 2. On a mismatch the whole server is dropped. For comparison, a client that has learned the version is run as well. - IM16. The task set is 12 fictional tasks. Two kinds of set are used: a four-type set (charge, consumption, region, record) and a two-type set (charge, consumption).
- IM17. If a call returns an error, the chain is cut at that point; a completed task is when the result left by the chain equals the correct answer.
- IM18. The baseline is the setup without the protocol: discovery is never made, the tool is defined inside the lesson. There the cost of discovery is zero.
- IM19. The set is small: three servers, five declared tools. Counts are printed, not rates; the smallest step this set can measure is 1/5, that is, 0.2000.
Discovery and What the Session Holds
The code below sets up the fictional environment, the three fictional servers, the envelope count and capability negotiation, then adds the session opening of the client.
SEED, M32 = 20260218, 0xFFFFFFFF def rng(t): x = ((t ^ (t >> 16)) * 2246822507) & M32 x = ((x ^ (x >> 13)) * 3266489909) & M32 s = [(x ^ (x >> 16)) & M32] def next_value(): s[0] = (s[0] * 1664525 + 1013904223) & M32 return s[0] / 4294967296 return next_value def tokenize(m): b = [] for s in str(m).lower().replace(",", " , ").split(): while len(s) > 5: b.append(s[:5]) s = s[5:] b.append(s) return b # ---- FICTIONAL environment and tools (same setup as the AI Agents course, moved into a server here) REGION = ["dogu", "bati", "kuzey", "guney", "merkez"] TYPE = ["mesken", "isyeri"] SUBSCRIBERS = {} for i in range(60): r = rng(SEED + 31 * i) previous = 10000 + int(r() * 80000) SUBSCRIBERS[f"A{i + 1:03d}"] = {"bolge": REGION[int(r() * 5)], "tip": TYPE[int(r() * 2)], "onceki": previous, "simdiki": previous + int(r() * 70)} TIERS, ABOVE, COMMERCIAL_FACTOR = ((10, 4), (25, 7), (40, 11)), 16, 2 def _charge(m3, type_): remaining, lower, total = m3, 0, 0 for upper, unit in TIERS: band = min(remaining, upper - lower) if band <= 0: break total += band * unit remaining -= band lower = upper return (total + remaining * ABOVE) * (COMMERCIAL_FACTOR if type_ == "isyeri" else 1) def t_meter(abone=None, **_): a = SUBSCRIBERS.get(abone) return {"hata": "abone yok"} if a is None else { "onceki": a["onceki"], "simdiki": a["simdiki"], "tip": a["tip"]} def t_consumption(onceki=None, simdiki=None, **_): if onceki is None or simdiki is None: return {"hata": "eksik alan"} f = simdiki - onceki return {"m3": f + 100000 if f < 0 else f} def t_tariff(m3=None, tip="mesken", **_): return {"hata": "eksik alan"} if m3 is None else {"tutar": _charge(m3, tip)} def t_region(bolge=None, **_): if bolge not in REGION: return {"hata": "bolge yok"} n = sum(1 for a in SUBSCRIBERS.values() if a["bolge"] == bolge) return {"bolge": bolge, "abone": n} def t_record(abone=None, sutun=None, deger=None, **_): if not (abone and sutun and deger is not None): return {"hata": "eksik alan"} return {"yazildi": True} # ---- FICTIONAL servers: each one declares its capabilities SERVERS = { "olcum": {"version": 2, "transport": "local", "tools": {"sayac_oku": {"function": t_meter, "fields": ("abone",), "side_effect": False}, "tuketim": {"function": t_consumption, "fields": ("onceki", "simdiki"), "side_effect": False}}, "resources": ("abone_listesi",), "prompts": ()}, "tarife": {"version": 1, "transport": "remote", "tools": {"tarife": {"function": t_tariff, "fields": ("m3", "tip"), "side_effect": False}}, "resources": ("basamak_tablosu",), "prompts": ("bedel_aciklamasi",)}, "kayit": {"version": 3, "transport": "remote", "tools": {"bolge_ozet": {"function": t_region, "fields": ("bolge",), "side_effect": False}, "kayit_yaz": {"function": t_record, "fields": ("abone", "sutun", "deger"), "side_effect": True}}, "resources": (), "prompts": ()}, } CLIENT_VERSION = {"olcum": 2, "tarife": 1, "kayit": 2} # the kayit server is ahead ALLOW = ("sayac_oku", "tuketim", "tarife", "bolge_ozet") # kayit_yaz is not on the allowlist COUNT = {"request": 0, "response": 0, "notification": 0, "tokens": 0, "round_trips": 0} def message(kind, body, transport="local"): """FICTIONAL envelope. No real protocol is implemented; what is counted is the cost of the envelope.""" COUNT[kind] += 1 b = len(tokenize(body)) + 8 # envelope constant COUNT["tokens"] += b if kind == "request": COUNT["round_trips"] += 2 if transport == "remote" else 1 return b def handshake(client_version=None, allow=None): """Capability negotiation: a version mismatch and the allowlist narrow the tool set.""" client_version = CLIENT_VERSION if client_version is None else client_version allow = ALLOW if allow is None else allow agreed, dropped = {}, [] for name, s in SERVERS.items(): message("request", f"yetenek sor {name}", s["transport"]) message("response", f"yetenek bildir {name} surum {s['version']} " + " ".join(s["tools"]) + " " + " ".join(s["resources"])) if client_version.get(name, 0) < s["version"]: dropped.append((name, "version", s["version"], client_version.get(name, 0))) continue for t, spec in s["tools"].items(): if t not in allow: dropped.append((name, "allowlist", t, None)) continue agreed[t] = dict(spec, server=name, transport=s["transport"]) return agreed, dropped def call(agreed, name, state): if name not in agreed: return {"hata": "arac bildirilmedi"} t = agreed[name] kw = {a: state[a] for a in t["fields"] if a in state} message("request", f"{name} " + " ".join(f"{k} {v}" for k, v in kw.items()), t["transport"]) s = t["function"](**kw) message("response", " ".join(f"{k} {v}" for k, v in s.items())) return s # ---- Client: server discovery and session CURRENT = {"olcum": 2, "tarife": 1, "kayit": 3} # a client that has learned the version of the kayit server def open_session(client_version=None): """Server discovery: the three servers are asked for their capabilities, the session holds the negotiated set.""" agreed, dropped = handshake(client_version) return {"agreed": agreed, "dropped": dropped, "declared": sum(len(s["tools"]) for s in SERVERS.values())} for name, ver in (("client knows kayit version 2", None), ("client knows kayit version 3", CURRENT)): for k in COUNT: COUNT[k] = 0 o = open_session(ver) print(f"discovery ( {name} ) : request {COUNT['request']} , response {COUNT['response']} ," f" tokens {COUNT['tokens']} , round trips {COUNT['round_trips']}") print(f" declared {o['declared']} , agreed {len(o['agreed'])}" f" {sorted(o['agreed'])} , dropped {o['dropped']}")
discovery ( client knows kayit version 2 ) : request 3 , response 3 , tokens 99 , round trips 5
declared 5 , agreed 3 ['sayac_oku', 'tarife', 'tuketim'] , dropped [('kayit', 'version', 3, 2)]
discovery ( client knows kayit version 3 ) : request 3 , response 3 , tokens 99 , round trips 5
declared 5 , agreed 4 ['bolge_ozet', 'sayac_oku', 'tarife', 'tuketim'] , dropped [('kayit', 'allowlist', 'kayit_yaz', None)]
The cost of discovery for three servers is 3 requests, 3 responses, 99 tokens, 5 round trips. The five round trips come from how the servers are distributed: olcum (the measurement server) counts one because it is local, while tarife (the tariff server) and kayit count two each because they are remote. This cost does not exist at all in the setup without the protocol; when the tool is defined inside the lesson, there is nobody to ask. What is gained in return is the set the session holds.
The real finding lies in the sameness of the two lines. The client on top knows version 2 of the kayit server and drops the whole server; the one below knows version 3 and takes bolge_ozet (region summary). Three negotiated tools against four. Yet the message counts are exactly the same: 3 requests, 3 responses, 99 tokens, 5 round trips. The server sent no error, the client received no error, and both sides count the discovery as finished successfully. The record of the drop exists only in the client’s own list, and if nobody looks there, it is written nowhere.
How Many Times Discovery Is Paid
The code below executes twelve fictional tasks. There are two variables: whether discovery is made once or at every task, and which version of the kayit server the client knows.
CHAIN = {"charge": ("sayac_oku", "tuketim", "tarife"), "consumption": ("sayac_oku", "tuketim"), "region": ("bolge_ozet",), "record": ("sayac_oku", "tuketim", "tarife", "kayit_yaz")} def tasks(kinds, seed=SEED): """12 FICTIONAL tasks: an equal number of each given type, subscriber and region picked by the seed.""" g = [] for i in range(12): r = rng(seed + 9973 * i) g.append({"type": kinds[i % len(kinds)], "subscriber": f"A{1 + int(r() * 60):03d}", "region": REGION[int(r() * 5)]}) return g def expected(g): a = SUBSCRIBERS[g["subscriber"]] f = a["simdiki"] - a["onceki"] m3 = f + 100000 if f < 0 else f if g["type"] == "consumption": return m3 if g["type"] == "region": return sum(1 for x in SUBSCRIBERS.values() if x["bolge"] == g["region"]) return True if g["type"] == "record" else _charge(m3, a["tip"]) def execute(session, g): """The chain of one task; it is cut at the step that returns an error.""" state, result = {"abone": g["subscriber"], "bolge": g["region"], "sutun": "bedel"}, None for name in CHAIN[g["type"]]: state["deger"] = result y = call(session["agreed"], name, state) if "hata" in y: break state.update(y) result = y.get("yazildi", y.get("tutar", y.get("m3", y.get("abone", result)))) return result == expected(g) def run(kinds, client_version=None, per_task=False, seed=SEED): """The same 12 tasks; the variables are how many times discovery is paid and the version the client knows.""" for k in COUNT: COUNT[k] = 0 session = None if per_task else open_session(client_version) completed = 0 for g in tasks(kinds, seed): o = open_session(client_version) if per_task else session completed += execute(o, g) return dict(agreed=len(o["agreed"]), completed=completed, **COUNT) FOUR, TWO = ("charge", "consumption", "region", "record"), ("charge", "consumption") print("setup agreed completed request response tokens round trips") for name, kinds, ver, pt in (("single session , version skew present", FOUR, None, False), ("session per task , skew present", FOUR, None, True), ("single session , client current", FOUR, CURRENT, False), ("single session , no region task", TWO, None, False), ("single session , no region , current", TWO, CURRENT, False)): a = run(kinds, ver, pt) print(f"{name:<38} {a['agreed']:>6} {a['completed']:>10} {a['request']:>7} {a['response']:>8}" f" {a['tokens']:>8} {a['round_trips']:>11}") b = run(FOUR, None, False, SEED + 1) print("second run ( task seed 20260219 , single session ) :", dict( completed=b["completed"], request=b["request"], response=b["response"], tokens=b["tokens"]))
setup agreed completed request response tokens round trips
single session , version skew present 3 6 27 27 744 35
session per task , skew present 3 6 60 60 1833 90
single session , client current 4 9 30 30 816 41
single session , no region task 3 12 33 33 909 41
single session , no region , current 4 12 33 33 909 41
second run ( task seed 20260219 , single session ) : {'completed': 6, 'request': 27, 'response': 27, 'tokens': 744}
The first two rows give in numbers what the session is for. The work done is the same in both: the same twelve tasks, the same chains, the same six completed tasks. In a single session discovery is paid once; the total is 27 requests, 27 responses, 744 tokens, 35 round trips. Renewing discovery at every task raises the same work to 60 requests, 60 responses, 1833 tokens and 90 round trips. The work tokens are 645 in both rows; the only thing that changes is discovery, and it rises from 99 to 1188. That is, most of the tokens spent in the second row went not to the work but to asking the same three questions twelve times.
The third row is the client that has learned the version. The negotiated tools rise to four, the completed tasks from six to nine; the cost is three requests, three responses, 72 tokens and six round trips. The remaining three tasks are record tasks, and because kayit_yaz is not on the allowlist they are not completed in any setup. In the second run the tasks are drawn with a different seed; the completed tasks, requests, responses and tokens come out exactly the same. What is counted is a property of the contract, not of the data.
The Silently Dropped Tool
The last two rows are the real finding of this lesson. The region tasks have been removed from the task set, leaving only charge and consumption tasks. On this set the skewed client and the current client produce exactly the same numbers: 12 completed tasks, 33 requests, 33 responses, 909 tokens, 41 round trips. One works with three tools, the other with four; no message, no error, no number carries this difference.
This is precisely the silence of version skew. The loss is not an error, because there is no step that returns an error; the handshake was completed successfully on both sides. The loss appears only as the absence of a tool that is used, and only when somebody tries to call that tool. If nobody calls, it never appears, and the system keeps working with three tools while believing it works with a set of four.
The only thing that makes it visible is the count at session open: 5 declared, 3 negotiated, 2 dropped. Unless the three numbers are printed side by side, version skew is written nowhere. The course’s rule is read directly here: an interface whose breakage for whom is not written down has not been measured. The set has five tools and the smallest step it can measure is 0.2000; so the three numbers themselves are printed, not a rate.
Summary
- The client has two jobs, server discovery and the session: discovery asks, the session holds the negotiated tool set.
- Discovery against three servers costs 3 requests, 3 responses, 99 tokens and 5 round trips; the five round trips come from one local and two remote servers.
- The same twelve tasks spend 27 requests and 744 tokens in a single session; when discovery is renewed at every task, this rises to 60 requests and 1833 tokens, of which 1188 go to discovery.
- For a client that has learned the version, the negotiated tools rise from 3 to 4 and the completed tasks from 6 to 9; the cost is 72 tokens and six round trips.
- On a set with no region tasks the skewed and current clients give exactly the same numbers: a dropped tool produces no error and, as long as it is not used, is never seen.
- The only thing that makes the loss visible is the count printed at session open, 5 declared, 3 negotiated, 2 dropped.
Next Step
In these two lessons the server and the client were written, the cost of discovery was counted and the silence of the dropped tool was seen. Both rested on a single assumption: the server was there. A local server, however, is a separate process; it has to be started, it says that it is ready with a notification message that expects no response, and the life of the session is bounded by its life. The next lesson makes this connection: it counts how many messages pass before the first call, the round trips and envelope tokens of the same chain, and how many messages the connection setup costs again when the process drops.
To keep your progress and take notes, Log in
My notes
Log in to take notes.