Vier Bausteine zu einem KI-Bestellagenten. Rund 55 Min.: Schritt 0 ~5, dann 10 · 10 · 15 · 15.
Du steuerst einen KI-Coding-Agenten und siehst zu, was er baut. Alles läuft in ~/projects/bpm-pizza-vibecoding; die Zugangsdaten stehen in ~/.env und sind fest in den Rechner eingebacken — sie sind schon da. Der Trainer hat die Maschinentests bereits laufen lassen: Hier wird nichts heruntergeladen, nichts angelegt und nichts konfiguriert. Secrets tippst du nirgendwo ab.
.env — So könnte sie aussehen
# LLM gateway (OpenAI-compatible) LITELLM_PIZZA_URL=https://litellm.example.com/v1 LITELLM_PIZZA_KEY=example-not-a-real-key # PizzaSim API PIZZASIM_URL=https://www.aipizzasim.com PIZZASIM_API_KEY=example-not-a-real-key PIZZERIA_ID=
.env, Secrets und nanoDeine eigene Datei ansehen:
nano ~/.env
Gelöst wird primär per Vibe Coding: Du öffnest Claude Code (oder Codex) in WSL und fügst einen Prompt ein. Du führst keine vorbereiteten Skripte aus — du steuerst einen KI-Coding-Agenten und beobachtest, was er erzeugt.
Das Projektverzeichnis liegt bereits auf dem Rechner, du legst nichts an. ~/.env steht eine Ebene über ~/projects. Jeder Codeblock und jeder Prompt verweist nur auf Variablennamen, und jedes Kommando nennt ~/.env mit vollem Pfad — damit ist es egal, in welchem Verzeichnis du gerade stehst.
PIZZERIA_ID steht bewusst ohne Wert da — die trägst du in Stufe 0c ein. Hinter dem = darf kein Kommentar stehen: Beim Kopieren landet ein solcher Kommentar als Wert in der Datei, und source versucht anschließend, den Resttext als Kommando auszuführen.
Der Trainer hat .env vor der Session auf jedem Rechner angelegt und getestet. Du musst nichts prüfen — wenn du magst, bestätigt simbox-test in Stufe 0a das noch einmal.
.env bearbeiten (nano)Wenn eine Aufgabe dich einen Wert in .env eintragen oder ändern lässt, nutze den Terminal-Editor nano — im WSL-Terminal, also demselben Fenster, in dem du auch claude startest (WSL öffnen: Windows-Start → „Ubuntu“ tippen). Der volle Pfad steht im Befehl, du kannst ihn also von überall ausführen:
nano ~/.env
Speichern mit Strg+O, Enter — beenden mit Strg+X (die Befehle stehen unten im Editor). Danach die Umgebung neu laden — mit demselben Ladekommando wie in Stufe 0a. Ohne dieses erneute Laden kennt die laufende Shell den geänderten Wert nicht.
Zugang & Pizzeria-Zuordnung, ~5 Min. Gearbeitet wird im vorbereiteten Klon ~/projects/bpm-pizza-vibecoding — du legst nichts an und installierst nichts. Drei Schritte, jeder lehrt ein Konzept. Im Workshop gibt es vier Team-Pizzerien: Elena, Giulia, Giovanni, Roberto. Welche davon deine ist, steht auf dem Aufkleber an deinem Monitor — das ist die verbindliche Quelle.
cd ~/projects/bpm-pizza-vibecoding
set -a; source ~/.env; set +a
Das lädt die Umgebung in die aktuelle Shell — oder lädt sie erneut, falls das schon geschehen ist; erneutes Source ist unschädlich. Die Curl-Befehle weiter unten (0b) brauchen sie.
Optional: Der Trainer hat die Maschinentests auf diesem Rechner bereits ausgeführt. Wenn du sie selbst noch einmal laufen lassen willst:
simbox-test
Erwartet: keine Fehler — nichts rot, kein FAILED und kein ERROR.
set -a tutKein WSL-Fenster mehr offen? Zurück hinein — in PowerShell oder der Eingabeaufforderung:
wsl --list --verbose # what exists wsl -d pizza-sim # enter wsl # enter, if it is the WSL default
Die Login-Shell startet dich bereits in ~/projects — der erste Schritt bringt dich sicher in den richtigen Unterordner, egal wo du gerade bist.
Eine .env-Datei wird von bash nicht automatisch gelesen — sie ist eine Konvention für Programme, nicht für die Shell. set -a sorgt dafür, dass alle beim Einlesen gesetzten Variablen exportiert werden. Dies ist die einzige Stelle in der Aufgabe, an der geladen wird — merke dir das Kommando: nach jedem neuen Terminal und nach jeder Änderung an .env erneut ausführen.
Konzept: ein API-Aufruf und seine Antwortstruktur.
curl -fsS "$PIZZASIM_URL/pizzerias" -H "X-API-Key: $PIZZASIM_API_KEY" | jq
Erwartetes Ergebnis: Ein JSON-Objekt mit einem pizzerias-Array; im Workshop siehst du die vier vorbereiteten Team-Pizzerien (jede Zeile mit id, name und weiteren Feldern).
Dasselbe noch einmal, aber auf das Wesentliche gefiltert — Name und ID je Zeile. Diese Liste brauchst du gleich in Stufe 0c:
curl -fsS "$PIZZASIM_URL/pizzerias" -H "X-API-Key: $PIZZASIM_API_KEY" \ | jq -r '.pizzerias[] | "\(.name) \(.id)"'
Erwartetes Ergebnis: Vier Zeilen, jeweils Pizzeria Elena, Pizzeria Giulia, Pizzeria Giovanni oder Pizzeria Roberto, gefolgt von der zugehörigen UUID. Notiere dir die Zeile, deren Name zum Aufkleber an deinem Monitor passt.
GET /menu ist öffentlich — kein Key nötig. Die /pizzerias-Routen verlangen den Header X-API-Key, wenn der Server mit gesetztem API-Key läuft. PIZZASIM_API_KEY ist dieser Server-Key vom Trainer — er ist nicht die Pizzeria-ID. Welche Pizzeria gemeint ist, kommt immer aus dem URL-Pfad: /pizzerias/$PIZZERIA_ID/…, nie aus einem Header.
Vor dem Ausführen: Ersetze im Prompt unten den Platzhalter PIZZERIANAME durch den Namen vom Aufkleber an deinem Monitor.
Steht PIZZERIANAME beim Start noch wortwörtlich im Prompt, bricht der Agent sofort ab und sagt dir, dass du zuerst deinen Namen eintragen musst — er rät nicht und setzt keinen Standardnamen ein. Genau das steht so im Prompt-Text; bearbeite die Befehlszeile trotzdem VOR dem Ausführen.
claude --dangerously-skip-permissions -p "Set PIZZERIA_ID= in ~/.env for PIZZERIANAME. If you still see the literal placeholder PIZZERIANAME in this prompt, STOP immediately and tell the user to replace it with their actual pizzeria name (from the label on their monitor) before running this again - do not guess or default to any name. Resolve the id via GET \$PIZZASIM_URL/pizzerias with header X-API-Key: \$PIZZASIM_API_KEY, both read from ~/.env. If those variables are already set in your environment, find the file that defines them - never create an env file this task describes as pre-existing. Then test with the API and read the last 10 orders."
Claude gibt während der Arbeit nichts aus — das kann eine Minute dauern. Warte auf das Ergebnis, der Befehl hängt nicht.
Und jetzt Codex — nicht dieselbe Aufgabe noch einmal, sondern die Gegenprobe: Ein zweiter, unabhängiger Agent prüft nach, ob der erste seine Aufgabe richtig erledigt hat. Genau dieses Muster — einer arbeitet, ein anderer prüft:
Auch hier: Ersetze PIZZERIANAME im Prompt unten durch deinen Namen — sonst bricht auch dieser Agent ab, statt zu raten.
codex exec --skip-git-repo-check --sandbox workspace-write -c sandbox_workspace_write.network_access=true "Verify the PIZZERIA_ID that is in ~/.env is for PIZZERIANAME. If you still see the literal placeholder PIZZERIANAME in this prompt, STOP immediately and tell the user to replace it with their actual pizzeria name before running this again - do not guess or default to any name. Read PIZZERIA_ID from ~/.env, resolve the expected id via GET \$PIZZASIM_URL/pizzerias with header X-API-Key: \$PIZZASIM_API_KEY, both read from ~/.env, and compare the two values. Change nothing - this is a check, not a task. Answer with a clear verdict: VERIFIED if they match, otherwise name exactly what is wrong."
Erwartet: Ein Urteil, keine zweite Eintragung — VERIFIED, wenn die ID in ~/.env zu deinem Team gehört, sonst die Aussage, was genau nicht stimmt.
Konzept: Ein KI-Agent lässt sich auch von außen steuern — als einmaliger Auftrag direkt aus der Shell, ohne interaktive Sitzung. Genau das ist claude -p: ein One-Shot-Prompt. Der Agent bekommt den Auftrag, erledigt ihn und beendet sich wieder. Den Gegensatz dazu siehst du gleich in Stufe 1: Dort startest du den Agenten und gibst deinen Prompt innerhalb der laufenden Sitzung ein. Diese beiden Aufrufarten zu unterscheiden ist der Lernpunkt dieser Stufe. Das nennt man Headless: Der Agent nimmt den Auftrag von der Kommandozeile entgegen, arbeitet, gibt ein Ergebnis aus und beendet sich — ohne Sitzung, ohne Nachfragen. Wichtig ist das, weil alles, was von der Kommandozeile aufrufbar ist, auch aus einem Skript oder einer Pipeline aufrufbar ist: der Schritt von „ich chatte mit einer KI“ zu „ein KI-Schritt läuft in einem Prozess“.
--sandbox workspace-write lässt Codex überhaupt schreiben, -c sandbox_workspace_write.network_access=true lässt es ins Netz — ohne das zweite Flag scheitert der API-Aufruf schon an der Namensauflösung, und der Agent sucht minutenlang nach einem Ausweg. Bei Claude schaltet --dangerously-skip-permissions die Rückfragen ab: Ein One-Shot hat niemanden, der sie beantwortet, also bliebe der Lauf bei der ersten Frage stehen. Vertretbar ist das hier, weil dieser Rechner eine vorbereitete Wegwerf-Umgebung ist, die jederzeit neu aufgesetzt werden kann — auf deinem eigenen Arbeitsrechner würdest du das nicht tun. Dahinter steht ein Gedanke, und der ist der Lernpunkt dieser Stufe: Die Grenzen eines Agenten setzt der, der ihn aufruft.
Erwartetes Ergebnis: Der ausführende Agent — der claude-One-Shot — meldet am Ende, dass er die ID ermittelt und in ~/.env geschrieben hat, und zeigt die Bestellungen, die er danach abgefragt hat. Der Wortlaut ist bei jedem Agenten und jedem Lauf anders — prüfe, dass eine ID geschrieben wurde und dass Bestellungen zurückkamen, nicht den Text. Ein erfolgreicher Lauf sah ungefähr so aus (gekürzt, hier für Roberto — bei dir steht dort dein Team vom Aufkleber):
Updated ~/.env with the correct PIZZERIA_ID. Here's what happened: - Fetched pizzerias list — found 4: Elena, Giulia, Giovanni, and Roberto. - Set PIZZERIA_ID to 86517183-…-e0db8f8b2131 in ~/.env (it was empty before). API test successful — last 10 orders from Pizzeria Roberto: # Customer Status Created (epoch) Delivered At 1 Beatrice delivered 1779573960 1779574503 2 Sabrina delivered 1779573060 1779573255 3 Andrea delivered 1779572220 1779572787 … All 10 are delivered.
Prüfen — nicht dem Agenten glauben, sondern der Datei:
grep '^PIZZERIA_ID=' ~/.env
Erwartetes Ergebnis: PIZZERIA_ID= gefolgt von einer UUID. Danach das Ladekommando aus Stufe 0a noch einmal ausführen — Schreiben in .env aktualisiert die laufende Shell nicht, und ohne dieses erneute Laden schlägt Stufe 3 später ohne erkennbare Ursache fehl.
Agenten sind nicht deterministisch — manchmal fragen sie nach, manchmal schreiben sie an die falsche Stelle. Das ist kein Grund steckenzubleiben: Nimm aus der gefilterten Liste in Stufe 0b die UUID zu deinem Namen, öffne die Datei und trage sie ein — direkt hinter das =, ohne Leerzeichen und ohne Kommentar dahinter:
nano ~/.envDie Zeile lautet danach
PIZZERIA_ID= plus deine UUID. Speichern, schließen, Ladekommando aus Stufe 0a ausführen, grep von oben wiederholen — und weiter mit Stufe 1.
Jetzt der Gegensatz zu Stufe 0c: Du startest Claude Code und gibst den Prompt innerhalb der laufenden Sitzung ein — nicht über claude -p. Diese beiden Aufrufarten zu unterscheiden ist der Lernpunkt dieser Stufe. Im Arbeitsverzeichnis aus Stufe 0a eingeben:
claude
Sieht dann ungefähr so aus — entscheidend ist die letzte Zeile und das Eingabefeld: Du bist jetzt im Agenten, nicht mehr in der Shell.
╭─── Claude Code ───────────────────────────────╮ │ Welcome back! │ │ ~/projects/bpm-pizza-vibecoding │ ╰───────────────────────────────────────────────╯ ─────────────────────────────────────────────── ❯ ─────────────────────────────────────────────── ⏸ manual mode on
Zweite Aufgabe in diesem Baustein, jetzt wo Claude läuft: sieh dir die lokalen Skills an. Unter .claude/skills/ liegen drei: pizza-api (Endpunkte und der genaue Bestell-Body), devils-advocate (Kritiker-und-Richter-Review für automatisierte Läufe) und python-basics (Hausregeln für den Code hier). Claude Code zieht sie von selbst heran — du musst sie nicht nennen, und du siehst nicht, wann es passiert.
Frag deinen laufenden Agenten:
Welche lokalen Skills gibt es hier und was tun sie?
Etwa so:
This project has 3 local skills defined in .claude/skills/:
1. devils-advocate (/devils-advocate)
Purpose: An optional reviewed build process for exercise 4. Uses Codex as a second harness to score a plan against a fixed rubric before implementation.
Key points:
- Runs after the plain build is finished
- Codex evaluates plans on completeness, correctness, error handling, simplicity, and spec_compliance
- Requires a TOTAL >= 20 and no single dimension below 4 for APPROVED verdict
- Max 3 rounds of review
- Review output goes to reviews/ directory
---
2. pizza-api (/pizza-api)
Purpose: Interface with the pizza simulation API — credentials, endpoints, and security rules.
Key points:
- Credentials live in ~/.env (never commit them)
- Important variables: PIZZASIM_URL, PIZZASIM_API_KEY, PIZZERIA_ID, LITELLM_PIZZA_URL, LITELLM_PIZZA_KEY
- Key travels in X-API-Key header
- Main endpoints: GET /menu, GET /pizzerias, POST /pizzerias/{id}/orders, GET /pizzerias/{id}/orders/{order-id}
- Never print or log the API key
---
3. python-basics (/python-basics)
Purpose: House rules for Python code in this repository.
Key points:
- No credentials in source — read from environment at runtime
- HTTP calls go through curl (shell out via subprocess, don't use requests or urllib)
- Standard library plus curl covers everything; use a virtualenv if you absolutely need a package
- Report actual status codes and response bodies when calls fail
Und dann prüf die Antwort selbst: öffne .claude/skills/ und lies eine der Dateien. Skills sind lesbare Dateien im Repo, die der Agent lädt — keine Magie. Was der Agent über sich selbst sagt, ist eine Auskunft; die Datei ist der Beleg.
Du darfst eigene Skills dazulegen — auch fremde von aussen. Zwei Dinge dazu: Du holst damit fremden Code auf diese Maschine, also sieh nach, woher er kommt. Und alles, was du hier anlegst, ist beim nächsten Zurücksetzen der Maschine weg — genau das macht das Ausprobieren billig.
git push ist verboten. Lokal committen darfst du, so viel du willst — aber nichts verlässt diese Maschine. Dieses Repo klonen alle vier Teams: Ein Push würde die Aufgabe unter den anderen drei verändern, während sie daran arbeiten.
Widersprechen sich ein Skill und diese Seite, gilt diese Seite. Die Skills sind Arbeitsmaterial und können hinterherhinken; was hier steht, ist geprüft.
Diese Sitzung fragt bei jedem Tool-Aufruf nach — das ist manual mode, und du spürst gleich, was das Flag aus Stufe 0c dir abgenommen hat. Mit Shift+Tab wechselst du den Modus in der laufenden Sitzung, ohne sie zu verlassen: unter anderem nach auto mode, wo das Modell jeden Aufruf selbst auf riskante Aktionen prüft und Harmloses durchlässt, und nach bypass permissions, wo gar nichts mehr gefragt wird. Wichtig dabei: auto mode nimmt die Frage nicht weg, sondern übergibt sie dem Modell — und laut Anbieter kann das Modell sich irren, weshalb er den Einsatz nur in isolierten Umgebungen empfiehlt.
| Rolle | Dialog |
|---|---|
| Agent | „Willkommen bei Roberto's Pizzeria! Was darf es sein?“ |
| Kunde | „Was habt ihr denn?“ |
| Agent | Ruft get_menu auf, liest die Karte vor, empfiehlt eine Pizza + eine Pasta |
Prompt — in die laufende Claude-Code-Sitzung einfügen:
Read LITELLM_PIZZA_URL, LITELLM_PIZZA_KEY, CLAUDE_MODEL, CODEX_MODEL and PIZZASIM_URL from the file ~/.env in the home directory (not the current working directory, and do not create a new one). Make every HTTP request by running curl through subprocess and parsing its output - never urllib, never requests. Write chat.py that talks to $LITELLM_PIZZA_URL/chat/completions with Bearer $LITELLM_PIZZA_KEY. Send the value of CLAUDE_MODEL as the "model" field. If a request fails with a model-related error (HTTP 400/404, "no healthy deployments", "model not found", rate limit or credit exhaustion), retry the same request once with CODEX_MODEL and print which model you switched to and why. Do not fall back on other errors - report those and stop. Never hardcode, guess or substitute a model name anywhere in the file; if both variables are missing or empty, report the error and stop. Define one tool "get_menu": GET $PIZZASIM_URL/menu (public, no API key), returning the JSON body. Pass it in the "tools" array of the request. Then run the full tool-calling loop for the question "What do you have?": 1. first request -> the assistant message containing tool_calls 2. execute get_menu 3. second request with the tool result appended to the messages 4. the final recommendation Print every step as it happens: the customer question, which model is in use, the raw tool_calls array, the tool result message you send back, and the final answer. Run it and show me the output.
Erwartet: Das Modell empfiehlt eine Pizza und eine Pasta, die es auf der Karte wirklich gibt — und du hast den Tool-Aufruf roh gesehen.
Das siehst du im Output: nicht nur die Antwort, sondern jede Erlaubnisfrage davor. Der ganze Ablauf in drei Teilen:
1. Vorschlag, chat.py anzulegen — das Skript könnte etwa so aussehen, nicht Wort für Wort garantiert:
#!/usr/bin/env python3
"""Chat completion with tool calling for pizza menu."""
import json
import os
import subprocess
def get_env_var(name):
"""Read environment variable from ~/.env file. None if missing."""
result = subprocess.run(
["grep", f"^{name}=", os.path.expanduser("~/.env")],
capture_output=True,
text=True
)
if result.returncode != 0:
return None
return result.stdout.strip().split("=", 1)[1]
def curl_post(url, headers, data):
"""Send a POST request through curl. Returns (status_code, parsed_body)."""
cmd = ["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url]
for key, value in headers.items():
cmd.extend(["-H", f"{key}: {value}"])
cmd.extend(["-d", json.dumps(data)])
result = subprocess.run(cmd, capture_output=True, text=True)
body, _, status = result.stdout.rpartition("\n")
return int(status), json.loads(body)
def curl_get(url, headers=None):
"""Make a GET request using curl."""
cmd = ["curl", "-s", url]
if headers:
for key, value in headers.items():
cmd.extend(["-H", f"{key}: {value}"])
result = subprocess.run(cmd, capture_output=True, text=True)
return json.loads(result.stdout)
def get_menu():
"""Get the pizza menu. Public endpoint, no API key needed."""
url = f"{os.environ['PIZZASIM_URL']}/menu"
return curl_get(url)
def is_model_error(status_code, body):
"""Model-related failure: bad/unavailable model, not our fault."""
if status_code in (400, 404, 429):
return True
text = json.dumps(body).lower()
return any(s in text for s in (
"no healthy deployments", "model not found", "credit", "rate limit"
))
def chat_completion(chat_url, headers, payload, primary_var, primary_model, fallback_var, fallback_model):
"""POST with one retry: on a model-related error, switch model once."""
status, body = curl_post(chat_url, headers, {**payload, "model": primary_model})
if status < 300:
return body
if not is_model_error(status, body):
print(f"Request failed (HTTP {status}), not a model-related error. Stopping.")
print(json.dumps(body, indent=2))
raise SystemExit(1)
if not fallback_model:
print(f"Request failed (HTTP {status}) and no fallback model is configured. Stopping.")
raise SystemExit(1)
print(f"{primary_var} failed (HTTP {status}) - switching to {fallback_var} and retrying once.")
status, body = curl_post(chat_url, headers, {**payload, "model": fallback_model})
if status >= 300:
print(f"Retry with {fallback_var} also failed (HTTP {status}). Stopping.")
print(json.dumps(body, indent=2))
raise SystemExit(1)
return body
def main():
litellm_url = get_env_var("LITELLM_PIZZA_URL")
litellm_key = get_env_var("LITELLM_PIZZA_KEY")
pizzasim_url = get_env_var("PIZZASIM_URL")
claude_model = get_env_var("CLAUDE_MODEL")
codex_model = get_env_var("CODEX_MODEL")
# CLAUDE_MODEL is primary; if it is empty, CODEX_MODEL becomes the only model
# tried. Only stop when BOTH are empty or missing.
if not claude_model and not codex_model:
print("Neither CLAUDE_MODEL nor CODEX_MODEL is set in ~/.env. Stopping.")
raise SystemExit(1)
primary_var, primary_model = ("CLAUDE_MODEL", claude_model) if claude_model else ("CODEX_MODEL", codex_model)
fallback_var, fallback_model = ("CODEX_MODEL", codex_model) if claude_model else (None, None)
os.environ["PIZZASIM_URL"] = pizzasim_url
chat_url = f"{litellm_url}/chat/completions"
user_message = "What do you have?"
print(f"Customer asked: {user_message}")
print(f"Model in use: {primary_var}")
print()
headers = {
"Authorization": f"Bearer {litellm_key}",
"Content-Type": "application/json"
}
tools = [
{
"type": "function",
"function": {
"name": "get_menu",
"description": "Get the pizza menu items available",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
payload = {
"messages": [{"role": "user", "content": user_message}],
"tools": tools,
"tool_choice": "auto"
}
response = chat_completion(chat_url, headers, payload, primary_var, primary_model, fallback_var, fallback_model)
message = response["choices"][0]["message"]
tool_calls = message.get("tool_calls", [])
print(f"Raw tool_calls array: {json.dumps(tool_calls, indent=2)}")
print()
if not tool_calls:
print("No tool calls made.")
return
# Process tool calls
messages = [
{"role": "user", "content": user_message}
]
assistant_message = {
"role": "assistant",
"content": message.get("content", ""),
"tool_calls": tool_calls
}
messages.append(assistant_message)
tool_results = []
for tool_call in tool_calls:
function_name = tool_call["function"]["name"]
print(f"Tool call: {function_name}")
if function_name == "get_menu":
result = get_menu()
print(f"Tool result: {json.dumps(result, indent=2)}")
tool_results.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": function_name,
"content": json.dumps(result)
})
print()
messages.extend(tool_results)
# Second chat completion with tool results
final_response = chat_completion(
chat_url, headers, {"messages": messages}, primary_var, primary_model, fallback_var, fallback_model
)
final_message = final_response["choices"][0]["message"]
print(f"Final answer: {final_message['content']}")
if __name__ == "__main__":
main()
Dann: „Datei erstellen?“ — Antwort: Yes (1 + Enter).
2. Der Agent fragt um Erlaubnis, es auszuführen: „python3 chat.py – Run chat.py to test chat completion with tool calling“ — Antwort: Yes (1 + Enter).
3. Der tatsächliche Output:
The script ran successfully. Here's the complete exchange:
1. Customer asked: "What do you have?"
2. Raw tool_calls array: The model decided to call get_menu with empty arguments {}
3. Tool call made: get_menu
4. Tool result: The full menu JSON containing 11 pizzas, 4 pasta dishes, and available toppings
5. Final answer: A friendly, formatted menu with all items listed with prices and ingredients
Erwartetes Ergebnis: Claude Code legt chat.py an, führt es aus, und im Terminal erscheint eine Antwort des Modells. Weil diese Sitzung im manual mode läuft, bestätigst du jeden Schritt selbst — das Schreiben der Datei und das Ausführen. Rechne mit mehreren Anläufen: Das Modell schreibt, führt aus, liest den Fehler und bessert nach. Genau diese Schleife ist die Arbeit eines Agenten — ihr zuzusehen ist der Sinn dieser Stufe.
Im Prompt oben hast du zwei Umgebungsvariablen benannt: CLAUDE_MODEL und CODEX_MODEL — das ist das Modellpaar, das dein Skript aufruft: zuerst CLAUDE_MODEL, und bei einem modellbedingten Fehler einmalig CODEX_MODEL als Rückfall. Claude Code selbst läuft aber ebenfalls auf einem Modell, und das ist keins von Anthropic: Claude Code und Codex sprechen beide über das OpenRouter-Gateway, vermittelt von LiteLLM, also über die API — und bewusst nicht auf den neuesten und teuersten Modellen, denn dies ist eine Schulung.
Welches Modell dein Harness gerade benutzt, steht bewusst nicht auf dieser Seite — das findest du selbst heraus, an einer von drei Stellen: grep MODEL ~/.env auf deinem Rechner, die Konfiguration des Harness selbst, oder die Liste /models des Gateways. Eine hier gedruckte Modellbezeichnung wäre schon morgen vielleicht falsch; die Nachschau ist es nie.
Ein Harness ist die Schleife um das Modell herum: Es ruft Werkzeuge auf, holt bei dir die Erlaubnis dafür ein, bearbeitet Dateien und hält den Gesprächsverlauf fest. Das Modell sind die Gewichte am anderen Ende eines HTTP-Aufrufs — es sieht nur Text und antwortet mit Text. Beide Hälften sind austauschbar, und deshalb sind es zwei Entscheidungen und nicht eine: welches Harness zu unserem Arbeitsablauf passt, und welches Modell zu Aufgabe, Budget und Datenschutz passt.
Sieh es dir an: derselbe Auftrag durch beide Harnesses — beide als One-Shot von außen, wie in Stufe 0c.
claude --dangerously-skip-permissions -p "Explain in one sentence what the file chat.py does."
codex exec --skip-git-repo-check --sandbox workspace-write "Explain in one sentence what the file chat.py does."
Erwartetes Ergebnis: Zwei Antworten auf denselben Auftrag — aus zwei verschiedenen Modellen, deren Namen du dir oben selbst herausgesucht hast. An deinem Vorgehen hast du dazwischen nichts geändert. Genau das ist der Punkt: Harness und Modell sind trennbar, und ein Harness muss nicht auf dem Modell seines eigenen Herstellers laufen.
Ein Modell nennt regelmäßig den Hersteller seines Harness oder dasjenige Modell, über das in seinen Trainingsdaten am meisten geschrieben wurde. Die Selbstauskunft ist kein Beleg. Verbindlich ist die Konfiguration: Die Zuordnung wird zentral gesetzt (CLAUDE_MODEL und CODEX_MODEL), nicht auf deinem Rechner — und genau die siehst du nach: grep MODEL ~/.env, die Konfiguration des Harness, oder die Liste /models des Gateways.
Ein Teil der hier eingesetzten Modelle sind Reasoning-Modelle: Sie verbrauchen einen Teil der Token für ihren Denkweg, bevor sichtbarer Text entsteht. Bei einem knappen Token-Limit kommt deshalb eine leere Antwort zurück, obwohl das Modell einwandfrei arbeitet — erst das Limit erhöhen, dann einen Fehler vermuten. Und wenn CLAUDE_MODELs Route mit einem modellbedingten Fehler aussteigt — zum Beispiel HTTP 402 bei erschöpftem Guthaben — springt das Skript aus Stufe 1 genau einmal auf CODEX_MODEL um: Deshalb trägt das Skript zwei konfigurierte Modelle statt eines — die Aufgabe hängt nicht an einer einzigen Route.
Dafür gibt es diese Aufgabe: Am Ende steht deine Bestellung im Dashboard deiner eigenen Pizzeria.
Öffne das Dashboard, klicke dann in deine eigene Pizzeria hinein — die mit dem Namen vom Aufkleber an deinem Monitor, demselben Namen hinter deiner PIZZERIA_ID aus Stufe 0c — und scrolle dort zur Tabelle Order History: dort stehen tausende abgeschlossene Bestellungen. Nimm einen Namen aus der Spalte Customer: Als der bestellst du gleich, und dein Agent trifft damit auf einen Kunden, den es in deiner Pizzeria wirklich gibt. Die Tabelle Current Orders darüber zeigt die laufenden Bestellungen — dort taucht deine eigene am Ende dieser Stufe auf.
Kunden sind pro Pizzeria über den Vornamen eindeutig. Nennst du dem Agenten einen Namen, den es in deiner Pizzeria noch nicht gibt, legt die API ihn beim Bestellen automatisch an — das ist kein Fehler, sondern das Verhalten der Schnittstelle. Wer den Weg über einen bestehenden Kunden sehen will, nimmt einen Namen aus der Bestellhistorie.
| Rolle | Dialog |
|---|---|
| Kunde | „Ich hätte gern zwei Pizza Regina und eine Pasta Aglio e Olio.“ |
| Agent | „Gern! Auf welchen Namen und an welche Adresse?“ |
| Kunde | „Matteo, via dei Giubbonari.“ |
| Agent | Fasst zusammen, wartet auf Bestätigung, ruft place_order auf → bestätigt Bestell-ID + Wartezeit |
Prompt — in die laufende Claude-Code-Sitzung einfügen:
Extend chat.py into an interactive terminal chat agent for our pizzeria.
Print each turn as it happens - what the customer said, your reply, any tool call - never only a final summary.
Read PIZZASIM_URL, PIZZASIM_API_KEY and PIZZERIA_ID from ~/.env.
Add a tool place_order: POST with curl through subprocess to
$PIZZASIM_URL/pizzerias/$PIZZERIA_ID/orders with header
"X-API-Key: $PIZZASIM_API_KEY" and a JSON body like
{"customer": {"first_name": "Marco", "street_one": "via di Cantieri"},
"items": [{"type": "pizza", "code": "margherita", "qty": 2, "extras": []}]}
The response contains order_id, status and eta_seconds.
No Python HTTP library anywhere - curl through subprocess only.
System prompt: you are a friendly ordering assistant. Greet first.
Call get_menu for menu questions. Collect the customer's first name
and street. Summarize the order and call place_order only after the
customer confirms. Confirm order_id and the wait time from
eta_seconds. Explain API errors in plain language.
Wie in Baustein 1 fragt der Agent um Erlaubnis, es auszuführen — Antwort: Yes (1 + Enter). Weil das Skript jetzt interaktiv nach Eingaben fragt, tippst du deine Antworten direkt in diesem Fenster ein, ohne eine zweite Shell zu öffnen:
python3 chat.py
Es begrüßt dich zuerst. Danach tippst du diese Zeilen ein, wenn danach gefragt wird — es gibt noch kein Frontend, dieses Fenster ist die Oberfläche:
Ich hätte gern zwei Pizza Regina und eine Pasta Aglio e Olio.
Matteo, via dei Giubbonari
Echter Output (gekürzt):
[SYSTEM] Getting greeting from agent...
[AGENT] Hi! Welcome to our pizzeria — what can I get you today?
[YOU] Ich hätte gern zwei Pizza Regina und eine Pasta Aglio e Olio.
[AGENT] Got it! And your first name and delivery street, please?
[YOU] Matteo, via dei Giubbonari
[AGENT] To confirm: 2x Pizza Regina, 1x Pasta Aglio e Olio for Matteo, via dei Giubbonari — shall I place it?
[YOU] Yes, please.
[RAW TOOL CALLS] [{"function": {"name": "place_order", "arguments": "{…}"}}]
[TOOL] Calling place_order with customer={'first_name': 'Matteo', …}, items=[…]
[TOOL] place_order returned: {"order_id": "3f9a21d4-…-7a01", "status": "ordered", "eta_seconds": 900}
[AGENT] All set! Order 3f9a21d4-…-7a01 is on its way — about 15 minutes.
place_order wird erst nach Bestätigung aufgerufenDie API verlangt nur customer.first_name und mindestens ein Item; street_one ist optional und wird nicht auf Lieferbarkeit geprüft. Die Adresse abzufragen ist eine Trainingsregel des Szenarios. Wer mehr will: Es gibt einen separaten öffentlichen Check GET /location?street=… — den kann dein Agent als drittes Tool einbauen (freiwillig).
Prompt — in die laufende Claude-Code-Sitzung einfügen:
Add web.py: FastAPI + uvicorn on port 8888, serving a chat page at / and the chat endpoint at POST /api/chat. Extract the agent loop from chat.py into agent.py so CLI and web share one implementation, and keep chat.py working. The header shows the pizzeria BY NAME, a selector filled by curl through subprocess from GET $PIZZASIM_URL/pizzerias with header "X-API-Key: $PIZZASIM_API_KEY" and from no other source, and a link to that pizzeria's dashboard. The footer shows a version number. A language switch offers German and English. Every API call still goes through curl in a subprocess, in agent.py and in the web path too - no Python HTTP library. One static HTML file, vanilla JS, no build step, no framework.
Erwartet: http://127.0.0.1:8888 zeigt eine Chat-Seite mit dem Namen deiner Pizzeria im Kopf. Du bestellst darüber und prüfst das Ergebnis über den Dashboard-Link daneben.
Prompt — in die laufende Claude-Code-Sitzung einfügen:
Harden web.py and agent.py so the page never lies: Every sent message ends in either a rendered reply or a rendered error - never in silence. Render tool calls and tool results as they stream, do not discard them. Show the basket with quantities and what it comes to. Explain errors in plain language, with no status codes.
Erwartet: Zieh absichtlich etwas ab — stopp den Agenten, nimm das Netz weg — und die Seite sagt in klaren Worten, was passiert ist. Kein Statuscode, kein stilles Nichts.
Dass dabei etwas schiefgeht, ist der Normalfall und nicht dein Fehler. Der Agent wird dir Fragen stellen und so lange stehenbleiben, bis du antwortest — genau dieses Hin und Her ist die Arbeit.
Die Referenzlösung liegt im Repo bpm-pizza-vibecoding-solution. Zwei Versions-Spezifikationen beschreiben sie: SPEC-RELEASE-1.0.0.md und SPEC-RELEASE-1.1.0.md, dazu SPEC-RELEASE-DIFF.md mit dem Unterschied zwischen beiden.
Ein KI-Agenten-Team wurde über Nacht auf eine dieser Spezifikationen losgelassen und hat daraus die Lösung gebaut. Ein echter One-Shot war das nicht: beide Spec-Versionen brauchten hinterher noch Nacharbeit, bis sie wirklich liefen. Der Prompt kommt weit, weil die Spezifikationen wirklich gut sind — fertig ist er damit nicht.
Wenn du magst, probiere denselben Prompt selbst aus — ersetze den Platzhalter <SPEC_FILE_PATH> durch den Pfad zu einer der beiden Spezifikationen (welche, ist dir überlassen):
/goal Build the entire project specified in <SPEC_FILE_PATH> and get it approved by an independent reviewer. FIRST, before anything else: mkdir -p decisions reviews DONE means all of the following hold AND each has been demonstrated in this conversation, because the evaluator reads only what you surfaced here: 1. The "Open contract questions" in <SPEC_FILE_PATH> are answered against the live API, the section is rewritten with the verified facts, and that change is committed. 2. PLAN.md exists and its review verdict is APPROVED (TOTAL >= 20, no dimension below 4). 3. Every deliverable listed in <SPEC_FILE_PATH> exists; you showed the tree. 4. The test suite runs and exits 0; you showed the output. 5. The implementation review verdict is APPROVED under the same rubric. 6. With SPEECH_URL unset, the web channel starts and places an order; you showed it. 7. reviews/scores.md holds every round, and git status is clean. ROLES: - You are the implementer. Nothing else writes code. - Codex is the reviewer and the oracle. It runs read-only and never writes code: codex exec --skip-git-repo-check --sandbox read-only "REQUEST" Run "codex exec --help" once first; flags differ between versions. WORKING RULES: - Never ask me anything. Nobody is awake. There is no human until morning. - When blocked or uncertain, ask Codex instead of stopping. Append question and answer to decisions/qa.md, take the answer as the decision, continue. - Capture every review into reviews/, append scores to reviews/scores.md, commit reviews/. - Commit before every review and after every green test run. - Use the installed skills: writing-plans and executing-plans for stage 1, test-driven-development while implementing, systematic-debugging when something breaks, requesting-code-review and receiving-code-review around each round, verification-before-completion before you claim anything is done, subagent-driven-development where parallel work helps. - The models this build runs on have nothing to do with the models the product calls. Never wire the agent to your own model, and never let a build-time model name end up in source, config, prompts, tests or docs. - Do not read git history for the previous implementation at tag pre-spec-rebuild. - If the same failure repeats three times, stop retrying. Write it to decisions/blockers.md, ask Codex for a different route, take it. - End every turn with: which of the seven items hold, which do not, what you do next. - Stop after 60 turns even if not done, and summarise the state.
Zwei fertige Instanzen laufen bereits — spiel mit beiden herum:
Zum Schluss: öffne das Dashboard, such dort die richtige Pizzeria heraus und prüfe, ob deine Bestellung wirklich angekommen ist.