Skip to content
academia.sh

Lesson 02 / 11

Host, Client and Server

Counting the division of responsibility among the three roles by messages: ten items are distributed, the correct distribution needs no negotiation message, and every item that crosses a role boundary adds messages equal to its scope times its distance. Gathering all ten items in one role piles up 51 messages in the host, 45 in the server, 33 in the client, and zero in the correct distribution. The same three-tool chain costs 6 requests, 6 responses, 179 envelope tokens and 9 round trips in the correct distribution, and 12, 12, 341 and 15 with the allowlist on the server.

Published Updated

In this lesson

  • Matches the ten items to the roles they belong to among the host application, client and server.
  • Calculates the number of messages an item adds when it crosses a role boundary as the product of scope and distance.
  • Shows by counting the cost of there being no direct connection between the host application and the server.
  • Runs the same tool chain in four placements and compares the difference in requests, responses, envelope tokens and round trips.
Contents

The previous lesson named the three roles and counted the messages between them: the same three-tool chain gave 844 with zero messages at the baseline without the protocol, and with 6 requests, 6 responses, 179 envelope tokens and 9 round trips in the setup with the protocol. The naming was done, the distribution was not. Does the allowlist sit in the host application or the client, what is the single right place for the tool definition, who holds the session, who asks for approval?

The answer is not a matter of style. When an item sits in the wrong role, the side that uses it does not hold it and has to ask the other side; every ask is a request, every reply a message. The division of responsibility is therefore countable: the correct distribution is the one where no item needs a negotiation message, and the price of a wrong distribution is written in messages. This lesson distributes ten items across the three roles, counts how many messages each item adds when it crosses a boundary, and runs the same chain in four placements.

  • PS10. The environment, the three servers and the protocol simulator are fictional; no real server or runtime is called. Seed 20260218. The envelope’s fixed cost is eight tokens.
  • PS11. Ten items are distributed across three roles. The correct distribution is the one where no item needs a negotiation message, and this is the baseline.
  • PS12. The host and the server never talk directly; the client sits between them. So the distance between the host and the server is two, and the distance to the client is one.
  • PS13. An item’s scope is how many times it must be negotiated: three for items that are per server or per call, one for items that are per session.
  • PS14. An item’s form takes two values: pair produces a request and a response, single produces a notification with no response.
  • PS15. The added messages are counted at the local transport price; on remote transport the round trip is twice as much, and this lesson prints only the lower bound.
  • PS16. The counts do not depend on the run; the second run executes the same four placements for another subscriber. This lesson prints counts, not ratios; the resolution of the declared five-tool set is not binding here.
  • PS17. The allowlist’s and the approval point’s own measurement was done in the AI Agents course and is not repeated: the allowlist had lowered performance from 0.9583 to 0.8333 relative to full authority and stopped 20 calls. What is measured here is the cost of that item’s location.

The Correct Role of an Item

The three items on the server are arac tanimi (tool definition), arac yurutmesi (tool execution) and yetenek bildirimi (capability declaration). What they share is that all three belong to the tool itself and change together with it. When a tool’s field names change, the definition, the handler and the declaration are updated at once; if the three sat in different places, the change would have to be made in three places simultaneously.

The three items on the client are oturum (session), zarflama ve sayim (enveloping and counting) and aktarim secimi (transport choice). What they share is that all three belong to the connection, not the tool. Whether a server is local or remote concerns neither the host application nor the tool; both expect the same result. If the transport choice moves to the host application, the host application has to know every server’s connection style and changes whenever the number of servers changes.

The four items on the host application are izin listesi (allowlist), onay noktasi (approval point), baglam ve durum (context and state) and model cagrisi (model call). What they share is that all four belong to the user and the decision. The most critical member of this group is the allowlist: a server cannot be asked “which tools are you allowed to use”, because if the server determines its own authority surface, there is no such thing as an allowlist. The authority decision sits with the party that bears the consequence of that decision.

The code below sets up the previous lesson’s fictional environment and simulator, then writes the ten items into a table. When an item is placed in another role, move actually produces the messages the two sides need to explain it to each other; the number comes from the product of distance and scope.

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: subscriber records of the measurement network
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


# ---- FICTIONAL responsibility distribution: item -> (correct role , scope , message form)
ROLES = ("host", "client", "server")
ITEMS = {
    "arac tanimi":       ("server", 3, 2), "arac yurutmesi":  ("server", 3, 2),
    "yetenek bildirimi": ("server", 3, 2), "oturum":          ("client", 3, 2),
    "zarflama ve sayim": ("client", 3, 1), "aktarim secimi":  ("client", 3, 2),
    "izin listesi":      ("host", 3, 2), "onay noktasi": ("host", 3, 2),
    "baglam ve durum":   ("host", 1, 1), "model cagrisi": ("host", 1, 2),
}


def distance(a, b):
    """The host and the server never talk directly; the client sits between them."""
    return 0 if a == b else (1 if "client" in (a, b) else 2)


def move(item, new_role):
    """If an item stays outside its correct role, the two sides explain it to each other
    at every scope unit; a pair is a request and a response, a single is a one-way notification."""
    correct, scope, form = ITEMS[item]
    n = distance(correct, new_role) * scope
    for i in range(n):
        if form == 2:
            message("request", f"{item} uzlas {i}")
            message("response", f"{item} uzlasildi {i}")
        else:
            message("notification", f"{item} bildir {i}")
    return n * form


print(f"{'item':<19}{'correct role':<14}" + "".join(f"{'in ' + r:>15}" for r in ROLES))
totals = dict.fromkeys(ROLES, 0)
for k, (correct, scope, form) in ITEMS.items():
    extra = []
    for r in ROLES:
        for j in COUNT:
            COUNT[j] = 0
        extra.append(move(k, r))
        totals[r] += extra[-1]
    print(f"{k:<19}{correct:<14}" + "".join(f"{e:>15}" for e in extra))
print(f"{'items per role':<19}{'':<14}"
      + "".join(f"{sum(1 for v in ITEMS.values() if v[0] == r):>15}" for r in ROLES))
print(f"{'all in one role':<19}{'':<14}" + "".join(f"{totals[r]:>15}" for r in ROLES))
item               correct role          in host      in client      in server
arac tanimi        server                     12              6              0
arac yurutmesi     server                     12              6              0
yetenek bildirimi  server                     12              6              0
oturum             client                      6              0              6
zarflama ve sayim  client                      3              0              3
aktarim secimi     client                      6              0              6
izin listesi       host                        0              6             12
onay noktasi       host                        0              6             12
baglam ve durum    host                        0              1              2
model cagrisi      host                        0              2              4
items per role                                 4              3              3
all in one role                               51             33             45

The Price of Crossing a Role Boundary

The table’s diagonal is zero: when every item sits in its own correct role, no negotiation message is needed. The distribution gives four items to the host application, three to the client and three to the server. This distribution is not a matter of taste but the definition of the zero column.

The numbers come from two factors. The first is distance: the tool definition adds 6 messages when it slips to the client, 12 all the way to the host application. The doubling comes from the host application and the server never talking directly; all information going to the host application passes through the client once more. The second is scope: the allowlist is an item that must be negotiated per call and adds 12 messages when it slips to the server, whereas context and state are negotiated once per session and add only 2 notifications across the same boundary. The same mistake is six times as expensive for an item with a large scope.

The last row is the distribution’s total. Gathering all ten items in the host application piles up 51 messages, in the server 45, in the client 33; in the correct distribution zero. That the client is the cheapest place to gather is no coincidence: the client is in the middle and is two steps away from no item. This does not mean piling everything on the client is right: the 33 messages in the third row are still a price paid, and this lesson counts only messages, not the fact that when the allowlist slips to the server who decides changes.

The Same Chain, Four Placements

The table measured the items one by one. The code below runs the same chain in four placements and prints the total count. The second run repeats the same four placements for another subscriber.

CORRECT = {k: v[0] for k, v in ITEMS.items()}
SCENARIOS = [("correct distribution", CORRECT),
             ("session on server", dict(CORRECT, **{"oturum": "server"})),
             ("allowlist on server", dict(CORRECT, **{"izin listesi": "server"})),
             ("tool definition on host", dict(CORRECT, **{"arac tanimi": "host"}))]


def setup(placement, subscriber):
    """The same three-tool chain; the only difference is which role each item sits in."""
    for k in COUNT:
        COUNT[k] = 0
    for item, role in placement.items():
        move(item, role)
    agreed, _ = handshake()
    state = {"abone": subscriber}
    for name in ("sayac_oku", "tuketim", "tarife"):
        state.update(call(agreed, name, state))
    return state["tutar"], dict(COUNT)


print(f"{'subscriber':<12}{'placement':<28}{'result':>6}{'requests':>10}{'responses':>11}"
      f"{'notifications':>15}{'envelope tokens':>17}{'round trips':>14}")
for subscriber in ("A017", "A031"):
    for name, p in SCENARIOS:
        t, s = setup(p, subscriber)
        print(f"{subscriber:<12}{name:<28}{t:>6}{s['request']:>10}{s['response']:>11}"
              f"{s['notification']:>15}{s['tokens']:>17}{s['round_trips']:>14}")
subscriber  placement                   result  requests  responses  notifications  envelope tokens   round trips
A017        correct distribution           844         6          6              0              179             9
A017        session on server              844         9          9              0              254            12
A017        allowlist on server            844        12         12              0              341            15
A017        tool definition on host        844        12         12              0              341            15
A031        correct distribution            89         6          6              0              179             9
A031        session on server               89         9          9              0              254            12
A031        allowlist on server             89        12         12              0              341            15
A031        tool definition on host         89        12         12              0              341            15

The four placements give the same result: 844 for A017, 89 for A031. The placement does not change the computation, only its price. In the correct distribution the chain costs 179 envelope tokens; when a single item slips one boundary it rises to 254, when it slips two steps to 341. An item sitting in the wrong role makes the work itself nearly twice as expensive, and the chain’s round trips climb from nine to fifteen.

That the third and fourth rows are the same is instructive. The allowlist slips from the host application to the server, the tool definition from the server to the host application; the directions are opposite, the numbers the same. What determines the price is not what the item is but how many boundaries it crosses and how many times it is negotiated. The two placements cannot be told apart in messages; what tells them apart is not the message but who holds the authority decision, and that does not appear in this table.

The four rows of the two subscribers are also the same. The price of a placement does not depend on the data; it depends on the shape of the contract. The count of a tool connection is not the number of tools but the number of contracts.

Summary

  • The correct distribution of the three roles is the one where no item needs a negotiation message: four items sit in the host application, three in the client, three in the server.
  • The items on the server belong to the tool, those on the client to the connection, those on the host application to the user and the decision; the allowlist cannot be asked of the server, because the party that bears the consequence of the decision is the host application.
  • An item crossing a role boundary adds messages equal to distance times scope: the tool definition adds 6 when it slips to the client and 12 to the host application; context and state add 2 notifications when they slip to the server.
  • Because the host and the server never talk directly, the distance between them is two, and this makes a wrong placement of the items at the two ends twice as expensive.
  • Gathering all ten items in one role piles up 51 messages in the host application, 45 in the server, 33 in the client; zero in the correct distribution.
  • The same chain costs 179 envelope tokens and 9 round trips in the correct distribution, and 341 envelope tokens and 15 round trips when the allowlist slips to the server; for two subscribers the count is the same.

Next Step

In this lesson the placement changed and the transport did not: olcum stayed local, tarife and kayit stayed remote, and all the added messages were counted at the local price. Yet the round trip column moves independently of the request column; in the correct distribution 6 requests produced 9 round trips. The source of this difference is the transport, and it deserves to be measured on its own. The next lesson runs the same workload in three transport setups: all local, all remote and a mix of the two. It will show what the round trip does while the request count stays fixed.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close