Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ jobs:
- run: python -m pip install -e '.[dev]'
- run: pytest -q
- run: ruff check .
- run: python -m agent_system.walkthrough
43 changes: 43 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__/
*.egg-info/
*.py[cod]
dist/
_site/
25 changes: 11 additions & 14 deletions LINKEDIN.md
Original file line number Diff line number Diff line change
@@ -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).
211 changes: 120 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

![Loop Agent cockpit](docs/cockpit.png)
**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.
[![CI](https://github.com/LobsterQBA/loop-agent/actions/workflows/ci.yml/badge.svg)](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.
[![Loop Agent: task, result, and expandable execution trace](docs/cockpit.png)](https://lobsterqba.github.io/loop-agent/)

## Run it in two minutes
<details>
<summary><strong>Watch the three examples (short step-through GIF)</strong></summary>

Requires Python 3.11+.
![Actual interface states: calculation, recall, and a failed calculation](docs/demo.gif)

```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).
</details>

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:
<details>
<summary><strong>Optional: connect a live model</strong></summary>

```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
</details>

This project deliberately does **not** include shell access, browser control, email, messaging,
calendar writes, or arbitrary filesystem tools.
<details>
<summary><strong>Optional: use the local JSON API</strong></summary>

- 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.
</details>

## 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).
2 changes: 1 addition & 1 deletion agent_system/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading