Weekend 2/Harness Engineering
/ Course 2 of the series

Harness Engineering

Evolve your POI agent into a deployed, self-running product.

Where you left off: in Weekend 1 you built a local Gemma pipeline that turns compliant data into confidence-scored POIs, and you shipped a web app to a VPS. This weekend you wrap that pipeline in a real harness — the scaffolding that makes a model a reliable agent — then put it online, running itself.

The seven harness layers — they light up as you build them
Loop Tools Memory & state Observability Evals-in-loop Guardrails Scheduling
Prep · before the weekend0%
Saturday · build the harness0%
Sunday · make it a product0%
Copy any code with the button. Tick boxes to fill the bars and light the harness layers. Guidance boxes explain every term. 📌 For later notes hold alternatives.
/ Pack 0

Before the weekend

~90 min. Mostly verifying Weekend 1 still works, plus a few new free accounts.

0.1 Carried over from Weekend 1 — verify

For the non-tech learner

You're building on what you already have. Just confirm each still works — don't rebuild.

0.2 New for Weekend 2 — install / sign up

Set your spend cap first — do this now

In the Claude Console, set a workspace spend limit of $5–10 below your tier cap. Most work runs locally on Gemma (free); only hard POIs escalate to Claude. The cap means escalation physically cannot overspend. This is the most important box on the page.

Alternatives (for later)
  • Database: self-hosted Postgres or SQLite on your VPS instead of Supabase.
  • Monitoring: self-hosted Uptime Kuma instead of Healthchecks.io.
  • Scheduler: cron instead of launchd (note: cron won't run if the Mac was asleep at the scheduled time).

The picture: from pipeline to product

For the non-tech learner

Left is what you have. Right is where you're going. Saturday adds the loop and tools in the middle; Sunday adds the database, API and UI on the right.

flowchart LR
  I[Ingest data] --> X[Gemma extract]
  X --> C[Confidence score]
  C --> O[(CSV / JSON)]
  O --> E[Accuracy eval]
      

Weekend 1: a straight line — a model with almost no harness.

/ Pack 1 · Saturday

Build the harness

Goal: an agent that re-checks its own work, recovers from crashes, passes an eval gate, and respects hard limits.

1.1 The agent loop

For the non-tech learner

A harness is all the code around the model that makes it a reliable agent — the loop, tools, memory, retries, guardrails, checks. (Claude Code is a harness.) The agent loop is plan → act → observe → repeat, where the model picks the next action and decides when it's done. Anthropic's rule: use the simplest thing that works, so we only loop the one step that benefits — re-checking low-confidence POIs — and leave the bulk as a fixed pipeline.

flowchart TD
  P[Plan: pick next action] --> A[Act: call a tool]
  A --> O[Observe: read the result]
  O --> D{Done, or limit hit?}
  D -->|No| P
  D -->|Yes| END[Stop and report]
      
# agent_loop.py — the smallest real loop
MAX_STEPS = 3   # a guardrail (see 1.6)

def recheck_poi(poi):
    for step in range(MAX_STEPS):
        if open("STOP", "r").read().strip() == "1" if __import__("os").path.exists("STOP") else False:
            break  # kill switch
        action = decide_next_action(poi)      # model plans
        if action["type"] == "done":
            return action["poi"]
        result = run_tool(action)             # you act
        poi = observe(poi, result)            # feed result back
    return poi  # ran out of steps — keep best effort
Alternatives (for later)
  • Agent frameworks (LangGraph, etc.) give you loops + state for free. Worth it once your hand-written loop gets complex — not before.

1.2 Tool use, done properly

For the non-tech learner

Tool use / function calling: you describe tools (a name, a description, and parameters as a schema); when the model needs one, it outputs a structured request; your code runs it and returns the result. The model never runs code itself — you stay in control. Give the POI agent: a geocoder lookup, a database query, and a re-check.

sequenceDiagram
  participant M as Model
  participant Y as Your code
  participant T as Tool / API
  M->>Y: request geocode(address)
  Y->>T: run the tool
  T-->>Y: result or error
  Y-->>M: here is the result
  M->>M: continue reasoning
      
from pydantic import BaseModel

class GeocodeArgs(BaseModel):
    address: str   # the address to look up

# Tool definition the model reads (name + description + schema)
GEOCODE_TOOL = {
    "type": "function",
    "function": {
        "name": "geocode_address",
        "description": "Look up coordinates for an address via OpenStreetMap Nominatim.",
        "parameters": GeocodeArgs.model_json_schema(),
    },
}
# Give tools to Ollama:  ollama.chat(model=..., messages=..., tools=[GEOCODE_TOOL])
Compliance: Nominatim has hard limits

Max 1 request per second, set a real custom User-Agent (stock ones are blocked), and cache results. For scheduled/bulk use it's stricter still. Never scrape Facebook — stay on OSM / Overture / Foursquare-OS as in Weekend 1.

1.3 Handle tool failures

For the non-tech learner

Tools fail — timeouts, rate limits, bad data. Two rules: (1) never let a tool failure crash the run — return a readable error the model can react to; (2) retry with backoff (wait longer each try) but always cap the retries.

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import requests

@retry(stop=stop_after_attempt(3),
       wait=wait_exponential(multiplier=1, min=1, max=10),
       retry=retry_if_exception_type(requests.RequestException))
def geocode_address(address: str) -> dict:
    r = requests.get("https://nominatim.openstreetmap.org/search",
                     params={"q": address, "format": "json", "limit": 1},
                     headers={"User-Agent": "weekend2-poi-agent/1.0 (you@example.com)"},
                     timeout=10)
    r.raise_for_status()
    return r.json()[0] if r.json() else {}

1.4 Memory & state — survive a crash

For the non-tech learner

State = what's done and what's left. A checkpoint saves it so a crashed job can resume instead of restarting. Idempotency means running a step twice is the same as once — so resuming never duplicates work (key on the record's id).

flowchart TD
  S[Start or re-run] --> L[Load checkpoint]
  L --> F{Pending records left?}
  F -->|Yes| P[Process one record]
  P --> SV[Mark done in checkpoint]
  SV --> F
  F -->|No| DONE[Batch complete]
      
import json, os

CKPT = "checkpoint.json"

def load(): return json.load(open(CKPT)) if os.path.exists(CKPT) else {}
def save(state): json.dump(state, open(CKPT, "w"))

def run(records):
    state = load()                       # resume-aware
    for rec in records:
        if state.get(rec["id"]) == "done":
            continue                     # idempotent skip
        process(rec)                     # your work
        state[rec["id"]] = "done"
        save(state)                      # checkpoint after each unit
Try it

Run it, kill it halfway (Ctrl-C), run again — it should skip what's done and finish. That's resumability.

1.5 Observability — see WHY

For the non-tech learner

An agent is a black box until you make it talk. Structured logging = machine-readable log lines (JSON) you can search. A trace / transcript = the full step-by-step record of one run. This is the most underrated skill: when the agent does something weird, the trace is how you find out why. Start with plain file logs — no new service needed.

import logging, json

logging.basicConfig(filename="agent.log", level=logging.INFO,
                    format="%(message)s")

def log_decision(poi_id, step, decision, confidence, tokens=0):
    logging.info(json.dumps({
        "poi_id": poi_id, "step": step, "decision": decision,
        "confidence": confidence, "tokens": tokens,
    }))
# Later:  grep poi_id agent.log   to replay one POI's whole journey
Alternatives (for later)
  • Graduate to Langfuse (open-source, free tier), LangSmith, Phoenix, or Weave when log volume justifies a real trace viewer. Instrument once with OpenTelemetry-style traces, switch backends later.

1.6 Evals-in-the-loop — make it a gate

For the non-tech learner

Weekend 1's one-off accuracy check becomes a gate: output is only published if the eval passes a threshold (e.g. F1 ≥ 0.85). Two kinds: capability evals ("what can it do?", start low) and regression evals ("does it still do what it used to?", aim near 100%). Build the threshold from real past failures, and read the transcripts before blaming the agent.

flowchart LR
  B[Batch output] --> EV[Run regression eval]
  EV --> Q{F1 above threshold?}
  Q -->|Yes| PUB[Publish to database]
  Q -->|No| QU[Quarantine and alert]
      
def eval_gate(pred_csv, golden_csv, threshold=0.85):
    precision, recall, f1 = evaluate(pred_csv, golden_csv)  # from Weekend 1
    passed = f1 >= threshold
    print(f"F1={f1:.2f}  gate={'PASS' if passed else 'FAIL'}")
    return passed   # if False, quarantine the batch instead of publishing

1.7 Guardrails — hard limits for an autonomous loop

For the non-tech learner

An agent that loops and spends money needs hard limits so a bug can't run forever or run up a bill. Layer them: max iterations, a per-run cost budget that aborts, max_tokens caps, rate limits, a kill switch, and the platform spend cap you set in Prep.

import os

class Guardrails:
    def __init__(self, max_steps=3, cost_budget=1.00):
        self.max_steps, self.cost_budget = max_steps, cost_budget
        self.steps, self.cost = 0, 0.0
    def check(self):
        if os.path.exists("STOP"): raise SystemExit("kill switch")
        if self.steps >= self.max_steps: raise SystemExit("max steps")
        if self.cost  >= self.cost_budget: raise SystemExit("over budget")
    def spend(self, dollars): self.cost += dollars
    def tick(self): self.steps += 1; self.check()

Saturday capstone

What "done" looks like

Run the full agent on a real batch and confirm all five: a forced crash resumes cleanly; low-confidence POIs trigger tool-backed re-checks; the eval gate blocks bad output; a guardrail demonstrably trips; and you can open the trace and narrate one POI's journey end-to-end.

/ Pack 2 · Sunday

Make it a product

Goal: the agent's reviewed results live on your subdomain, refreshing on a schedule. Building this is the lesson — you're using the very ship-a-product skills the course teaches.

2.1 A real database

For the non-tech learner

A database is a typed spreadsheet that programs read and write. A table = a sheet, a row = a record, a schema = the column definitions. Supabase gives you a managed Postgres database on a free tier (and auto-generates an API). Point your agent's output here instead of a CSV.

create table pois (
  id          text primary key,
  name        text,
  category    text,
  lat         double precision,
  lon         double precision,
  confidence  real,
  status      text default 'pending',   -- pending | done | needs_review
  source      text,
  updated_at  timestamptz default now()
);
Alternatives (for later)
  • Self-hosted Postgres or a single-file SQLite on your VPS if you want full control and no third party.

2.2 A small API

For the non-tech learner

An API is a URL your code exposes; ask it and get structured data (JSON) back. FastAPI is a friendly Python way to build one, and it auto-creates a test page at /docs so you can click instead of code.

from fastapi import FastAPI
app = FastAPI()

@app.get("/pois")
def list_pois(min_confidence: float = 0.0):
    rows = db_query("select * from pois where confidence >= %s", (min_confidence,))
    return rows                      # FastAPI returns it as JSON

@app.post("/pois/{poi_id}/correct")
def correct(poi_id: str, name: str, category: str):
    db_exec("update pois set name=%s, category=%s, status='done' where id=%s",
            (name, category, poi_id))
    return {"ok": True}
# run it:  uvicorn main:app --reload    then open http://localhost:8000/docs

2.3 A review UI (human-in-the-loop)

For the non-tech learner

Reuse your Weekend 1 web skills: a page that lists POIs, highlights the low-confidence ones, and lets a human fix them. Corrections write back through the API — and feed your golden set, so the evals from Saturday keep improving. Let Claude Code scaffold this for you.

<!-- review.html (sketch) -->
<script>
async function load() {
  const pois = await (await fetch("/pois?min_confidence=0")).json();
  // render rows; flag confidence < 0.7 in coral
}
async function correct(id, name, category) {
  await fetch(`/pois/${id}/correct?name=${name}&category=${category}`, {method:"POST"});
}
load();
</script>

2.4 Containerize with Docker

For the non-tech learner

Docker packages your app + everything it needs into a portable box (a container) that runs the same on your Mac and on the VPS — killing "works on my machine" bugs. The recipe is a Dockerfile.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Gotcha

Your Mac is ARM; many VPSs are x86. Build for the server: docker build --platform linux/amd64 -t poi-api .

2.5 Auto-deploy with GitHub Actions

For the non-tech learner

CI/CD = every git push automatically builds and deploys — no manual SSH. GitHub Actions builds the image, pushes it to a registry, then SSHes into your VPS to restart the container. The VPS is a runtime server, not a build server. Store all secrets in GitHub Secrets, never in the file.

flowchart LR
  G[git push] --> GA[GitHub Actions]
  GA --> BLD[Build Docker image]
  BLD --> REG[Push to registry]
  REG --> SSH[SSH to VPS]
  SSH --> UP[docker compose up -d]
  UP --> HC[Health check]
      
name: deploy
on: { push: { branches: [main] } }
jobs:
  ship:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /opt/poi-api
            git pull && docker compose up -d --build
            curl -fsS https://your.subdomain/pois >/dev/null  # health check

2.6 Schedule it + monitor + cost alarm

For the non-tech learner

Make the agent run itself. On a Mac, launchd is the modern scheduler (it even runs on wake if a scheduled time was missed while asleep — cron won't). A dead-man's-switch alerts you when the job fails to ping on success — the best way to catch silent failures. Plus the spend cap from Prep is your cost alarm.

<!-- ~/Library/LaunchAgents/com.you.poiagent.plist -->
<plist version="1.0"><dict>
  <key>Label</key><string>com.you.poiagent</string>
  <key>ProgramArguments</key>
  <array><string>/path/.venv/bin/python</string><string>/path/run_batch.py</string></array>
  <key>StartCalendarInterval</key><dict><key>Hour</key><integer>3</integer></dict>
</dict></plist>

# load it:  launchctl load ~/Library/LaunchAgents/com.you.poiagent.plist
# at the end of run_batch.py, on success:
#   requests.get("https://hc-ping.com/YOUR-UUID")   # dead-man's-switch

The finished product

flowchart TD
  SCH[Scheduled agent<br/>launchd] --> DB[(Database<br/>Supabase Postgres)]
  DB --> API[FastAPI service]
  API --> NG[nginx + HTTPS<br/>your subdomain]
  NG --> UI[Review UI]
  UI -->|corrections| DB
  DB -.feeds.-> GS[Golden set / evals]
      
Sunday capstone — what "done" looks like

Your subdomain serves reviewed POIs over HTTPS; a git push auto-deploys; the scheduled run pings the dead-man's-switch; the cost alarm is armed. That's a real self-running product.

/ Pack 3

Reference cards

Keep these open while you work.

Command cheat-sheet

PYTHON / API
  python3 -m venv .venv && source .venv/bin/activate
  uvicorn main:app --reload          run the API + /docs

DOCKER
  docker build --platform linux/amd64 -t poi-api .
  docker run -p 8000:8000 poi-api
  docker compose up -d --build

DEPLOY / OPS
  git push                           triggers GitHub Actions deploy
  launchctl load ~/Library/LaunchAgents/com.you.poiagent.plist
  launchctl list | grep poiagent     is it scheduled?
  echo 1 > STOP                       kill switch on the agent loop

Troubleshooting

SymptomFirst thing to try
Model returns tool calls as plain texttemperature 0 + Pydantic structured output, or a tool-tuned model
Agent loops forever / costs spikecheck max-steps + cost budget; create the STOP file
Resume re-does finished workcheckpoint isn't saving, or you're not keying on id
Nominatim 403 / blockedset a real custom User-Agent; stay ≤1 req/sec; cache
Eval gate keeps failingread the transcripts before changing prompts
Docker image won't run on VPSbuild for linux/amd64
Scheduled job never runslaunchd plist syntax/permissions; test a trivial job first
Supabase project pausedfree tier auto-pauses after 7 idle days; the daily run keeps it warm

Glossary

Harness / scaffold
all the code around the model that makes it a reliable agent
Agent loop
plan → act → observe → repeat, with the model deciding the next step
Workflow vs agent
you hardcode the steps / the model chooses the steps
Tool use / function calling
giving the model functions it can request; your code runs them
Tool schema
the structured description (name, params) the model reads to call a tool
State / checkpoint
a saved record of progress so a crash can resume
Idempotency
doing a step twice equals doing it once — no duplicate work
Observability
being able to see why the agent did something, via logs + traces
Trace / transcript
the full step-by-step record of one agent run
Eval gate
a rule that blocks output unless the eval passes a threshold
Capability vs regression eval
"what can it do?" (starts low) vs "does it still?" (near 100%)
Guardrail
a hard limit (max steps, cost cap, kill switch) that stops a runaway agent
Retry / backoff
re-trying a failed call, waiting longer each time, capped
Database / table / row / schema
structured storage; a typed sheet; a record; the columns
API / endpoint / JSON
a URL returning structured data; one such URL; the data format
FastAPI / uvicorn
a Python API framework; the server that runs it
Docker / container / Dockerfile
packaging tech; a running instance; the build recipe
CI/CD / GitHub Actions
auto build+deploy on push; GitHub's tool for it
launchd / cron
macOS's modern scheduler; the older Unix one
Dead-man's-switch
a monitor that alerts when an expected "I'm alive" ping fails to arrive

Go deeper (official first)

Building Effective AgentsEffective harnesses for long-running agents Demystifying evals for AI agentsClaude tool-use docs Anthropic AcademyFastAPI docsSupabase docs Docker get-startedGitHub Actions docs Ollama tools / structured outputsApple launchd Nominatim usage policytenacityLangfuseHealthchecks.io
A note on accuracy

Prices, free tiers, and model tool-calling quality change month to month. Confirm Claude pricing, Supabase/GitHub free-tier limits, and your model's tool support on the official pages before you rely on them.