AI Agents·8 min read·

What an AI Agent Is: The Observe-Plan-Act Loop

Turn a chat model into a working agent on a Linux VPS by implementing the observe-plan-act loop, tool calls, and a stop condition you can audit.

NB

Netbay Engineering

Netbay Engineering

On this page

A chat model answers a prompt and stops. An agent keeps going. It looks at the current state, decides what to do next, calls a tool, reads the result, and repeats until a stop condition fires. That cycle is the observe-plan-act loop. Everything else in the agent literature — frameworks, memory stores, evaluation harnesses — is an elaboration of those three steps. If you can write the loop on a Linux VPS and log each turn, you understand agents. If you cannot, a framework will hide the failure until production.

This post stays at that core. No product pitch. The model lives behind an HTTPS API you already pay for. The loop, the tools, and the logs live on your box in Lucknow.

Chat Completions Are Not Agents

A completion is one round trip: messages in, one assistant message out. That is useful for drafting a nginx snippet or explaining a journalctl line. It is not an agent. The moment you need the model to list a directory, read a unit file, and then decide whether to restart a service, you need a loop that feeds tool results back as new observations.

The distinction matters because vendors blur it. Anything with a system prompt gets called an agent. In operations work the test is simple: can the program change the world, see the new world, and change it again without you pasting the output back in? If not, it is a chatbot with extra tokens.

The Three Steps, Named Honestly

Observe means: collect the latest facts the model is allowed to see. That can be the user request, the last tool result, a systemd status line, or a truncated transcript. It is not a mystical perception module. It is the messages array plus whatever you append after each tool call.

Plan means: the model emits either a final answer or one or more structured tool calls. You do not parse free-form "I will now run ls". You parse a JSON tool call the API already validated against a schema. If the model cannot produce a valid call, that is a failed plan, not a creative one.

Act means: your process runs the chosen tool in a sandbox you defined — HTTP to an API, a read of a file, an allowlisted command — then returns a string observation. The model never holds a shell. Your code does.

Those three steps run until you hit a terminal state: a final message with no tool calls, a step budget, a wall-clock timeout, or an explicit abort from a human.

A Loop You Can Read in One Screen

The following Python runs on Ubuntu with the stdlib plus whatever HTTPS client you already use. It talks to an OpenAI-compatible chat API. Tools are a dict of name to callable. The model never sees your API key.

python
import json
import os
import urllib.request

API_URL = os.environ["MODEL_API_URL"]
API_KEY = os.environ["MODEL_API_KEY"]
MAX_STEPS = 8

def chat(messages, tools):
    body = json.dumps({
        "model": os.environ.get("MODEL_NAME", "gpt-4.1-mini"),
        "messages": messages,
        "tools": tools,
    }).encode()
    req = urllib.request.Request(
        API_URL,
        data=body,
        headers={
            "Authorization": "Bearer " + API_KEY,
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.loads(resp.read().decode())

def run_agent(user_text, tools, dispatch):
    messages = [
        {"role": "system", "content": "You operate a Linux VPS. Use tools. Stop when done."},
        {"role": "user", "content": user_text},
    ]
    for step in range(MAX_STEPS):
        data = chat(messages, tools)
        msg = data["choices"][0]["message"]
        messages.append(msg)
        calls = msg.get("tool_calls") or []
        if not calls:
            return msg.get("content") or "", messages
        for call in calls:
            name = call["function"]["name"]
            args = json.loads(call["function"]["arguments"] or "{}")
            result = dispatch[name](args)
            messages.append({
                "role": "tool",
                "tool_call_id": call["id"],
                "content": result[:8000],
            })
    raise RuntimeError("step budget exceeded")

Read it top to bottom. Observe is the messages list. Plan is the chat() call. Act is dispatch[name](args). The step budget is the only thing standing between a confused model and an infinite bill.

Stop Conditions Are Part of the Design

Teams forget to define done. The model will happily emit another tool call forever if the prompt says "keep investigating". Cap steps. Cap tokens. Cap wall clock. Treat a final assistant message with no tool_calls as success only if it cites the last observation, not a guess.

Log every turn as one JSON object: step index, tool name, argument hash, duration, truncated result, finish reason. That log is the agent. Without it you cannot debug why it restarted nginx twice.

Run the process under systemd as a non-root user. Give it High-Speed SSD for logs, not a tmpfs that vanishes on reboot. The VPS does not need a GPU; the model is remote. Intel Xeon Platinum cores are plenty for JSON parsing, HTTPS, and a few child processes.

Run the Loop as a User Service

Treat the agent like any other daemon. Dedicated user, EnvironmentFile for the API key, WorkingDirectory the user owns, StandardOutput=journal so loop panics are greppable apart from the JSONL traces. Restart=on-failure is fine if the budget file is on disk; it is not fine if the only cap lived in RAM.

bash
sudo useradd --system --home /var/lib/ops-agent --shell /usr/sbin/nologin opsagent
sudo mkdir -p /var/lib/ops-agent/runs
sudo chown -R opsagent:opsagent /var/lib/ops-agent
sudo chmod 700 /var/lib/ops-agent
sudo -u opsagent python3 /opt/ops-agent/loop.py --goal "report nginx state"

That is the hosting story. The box does JSON and HTTPS. The model stays off-box. If you cannot name the Unix user and the log directory, you do not have an agent in production. You have a notebook.

Observe-plan-act loop Observe messages + tool results Plan model emits tool calls Act your code runs tools Stop budget or answer tool result becomes the next observation Linux VPS loop + remote model API systemd user, JSONL traces, MAX_STEPS cap

Takeaway

An agent is a loop with tools and a budget, not a personality. Write the loop, log every turn, and refuse to run a tool the schema did not name. You can spin up an Ubuntu 24.04 instance on Netbay in Lucknow in under 60 seconds and follow along at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance