🌍 Agent kao proizvod
Lekcija 01 · Zašto ovaj tutorijalLesson 01 · Why this tutorial

Ako samo ti koristiš agenta, to nije proizvodAn agent only you use isn't a product

Prva dva dela su napravila agenta i ojačala ga za produkciju. Fali treći čin: pustiti ga u svet da bude koristan drugima. To menja sve — alate, način pokretanja, i to ko sme šta.The first two parts built an agent and hardened it for production. The third act is missing: releasing it into the world to be useful to other people. That changes everything — the tools, how it runs, and who is allowed to do what.

Skripta · ti u terminaluScript · you in a terminal

Igračka-zadatak. Agent popravlja mod.py, ti gledaš, tvoj ključ, tvoja mašina. Dovoljan je jedan Python fajl.A toy task. The agent fixes mod.py, you watch, your key, your machine. All it takes is one Python file.

Proizvod · drugi ljudiProduct · other people

Pravi svet. Agent zove stvarne API-je, radi iza HTTP endpointa, opslužuje mnogo korisnika odjednom, i mora da bude i brz i jeftin da bi uopšte išao u primenu.The real world. The agent calls real APIs, runs behind an HTTP endpoint, serves many users at once, and has to be both fast and cheap to ship at all.

U prva dva dela agent je imao jedan lažni alat: pokreni pytest nad pokvarenim fajlom. Sad ga vodimo do stvarnih alata, standardnog protokola za njih (MCP), servisa koji opslužuje korisnike, i ograničenja koja na kraju odlučuju: trošak i latencija.In the first two parts the agent had one toy tool: run pytest against a broken file. Now we take it to real tools, the standard protocol for them (MCP), a service that serves users, and the constraints that ultimately decide: cost and latency.

  • Pravi alatiReal toolsStvarni API umesto pytesta.A real API instead of pytest. Lekcije 02–03.Lessons 02–03.
  • MCPMCPStandard koji odvaja alate od agenta.The standard that decouples tools from the agent. Lekcije 04–06.Lessons 04–06.
  • ServisServiceAgent iza HTTP-a, stanje po korisniku, konkurentnost.The agent behind HTTP, per-user state, concurrency. Lekcije 07–09.Lessons 07–09.
  • SkalaScaleOrkestracija, trošak i latencija, deploy.Orchestration, cost and latency, deploy. Lekcije 10–12.Lessons 10–12.

Cela serija u tri redaThe whole series in three rows

  • DemoDemo1. deo.Part 1. Lokalni fajl, jedan korisnik, ti u kontroli.A local file, one user, you in control.
  • ProdukcijaProduction2. deo.Part 2. Ograde, trag, evali, budžet — stoji na nogama bez tebe.Guardrails, tracing, evals, budget — it stays upright without you.
  • ProizvodProduct3. deo.Part 3. Pravi alati, MCP, auth, stanje, konkurentnost, deploy, trošak.Real tools, MCP, auth, state, concurrency, deploy, cost.
Konkretan ciljConcrete goal Na kraju ćeš imati:By the end you'll have: MCP server (mcp_server.py) koji koristi i tvoj Claude Code, i FastAPI servis (uvicorn servis:app) sa stanjem po korisniku koji odgovara na curl zahtev.an MCP server (mcp_server.py) your own Claude Code can use, and a FastAPI service (uvicorn servis:app) with per-user state that answers a curl request.
PreduslovPrerequisite Ovo je 3. deo. Pretpostavljamo da ti je petlja jasna iz Agentske petlje i da si video ograde, trag i evale iz Agenta bez nadzora. Sav kod je i dalje Python + anthropic SDK.This is Part 3. We assume the loop is clear from The Agent Loop and that you've seen the guardrails, tracing, and evals from The Unattended Agent. All the code is still Python + the anthropic SDK.
Lekcija 02 · Stvarni alatiLesson 02 · Real tools

Neka agent dela na stvarnim podacimaLet the agent act on real data

Igračka-alat vraća izlaz pytesta. Pravi alat zove spoljni servis — bazu, HTTP API, red poruka. Princip je isti kao u 1. delu: model traži poziv, tvoj kod ga izvrši. Menja se samo šta je unutar funkcije.The toy tool returns pytest output. A real tool calls an external service — a database, an HTTP API, a message queue. The principle is the same as Part 1: the model requests the call, your code runs it. Only what's inside the function changes.

alat_api.py
import httpx
import anthropic

client = anthropic.Anthropic()

tools = [{
    "name": "kurs_valute",
    "description": "Vrati kurs jedne valute prema evru sa javnog API-ja.",
    "input_schema": {
        "type": "object",
        "properties": {"valuta": {"type": "string",
                                  "description": "npr. USD, GBP, CHF"}},
        "required": ["valuta"],
    },
}]

def kurs_valute(valuta):
    # pravi HTTP poziv — ovo je alat koji dela u svetu
    r = httpx.get("https://api.frankfurter.dev/v1/latest",
                  params={"base": "EUR", "symbols": valuta.upper()},
                  timeout=10)
    r.raise_for_status()
    return str(r.json()["rates"])

messages = [{"role": "user",
             "content": "Koliko je 1 evro u dolarima danas?"}]

resp = client.messages.create(
    model=MODEL, max_tokens=1024,
    tools=tools, messages=messages,
)
import httpx
import anthropic

client = anthropic.Anthropic()

tools = [{
    "name": "currency_rate",
    "description": "Return the rate of one currency against the euro from a public API.",
    "input_schema": {
        "type": "object",
        "properties": {"currency": {"type": "string",
                                    "description": "e.g. USD, GBP, CHF"}},
        "required": ["currency"],
    },
}]

def currency_rate(currency):
    # a real HTTP call — this is a tool that acts in the world
    r = httpx.get("https://api.frankfurter.dev/v1/latest",
                  params={"base": "EUR", "symbols": currency.upper()},
                  timeout=10)
    r.raise_for_status()
    return str(r.json()["rates"])

messages = [{"role": "user",
             "content": "How much is 1 euro in dollars today?"}]

resp = client.messages.create(
    model=MODEL, max_tokens=1024,
    tools=tools, messages=messages,
)

Petlja oko ovoga je ista petlja iz 1. dela — čitaš resp.stop_reason, izvršiš traženi alat, vratiš tool_result. Samo je sada rezultat živ podatak iz sveta, ne izlaz pytesta.The loop around this is the same loop from Part 1 — you read resp.stop_reason, run the requested tool, return a tool_result. Only now the result is live data from the world, not pytest output.

Isečak vs pun kodSnippet vs full code Isečci pokazuju suštinu; kompletni fajlovi spremni za pokretanje (alat_api.py, mcp_server.py, servis.py) su na GitLab-u u folderu deo-3-agent-u-stvarnom-svetu/. Nastavljaju se na 1. deo: client je anthropic.Anthropic(), a MODEL je konstanta sa ID-jem modela — naziv namerno nije upisan jer se modeli smenjuju brže nego što se tutorijali ažuriraju; aktuelni uzmi sa zvanične liste modela.The snippets show the essence; the complete, runnable files (alat_api.py, mcp_server.py, servis.py) are on GitLab in the deo-3-agent-u-stvarnom-svetu/ folder. They continue from Part 1: client is anthropic.Anthropic(), and MODEL is a constant holding the model ID — the name is deliberately left out because models turn over faster than tutorials get updated; take the current one from the official model list.
Agent je sposoban tačno onoliko koliko su sposobni alati koje mu daš. Prelazak sa igračke na pravi alat ne menja petlju — menja domet agenta.An agent is exactly as capable as the tools you hand it. Going from a toy to a real tool doesn't change the loop — it changes the agent's reach.
CheckpointCheckpoint Pre nego što alat uopšte spojiš sa modelom, pozovi funkciju sam iz Pythona i ispiši šta vrati. Mora da radi bez agenta. Zvuči trivijalno, ali polovina „agent ne radi" prijava završi ovde — alat je bio pokvaren, a petlja je samo uredno prenosila njegov kvar.Before you connect the tool to the model at all, call the function yourself from Python and print what it returns. It has to work without the agent. It sounds trivial, but half of all "the agent doesn't work" reports end here — the tool was broken, and the loop was faithfully relaying its failure.
Lekcija 03 · RobusnostLesson 03 · Robustness

Pravi svet otkazuje — alat mora da to podneseThe real world fails — the tool must handle it

Pytest ili prođe ili padne. Stvarni API kasni, vrati 429, prekine vezu ili udari u rate limit. Alat koji pukne na prvoj prepreci obara celog agenta. Dobar alat te prepreke pretvara u jasnu poruku koju model može da pročita i zaobiđe.Pytest either passes or fails. A real API is slow, returns 429, drops the connection, or hits a rate limit. A tool that breaks on the first edge takes down the whole agent. A good tool turns those edges into a clear message the model can read and route around.

robustan_alat.py
import time, httpx

def kurs_valute(valuta):
    for pokusaj in range(3):                 # retry uz backoff
        try:
            r = httpx.get("https://api.frankfurter.dev/v1/latest",
                          params={"base": "EUR", "symbols": valuta.upper()},
                          timeout=10)         # timeout: ne visi zauvek
            if r.status_code == 429:          # rate limit -> sačekaj i probaj opet
                time.sleep(2 ** pokusaj)
                continue
            r.raise_for_status()
            rates = r.json().get("rates", {})
            if not rates:
                return f"GREŠKA: nepoznata valuta '{valuta}'."
            return str(rates)
        except httpx.TimeoutException:
            return "GREŠKA: servis ne odgovara (timeout). Pokušaj kasnije."
        except httpx.HTTPError as e:
            return f"GREŠKA pri pozivu API-ja: {e}"
    return "GREŠKA: rate limit — previše zahteva, stani i javi korisniku."
import time, httpx

def currency_rate(currency):
    for attempt in range(3):                 # retry with backoff
        try:
            r = httpx.get("https://api.frankfurter.dev/v1/latest",
                          params={"base": "EUR", "symbols": currency.upper()},
                          timeout=10)         # timeout: don't hang forever
            if r.status_code == 429:          # rate limit -> wait and retry
                time.sleep(2 ** attempt)
                continue
            r.raise_for_status()
            rates = r.json().get("rates", {})
            if not rates:
                return f"ERROR: unknown currency '{currency}'."
            return str(rates)
        except httpx.TimeoutException:
            return "ERROR: the service is not responding (timeout). Try later."
        except httpx.HTTPError as e:
            return f"ERROR calling the API: {e}"
    return "ERROR: rate limit — too many requests, stop and tell the user."
  • TimeoutTimeoutUvek, na svakom pozivu.Always, on every call. Bez njega jedan spor servis zamrzne celog agenta i troši ti resurse.Without it, one slow service freezes the whole agent and burns your resources.
  • RetryRetryUz backoff, ograničen broj puta.With backoff, a bounded number of times. Prolazne greške (429, kratak mrežni prekid) same prođu — ne obaraj agenta zbog njih.Transient errors (429, a network blip) pass on their own — don't crash the agent over them.
  • Jasna greškaClear errorVrati poruku, ne izuzetak.Return a message, not an exception. Isto pravilo kao „progutana greška" iz 1. dela: greška je informacija za model.The same rule as the "swallowed error" from Part 1: an error is information for the model.
Dva nivoa retry-aTwo levels of retry Ponavljanje pokušaja gore pišeš sam, za svoj alat (spoljni API). Za poziv modelu ne moraš — anthropic SDK sam ponavlja pokušaj na 429/529 i mrežne greške (max_retries, podrazumevano 2). Podesiš ga kroz Anthropic(max_retries=...).Above, you retry your tool (the external API). You don't have to retry the model call — the anthropic SDK retries 429/529 and network errors on its own (max_retries, default 2). Tune it via Anthropic(max_retries=...).
Najgori slučajWorst case Agent je bezbedan koliko i najgori granični slučaj njegovog najgoreg alata.An agent is as safe as the worst edge case of its worst tool. Zato dizajn alata — timeouts, retry, validacija ulaza, jasne greške — nije sitnica, nego glavni posao kad ideš u produkciju.That's why tool design — timeouts, retries, input validation, clear errors — isn't a detail but the main job when you go to production.
CheckpointCheckpoint Postavi timeout na 0.001 i pozovi alat. Treba da dobiješ rečenicu o isteklom vremenu, ne stack trace — i agent posle toga mora da nastavi. Pa vrati timeout i isključi mrežu na trenutak: ista stvar, druga poruka. Alat koji pukne umesto da objasni je alat koji obara celog agenta.Set timeout to 0.001 and call the tool. You should get a sentence about the timeout, not a stack trace — and the agent has to carry on afterwards. Then restore the timeout and cut your network for a moment: same thing, different message. A tool that crashes instead of explaining is a tool that takes the whole agent down.
Lekcija 04 · MCPLesson 04 · MCP

Zašto se alati odvajaju od agentaWhy tools get decoupled from the agent

Do sada su alati živeli unutar agenta — ista datoteka, ista petlja. Na duže staze to ne valja: svaki agent iznova piše iste alate, a svaki alat je vezan za jednog agenta. Model Context Protocol (MCP) je standard koji alat pretvara u zaseban servis koji bilo koji agent može da koristi.Until now the tools lived inside the agent — same file, same loop. That doesn't scale: every agent rewrites the same tools, and every tool is locked to one agent. The Model Context Protocol (MCP) is a standard that turns a tool into a separate service any agent can use.

Bez MCP-a · alat u agentuWithout MCP · tool inside the agent

Svaki agent ima svoju kopiju kurs_valute. Popraviš bug na jednom mestu — pa ga popravljaš i na svim ostalim.Every agent has its own copy of currency_rate. Fix a bug in one place — you have to fix it in all the others too.

Sa MCP-om · alat kao servisWith MCP · tool as a service

Alat je zaseban server. Napišeš ga jednom, a svaki agent — i sam Claude Code — ga koristi preko istog protokola. Jedna popravka, svuda važi.The tool is a standalone server. You write it once, and every agent — Claude Code included — uses it over the same protocol. One fix, applies everywhere.

MCP definiše zajednički jezik između klijenta (agent kome trebaju alati) i servera (proces koji izlaže alate). Klijent pita „koje alate imaš?", server odgovori spiskom, klijent traži poziv, server ga izvrši. Ista razmena kao između modela i tvoje petlje — samo standardizovana i preko mreže.MCP defines a shared language between a client (an agent that needs tools) and a server (a process that exposes tools). The client asks "which tools do you have?", the server replies with a list, the client requests a call, the server runs it. The same handshake as between the model and your loop — just standardized and over the network.

Zašto ti je ovo poznatoWhy this feels familiar Claude Code koristi baš MCP da se poveže na spoljne servere (GitHub, baze, tvoje interne alate). Kad u Claude Code dodaš MCP server, radiš tačno ono što ćemo u sledeće dve lekcije: napraviti server pa povezati klijenta.Claude Code uses exactly MCP to connect to external servers (GitHub, databases, your internal tools). When you add an MCP server in Claude Code, you're doing exactly what we'll do in the next two lessons: build a server, then connect a client.
MCP je za alate ono što je REST za servise: dogovor koji omogućava da stvari napisane nezavisno rade zajedno. Alat jednom napisan, svuda dostupan.MCP is to tools what REST is to services: a convention that lets independently-written things work together. A tool written once, available everywhere.
Lekcija 05 · MCP serverLesson 05 · MCP server

Izloži alat kao samostalan serverExpose a tool as a standalone server

Napravimo pravi MCP server sa našim alatom za kurs. Zvanični mcp paket ima FastMCP koji svu ceremoniju protokola skloni iza jednog dekoratora — ti samo napišeš funkciju.Let's build a real MCP server with our currency tool. The official mcp package ships FastMCP, which hides all the protocol ceremony behind a single decorator — you just write the function.

pip install "mcp[cli]" httpx
pip install "mcp[cli]" httpx
mcp_server.py
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("valute")           # ime servera

@mcp.tool()
def kurs_valute(valuta: str) -> str:
    """Vrati kurs valute prema evru sa javnog API-ja.

    valuta: oznaka valute, npr. USD, GBP, CHF
    """
    r = httpx.get("https://api.frankfurter.dev/v1/latest",
                  params={"base": "EUR", "symbols": valuta.upper()},
                  timeout=10)
    r.raise_for_status()
    rates = r.json().get("rates", {})
    return str(rates) if rates else f"Nepoznata valuta: {valuta}"

if __name__ == "__main__":
    mcp.run()                     # pokreće server (stdio transport)
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("currencies")       # server name

@mcp.tool()
def currency_rate(currency: str) -> str:
    """Return a currency's rate against the euro from a public API.

    currency: currency code, e.g. USD, GBP, CHF
    """
    r = httpx.get("https://api.frankfurter.dev/v1/latest",
                  params={"base": "EUR", "symbols": currency.upper()},
                  timeout=10)
    r.raise_for_status()
    rates = r.json().get("rates", {})
    return str(rates) if rates else f"Unknown currency: {currency}"

if __name__ == "__main__":
    mcp.run()                     # runs the server (stdio transport)

Primeti gde ide opis alata: u docstring funkcije, a tipovi argumenata (valuta: str) postaju šema. FastMCP od toga sam generiše ono što bismo u 1. delu pisali ručno kao input_schema. Server sada čeka da mu se neki klijent obrati.Notice where the tool description goes: in the function's docstring, and the argument types (currency: str) become the schema. FastMCP generates from that what we wrote by hand as input_schema in Part 1. The server now waits for a client to talk to it.

Dobar opis, ponovoGood description, again Isto pravilo iz 1. dela važi i ovde: model bira alat po opisu. U MCP-u je opis docstring — piši ga jasno, kao uputstvo kolegi koji nikad neće videti tvoj kod.The same rule from Part 1 holds: the model picks a tool by its description. In MCP the description is the docstring — write it clearly, like an instruction to a colleague who will never see your code.
CheckpointCheckpoint Pokreni server sa mcp dev mcp_server.py i otvori inspektor. Tvoj alat mora da se pojavi na spisku, a kao opis mora da stoji tvoj docstring, sa valuta kao stringom u šemi. Ako opis nedostaje, model ga neće imati po čemu izabrati — a to je greška koju ćeš videti tek kao „agent ne poziva alat".Start the server with mcp dev mcp_server.py and open the inspector. Your tool must show up in the list, and its description must be your docstring, with currency as a string in the schema. If the description is missing, the model has nothing to pick it by — a mistake you'll only ever see as "the agent doesn't call the tool".
Lekcija 06 · MCP klijentLesson 06 · MCP client

Agent koristi tvoj serverThe agent uses your server

Server izlaže alat; sad agent treba da ga upotrebi. Claude API ume da se poveže na udaljeni MCP server direktno — ti navedeš URL servera, a Anthropic sa svoje strane obavi MCP razmenu i pozove alate. Ne moraš sam da vrtiš petlju alata.The server exposes the tool; now the agent needs to use it. The Claude API can connect to a remote MCP server directly — you name the server's URL, and Anthropic performs the MCP handshake and calls the tools server-side. You don't have to run the tool loop yourself.

mcp_klijent.py
import anthropic

client = anthropic.Anthropic()

resp = client.beta.messages.create(
    model=MODEL,
    max_tokens=1024,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[{
        "type": "url",
        "url": "https://tvoj-server.primer.com/mcp",
        "name": "valute",
    }],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "valute"}],
    messages=[{"role": "user",
               "content": "Koliko je 1 evro u dolarima?"}],
)

for blok in resp.content:
    if blok.type == "text":
        print(blok.text)
import anthropic

client = anthropic.Anthropic()

resp = client.beta.messages.create(
    model=MODEL,
    max_tokens=1024,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[{
        "type": "url",
        "url": "https://your-server.example.com/mcp",
        "name": "currencies",
    }],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "currencies"}],
    messages=[{"role": "user",
               "content": "How much is 1 euro in dollars?"}],
)

for block in resp.content:
    if block.type == "text":
        print(block.text)

Dva dela idu zajedno: mcp_servers nabraja servere (tip, URL, ime), a mcp_toolset u tools pušta model da koristi alate tog servera po imenu. Model sam odluči da mu treba kurs_valute, Anthropic pozove tvoj server, i vrati odgovor — bez tvoje ručne petlje.Two parts go together: mcp_servers lists the servers (type, URL, name), and the mcp_toolset entry in tools lets the model use that server's tools by name. The model decides it needs currency_rate, Anthropic calls your server, and returns the answer — with no manual loop from you.

Bitna razlika: stdio vs URLKey difference: stdio vs URL Server iz lekcije 05 koristi stdio transport — lokalni proces, nema URL. Ovaj konektor traži javno dostupan HTTPS server, pa da spojiš baš svoj server moraš ga izložiti preko HTTP transporta (npr. FastMCP preko HTTP/SSE) ili koristiti lokalne MCP pomoćnike iz SDK-a. Ne možeš URL konektor uperiti direktno u stdio server.The server from lesson 05 uses stdio transport — a local process, no URL. This connector needs a publicly reachable HTTPS server, so to connect your own server you must expose it over an HTTP transport (e.g. FastMCP over HTTP/SSE) or use the SDK's local MCP helpers. You can't point the URL connector straight at a stdio server.
Isti server, dva korisnikaSame server, two users Kad ga jednom izložiš, tvoj MCP server koriste i ovaj agent i Claude Code, bez ijedne izmene. To je cela poenta MCP-a: napišeš alat jednom, koristi ga bilo koji klijent koji govori isti protokol.Once it's exposed, your MCP server is used by both this agent and Claude Code, with no changes. That's the whole point of MCP: write a tool once, any client that speaks the protocol can use it.
Ako ti server mora ostati lokalan ili tražiš više kontrole, anthropic SDK ima i MCP pomoćnike za lokalne servere. Princip je isti — samo ti sam vodiš vezu umesto Anthropic-a.If your server must stay local or you want more control, the anthropic SDK also has MCP helpers for local servers. The principle is identical — you just drive the connection instead of Anthropic.
Lekcija 07 · ServisLesson 07 · Service

Sakrij petlju iza HTTP endpointaHide the loop behind an HTTP endpoint

Dok agent radi samo u tvom terminalu, koristiš ga samo ti. Da bi ga koristili drugi, umotaj petlju u HTTP servis. Sa FastAPI to je nekoliko redova: korisnik pošalje zahtev, tvoj endpoint pokrene agenta i vrati odgovor.While the agent runs only in your terminal, only you can use it. To let others use it, wrap the loop in an HTTP service. With FastAPI that's a few lines: a user sends a request, your endpoint runs the agent and returns the answer.

pip install fastapi uvicorn anthropic
pip install fastapi uvicorn anthropic
servis.py
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.Anthropic()

class Zahtev(BaseModel):
    poruka: str

def pokreni_agenta(poruka: str) -> str:
    # ovde je petlja iz prethodnih delova (tools, verifikacija, kočnice)
    resp = client.messages.create(
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": poruka}],
    )
    return next((b.text for b in resp.content if b.type == "text"), "")

@app.post("/pitaj")
def pitaj(z: Zahtev):
    return {"odgovor": pokreni_agenta(z.poruka)}

# pokretanje:  uvicorn servis:app --reload
# poziv:       curl -X POST localhost:8000/pitaj \
#                -H "Content-Type: application/json" \
#                -d '{"poruka": "Koliko je 1 evro u dolarima?"}'
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.Anthropic()

class Request(BaseModel):
    message: str

def run_agent(message: str) -> str:
    # here goes the loop from the previous parts (tools, verify, brakes)
    resp = client.messages.create(
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": message}],
    )
    return next((b.text for b in resp.content if b.type == "text"), "")

@app.post("/ask")
def ask(req: Request):
    return {"answer": run_agent(req.message)}

# run:   uvicorn service:app --reload
# call:  curl -X POST localhost:8000/ask \
#          -H "Content-Type: application/json" \
#          -d '{"message": "How much is 1 euro in dollars?"}'

Sad agent više nije skripta koju pokrećeš rukom — to je servis koji svako sa pristupom endpointu može da pozove. Petlja, alati i kočnice iz prethodnih delova žive netaknuti unutar pokreni_agenta; menja se samo kako se poziva.Now the agent is no longer a script you run by hand — it's a service anyone with access to the endpoint can call. The loop, tools, and brakes from the previous parts live untouched inside run_agent; only how it's invoked changes.

CheckpointCheckpoint Pokreni uvicorn servis:app --reload, pa u drugom terminalu curl -X POST localhost:8000/pitaj -H "Content-Type: application/json" -d '{"poruka":"Zdravo"}'. Treba da dobiješ JSON {"odgovor": "..."}.Run uvicorn service:app --reload, then in another terminal curl -X POST localhost:8000/ask -H "Content-Type: application/json" -d '{"message":"Hi"}'. You should get JSON {"answer": "..."}.
Pazi: ključ i ogradeWatch out: key and guardrails Čim je agent na mreži, tvoj API ključ radi za tuđe zahteve. Sad ograde iz 2. dela (allowlist, potvrde, budžet) nisu opcija nego obaveza — i dodaj autentikaciju na endpoint da ti ga bilo ko ne zloupotrebi.The moment the agent is on the network, your API key works for other people's requests. Now the guardrails from Part 2 (allowlist, confirmations, budget) aren't optional but mandatory — and add authentication to the endpoint so nobody abuses it.
Lekcija 08 · SesijeLesson 08 · Sessions

Svaki korisnik ima svoj razgovorEvery user has their own conversation

Servis iz prethodne lekcije zaboravlja sve između zahteva. Za pravi proizvod svaki korisnik mora imati svoju istoriju — i ne sme videti tuđu. To je stanje po korisniku: mapa koja id korisnika vezuje za njegovu listu messages.The service from the previous lesson forgets everything between requests. For a real product each user must have their own history — and must not see anyone else's. That's per-user state: a map from user id to their messages list.

sesije.py
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.Anthropic()

# stanje po korisniku — u pravoj primeni: baza ili Redis, ne dict u procesu
sesije: dict[str, list] = {}

class Zahtev(BaseModel):
    korisnik: str
    poruka: str

@app.post("/pitaj")
def pitaj(z: Zahtev):
    istorija = sesije.setdefault(z.korisnik, [])   # izolovano po korisniku
    istorija.append({"role": "user", "content": z.poruka})

    resp = client.messages.create(
        model=MODEL, max_tokens=1024, messages=istorija,
    )
    tekst = next((b.text for b in resp.content if b.type == "text"), "")
    istorija.append({"role": "assistant", "content": tekst})
    return {"odgovor": tekst}
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.Anthropic()

# per-user state — in real use: a database or Redis, not a dict in the process
sessions: dict[str, list] = {}

class Request(BaseModel):
    user: str
    message: str

@app.post("/ask")
def ask(req: Request):
    history = sessions.setdefault(req.user, [])    # isolated per user
    history.append({"role": "user", "content": req.message})

    resp = client.messages.create(
        model=MODEL, max_tokens=1024, messages=history,
    )
    text = next((b.text for b in resp.content if b.type == "text"), "")
    history.append({"role": "assistant", "content": text})
    return {"answer": text}
  • IzolacijaIsolationKljuč po korisniku.Key per user. Nikad ne deli istoriju između korisnika — to je i greška u proizvodu i curenje podataka.Never share history between users — that's both a product bug and a data leak.
  • TrajnostPersistenceDict koji živi u procesu nestane pri restartu.A dict that lives in the process dies on restart. Za pravu primenu stanje ide u bazu ili Redis — veza sa memorijom iz 2. dela.For real use, state goes to a database or Redis — the link to memory from Part 2.
  • RastGrowthIstorija raste dok ne udari u prozor.History grows until it hits the window. Ovde upada sažimanje iz 2. dela — inače svaki zahtev postaje sve skuplji.This is where compaction from Part 2 fits — otherwise each request gets pricier.
CheckpointCheckpoint Pošalji dva zahteva sa različitim korisnik vrednostima — recimo „ana" kaže neku činjenicu, pa „marko" pita za nju. Marko ne sme da zna. Onda ista provera za Anu u drugom zahtevu: ona mora da zna. Ako oba korisnika vide istu istoriju, ne deliš stanje po ključu — a to nije samo bug, to je curenje tuđih podataka.Send two requests with different user values — say "ana" states some fact, then "marko" asks about it. Marko must not know it. Then the same check for Ana in a second request: she must. If both users see the same history, you're not keying state per user — and that isn't just a bug, it's a leak of someone else's data.
Lekcija 09 · KonkurentnostLesson 09 · Concurrency

Šta se lomi kad naiđe 100 korisnikaWhat breaks when 100 users show up

Jedan agentski zahtev je spor: nekoliko poziva modelu, svaki čeka mrežu. Sinhroni endpoint blokira ceo servis dok jedan zahtev traje. Rešenje je async — dok jedan zahtev čeka odgovor modela, servis obrađuje druge.One agent request is slow: several model calls, each waiting on the network. A synchronous endpoint blocks the whole service while one request runs. The fix is async — while one request waits on the model, the service handles others.

async_servis.py
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.AsyncAnthropic()      # async klijent

class Zahtev(BaseModel):
    poruka: str

@app.post("/pitaj")
async def pitaj(z: Zahtev):              # async endpoint
    resp = await client.messages.create( # await ne blokira druge zahteve
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": z.poruka}],
    )
    tekst = next((b.text for b in resp.content if b.type == "text"), "")
    return {"odgovor": tekst}
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic

app = FastAPI()
client = anthropic.AsyncAnthropic()      # async client

class Request(BaseModel):
    message: str

@app.post("/ask")
async def ask(req: Request):             # async endpoint
    resp = await client.messages.create( # await doesn't block other requests
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": req.message}],
    )
    text = next((b.text for b in resp.content if b.type == "text"), "")
    return {"answer": text}

Za zadatke koji traju predugo za jedan HTTP zahtev (agent koji radi minutima), ne teraj korisnika da čeka na otvorenoj vezi. Stavi zahtev u red poslova, vrati mu odmah id posla, a on kasnije pita „je li gotovo?". Tako veza ne visi i servis diše.For tasks too long for a single HTTP request (an agent that runs for minutes), don't make the user wait on an open connection. Put the request in a job queue, return a job id immediately, and let them poll "is it done?" later. That way the connection doesn't hang and the service breathes.

Konkurentnost je tačka na kojoj demo koji radi za jednog korisnika pukne za stotinu. Async i redovi poslova nisu optimizacija — oni su razlika između „radi na mojoj mašini" i „radi u produkciji".Concurrency is where a demo that works for one user breaks for a hundred. Async and job queues aren't an optimization — they're the difference between "works on my machine" and "works in production."
Cela vertikala na jednom mestuThe whole vertical in one place Fajl servis_pun.py na GitLab-u spaja sve iz 3. dela u jedan gotov servis: pravi alat (timeout + retry) → agentska petlja → kočnica → stanje po korisniku → async → X-API-Key autentikacija. uvicorn servis_pun:app --reload i imaš kompletan primer.The file servis_pun.py on GitLab stitches everything from Part 3 into one runnable service: real tool (timeout + retry) → agent loop → step brake → per-user state → async → X-API-Key auth. uvicorn servis_pun:app --reload and you have an end-to-end example.
Lekcija 10 · OrkestracijaLesson 10 · Orchestration

Planer i radnici kao odvojeni servisiPlanner and workers as separate services

U 2. delu smo videli planer + radnike kao obrazac. Kad obim poraste, u pravom proizvodu ti radnici postaju odvojeni servisi ili async poslovi koji rade paralelno. Ključna prednost je ista: svaki podzadatak dobija svež, čist kontekst.In Part 2 we saw planner + workers as a pattern. At scale, in a product, those workers become separate services or async jobs running in parallel. The key advantage is the same: each subtask gets a fresh, clean context.

orkestrator.py
import asyncio
import anthropic

client = anthropic.AsyncAnthropic()

async def radnik(podzadatak: str) -> str:
    # svaki radnik ima svežu, izolovanu petlju (svoj čist kontekst)
    resp = await client.messages.create(
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": podzadatak}],
    )
    return next((b.text for b in resp.content if b.type == "text"), "")

async def orkestriraj(podzadaci: list[str]) -> list[str]:
    # svi radnici rade PARALELNO, ne jedan za drugim
    return await asyncio.gather(*(radnik(p) for p in podzadaci))

# planer razbije veliki zadatak, orkestrator ga rasporedi
podzadaci = ["Istraži X", "Sažmi Y", "Proveri Z"]
rezultati = asyncio.run(orkestriraj(podzadaci))
import asyncio
import anthropic

client = anthropic.AsyncAnthropic()

async def worker(subtask: str) -> str:
    # each worker has a fresh, isolated loop (its own clean context)
    resp = await client.messages.create(
        model=MODEL, max_tokens=1024,
        messages=[{"role": "user", "content": subtask}],
    )
    return next((b.text for b in resp.content if b.type == "text"), "")

async def orchestrate(subtasks: list[str]) -> list[str]:
    # all workers run IN PARALLEL, not one after another
    return await asyncio.gather(*(worker(s) for s in subtasks))

# the planner splits the big task, the orchestrator schedules it
subtasks = ["Research X", "Summarize Y", "Verify Z"]
results = asyncio.run(orchestrate(subtasks))

asyncio.gather pušta sve radnike u isto vreme — ukupno vreme je najsporiji radnik, ne zbir svih. To je ista ideja kao paralelni podagenti u Claude Code: orkestrator razbije posao, pusti kopije da rade istovremeno, pa spoji rezultate.asyncio.gather launches all workers at once — total time is the slowest worker, not the sum of all. This is the same idea as parallel subagents in Claude Code: the orchestrator splits the work, lets copies run concurrently, then merges the results.

Ne komplikuj preranoDon't over-engineer early Svaki nivo orkestracije množi troškove i načine otkaza. Pređi na roj tek kad ti eval iz 2. dela crno na belo pokaže da jedan agent ne stiže — ne zato što „paralelno zvuči moćnije".Each orchestration level multiplies cost and failure modes. Move to a swarm only when the eval from Part 2 proves in black and white that a single agent can't keep up — not because "parallel sounds more powerful."
Lekcija 11 · EkonomijaLesson 11 · Economics

Ono što na kraju odlučuje da li ide u primenuWhat ultimately decides whether it ships

Agent može biti tačan i bezbedan, a da i dalje ne ide u primenu — jer je preskup ili prespor pod pravim opterećenjem. Tri poluge to menjaju: keširanje, izbor modela po koraku, i — najvažnije — pitanje da li agent uopšte treba.An agent can be correct and safe and still not ship — because it's too expensive or too slow under real load. Three levers change that: caching, choosing the model per step, and — most important — asking whether you even need an agent.

1 — Keširanje prompta1 — Prompt caching

Ako svaki zahtev deli isti veliki sistemski prompt ili kontekst, keširaj ga: sledeći pozivi plaćaju taj deo ~10× jeftinije.If every request shares the same large system prompt or context, cache it: subsequent calls pay ~10× less for that part.

resp = client.messages.create(
    model=MODEL, max_tokens=1024,
    system=[{"type": "text", "text": VELIKI_KONTEKST,
             "cache_control": {"type": "ephemeral"}}],   # keširaj prefiks
    messages=messages,
)
# proveri: resp.usage.cache_read_input_tokens  (koliko je pročitano iz keša)
resp = client.messages.create(
    model=MODEL, max_tokens=1024,
    system=[{"type": "text", "text": BIG_CONTEXT,
             "cache_control": {"type": "ephemeral"}}],    # cache the prefix
    messages=messages,
)
# check: resp.usage.cache_read_input_tokens  (how much was read from cache)

2 — Model po koraku2 — Model per step

  • Jak modelStrong modelPlaniranje i teško rasuđivanje.Planning and hard reasoning. Opus za korake gde tačnost odlučuje.Opus for steps where correctness decides.
  • Brz modelFast modelMehanički koraci.Mechanical steps. Haiku za klasifikaciju, kratke odgovore, rutinske pozive — brže i jeftinije.Haiku for classification, short answers, routine calls — faster and cheaper.
  • LatencijaLatencySvaki krug petlje je poziv mreži.Every loop round is a network call. Manje koraka i paralelizam (lekcija 10) su tvoje glavne poluge.Fewer steps and parallelism (lesson 10) are your main levers.
Najveća ušteda je često najprostija: ne koristiti agenta. Ako zadatak rešava jedan poziv modela ili obična funkcija, petlja sa alatima je samo skuplji i sporiji način da dobiješ isti rezultat.The biggest saving is often the simplest: don't use an agent. If a task is solved by a single model call or a plain function, a tool loop is just a costlier, slower way to get the same result.
Lekcija 12 · Kraj serijeLesson 12 · End of the series

Pusti ga u svetRelease it into the world

Imaš agenta koji dela na stvarnim podacima, izlaže alate preko MCP-a, radi iza servisa, i pazi na trošak. Ostaje da ga pustiš u svet — i da ga i tamo držiš na oku.You have an agent that acts on real data, exposes tools over MCP, runs behind a service, and watches cost. What remains is to release it — and keep an eye on it there too.

  • DeployDeployKontejner, promenljive okruženja, HTTPS.A container, environment variables, HTTPS. Ključ ide u menadžer tajni, nikad u sliku ni u git.The key goes in a secrets manager, never in the image or git.
  • MonitoringMonitoringTrag iz 2. dela, ali u produkciji.The trace from Part 2, but in production. Loguj svaki korak, prati trošak i latenciju po zahtevu, digni alarm kad skoče.Log every step, track cost and latency per request, alert when they spike.
  • Evali u CIEvals in CIEval skup iz 2. dela pre svakog deploya.The eval set from Part 2 before every deploy. Tako izmena prompta ili modela ne obori stopu uspeha a da ne primetiš.So a prompt or model change can't drop the success rate unnoticed.
Cela serija u jednoj rečenici: agent je petlja (1. deo), koju memorija, ograde, nadzor i evali drže na nogama (2. deo), a pravi alati, MCP i servis pretvaraju u proizvod (3. deo). Model je isti; razlika je sve što sagradiš oko njega.The whole series in one sentence: an agent is a loop (Part 1), which memory, guardrails, observability, and evals hold upright (Part 2), and which real tools, MCP, and a service turn into a product (Part 3). The model is the same; the difference is everything you build around it.

A da li petlju uopšte pišeš sam?But do you even write the loop yourself?

Kroz celu seriju petlju smo pisali rukom, i to je bilo namerno — dok je ne sklopiš sam, svaki gotov alat ti je crna kutija. Ali u pravom projektu retko krećeš od praznog fajla. Postoje tri sprata gotovih rešenja, i biraš po tome koliko posla hoćeš da zadržiš kod sebe.Throughout this series we wrote the loop by hand, and that was deliberate — until you've assembled it yourself, every ready-made tool is a black box. But on a real project you rarely start from an empty file. There are three tiers of ready-made options, and you pick by how much of the work you want to keep.

Tool runnerTool runnerpetlja, tvoji alatithe loop, your tools

Deo istog anthropic SDK-a. Ti napišeš samo funkcije alata, on vodi ciklus traži → izvrši → vrati. Nema gotovih alata i ne hostuje ništa umesto tebe — sve iz 2. dela (ograde, trag, kočnice) i dalje je tvoj posao, samo ne pišeš while petlju.Part of the same anthropic SDK. You write only the tool functions; it drives the ask → run → return cycle. No built-in tools and it hosts nothing for you — everything from Part 2 (guardrails, tracing, brakes) is still your job, you just don't write the while loop.

Claude Agent SDKgotov harnessa full harness

Zasebna biblioteka — Claude Code upakovan kao alat za programere. Dobijaš gotove alate (čitanje i pisanje fajlova, bash, pretraga), celu petlju, upravljanje kontekstom, dozvole i podagente. Ti i dalje sam hostuješ i deployuješ. Najbolji izbor kad ti treba agent koji radi nad kodom ili fajl-sistemom.A separate library — Claude Code packaged for developers. You get built-in tools (file read/write, bash, search), the whole loop, context management, permissions, and subagents. You still host and deploy it yourself. The best choice when you need an agent that works over code or a filesystem.

Upravljani agentiManaged agents

Anthropic vrti i petlju i sandboks u kom se alati izvršavaju. Ti šalješ konfiguraciju agenta, ne držiš infrastrukturu. Ovde odustaješ od najviše kontrole — i dobijaš najmanje posla oko 7. i 9. lekcije ovog dela (servis, stanje, konkurentnost).Anthropic runs both the loop and the sandbox where tools execute. You send an agent config and hold no infrastructure. This is where you give up the most control — and get the least work around lessons 7 and 9 of this part (the service, state, concurrency).

Ono što si naučio ne propada ni u jednom od tri slučaja. Verifikacija, ograde, trag i evali nisu deo petlje koju gotov alat piše umesto tebe — to su odluke koje i dalje donosiš ti, samo na višem spratu. Zato je redosled ovakav: prvo sklopi petlju rukom da razumeš šta se dešava, pa uzmi gotovo rešenje kad primetiš da pišeš isti kod po treći put. Aktuelne pakete i imena proveri u zvaničnoj dokumentaciji — ovaj sprat se menja najbrže.Nothing you've learned goes to waste in any of the three. Verification, guardrails, tracing, and evals aren't part of the loop a ready-made tool writes for you — they're decisions you still make, just one floor up. That's why the order is this way: first assemble the loop by hand so you understand what's happening, then reach for something ready when you notice you're writing the same code for the third time. Check the current packages and names in the official documentation — this floor changes fastest.

Šta da uradiš sledećeWhat to do next

  • Zameni pytest pravim alatomSwap pytest for a real tool iz lekcije 02 — daj agentu API koji ti stvarno treba.from lesson 02 — give the agent an API you actually need.
  • Napravi MCP serverBuild an MCP server iz lekcije 05 i dodaj ga u svoj Claude Code — koristićeš ga svakodnevno.from lesson 05 and add it to your own Claude Code — you'll use it daily.
  • Umotaj agenta u FastAPI servisWrap the agent in a FastAPI service iz lekcije 07, sa stanjem po korisniku i autentikacijom.from lesson 07, with per-user state and authentication.
  • Izmeri trošak i latencijuMeasure cost and latency pre nego što optimizuješ — pa uključi keširanje tek gde stvarno boli.before you optimize — then turn on caching only where it actually hurts.
Bezbedno pokretanjeRunning it safely Ovaj kod pokreće HTTP servis, zove spoljne API-je i koristi tvoj ključ.This code runs an HTTP service, calls external APIs, and uses your key. Pročitaj fajlove pre pokretanja, radi u venv-u, ključ drži u promenljivoj okruženja, a servis ne izlaži na internet bez autentikacije i ograda iz 2. dela.Read the files before running, work in a venv, keep the key in an environment variable, and don't expose the service to the internet without authentication and the Part 2 guardrails.

Vraćaš se na zoranmaric.com, ili ponovo na Agentsku petlju i Agenta bez nadzora. Sav kod sve tri serije je na GitLab-u: gitlab.com/webmaric/tutorials — kod za ovaj deo je u folderu deo-3-agent-u-stvarnom-svetu/.Head back to zoranmaric.com, or revisit The Agent Loop and The Unattended Agent. All the code for all three parts is on GitLab: gitlab.com/webmaric/tutorials — this part's code is in the deo-3-agent-u-stvarnom-svetu/ folder.