diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7fd7429..09f0f07 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,3 +23,4 @@ jobs:
- run: python -m pip install -e '.[dev]'
- run: pytest -q
- run: ruff check .
+ - run: python -m agent_system.walkthrough
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..af898ad
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,43 @@
+name: Deploy walkthrough
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - run: python -m pip install -e '.[dev]'
+ - run: pytest -q
+ - run: ruff check .
+ - run: python -m agent_system.walkthrough
+ - run: python -m agent_system.build_site
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: _site
+ deploy:
+ needs: build
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Deploy
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 34fd6b5..bb61bfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ __pycache__/
*.egg-info/
*.py[cod]
dist/
+_site/
diff --git a/LINKEDIN.md b/LINKEDIN.md
index d7aad74..ab96b73 100644
--- a/LINKEDIN.md
+++ b/LINKEDIN.md
@@ -1,20 +1,17 @@
-# LinkedIn launch draft
+# Loop Agent — project description draft
-I wanted to understand what is actually inside an AI agent, so I built a small one from first principles.
+Loop Agent is a small Python project for checking what happens inside an agent turn.
-**Loop Agent** has four parts:
+Ask it to calculate a value and remember it, expand the tool calls and observations, then restart
+the app and retrieve the saved fact. The default demo needs no API key; an optional function-calling
+model uses the same loop and tools.
-→ a readable reason–act–observe loop
-→ a registry of safe local tools
-→ durable SQLite memory
-→ a live trace that shows every step of a turn
+The design centers on a simple distinction: requesting an action is not evidence that it succeeded.
+One practical example is the failure path: a calculator error stays an error and is not saved as a result
+in demo mode.
-The demo runs without an API key, and the whole backend is plain Python. You can ask it to calculate something, save the result, restart it, and recall the memory later.
+Python · SQLite · local tools · expandable execution traces
-This was inspired by transparent agent projects such as Waku, but implemented from scratch as a smaller learning build. My goal was not to create another chatbot. It was to make the system behind one visible and understandable.
+Repository: https://github.com/LobsterQBA/loop-agent
-Code: **https://github.com/LobsterQBA/loop-agent**
-
-What is the smallest agent architecture you would still call useful?
-
-#AIEngineering #AIAgents #Python #BuildInPublic #LLMOps
+For resume wording and technical discussion prompts, see [Presenting Loop Agent](docs/presentation.md).
diff --git a/README.md b/README.md
index 43aa458..4bded74 100644
--- a/README.md
+++ b/README.md
@@ -1,147 +1,176 @@
# Loop Agent
-**A small, readable agent system: loop + tools + memory + trace.**
+**An agent you can inspect: what it called, what came back, and what it remembered.**
-Loop Agent is a from-scratch learning build for seeing what happens inside one agent
-turn. It is intentionally smaller than a framework and safer than a general-purpose
-computer-use agent. The default demo needs no API key.
+A small Python project by [Leo Zhao](https://github.com/LobsterQBA). Give it a task such as
+“calculate a number and remember it,” then expand the execution record to check the result.
+Restart the app and retrieve the saved fact from SQLite.
-
+**The design question:** how can a reader verify an agent's work instead of trusting its final answer?
+Loop Agent makes the execution record part of the product: tool arguments, success or failure,
+loop limits, and durable state are visible. The scope stays small enough to follow in source.
-## Why this exists
+**[Open the interactive walkthrough →](https://lobsterqba.github.io/loop-agent/)**
-Most agent demos show only the final answer. This one makes the mechanism visible:
+No installation. Three recorded Python runs: save a result, recall it after restart, and inspect a failure.
+The hosted page replays actual execution records; run locally to enter your own tasks.
-1. the model reasons about the task;
-2. it requests a registered tool;
-3. the tool returns structured data;
-4. the result goes back into working context;
-5. the loop repeats until the model replies;
-6. memory and the full trace persist in one SQLite file.
+[](https://github.com/LobsterQBA/loop-agent/actions/workflows/ci.yml)
-The architecture was inspired by readable agent projects such as
-[Waku](https://github.com/ShenSeanChen/waku-agent), but this repository was implemented from
-scratch with a narrower scope and no copied source.
+[](https://lobsterqba.github.io/loop-agent/)
-## Run it in two minutes
+
+Watch the three examples (short step-through GIF)
-Requires Python 3.11+.
+
-```bash
-git clone https://github.com/LobsterQBA/loop-agent.git
-cd loop-agent
-python -m agent_system
-```
+Captured from the interactive replay, with pauses between examples; not a real-time LLM recording.
-Open [http://127.0.0.1:8787](http://127.0.0.1:8787).
+
-Try:
+## Choose your depth
-> Calculate 17 × 23 and remember the result as launch score.
+| Time | Start here | What you will see |
+| --- | --- | --- |
+| 30 seconds | This page | The problem, working example, and engineering choices |
+| 3 minutes | [Run the demo](#try-it-locally) | A calculation, saved memory, and a checkable execution record |
+| 5 minutes, no setup | [Annotated walkthrough](docs/walkthrough.md) | Expand each step, including a failure case |
+| 10 minutes | [Architecture and tradeoffs](docs/architecture.md) | Source links, data flow, limits, and what would change for production |
-Then restart the server and ask:
+## Try it locally
-> What do you remember about launch score?
+Requires **Python 3.11+** and Git. The default demo has **no dependencies, no API key, and no model charges**.
+Use `python` instead of `python3` if that is your Python 3.11+ command.
-The answer survives because `.agent-mini/state.db` is the source of truth.
+```bash
+git clone https://github.com/LobsterQBA/loop-agent.git
+cd loop-agent
+python3 -m agent_system
+```
+
+Open [localhost:8787](http://127.0.0.1:8787), then:
-## Local API contract
+1. Run the prefilled instruction: **Calculate 17 × 23 and remember the result as launch score.**
+ Expect **391**, **2 tool calls**, and **3 planner calls**.
+2. Expand **Inspect recorded data** under a tool call and its observation. Compare the requested
+ expression, returned number, and saved value. **Download turn JSON** exports that completed turn.
+3. Stop the server with **Ctrl+C**, start it with the same command from the same directory, then click
+ **recall memory** and run it. Expect `launch score: 391`; the database survived the restart.
+4. Click **try a failure** and run it. Division by zero returns an error, and no result is saved.
-The cockpit calls a local JSON API, which is also useful when trying the agent from a script. Requests must use `Content-Type: application/json`; other media types receive HTTP 415.
+No browser? Run the same core flow, including a restart check, in an isolated temporary database:
```bash
-curl -X POST http://127.0.0.1:8787/api/run \
- -H 'Content-Type: application/json' \
- -d '{"message":"Calculate 8 * 9","mode":"demo"}'
+python3 -m agent_system.walkthrough
```
-`message` must be a non-empty string of at most 2,000 characters. Invalid requests return
-HTTP 400 before an agent turn, model call, or trace entry is created. The only supported modes
-are `demo` and `live`; `live` additionally requires `AGENT_API_KEY` and `AGENT_MODEL`.
+It exits with a nonzero status if an expected behavior fails. It does not touch your saved app state.
+See the [walkthrough](docs/walkthrough.md) for expected output and troubleshooting.
+
+## What this demonstrates
+
+| Engineering choice | Why it matters | Evidence |
+| --- | --- | --- |
+| A readable tool loop | Separate a requested action from its observed result | [Loop](agent_system/agent.py), expandable UI trace |
+| A deterministic demo and an optional LLM adapter | Make the project reproducible before adding model variability | [Adapters](agent_system/models.py), [walkthrough](agent_system/walkthrough.py) |
+| Explicit SQLite memory | Show what persists; don't pretend a new turn remembers the conversation | [Store](agent_system/memory.py), restart check |
+| Structured tool errors | A failed calculation must not become a saved result | [Regression tests](tests/test_agent.py) |
+| A bounded tool surface | Explore agent control with four local functions | [Registry and calculator](agent_system/tools.py) |
+
+The default planner uses rules, **not an LLM**. It runs real tools and writes real SQLite records.
+Live mode uses the same loop with an OpenAI-compatible function-calling model. The trace records
+calls and results; it does **not** expose private model reasoning. It appears after the turn completes,
+not as a live stream.
-## The four pieces
+## How a turn works
```mermaid
flowchart LR
- UI[Local cockpit] --> LOOP[Agent loop]
- LOOP --> MODEL[Model]
- MODEL -->|tool request| TOOLS[Safe local tools]
- TOOLS -->|observation| MODEL
- MODEL -->|final reply| UI
- TOOLS --> DB[(SQLite memory)]
- LOOP --> TRACE[Step-by-step trace]
- TRACE --> UI
+ U[Instruction] --> L[Bounded loop]
+ L --> M[Demo planner or LLM]
+ M -->|Tool request| T[Registered local function]
+ T -->|Observed result| L
+ T <-->|Remember / recall| D[(SQLite)]
+ M -->|Final text| R[Reply]
+ R --> P[Persist turn and trace]
+ P --> V[Expandable execution record]
```
-| Piece | What it does | Main file |
-| --- | --- | --- |
-| Loop | reason → act → observe, with a hard iteration limit | `agent_system/agent.py` |
-| Tools | calculator, local time, remember, recall | `agent_system/tools.py` |
-| Memory | durable facts and a ledger of turns | `agent_system/memory.py` |
-| Trace | records every decision and renders it in the cockpit | `agent.py` + `static/app.js` |
-
-Read [`docs/architecture.md`](docs/architecture.md) for the turn lifecycle and constraints.
+Each iteration asks the planner/model what to do next. A tool result becomes input to the next
+iteration. A text reply ends the loop; a six-iteration budget prevents indefinite repetition.
+A single iteration can request multiple tools, so this is not a six-tool-call or dollar-cost cap.
-## Demo mode and Live mode
+[Read the architecture](docs/architecture.md) for the full lifecycle, source map, and limitations.
-**Demo mode** is the default. It uses a small deterministic planner so the repository works
-immediately and the tool loop is reproducible. It makes no model request.
-
-**Live mode** is optional. It uses an OpenAI-compatible function-calling model:
+
+Optional: connect a live model
```bash
-python -m venv .venv
+python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[live]'
cp .env.example .env
-# Add AGENT_API_KEY and AGENT_MODEL to .env
-agent-mini
+# Set AGENT_API_KEY and AGENT_MODEL in .env
+loop-agent
```
-The key stays in the Python process and is never sent to the browser. In live mode, the model
-provider receives the instruction, working messages, tool schemas, and tool results. Do not put
-sensitive data into a hosted model unless its data policy fits your use case.
+On Windows, activate with `.venv\Scripts\activate`. Set `AGENT_BASE_URL` only if using another
+OpenAI-compatible endpoint. Select **Live** in the app after restarting the server.
+The API key stays on the server. The provider receives the instruction, tool schemas, and tool results;
+provider fees apply. Demo tests do not establish live-model quality or provider compatibility.
-## Safety boundary
+
-This project deliberately does **not** include shell access, browser control, email, messaging,
-calendar writes, or arbitrary filesystem tools.
+
+Optional: use the local JSON API
-- The server binds to `127.0.0.1`.
-- Calculator expressions are parsed with a restricted AST, never `eval`.
-- Only registered functions can be called.
-- Tool exceptions become structured observations instead of crashing the loop.
-- Every turn has a maximum of six model iterations.
-- Local runtime data and secrets are gitignored.
+```bash
+curl -X POST http://127.0.0.1:8787/api/run \
+ -H 'Content-Type: application/json' \
+ -d '{"message":"Calculate 8 * 9","mode":"demo"}'
+```
+
+The response includes `reply`, `trace`, `iterations`, `tool_calls`, `mode`, `model`, and `turn_id`.
+`GET /api/status` describes configuration; `GET /api/memory` returns up to 20 recent memories and
+8 recent turn summaries. These are limited lists, not lifetime totals.
+
+Requests require JSON (otherwise HTTP 415), a nonempty string of at most 2,000 characters, and mode
+`demo` or `live`. Invalid input returns HTTP 400 before a turn is created. Unconfigured live mode
+returns HTTP 409. State lives at `.agent-mini/state.db`, relative to the launch directory, unless
+`AGENT_HOME` is set.
-This is a learning and portfolio project, not a production security boundary.
+
## Verify it
```bash
-python -m venv .venv
+python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
pytest -q
ruff check .
+python -m agent_system.walkthrough
```
-The tests cover the arithmetic sandbox, durable memory, multi-tool looping, iteration guardrail,
-and local HTTP API.
+CI runs the deterministic tests and walkthrough on Python 3.11 and 3.12. Tests cover multi-tool
+execution, restart persistence, failed calculations, iteration exhaustion, restricted arithmetic,
+and HTTP input validation. They do not benchmark LLM accuracy, latency, or production throughput.
-## Project map
+## Hosting and presentation
-```text
-agent_system/
- agent.py # one complete agent turn
- models.py # deterministic demo + optional live adapter
- tools.py # registry and four safe tools
- memory.py # SQLite persistence
- server.py # localhost API + static cockpit
- static/ # framework-free interface
-tests/ # deterministic behavior and API tests
-```
+The [GitHub Pages walkthrough](https://lobsterqba.github.io/loop-agent/) is generated from real demo
+turns in fresh processes. It serves static assets and no API keys or visitor state.
+[Build and deployment details](docs/hosting.md) · [Resume and interview notes](docs/presentation.md).
+
+## Scope and next decisions
+
+This is a local portfolio project, not a hosted service. There is no shell, browser, messaging,
+or arbitrary-file tool. The server binds to localhost; it has no authentication or multi-user isolation.
+Memory writes and the final trace are separate database transactions, so a failed turn can leave
+partial effects. A provider failure can happen before a trace is saved.
-## License
+The next engineering priority would be durable failure records and explicit turn status, followed
+by live-model evaluation against task-specific criteria. See [the tradeoffs](docs/architecture.md#tradeoffs-and-next-decisions).
-MIT
+Architecture inspiration: [Waku](https://github.com/ShenSeanChen/waku-agent). This repository was
+implemented from scratch with a smaller scope. [MIT license](LICENSE).
diff --git a/agent_system/agent.py b/agent_system/agent.py
index 3576ed4..7bf492d 100644
--- a/agent_system/agent.py
+++ b/agent_system/agent.py
@@ -81,7 +81,7 @@ def emit(kind: str, title: str, detail) -> None:
iterations = 0
for iteration in range(1, self.max_iterations + 1):
iterations = iteration
- emit("reason", f"Reason · iteration {iteration}", {"model": self.model.name})
+ emit("reason", f"Model call · iteration {iteration}", {"model": self.model.name})
model_reply = self.model.complete(messages, self.tools.schemas())
if not model_reply.tool_calls:
diff --git a/agent_system/build_site.py b/agent_system/build_site.py
new file mode 100644
index 0000000..2059248
--- /dev/null
+++ b/agent_system/build_site.py
@@ -0,0 +1,98 @@
+"""Build a static, explicitly recorded walkthrough from real demo executions."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import shutil
+import subprocess
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+from agent_system.walkthrough import run_turn
+
+MESSAGES = [
+ "Calculate 17 * 23 and remember the result as launch score.",
+ "What do you remember about launch score?",
+ "Calculate 1 / 0 and remember the result as invalid score.",
+]
+
+
+def build_site(destination: Path) -> None:
+ source = Path(__file__).parent / "static"
+ destination.mkdir(parents=True, exist_ok=True)
+ for name in ("app.js", "style.css"):
+ shutil.copyfile(source / name, destination / name)
+ html = (source / "index.html").read_text()
+ html = html.replace('
', '')
+ html = html.replace('href="/style.css"', 'href="./style.css"')
+ html = html.replace('src="/app.js"', 'src="./app.js"')
+ start = html.index('
+
Recorded walkthrough. These are real Python runs, recorded at build time;
+ it does not run an AI model or save your data. Select an example below.
+
+ Three recorded examples
+
+
+''' + html[end:]
+ html = html.replace('