Skip to content
academia.sh

Lesson 08 / 11

Connecting to a Local Server

Starting a local server as a separate process and counting the cost of the connection as the messages that pass before the first call: starting the process costs 1 message and 10 tokens with its ready notification, server discovery costs 6 messages and 99 tokens, so 7 messages and 109 tokens are spent before the first tool call is sent. The three-tool chain that runs after the same setup yields 844 for subscriber A017 with 3 requests, 3 responses, 80 tokens and 4 round trips; in a single-task session the setup costs more than the work. Over twelve tasks the setup stays at 7 messages while the work rises to 60. When the process drops, neither an error nor a message is produced, the loss is noticed only when the next call cannot be sent, and recovery makes the whole setup be paid again: one drop raises the tokens from 919 to 1028, three drops to 1246, and the completed tasks stay at twelve.

Published Updated

In this lesson

  • Writes a connection that starts a local server as a process and counts its ready notification.
  • Measures the messages, tokens and round trips that pass until the first tool call.
  • Tells apart the round-trip cost of the local and remote steps of the same chain.
  • Shows with numbers that a process drop is silent and that recovery makes the setup be paid again.
Contents

In the previous two lessons the server and the client were written: the completeness of the capability declaration was counted as callability, the cost of discovery was derived, and it was seen that a tool dropped by version skew produces no error. Both rested on a single assumption. The server was there: when asked for its capabilities it answered, when a call was sent it received it.

For a local server this assumption is not given, it is built. A local server is a separate process running on the same machine: somebody has to start it, it has to say that it is ready, and when it dies everything the session holds goes with it. The question of this lesson is this: how many messages pass until the first useful call is sent, what does that chain cost, and who pays again when the process drops.

  • IM20. The environment, the three servers, the process and the protocol simulator in the lesson are fictional; no real process is started, no real server or runtime is called. Seed 20260218, second run 20260219.
  • IM21. The olcum (measurement) server is local and is a process. tarife (tariff) and kayit (records) are remote; their connection form is left to the next lesson, and here they appear only inside the chain.
  • IM22. When the process is started, the server says it is ready with a notification message. A notification expects no response: it pays the envelope cost but adds no round trip.
  • IM23. Server discovery is made after the process is started. The setup is the sum of starting the process and discovery.
  • IM24. A call to a local server whose process is closed cannot be sent at all; it ends inside the client with surec kapali (process not running) and spends no message.
  • IM25. The process dies silently: the death produces neither an error nor a message. The client understands it only when it cannot send the next call.
  • IM26. Recovery is this: the process is restarted, the session is set up again from the start, and the failed call is retried once. The whole setup is paid again.
  • IM27. The task set is 12 fictional tasks; six of each of two types (charge and consumption). On this set all tools are in the negotiated set, so the only thing that changes is the process.
  • IM28. The constant cost of the envelope is 8 tokens; on remote transport the round trip is doubled.
  • IM29. The baseline is the setup without the protocol: the tool is defined inside the lesson, and there is no process to start and no capability to ask about. There the cost of setup is zero.

The Process, the Ready Notification and the First Call

The code below sets up the fictional environment, the three fictional servers and capability negotiation, then connects the local server to a process: start opens the process and sends the ready notification, call_local prevents a call from being sent to a server whose process is closed, and setup combines the two.

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: olcum is a local process , the others are remote
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


# ---- A local server is a process: it is started , announces that it is ready , dies
PROCESS = {name: False for name, s in SERVERS.items() if s["transport"] == "local"}


def start(name):
    """Starts the process; the server says it is ready with a notification that expects no response."""
    PROCESS[name] = True
    message("notification", f"{name} hazir")


def call_local(agreed, name, state):
    """A call to a local server whose process is closed cannot be sent at all."""
    t = agreed.get(name)
    if t is not None and PROCESS.get(t["server"]) is False:
        return {"hata": "surec kapali"}
    return call(agreed, name, state)


def setup(client_version=None):
    """Local process + server discovery: everything that passes until the first call."""
    for k in COUNT:
        COUNT[k] = 0
    for name in PROCESS:
        start(name)
    agreed = handshake(client_version)[0]
    return agreed, dict(COUNT)


agreed, k = setup()
print(f"until first call : notification {k['notification']} , request {k['request']} ,"
      f" response {k['response']} , total messages {k['notification'] + k['request'] + k['response']} ,"
      f" tokens {k['tokens']} , round trips {k['round_trips']}")
for key in COUNT:
    COUNT[key] = 0
state = {"abone": "A017"}
for name in ("sayac_oku", "tuketim", "tarife"):
    state.update(call_local(agreed, name, state))
print(f"first chain ( A017 ) : request {COUNT['request']} , response {COUNT['response']} ,"
      f" tokens {COUNT['tokens']} , round trips {COUNT['round_trips']} ,"
      f" result {state['tutar']}")
until first call : notification 1 , request 3 , response 3 , total messages 7 , tokens 109 , round trips 5
first chain ( A017 ) : request 3 , response 3 , tokens 80 , round trips 4 , result 844

The first line is the main measure of this lesson. Before a tool call is sent, 7 messages and 109 tokens have been spent: the process start notification is one message and 10 tokens, server discovery is six messages and 99 tokens. The ready notification pays the envelope cost but does not touch the round trips at all, because it expects no response; all five round trips belong to discovery.

The second line is the chain that runs on top of the same setup, and the comparison comes from here. The three-tool chain costs 3 requests, 3 responses, 80 tokens, 4 round trips and yields 844 for subscriber A017. Four round trips are counted for three requests because the chain is split in two: sayac_oku (read meter) and tuketim (consumption) talk to the local process and count one round trip each, while tarife (tariff) is remote and counts two. In a single-task session the setup is more expensive than the work: 7 messages against 6, 109 tokens against 80. In the setup without the protocol the counterpart of these 109 tokens is zero, because there is neither a process to start nor a capability to ask about.

Who Pays When the Process Drops

The code below runs twelve fictional tasks on the same process. All the tasks can be solved with the negotiated tool set, so the only thing that changes is whether the process stays up. The process dies silently at set task positions.

CHAIN = {"charge": ("sayac_oku", "tuketim", "tarife"), "consumption": ("sayac_oku", "tuketim")}


def tasks(n=12, seed=SEED):
    """FICTIONAL tasks: an equal number of each of two types, subscriber picked by the seed."""
    g = []
    for i in range(n):
        r = rng(seed + 9973 * i)
        g.append({"type": list(CHAIN)[i % 2], "subscriber": f"A{1 + int(r() * 60):03d}"})
    return g


def expected(g):
    a = SUBSCRIBERS[g["subscriber"]]
    f = a["simdiki"] - a["onceki"]
    m3 = f + 100000 if f < 0 else f
    return m3 if g["type"] == "consumption" else _charge(m3, a["tip"])


def recover(session):
    """The dropped process is restarted and the session is set up again from the start: discovery is paid again."""
    for name, open_ in PROCESS.items():
        if not open_:
            start(name)
    session["agreed"] = handshake()[0]
    session["restarts"] += 1


def execute(session, g):
    state, result = {"abone": g["subscriber"]}, None
    for name in CHAIN[g["type"]]:
        y = call_local(session["agreed"], name, state)
        if y.get("hata") == "surec kapali":
            recover(session)
            y = call_local(session["agreed"], name, state)
        if "hata" in y:
            break
        state.update(y)
        result = y.get("tutar", y.get("m3", result))
    return result == expected(g)


def run(n=12, drops=(), seed=SEED):
    """The same tasks; the variable is how many times the process drops."""
    for name in PROCESS:
        PROCESS[name] = False
    agreed, k = setup()
    session = {"agreed": agreed, "restarts": 0}
    completed = 0
    for i, g in enumerate(tasks(n, seed)):
        if i in drops:
            PROCESS["olcum"] = False               # the process dies silently , no message
        completed += execute(session, g)
    return dict(restarts=session["restarts"], completed=completed, **COUNT)


print("setup                       restarts notification request response tokens round trips completed")
for name, n, drop in (("1 task , no drop", 1, ()), ("12 tasks , no drop", 12, ()),
                      ("12 tasks , 1 process drop", 12, (4,)),
                      ("12 tasks , 3 process drops", 12, (2, 5, 9))):
    a = run(n, drop)
    print(f"{name:<28} {a['restarts']:>8} {a['notification']:>12} {a['request']:>7} {a['response']:>8}"
          f" {a['tokens']:>6} {a['round_trips']:>11} {a['completed']:>9}")
b = run(12, (4,), SEED + 1)
print("second run ( task seed 20260219 , 1 drop ) :", dict(
    completed=b["completed"], messages=b["notification"] + b["request"] + b["response"],
    tokens=b["tokens"]))
setup                       restarts notification request response tokens round trips completed
1 task , no drop                    0            1       6        6    189           9         1
12 tasks , no drop                  0            1      33       33    919          41        12
12 tasks , 1 process drop           1            2      36       36   1028          46        12
12 tasks , 3 process drops          3            4      42       42   1246          56        12
second run ( task seed 20260219 , 1 drop ) : {'completed': 12, 'messages': 74, 'tokens': 1028}

The first two rows show how the setup behaves. In the single-task session, seven of the total 13 messages are setup; when it goes up to twelve tasks the setup stays at 7 messages and 109 tokens, while the work rises to 60 messages and 810 tokens. The setup is a fixed cost and its share falls as the session grows; this is exactly the price of keeping a session short.

The third and fourth rows turn the life of the process into numbers. When the process dies, neither an error message nor a notification is produced; the death is invisible in the message flow. The client understands it only when it cannot send the next call, and since that call never reaches the wire either, it does not enter the count. Recovery, however, makes the whole setup be paid again: one drop raises the messages from 67 to 74, the envelope tokens from 919 to 1028, the round trips from 41 to 46, that is, by exactly one setup. With three drops the tokens become 1246; the added 327 tokens are close to the cost of four charge chains.

The completed tasks did not fall in any of the four rows. This means recovery saved the work, and it also says where the cost should be written down: in this setup a process drop is not a problem of correctness, it is a problem of messages. In the second run the tasks are drawn with a different seed and all three numbers stay the same; the measure is a property of the contract and the process, not of the data.

Summary

  • A local server is a separate process: it is started, says it is ready with a notification message that expects no response, and the life of the session is bounded by its life.
  • 7 messages and 109 tokens pass until the first tool call: starting the process 1 message and 10 tokens, server discovery 6 messages and 99 tokens. The notification adds no round trip.
  • The three-tool chain costs 3 requests, 3 responses, 80 tokens and 4 round trips; the two local steps count one round trip each, the one remote step counts two.
  • In a single-task session the setup costs more than the work (7 messages against 6); over twelve tasks the setup stays at 7 messages while the work rises to 60.
  • The death of the process produces neither an error nor a message; it is noticed only when the next call cannot be sent.
  • Recovery makes the whole setup be paid again: one drop raises the tokens from 919 to 1028, three drops to 1246, and the completed tasks stay at twelve.

Next Step

In these three lessons the server was written, the client was written, and the connection between them was set up as a process. All the measurements had a common ground: the server was on the same machine. The party that starts the process, notices that it died and sets it up again, and the server itself, stood inside a single boundary; who was who was never asked, because there was nothing to ask. When the server moves beyond the network, the connection itself becomes a question of trust. The next lesson sets up the remote connection and counts only the steps it adds: the identity step, the authorization step and the round trip, side by side with the numbers of the local setup.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close