From 1dc9fc78949e259007c02b57c9c0f34bdb70adb0 Mon Sep 17 00:00:00 2001
From: LobsterQBA <101247207+LobsterQBA@users.noreply.github.com>
Date: Tue, 8 Sep 2026 19:57:37 -0700
Subject: [PATCH 1/2] Make Loop Agent inspectable through a guided public
walkthrough
---
.github/workflows/ci.yml | 1 +
.github/workflows/pages.yml | 43 ++
.gitignore | 1 +
LINKEDIN.md | 25 +-
README.md | 211 ++++----
agent_system/agent.py | 2 +-
agent_system/build_site.py | 98 ++++
agent_system/models.py | 28 +-
agent_system/static/app.js | 136 ++++-
agent_system/static/index.html | 154 ++++--
agent_system/static/style.css | 918 +++++++++++++--------------------
agent_system/walkthrough.py | 85 +++
docs/architecture.md | 142 ++++-
docs/cockpit.png | Bin 204623 -> 41274 bytes
docs/demo.gif | Bin 0 -> 214341 bytes
docs/hosting.md | 39 ++
docs/mobile.png | Bin 33933 -> 39763 bytes
docs/presentation.md | 34 ++
docs/walkthrough.md | 159 ++++++
tests/test_agent.py | 19 +
tests/test_site.py | 23 +
21 files changed, 1338 insertions(+), 780 deletions(-)
create mode 100644 .github/workflows/pages.yml
create mode 100644 agent_system/build_site.py
create mode 100644 agent_system/walkthrough.py
create mode 100644 docs/demo.gif
create mode 100644 docs/hosting.md
create mode 100644 docs/presentation.md
create mode 100644 docs/walkthrough.md
create mode 100644 tests/test_site.py
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.
+
+ Run it locally ↗
+ Read the design ↗
+
+ Three recorded examples
+
+
+''' + html[end:]
+ html = html.replace('', '
')
+ html = html.replace('''',
+ ''' ''')
+ for label, replacement in (
+ ("recall memory", "2. recall after restart"),
+ ("calculate + remember", "1. calculate + remember"),
+ ("try a failure", "3. try a failure"),
+ ):
+ html = re.sub(r">\s*" + re.escape(label) + r"\s*",
+ f">{replacement}", html)
+ html = html.replace('>Instruction', '>Recorded instruction')
+ html = html.replace('Recorded calls and results, shown after the turn completes.',
+ 'Recorded calls and results from the selected example.')
+ html = html.replace('What survives the turn', 'Memory after this recorded turn')
+ html = html.replace('Waiting for a task. The trace will reveal every step.',
+ 'Choose an example, then show its recorded turn. Expand each step to check the data.')
+ html = html.replace('Run a task, then expand each step to inspect its recorded data.',
+ 'Choose an example to inspect its recorded calls and results.')
+ html = html.replace('Ask the agent to remember something. It will appear here and remain after restart.',
+ 'Show a recorded turn to see its SQLite snapshot. This page does not store visitor data.')
+ (destination / "index.html").write_text(html)
+ (destination / ".nojekyll").touch()
+ try:
+ commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
+ except (OSError, subprocess.CalledProcessError):
+ commit = "local build"
+ examples = []
+ turns = []
+ with TemporaryDirectory(prefix="loop-agent-site-") as folder:
+ database = Path(folder) / "state.db"
+ for message in MESSAGES:
+ result = run_turn(database, message)
+ turns.append({"id": result["turn"]["turn_id"]})
+ examples.append({
+ "message": message,
+ "turn": result["turn"],
+ "memory": {"memories": result["memories"], "turns": list(turns)},
+ })
+ (destination / "examples.json").write_text(
+ json.dumps({"commit": commit, "examples": examples}, ensure_ascii=False, indent=2)
+ )
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--output", type=Path, default=Path("_site"))
+ args = parser.parse_args()
+ build_site(args.output)
+ print(f"Recorded walkthrough built at {args.output}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/agent_system/models.py b/agent_system/models.py
index f2e2e59..321aabf 100644
--- a/agent_system/models.py
+++ b/agent_system/models.py
@@ -93,7 +93,12 @@ def complete(self, messages: list[dict], tools: list[dict]) -> ModelReply:
if wants_memory and "remember" not in results:
if "calculate" in results:
calculated = results["calculate"][-1]
- value = str(calculated.get("result", calculated.get("error", "unknown")))
+ if not calculated.get("ok"):
+ return ModelReply(
+ text=f"Calculation failed: {calculated.get('error')}. "
+ "I did not save a result."
+ )
+ value = str(calculated["result"])
else:
value = re.sub(
r"^(please\s+)?(remember|save)(\s+that)?\s+",
@@ -117,13 +122,22 @@ def complete(self, messages: list[dict], tools: list[dict]) -> ModelReply:
summaries = []
for tool_name, entries in results.items():
latest = entries[-1]
- if latest.get("ok"):
- summaries.append(f"{tool_name} → {latest.get('result')}")
+ if not latest.get("ok"):
+ summaries.append(f"{tool_name} failed: {latest.get('error')}.")
+ continue
+ result = latest.get("result")
+ if tool_name == "calculate":
+ summaries.append(f"{expression} = {result}.")
+ elif tool_name == "remember":
+ summaries.append(f"Saved “{result['key']}” = {result['value']}.")
+ elif tool_name == "recall":
+ summaries.append(
+ "Found: " + "; ".join(f"{item['key']} = {item['value']}" for item in result)
+ if result else "No matching memories found."
+ )
else:
- summaries.append(f"{tool_name} failed → {latest.get('error')}")
- return ModelReply(
- text="Done. I used the local tools and observed: " + " · ".join(summaries)
- )
+ summaries.append(f"Local time: {result}.")
+ return ModelReply(text=" ".join(summaries))
return ModelReply(
text=(
diff --git a/agent_system/static/app.js b/agent_system/static/app.js
index 684577f..f8f4445 100644
--- a/agent_system/static/app.js
+++ b/agent_system/static/app.js
@@ -1,6 +1,9 @@
const state = {
mode: "demo",
liveConfigured: false,
+ turn: null,
+ recorded: document.body.dataset.preview === "recorded",
+ examples: [],
};
const form = document.querySelector("#agent-form");
@@ -16,11 +19,10 @@ function formatDetail(detail) {
return JSON.stringify(detail, null, 2);
}
-function traceStep(event, index) {
+function traceStep(event) {
const article = document.createElement("article");
article.className = "trace-step";
article.dataset.kind = event.kind;
- article.style.animationDelay = `${index * 45}ms`;
const dot = document.createElement("span");
dot.className = "trace-dot";
@@ -30,9 +32,31 @@ function traceStep(event, index) {
body.className = "trace-body";
const title = document.createElement("h3");
title.textContent = event.title;
+ const explanations = {
+ input: "The instruction starts a new turn with fresh working context.",
+ reason:
+ "The planner or model receives the instruction and any tool results so far.",
+ tool: "The requested function receives these arguments. A request alone does not mean success.",
+ observe: event.detail?.ok
+ ? "The tool returned successfully. This result goes back into working context."
+ : "The tool reported an error. Inspect it before trusting a result or saving a value.",
+ reply: "The planner or model returned text, so the loop stops.",
+ guardrail:
+ "The loop reached its iteration budget and stopped without completing the task.",
+ done: "This completed turn and its trace are stored in the local SQLite database.",
+ };
+ const explanation = document.createElement("p");
+ explanation.className = "step-explanation";
+ explanation.textContent =
+ explanations[event.kind] || "Inspect the recorded event below.";
+ const disclosure = document.createElement("details");
+ disclosure.open = event.kind === "observe";
+ const summary = document.createElement("summary");
+ summary.textContent = "Inspect recorded data";
const detail = document.createElement("pre");
detail.textContent = formatDetail(event.detail);
- body.append(title, detail);
+ disclosure.append(summary, detail);
+ body.append(title, explanation, disclosure);
const timing = document.createElement("span");
timing.className = "trace-time";
@@ -43,13 +67,15 @@ function traceStep(event, index) {
function renderTrace(trace) {
traceList.replaceChildren();
- trace.forEach((event, index) => traceList.append(traceStep(event, index)));
+ trace.forEach((event) => traceList.append(traceStep(event)));
+ traceList.scrollTop = 0;
const last = trace.at(-1);
traceClock.textContent = `${last?.elapsed_ms || 0} ms`;
}
function renderMemory(payload) {
- document.querySelector("#metric-memories").textContent = payload.memories.length;
+ document.querySelector("#metric-memories").textContent =
+ payload.memories.length;
document.querySelector("#metric-turns").textContent = payload.turns.length;
const grid = document.querySelector("#memory-grid");
grid.replaceChildren();
@@ -81,7 +107,7 @@ function renderMemory(payload) {
async function refreshMemory() {
const response = await fetch("/api/memory");
- if (!response.ok) return;
+ if (!response.ok) throw new Error("Memory display could not refresh.");
renderMemory(await response.json());
}
@@ -90,6 +116,7 @@ function setMode(mode) {
state.mode = mode;
document.querySelectorAll(".mode-button").forEach((button) => {
button.classList.toggle("selected", button.dataset.mode === mode);
+ button.setAttribute("aria-pressed", String(button.dataset.mode === mode));
});
modeNote.textContent =
mode === "demo"
@@ -104,7 +131,11 @@ document.querySelectorAll(".mode-button").forEach((button) => {
document.querySelectorAll("[data-example]").forEach((button) => {
button.addEventListener("click", () => {
input.value = button.dataset.example;
- input.focus();
+ if (!state.recorded) input.focus();
+ document.querySelectorAll("[data-example]").forEach((item) => {
+ item.setAttribute("aria-pressed", String(item === button));
+ });
+ if (state.recorded && !runButton.disabled) form.requestSubmit();
});
});
@@ -115,30 +146,101 @@ form.addEventListener("submit", async (event) => {
runButton.disabled = true;
runButton.querySelector("span").textContent = "Running the loop…";
- replyText.textContent = "The agent is reasoning…";
+ replyText.textContent = "Running tools and collecting results…";
+ state.turn = null;
+ document.querySelector("#download-trace").disabled = true;
+ document.querySelector("#turn-summary").textContent =
+ "Waiting for this turn to complete…";
+ traceList.replaceChildren();
traceClock.textContent = "running";
try {
- const response = await fetch("/api/run", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ message, mode: state.mode }),
- });
- const payload = await response.json();
- if (!response.ok) throw new Error(payload.error || "The agent turn failed.");
+ let payload;
+ if (state.recorded) {
+ const example = state.examples.find((item) => item.message === message);
+ if (!example) throw new Error("Select one of the recorded examples.");
+ payload = example.turn;
+ renderMemory(example.memory);
+ } else {
+ const response = await fetch("/api/run", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message, mode: state.mode }),
+ });
+ payload = await response.json();
+ if (!response.ok)
+ throw new Error(payload.error || "The agent turn failed.");
+ }
+ state.turn = payload;
replyText.textContent = payload.reply;
renderTrace(payload.trace);
- await refreshMemory();
+ document.querySelector("#turn-summary").textContent =
+ `${state.recorded ? "Recorded turn" : "Turn"} ${payload.turn_id} · ${payload.mode} / ${payload.model} · ` +
+ `${payload.iterations} planner/model calls · ${payload.tool_calls} tool call${payload.tool_calls === 1 ? "" : "s"}`;
+ document.querySelector("#download-trace").disabled = false;
+ try {
+ if (!state.recorded) await refreshMemory();
+ } catch {
+ document.querySelector("#turn-summary").textContent +=
+ " · Memory display could not refresh.";
+ }
} catch (error) {
replyText.textContent = error.message;
traceClock.textContent = "error";
+ document.querySelector("#turn-summary").textContent =
+ "No completed trace returned for this request.";
} finally {
runButton.disabled = false;
- runButton.querySelector("span").textContent = "Run one turn";
+ runButton.querySelector("span").textContent = state.recorded
+ ? "Show recorded turn"
+ : "Run one turn";
}
});
+document.querySelector("#download-trace").addEventListener("click", () => {
+ if (!state.turn) return;
+ const blob = new Blob([JSON.stringify(state.turn, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `loop-agent-turn-${state.turn.turn_id}.json`;
+ link.click();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+});
+
async function boot() {
+ setMode("demo");
+ if (state.recorded) {
+ runButton.disabled = true;
+ try {
+ const response = await fetch("./examples.json");
+ if (!response.ok)
+ throw new Error(
+ "Could not load the recorded examples. Reload to retry.",
+ );
+ const recording = await response.json();
+ state.examples = recording.examples;
+ input.value = state.examples[0].message;
+ input.readOnly = true;
+ modeNote.textContent =
+ "Recorded Python run · no model call in this browser";
+ runButton.querySelector("span").textContent = "Show recorded turn";
+ document.querySelector("#system-status").textContent =
+ "recorded walkthrough";
+ document.querySelector("#recording-source").textContent =
+ `Generated from commit ${recording.commit.slice(0, 7)}. Each turn ran in a fresh Python process.`;
+ runButton.disabled = false;
+ document
+ .querySelector("[data-example]")
+ .setAttribute("aria-pressed", "true");
+ form.requestSubmit();
+ } catch (error) {
+ replyText.textContent = error.message;
+ }
+ return;
+ }
try {
const response = await fetch("/api/status");
const status = await response.json();
diff --git a/agent_system/static/index.html b/agent_system/static/index.html
index 852cd35..7d9a162 100644
--- a/agent_system/static/index.html
+++ b/agent_system/static/index.html
@@ -7,67 +7,76 @@
name="description"
content="A small, readable agent system with a visible loop, local tools, SQLite memory, and trace."
/>
-
Loop Agent · The loop, in the open
+
Loop Agent · Inspect a turn
-
-
-
ONE TURN. EVERY DECISION VISIBLE.
-
The agent loop,in the open.
-
- A small agent system you can read before your coffee gets cold:
- reason, call a tool, observe the result, repeat, remember.
-
-
+ A SMALL AGENT, OPEN TO INSPECTION
+ Follow an agent’s work.
+
+ Follow a task from instruction to tool calls to saved memory. Every
+ result has a record you can open.
+
+
-
-
- 01
- Loop
- reason → act → observe
-
-
- 02
- Tools
- four safe local functions
-
-
- 03
- Memory
- one inspectable SQLite file
-
-
- 04
- Trace
- the turn, step by step
-
-
+
+ START HERE / NO API KEY NEEDED
+ One small task. Three things to check.
+
+
+ Run the example. Expect 391, two tool calls, and
+ three planner calls.
+
+
+ Expand the trace. Inspect the calculation and the
+ saved value.
+
+
+ Restart and recall. Stop Python with Ctrl+C, run it
+ again, then choose “recall memory”.
+
+
+
+ Demo uses fixed rules, not an LLM. Live mode swaps in a model using
+ the same loop and tools.
+
-
TASK / 001
+
TASK
Give the agent a job
- Demo
- Live
+
+ Demo
+
+
+ Live
+
@@ -76,14 +85,31 @@
Give the agent a job
+ >
+Calculate 17 × 23 and remember the result as launch score.
- calculate + remember
- recall memory
- read local time
+
+ calculate + remember
+
+
+ recall memory
+
+
+ try a failure
+
@@ -95,23 +121,42 @@ Give the agent a job
- AGENT / REPLY
- Waiting for a task. The trace will reveal every step.
+ RESULT
+
+ Waiting for a task. The trace will reveal every step.
+
-
LIVE TRACE
+
EXECUTION TRACE
Inside the turn
0 ms
+
+ Recorded calls and results, shown after the turn completes. This is
+ not the model’s private reasoning.
+
+
+ No turn yet.
+
+
+ Download turn JSON
+
↻
-
Run a task to watch the loop reason, act, and observe.
+
+ Run a task, then expand each step to inspect its recorded data.
+
@@ -120,26 +165,29 @@
Inside the turn
-
LOCAL STATE / SQLITE
+
SAVED STATE · SQLITE
What survives the turn
- 0 memories
- 0 traced turns
+ 0 recent memories
+ 0 recent turns
4 local tools
EMPTY BY DEFAULT
- Ask the agent to remember something. It will appear here and remain after restart.
+
+ Ask the agent to remember something. It will appear here and
+ remain after restart.
+
- Built from first principles. Inspired by transparent agent architecture.
+ Built by Leo Zhao · A small study in inspectable agent execution.
Python · SQLite · HTML/CSS/JS · no frontend framework
diff --git a/agent_system/static/style.css b/agent_system/static/style.css
index 82101d2..f3a5371 100644
--- a/agent_system/static/style.css
+++ b/agent_system/static/style.css
@@ -1,777 +1,557 @@
:root {
- --ink: #171712;
- --paper: #f1eee4;
- --paper-deep: #e4dfd2;
- --signal: #ff5a36;
- --acid: #d9ff43;
- --blue: #2c57e8;
- --muted: #68675e;
- --line: rgba(23, 23, 18, 0.2);
- --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Baskerville, serif;
- --mono: "SFMono-Regular", "Cascadia Code", Menlo, Consolas, monospace;
+ color-scheme: light;
+ --ink: #24262a;
+ --muted: #666b73;
+ --line: #e4e6e9;
+ --paper: #fff;
+ --surface: #f7f8fa;
+ --accent: #365ac5;
+ --mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
}
-
* {
box-sizing: border-box;
}
-
-html {
- scroll-behavior: smooth;
-}
-
body {
margin: 0;
- color: var(--ink);
background: var(--paper);
- font-family: var(--mono);
- min-width: 300px;
+ color: var(--ink);
+ font:
+ 14px/1.55 "Helvetica Neue",
+ Helvetica,
+ sans-serif;
}
-
button,
textarea {
font: inherit;
}
-
+button,
+a,
+summary {
+ -webkit-tap-highlight-color: transparent;
+}
button {
- color: inherit;
+ cursor: pointer;
}
-
-.grain {
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 20;
- opacity: 0.08;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
- mix-blend-mode: multiply;
+a {
+ color: var(--accent);
+ text-underline-offset: 3px;
+}
+button:disabled {
+ cursor: default;
+ opacity: 0.5;
+}
+button:focus-visible,
+summary:focus-visible,
+a:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 4px;
+}
+[hidden] {
+ display: none !important;
}
-
.topbar {
- min-height: 92px;
+ height: 58px;
+ padding: 0 32px;
+ border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
- padding: 18px clamp(20px, 4vw, 64px);
- border-bottom: 1px solid var(--ink);
+ gap: 16px;
}
-
.wordmark {
- display: inline-flex;
+ display: flex;
+ gap: 10px;
align-items: center;
- gap: 12px;
+ font-weight: 650;
color: var(--ink);
text-decoration: none;
- font-size: 11px;
- font-weight: 800;
- line-height: 1.05;
- letter-spacing: 0.08em;
- text-transform: uppercase;
}
-
.mark {
- width: 48px;
- height: 48px;
- display: grid;
- place-items: center;
- background: var(--ink);
- color: var(--acid);
- font-family: var(--serif);
- font-size: 23px;
- letter-spacing: -0.12em;
+ font: 24px var(--mono);
}
-
.topbar-meta {
display: flex;
align-items: center;
gap: 24px;
- font-size: 10px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
+ font-size: 12px;
+}
+.source-link {
+ color: var(--muted);
+ text-decoration: none;
}
-
.status-pill {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- border: 1px solid var(--ink);
- border-radius: 99px;
- padding: 8px 11px;
+ color: var(--muted);
}
-
.status-pill i {
- width: 7px;
- height: 7px;
+ display: inline-block;
+ height: 6px;
+ width: 6px;
border-radius: 50%;
- background: var(--signal);
- box-shadow: 0 0 0 3px rgba(255, 90, 54, 0.15);
+ margin-right: 6px;
+ background: #8a8f98;
}
-
.status-pill.ready i {
- background: #4a9f3d;
- box-shadow: 0 0 0 3px rgba(74, 159, 61, 0.15);
-}
-
-.version {
- color: var(--muted);
+ background: #388454;
}
-
main {
- overflow: hidden;
+ max-width: 1220px;
+ margin: auto;
+ padding: 0 32px;
}
-
.hero {
- padding: clamp(52px, 8vw, 116px) clamp(20px, 4vw, 64px) 0;
+ padding: 28px 0 18px;
}
-
-.hero-copy {
- display: grid;
- grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.55fr);
- align-items: end;
- gap: clamp(32px, 7vw, 108px);
-}
-
-.eyebrow,
-.panel-kicker {
- margin: 0 0 16px;
+.eyebrow {
+ color: var(--muted);
font-size: 10px;
- font-weight: 800;
- letter-spacing: 0.14em;
- text-transform: uppercase;
-}
-
-.hero h1 {
- grid-column: 1;
+ font-weight: 600;
+ letter-spacing: 0.09em;
+ margin: 0 0 12px;
+}
+h1 {
+ font-size: 32px;
+ line-height: 1.18;
+ letter-spacing: -0.9px;
+ font-weight: 550;
margin: 0;
- font-family: var(--serif);
- font-size: clamp(62px, 10vw, 148px);
- font-weight: 400;
- line-height: 0.75;
- letter-spacing: -0.075em;
}
-
-.hero h1 em {
- color: var(--signal);
- font-weight: 400;
+h1 br {
+ display: none;
+}
+h1 br::after {
+ content: " ";
}
-
.lede {
- grid-column: 2;
- grid-row: 1 / span 2;
- margin: 0 0 6px;
- max-width: 410px;
- font-family: var(--serif);
- font-size: clamp(20px, 2.2vw, 31px);
- line-height: 1.17;
+ color: var(--muted);
+ margin: 12px 0 0;
+ max-width: 650px;
+ font-size: 15px;
}
-
-.architecture {
- position: relative;
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- margin-top: clamp(56px, 10vw, 140px);
- border-top: 1px solid var(--ink);
- border-left: 1px solid var(--ink);
+h2 {
+ font-size: 17px;
+ font-weight: 550;
+ letter-spacing: -0.25px;
+ margin: 0;
}
-
-.pillar {
- min-height: 174px;
- padding: 20px;
- display: flex;
- flex-direction: column;
- border-right: 1px solid var(--ink);
- border-bottom: 1px solid var(--ink);
- background: var(--paper);
- transition: transform 180ms ease, background 180ms ease;
+.guide {
+ padding: 0 0 24px;
+ max-width: 850px;
}
-
-.pillar:hover {
- transform: translateY(-8px);
- background: var(--acid);
+.guide .eyebrow {
+ display: none;
}
-
-.pillar span {
- font-size: 10px;
+.guide h2 {
+ font-size: 14px;
+}
+.guide p {
+ margin: 8px 0;
color: var(--muted);
+ font-size: 12px;
}
-
-.pillar strong {
- margin-top: auto;
- font-family: var(--serif);
- font-size: 34px;
- font-weight: 400;
+.guide ol {
+ display: flex;
+ flex-wrap: wrap;
+ list-style-position: inside;
+ gap: 8px 24px;
+ margin: 12px 0;
+ padding: 0;
+ font-size: 12px;
}
-
-.pillar small {
- margin-top: 6px;
- font-size: 9px;
- letter-spacing: 0.06em;
- text-transform: uppercase;
+.guide li {
+ max-width: 250px;
}
-
-.pillar.active {
- background: var(--ink);
- color: var(--paper);
+.project-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 18px;
+ margin: 12px 0;
+ font-size: 12px;
}
-
-.pillar.active span,
-.pillar.active small {
- color: var(--paper-deep);
+#recording-source {
+ font-size: 11px;
}
-
.workbench {
+ border: 1px solid var(--line);
+ border-radius: 8px;
display: grid;
- grid-template-columns: minmax(0, 0.95fr) minmax(360px, 1.05fr);
- margin: 0 clamp(20px, 4vw, 64px);
- background: var(--ink);
- color: var(--paper);
+ grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
+ overflow: hidden;
}
-
.composer-panel,
.trace-panel {
- min-height: 720px;
- padding: clamp(24px, 4vw, 52px);
+ padding: 24px;
+ min-width: 0;
}
-
.composer-panel {
- border-right: 1px solid rgba(241, 238, 228, 0.28);
+ border-right: 1px solid var(--line);
+ background: #fcfcfd;
}
-
.panel-heading {
display: flex;
- align-items: flex-start;
justify-content: space-between;
- gap: 20px;
- margin-bottom: 44px;
+ align-items: flex-start;
+ gap: 14px;
+ margin: 0 0 24px;
}
-
.panel-kicker {
- color: var(--acid);
- margin-bottom: 9px;
-}
-
-.panel-heading h2,
-.ledger h2 {
- margin: 0;
- font-family: var(--serif);
- font-size: clamp(34px, 4vw, 58px);
- font-weight: 400;
- letter-spacing: -0.035em;
+ font-size: 10px;
+ color: var(--muted);
+ letter-spacing: 0.07em;
+ margin: 0 0 5px;
}
-
.mode-control {
display: flex;
- border: 1px solid rgba(241, 238, 228, 0.5);
- border-radius: 99px;
- padding: 3px;
+ padding: 2px;
+ background: #edeff2;
+ border-radius: 5px;
}
-
.mode-button {
- border: 0;
- border-radius: 99px;
- padding: 8px 12px;
- color: var(--paper);
+ border: none;
background: transparent;
- font-size: 9px;
- font-weight: 800;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- cursor: pointer;
+ border-radius: 3px;
+ color: var(--muted);
+ padding: 4px 9px;
+ font-size: 11px;
}
-
.mode-button.selected {
- background: var(--acid);
+ background: white;
color: var(--ink);
+ box-shadow: 0 1px 3px #00000010;
}
-
-.mode-button:disabled {
- opacity: 0.32;
- cursor: not-allowed;
-}
-
form label {
display: block;
- margin-bottom: 10px;
- color: rgba(241, 238, 228, 0.6);
- font-size: 9px;
- letter-spacing: 0.12em;
- text-transform: uppercase;
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: 8px;
}
-
textarea {
width: 100%;
- min-height: 176px;
resize: vertical;
- border: 1px solid rgba(241, 238, 228, 0.38);
- border-radius: 0;
- padding: 20px;
- color: var(--paper);
- background: rgba(255, 255, 255, 0.035);
- font-family: var(--serif);
- font-size: clamp(20px, 2vw, 27px);
- line-height: 1.35;
- outline: none;
- transition: border 160ms ease, background 160ms ease;
+ min-height: 108px;
+ border: 1px solid #d9dce1;
+ border-radius: 5px;
+ padding: 13px;
+ background: white;
+ color: var(--ink);
+ line-height: 1.6;
}
-
textarea:focus {
- border-color: var(--acid);
- background: rgba(217, 255, 67, 0.035);
+ outline: 2px solid #365ac529;
+ border-color: var(--accent);
+}
+textarea[readonly] {
+ resize: none;
+ background: #fff;
}
-
.examples {
display: flex;
flex-wrap: wrap;
- gap: 7px;
- margin-top: 12px;
+ gap: 6px;
+ margin-top: 10px;
}
-
.examples button {
- border: 1px solid rgba(241, 238, 228, 0.28);
- border-radius: 99px;
- padding: 7px 9px;
- color: rgba(241, 238, 228, 0.72);
- background: transparent;
- font-size: 9px;
- cursor: pointer;
+ border: 1px solid var(--line);
+ color: #505762;
+ background: #fff;
+ border-radius: 4px;
+ padding: 6px 9px;
+ font-size: 11px;
}
-
.examples button:hover,
-.examples button:focus-visible {
- border-color: var(--acid);
- color: var(--acid);
+.examples button[aria-pressed="true"] {
+ border-color: #b0bfdf;
+ color: #294796;
+ background: #f0f4fd;
}
-
.run-row {
+ margin: 20px 0 0;
display: flex;
+ flex-wrap: wrap;
align-items: center;
- gap: 18px;
- margin-top: 24px;
+ gap: 12px;
}
-
.run-button {
- min-width: 220px;
display: flex;
align-items: center;
justify-content: space-between;
- gap: 26px;
- border: 0;
- padding: 17px 18px;
- color: var(--ink);
- background: var(--signal);
- font-size: 11px;
- font-weight: 800;
- letter-spacing: 0.05em;
- text-transform: uppercase;
- cursor: pointer;
- transition: background 160ms ease, transform 160ms ease;
-}
-
-.run-button b {
- font-family: var(--serif);
- font-size: 22px;
+ gap: 20px;
+ padding: 8px 13px;
+ border: 1px solid #2e4ead;
+ border-radius: 5px;
+ color: #fff;
+ background: var(--accent);
+ font-size: 12px;
+ font-weight: 500;
}
-
.run-button:hover {
- background: var(--acid);
- transform: translateY(-2px);
+ background: #2e4ead;
}
-
-.run-button:disabled {
- opacity: 0.6;
- cursor: wait;
- transform: none;
+.run-button b {
+ font-size: 15px;
+ font-weight: 400;
}
-
.run-row p {
+ font-size: 11px;
+ color: var(--muted);
+ max-width: 240px;
margin: 0;
- color: rgba(241, 238, 228, 0.52);
- font-size: 9px;
- line-height: 1.5;
- text-transform: uppercase;
}
-
.reply-card {
- margin-top: 56px;
- padding: 18px 20px 22px;
- border-top: 4px solid var(--blue);
- color: var(--ink);
- background: var(--paper);
- transform: rotate(-0.35deg);
- box-shadow: 12px 14px 0 rgba(44, 87, 232, 0.24);
+ margin-top: 30px;
+ border-top: 1px solid var(--line);
+ padding-top: 20px;
}
-
.reply-index {
- color: var(--blue);
- font-size: 9px;
- font-weight: 800;
- letter-spacing: 0.12em;
+ font-size: 10px;
+ letter-spacing: 0.07em;
+ color: var(--muted);
}
-
.reply-card p {
- margin: 16px 0 0;
- font-family: var(--serif);
- font-size: clamp(19px, 2vw, 26px);
- line-height: 1.3;
+ font-size: 19px;
+ letter-spacing: -0.2px;
+ line-height: 1.5;
+ margin: 10px 0 0;
+ overflow-wrap: anywhere;
}
-
.trace-heading {
- padding-bottom: 18px;
- margin-bottom: 20px;
- border-bottom: 1px solid rgba(241, 238, 228, 0.28);
+ margin-bottom: 10px;
}
-
#trace-clock {
- margin-top: 9px;
- color: var(--acid);
+ font: 11px var(--mono);
+ color: var(--muted);
+ padding-top: 5px;
+}
+.trace-note {
font-size: 11px;
+ color: var(--muted);
+ margin: 8px 0;
+ line-height: 1.6;
}
-
-.trace-list {
- position: relative;
- min-height: 520px;
+.trace-download {
+ color: #4a5361;
+ background: #fff;
+ border: 1px solid var(--line);
+ border-radius: 4px;
+ padding: 5px 9px;
+ margin: 4px 0 16px;
+ font-size: 11px;
}
-
-.trace-list::before {
- content: "";
- position: absolute;
- top: 18px;
- bottom: 18px;
- left: 14px;
- width: 1px;
- background: rgba(241, 238, 228, 0.22);
+.trace-list {
+ max-height: 450px;
+ overflow-y: auto;
+ padding-right: 8px;
+ scrollbar-width: thin;
}
-
.trace-empty {
- min-height: 480px;
- display: grid;
- place-content: center;
- justify-items: center;
+ padding: 60px 20px;
text-align: center;
+ color: var(--muted);
+ font-size: 12px;
}
-
-.trace-empty p {
- max-width: 280px;
- color: rgba(241, 238, 228, 0.56);
- font-size: 11px;
- line-height: 1.6;
-}
-
.loop-glyph {
- width: 86px;
- height: 86px;
- display: grid;
- place-items: center;
- border: 1px solid rgba(241, 238, 228, 0.35);
- border-radius: 50%;
-}
-
-.loop-glyph span {
- color: var(--acid);
- font-family: var(--serif);
- font-size: 48px;
+ font-size: 24px;
+ color: #9aa1ad;
}
-
.trace-step {
- position: relative;
display: grid;
- grid-template-columns: 30px minmax(0, 1fr) auto;
- gap: 14px;
- padding: 14px 0;
- border-bottom: 1px solid rgba(241, 238, 228, 0.12);
- animation: step-in 360ms both;
+ grid-template-columns: 24px minmax(0, 1fr) auto;
+ gap: 10px;
+ padding: 12px 0;
+ border-top: 1px solid #eff0f2;
}
-
.trace-dot {
- z-index: 1;
- width: 29px;
- height: 29px;
- display: grid;
- place-items: center;
- border: 1px solid var(--paper);
- border-radius: 50%;
- color: var(--ink);
- background: var(--paper);
- font-size: 9px;
- font-weight: 800;
+ font: 10px var(--mono);
+ color: #8a8e96;
+ padding-top: 3px;
}
-
-.trace-step[data-kind="reason"] .trace-dot {
- background: var(--acid);
- border-color: var(--acid);
-}
-
-.trace-step[data-kind="tool"] .trace-dot,
-.trace-step[data-kind="observe"] .trace-dot {
- background: var(--signal);
- border-color: var(--signal);
+.trace-body h3 {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 550;
}
-
-.trace-step[data-kind="reply"] .trace-dot {
- color: var(--paper);
- background: var(--blue);
- border-color: var(--blue);
+.step-explanation {
+ margin: 4px 0 5px;
+ font-size: 11px;
+ color: var(--muted);
+ line-height: 1.55;
}
-
-.trace-body h3 {
- margin: 1px 0 6px;
+.trace-body summary {
+ color: var(--accent);
font-size: 11px;
- letter-spacing: 0.03em;
+ cursor: pointer;
+ padding: 3px 0;
}
-
.trace-body pre {
- margin: 0;
- color: rgba(241, 238, 228, 0.58);
- font-family: var(--mono);
- font-size: 9px;
- line-height: 1.45;
white-space: pre-wrap;
- word-break: break-word;
+ overflow-wrap: anywhere;
+ margin: 6px 0;
+ padding: 10px;
+ background: var(--surface);
+ border-radius: 4px;
+ font: 11px/1.6 var(--mono);
}
-
.trace-time {
- color: rgba(241, 238, 228, 0.42);
- font-size: 9px;
+ font: 10px var(--mono);
+ color: #767d87;
+ padding-top: 3px;
}
-
-@keyframes step-in {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
+.trace-step[data-kind="observe"] .trace-body pre {
+ border-left: 2px solid #b7c7ba;
}
-
.ledger {
- padding: clamp(68px, 9vw, 128px) clamp(20px, 4vw, 64px);
+ padding: 28px 0 36px;
}
-
.ledger-heading {
display: flex;
justify-content: space-between;
- align-items: end;
- gap: 40px;
- padding-bottom: 28px;
- border-bottom: 1px solid var(--ink);
+ align-items: center;
+ gap: 24px;
+ margin-bottom: 18px;
}
-
-.metrics {
- display: flex;
- gap: clamp(18px, 4vw, 56px);
+.ledger .eyebrow {
+ margin-bottom: 5px;
}
-
-.metrics span {
+.metrics {
display: flex;
- flex-direction: column;
+ gap: 20px;
+ font-size: 11px;
color: var(--muted);
- font-size: 9px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
}
-
.metrics b {
+ font-weight: 600;
color: var(--ink);
- font-family: var(--serif);
- font-size: 38px;
- font-weight: 400;
+ margin-right: 3px;
}
-
.memory-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 1px;
- margin-top: 1px;
- background: var(--ink);
-}
-
-.memory-card,
-.memory-empty {
- min-height: 180px;
- padding: 22px;
- background: var(--paper);
+ border-top: 1px solid var(--line);
}
-
-.memory-card:nth-child(3n + 2) {
- background: var(--paper-deep);
+.memory-card {
+ display: grid;
+ grid-template-columns: 100px minmax(100px, 1fr) 1fr;
+ gap: 16px;
+ align-items: baseline;
+ padding: 14px 0;
+ border-bottom: 1px solid var(--line);
+ overflow-wrap: anywhere;
}
-
.memory-card span,
.memory-empty span {
- color: var(--signal);
- font-size: 9px;
- font-weight: 800;
- letter-spacing: 0.12em;
- text-transform: uppercase;
+ font-size: 10px;
+ color: var(--muted);
}
-
.memory-card h3 {
- margin: 42px 0 8px;
- font-family: var(--serif);
- font-size: 28px;
- font-weight: 400;
+ margin: 0;
+ font-size: 13px;
+ font-weight: 500;
}
-
-.memory-card p,
-.memory-empty p {
+.memory-card p {
margin: 0;
- color: var(--muted);
- font-family: var(--serif);
- font-size: 17px;
- line-height: 1.35;
+ font: 13px var(--mono);
}
-
.memory-empty {
- grid-column: 1 / -1;
- display: flex;
- align-items: flex-end;
- justify-content: space-between;
- gap: 40px;
+ padding: 16px 0;
+ color: var(--muted);
+ font-size: 12px;
}
-
.memory-empty p {
- max-width: 450px;
+ margin: 6px 0;
}
-
footer {
+ max-width: 1220px;
+ margin: auto;
+ padding: 20px 32px;
+ border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
- gap: 24px;
- padding: 28px clamp(20px, 4vw, 64px);
- color: var(--paper);
- background: var(--blue);
- font-size: 9px;
- letter-spacing: 0.07em;
- text-transform: uppercase;
+ gap: 16px;
+ color: var(--muted);
+ font-size: 11px;
}
-
footer p {
margin: 0;
}
-
-@media (max-width: 900px) {
- .hero-copy {
- display: block;
- }
-
- .hero .lede {
- margin-top: 38px;
- }
-
- .architecture {
- grid-template-columns: repeat(2, 1fr);
- }
-
- .workbench {
- grid-template-columns: 1fr;
- }
-
- .composer-panel {
- border-right: 0;
- border-bottom: 1px solid rgba(241, 238, 228, 0.28);
- }
-
- .composer-panel,
- .trace-panel {
- min-height: auto;
- }
-
- .memory-grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (max-width: 600px) {
+@media (max-width: 760px) {
.topbar {
- min-height: 76px;
+ padding: 0 18px;
+ height: 58px;
}
-
- .mark {
- width: 40px;
- height: 40px;
+ .topbar-meta {
+ gap: 10px;
+ font-size: 10px;
}
-
- .version {
+ .source-link {
display: none;
}
-
+ main {
+ padding: 0 18px;
+ }
.hero {
- padding-top: 52px;
+ padding-top: 28px;
}
-
- .hero h1 {
- font-size: clamp(58px, 19vw, 90px);
- line-height: 0.8;
+ h1 {
+ font-size: 28px;
}
-
- .architecture {
- grid-template-columns: 1fr;
- margin-top: 64px;
+ h1 br {
+ display: block;
}
-
- .pillar {
- min-height: 126px;
+ .guide ol {
+ display: block;
+ }
+ .guide li {
+ max-width: none;
+ margin-bottom: 6px;
}
-
.workbench {
- margin: 0;
+ grid-template-columns: 1fr;
}
-
- .panel-heading,
- .ledger-heading,
- .memory-empty,
- footer {
- align-items: flex-start;
- flex-direction: column;
+ .composer-panel {
+ border-right: 0;
+ border-bottom: 1px solid var(--line);
}
-
- .panel-heading {
- margin-bottom: 30px;
+ .composer-panel,
+ .trace-panel {
+ padding: 18px;
}
-
- .run-row {
- align-items: stretch;
+ .ledger-heading {
+ align-items: flex-start;
flex-direction: column;
+ gap: 12px;
}
-
- .run-button {
- width: 100%;
- }
-
- .reply-card {
- margin-top: 40px;
- transform: none;
- }
-
- .trace-list {
- min-height: 420px;
- }
-
.metrics {
- width: 100%;
- justify-content: space-between;
+ gap: 12px;
}
-
- .metrics b {
- font-size: 30px;
+ .memory-card {
+ grid-template-columns: 70px minmax(0, 1fr) 60px;
+ gap: 8px;
}
-
- .memory-grid {
- grid-template-columns: 1fr;
+ .trace-list {
+ max-height: 520px;
}
-
footer {
- line-height: 1.5;
+ padding: 18px;
+ flex-direction: column;
}
}
-
@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
+ * {
scroll-behavior: auto !important;
- animation-duration: 0.01ms !important;
- transition-duration: 0.01ms !important;
}
}
+
+body[data-preview="recorded"] .guide {
+ max-width: none;
+ padding-bottom: 18px;
+}
+body[data-preview="recorded"] footer {
+ flex-wrap: wrap;
+}
+body[data-preview="recorded"] #recording-source {
+ flex-basis: 100%;
+ margin: 0;
+}
+body[data-preview="recorded"] .run-row {
+ display: none;
+}
+body[data-preview="recorded"] .lede {
+ max-width: 850px;
+}
diff --git a/agent_system/walkthrough.py b/agent_system/walkthrough.py
new file mode 100644
index 0000000..73d5df7
--- /dev/null
+++ b/agent_system/walkthrough.py
@@ -0,0 +1,85 @@
+"""Run the portfolio example in fresh processes without keys or persistent app state."""
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+# A fresh interpreter per turn rules out in-process state as the source of recall.
+_WORKER = """
+import json
+import sys
+from agent_system.agent import AgentSystem
+from agent_system.memory import MemoryStore
+from agent_system.models import DemoModel
+from agent_system.tools import build_tools
+memory = MemoryStore(sys.argv[1])
+agent = AgentSystem(model=DemoModel(), tools=build_tools(memory), memory=memory)
+turn = agent.run(sys.argv[2])
+print(json.dumps({"turn": turn.to_dict(), "memories": memory.recall()}))
+"""
+
+
+def run_turn(database: Path, message: str) -> dict:
+ env = os.environ.copy()
+ package_root = str(Path(__file__).resolve().parent.parent)
+ env["PYTHONPATH"] = os.pathsep.join(filter(None, [package_root, env.get("PYTHONPATH")]))
+ result = subprocess.run(
+ [sys.executable, "-c", _WORKER, str(database), message],
+ check=True,
+ capture_output=True,
+ text=True,
+ timeout=15,
+ env=env,
+ )
+ return json.loads(result.stdout)
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ raise RuntimeError(message)
+
+
+def main() -> None:
+ with TemporaryDirectory(prefix="loop-agent-walkthrough-") as folder:
+ database = Path(folder) / "state.db"
+ first = run_turn(
+ database, "Calculate 17 * 23 and remember the result as launch score."
+ )
+ require(first["turn"]["iterations"] == 3, "Expected three planner calls")
+ require(first["turn"]["tool_calls"] == 2, "Expected two tool calls")
+ require(
+ any(m["key"] == "launch score" and m["value"] == "391" for m in first["memories"]),
+ "Expected launch score = 391",
+ )
+ print("PASS calculate + remember: 391; 3 planner calls; 2 tool calls")
+ second = run_turn(database, "What do you remember about launch score?")
+ observed = [e["detail"] for e in second["turn"]["trace"] if e["kind"] == "observe"]
+ require(
+ any(
+ item["key"] == "launch score" and item["value"] == "391"
+ for event in observed if event.get("ok")
+ for item in event["result"]
+ ),
+ "Fresh process did not recall the saved value",
+ )
+ print("PASS restart + recall: launch score = 391 in a fresh process")
+ third = run_turn(
+ database, "Calculate 1 / 0 and remember the result as invalid score."
+ )
+ require(third["memories"] == first["memories"], "Failed calculation changed memory")
+ require(third["turn"]["tool_calls"] == 1, "Failed calculation should not write memory")
+ require(
+ any(e["kind"] == "observe" and not e["detail"]["ok"] for e in third["turn"]["trace"]),
+ "Expected a structured tool error",
+ )
+ print("PASS failed calculation: error observed; no memory written")
+ print("PASS all checks; temporary state removed")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/architecture.md b/docs/architecture.md
index 6a2cd98..f9d826d 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,36 +1,122 @@
-# Architecture notes
+# Architecture: make the work checkable
-Loop Agent keeps each layer behind one small interface:
+[← Project overview](../README.md) · [Step-by-step walkthrough](walkthrough.md)
+
+The core design separates **intent**, **execution**, and **evidence**. The planner requests a tool;
+the registry executes it; the observation records what happened. A final answer is one output of that
+process, not sufficient evidence that a requested action succeeded.
+
+## Source reading order
+
+| File | Responsibility | Question to ask while reading |
+| --- | --- | --- |
+| [agent.py](../agent_system/agent.py) | One bounded turn | When does the loop continue, stop, and persist? |
+| [models.py](../agent_system/models.py) | Demo and live adapters | What changes when fixed rules are replaced by an LLM? |
+| [tools.py](../agent_system/tools.py) | Registry, schemas, execution | What can actually run, and how do errors return? |
+| [memory.py](../agent_system/memory.py) | SQLite facts and turn records | Which state outlives the process? |
+| [server.py](../agent_system/server.py) | HTTP validation and static assets | Where does external input enter? |
+| [app.js](../agent_system/static/app.js) | Explanations and raw event disclosures | Can a reader distinguish a request from a successful result? |
+
+## Lifecycle of one turn
```mermaid
-flowchart LR
- UI[Local cockpit] --> APP[AgentSystem.run]
- APP --> MODEL[Model adapter]
- MODEL -->|tool call| LOOP{Loop guard}
- LOOP --> TOOLS[Tool registry]
- TOOLS -->|result| MODEL
- MODEL -->|final text| UI
- TOOLS --> DB[(SQLite memory)]
- APP --> DB
- APP --> TRACE[Trace events]
- TRACE --> UI
+sequenceDiagram
+ participant UI as Local UI
+ participant Loop as AgentSystem
+ participant Model as Planner / LLM
+ participant Tools as Registry
+ participant DB as SQLite
+ UI->>Loop: Validated instruction
+ loop At most 6 iterations by default
+ Loop->>Model: Working messages + tool schemas
+ alt Tool calls returned
+ Model-->>Loop: Names + arguments
+ Loop->>Tools: Execute registered function
+ opt remember or recall
+ Tools->>DB: Write or query facts
+ DB-->>Tools: Result
+ end
+ Tools-->>Loop: Structured success or error
+ Note over Loop: Append observation to working messages
+ else Text returned
+ Model-->>Loop: Final reply
+ Note over Loop: Exit loop
+ end
+ end
+ Loop->>DB: Save turn + trace
+ Loop-->>UI: Reply + trace + counts
```
-## One turn
+The loop uses the same `Model.complete(messages, tools)` interface for both modes.
+`DemoModel` recognizes a few patterns; it does not simulate LLM quality. `LiveModel` calls an
+OpenAI-compatible chat-completions endpoint with function schemas. No model SDK is needed for demo mode.
+
+An iteration is a model/planner call, not a tool call. Multiple tools returned in one iteration execute
+sequentially. Text without tool calls ends the turn. Exhaustion returns a guardrail reply and saves its trace.
+The default is six iterations; the constructor clamps overrides to 1–12.
+
+## What is stored, and what is not
+
+| State | Lifetime | Detail |
+| --- | --- | --- |
+| Working messages | One turn | System instruction, user input, tool requests, and observations |
+| Facts | Durable | Unique key; later writes replace its value; text normalized and length-limited |
+| Turn ledger | Durable after completion | User input, reply, mode, iterations, timestamp, JSON trace |
+| UI trace | Current completed turn | Expand raw data or export returned JSON; not streamed |
+| Live API key | Server configuration | Not sent to the browser or stored in the turn ledger |
+
+Facts and the ledger live in `.agent-mini/state.db` by default. Recall uses literal substring matching
+with escaped SQL wildcard characters and parameterized values. It is not vector retrieval.
+The memory endpoint returns capped recent lists. The UI labels these as recent counts.
+Old traces remain in SQLite, but browsing them in the UI is not yet implemented.
+
+A `reason` event means “a model call is starting”; this legacy event name does not imply access to
+private reasoning. A `done` event is assembled before the database write and returned only after that
+write succeeds; its elapsed time does not measure the commit duration.
+
+## Tradeoffs and next decisions
+
+| Choice | Benefit | Cost / when to revisit |
+| --- | --- | --- |
+| Plain Python loop, no agent framework | Control flow fits in one file and is easy to inspect | More orchestration, retries, and cancellation would need explicit implementation |
+| Keyless deterministic demo | Reproducible onboarding and regression checks | Limited language patterns; says nothing about live-model task success |
+| SQLite instead of a service | No infrastructure setup; restart persistence is easy to verify | No multi-user isolation; concurrent writes and growth need more design |
+| Explicit remember/recall tools | Clear distinction between working context and durable facts | Models may choose the wrong facts; no semantic retrieval or conflict resolution |
+| Four local tools | Small, inspectable action surface | Narrow task usefulness; adding external writes would require permission and recovery policies |
+| Completed-turn trace | Simple API and a stable inspection artifact | No progress streaming; model or persistence failure can leave no saved trace |
+
+The next priority is **durable failure records with explicit turn status**. Today, memory writes commit
+independently of the final trace. A later error or process crash can leave a saved fact without a turn
+record. A production design would need a policy for partial effects, tool-call budgets, timeouts,
+retries, and cancellation before adding more tools.
+
+After that, evaluate live mode with task-specific assertions (correct arithmetic, evidence-backed
+memory writes, correct recall, bounded failure handling). Pin the model and configuration and report
+success and failure counts. Deterministic tests are not an LLM benchmark.
+
+## Boundaries worth reviewing
+
+- HTTP input must be JSON, with a nonempty message of at most 2,000 characters. The request body is capped at 16,384 bytes.
+- The calculator allows a restricted AST and rejects calls, names, complex results, nonfinite results,
+ and results beyond its numeric bound. This is not a general code sandbox or CPU budget.
+- Registered tool exceptions become `{ok: false, error: ...}` observations. Schemas describe inputs
+ to the model; this registry does not implement full JSON Schema validation.
+- In demo mode, a failed calculation prevents a follow-on result write. The live model is instructed
+ to inspect results, but there is no equivalent general write-policy enforcement.
+- The server binds to `127.0.0.1`. It has no authentication, tenant isolation, or deployment hardening.
+- Live mode sends working messages, schemas, and tool results to the configured provider. Locally
+ stored instructions and facts are plain text in SQLite, not encrypted by this app.
-1. The server validates the instruction.
-2. `AgentSystem` creates working messages and a trace.
-3. The model returns either text or one or more tool calls.
-4. The registry executes only registered functions and returns JSON data.
-5. Tool results become new model messages.
-6. The loop repeats until the model replies or the iteration limit is reached.
-7. The turn and trace are written to SQLite.
+## Verification map
-## Deliberate constraints
+| Behavior | Evidence |
+| --- | --- |
+| Calculation feeds the next memory write | `test_demo_agent_chains_calculate_and_remember` |
+| Persistence survives fresh Python processes | `python -m agent_system.walkthrough` |
+| Failed calculation is not saved; prior value survives | Failure regression tests in [test_agent.py](../tests/test_agent.py) |
+| Endless tool requests stop | `test_iteration_guardrail_stops_endless_tool_calls` |
+| Restricted arithmetic and tool errors | [test_tools.py](../tests/test_tools.py) |
+| Input validation and local API | [test_server.py](../tests/test_server.py) |
-- The server binds to `127.0.0.1`.
-- There is no shell, browser, email, calendar, or network tool.
-- Arithmetic uses a restricted AST evaluator, never Python `eval`.
-- Demo mode is deterministic and makes no model request.
-- Live mode keeps the API key in the Python process, never the browser.
-- SQLite is the only durable source of truth.
+CI runs on Python 3.11 and 3.12. Browser layout, accessibility, provider compatibility, model quality,
+and production load require separate validation; a green Python suite does not establish them.
diff --git a/docs/cockpit.png b/docs/cockpit.png
index b23cc78eb897445afb2a3aa77b3451e88b76f506..4554b3d6ac0918ec85eb9774e98c55be4cb107ac 100644
GIT binary patch
literal 41274
zcmeFZ2~?BUx;Gwc)wT+jgP<~=LWNokGy-9U>ybfax=Lk87+TOk2qDZNjICCgBq>lC
z5{4F#s7xXm7!s&L$fY4r2oOmmF$97TA_-%d|7p*;_v^Xqtowc6|2y|v>%Z3jg|+j(
zd+$7Zzk5G>@8@}b&y#~!2X6o$Uvt0e4mflO063)j103)HmjOrLKXUZQ`$vx+IeP5a
z(GNcQ_@j?L{P3ew>L*WpeEQUxGpA2!YwPHrGt|*F*3;Jh^MyYfo0y*e{QOx%;1|Hp
zzBu>U`OkhMa_HExV;_C+k>*DqX?~`ot@GLc_I2
;qNlm5)`Whu%A^>i&E0A3k>Yy<-5?)B74n{`ARF
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 tA!lq-bJq^h4b`X?R
zi^yHFDKWN&Ns)Ft>s3u0!dwbDS9~(G+?tC~!^I+V074ZOKFg7I$86($KR#B;pWuge
zH*_;Hr(p~I@=a%*2=9T`@Kask3&V-)a)u%WDl#jHDzeRAO*3ut$f8`cymBsd7>F0t
zLPev0$fiRlYWX?_V`2E(txsnX%2PytDtFjO;KS9n3PgmEoZ|mtHHpA66uvrUUOU0b
z?<`DAxB-T*l**o}>G3$O&k}~tebTQnvMYfRtRn9fIN3lpUdV0VK1!ORpwJa^6cz8J
zwHF+`1AP)^es?0EvV4RTw9gGyQ;EWc{c38v#+soU_^~=@0FUTy1GxKyfZrR5(Z)=q
zxI*W~_1|eVKhsiQ7-whhnkv6|_GCOp_L40TI#;~+|bn7D&vSA!}jeWzcBOHJil~85QZbn2>JL_h-MNN6NzlrzKUme
z#m9Gt+LjS2Fjg*SsMv|c-$UeHN!KxmS;M2bU_Vn3&yKVxNtR-b#WUnKpYDZOlqvD$
ziGFlIPUD&KckLOp`Z0M&bJ+RkUMQsHu;bgv_PITwQ`jZ^)$`jO_!9s#){7$2szQqA||
zR{T=RE4mQj2LRsM(E-H1WYS!qxU;ir=!zQ0nK=z-P#?CCPGNQ5e?%L+kd
z_J;&5lOrSIWH)Z-uKVs2g%Oj*>r8R@yn1HoG-ROC(W}?4`@N=9R%~xVnlk_tUgB_y
zo|L+IFgK~NnShQ;b!*r^XWHxt1uB%_(vzgG7hgdn92aA57wzJ~dWWFNnpmhGh=pjz
z9#gK)y(%uOxI+&GQf3mDXZtV422?zn$FtafI74Q5MAaO5_C4%s<7x%k+8F=4{x3L`NPJWz*TuGiM@6V?se
zR&0HFr?*=9Nka2QKb}3(1PqUNjY|Zgz5p|$tIGqZJ-LQ+irMQ_PjJG(9=@&wPtd#E
zAj+E2^H=iMG>fz?+V;(iJ$6L;jkM>A)Q}>J#In*UYGOe$y5OLC
zR$z3?)_m#qgw>twVX0xFqq`blUN~qpOKYd%!enK1`@?K5ux4OCpAw_9sfz%wpr0kg
z>dx=A2ksH=DhN$7#At_8yZrHF5;PsCLgsuqj7}bFh|*?EM*w?in`xZns*k1{t`hu>
zc9p8Jg)K-+FIb9LRJw-ce2vH5(FJ@M2S~4`MJ8x)NC|awWrO_l4pM=Jgr2Uj5SZ)3
zOzdmNOjCS(^u)@L`sSZ}^MoPAqwX47T{|*z9{`JIDU30kOwp_VVO#ic;DZG|Sm1*N
vK3L#`1wL5dg9ScV;DZG|Sm1*NK3L#`1wL5dg9ScV;DZJJeiqpA?&bdl1&jPD
literal 204623
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