Smalltalk-agent

Description

Pharo under agentic control — live-image TDD via MCP. A coding agent develops, tests, visualizes, and debugs directly inside a running Smalltalk object memory.

Details

Source
GitHub
Dialect
pharo (65% confidence)
License
MIT
Created
Oct. 17, 2026
Updated
Oct. 19, 2026
Topics
game-of-life live-programming llm-agents mcp pharo smalltalk

Categories

Testing Games

README excerpt

<img width="986" height="875" alt="image" src="https://github.com/user-attachments/assets/a0fe3dbc-2a05-407a-9675-74626ad1acb1" />

# Reverse Smalltalk — Agentic Control PoC

A proof of concept for putting Smalltalk under agentic control: a live Pharo 13
image, running headless, that a coding agent can inspect, modify, and execute
directly — the original Smalltalk vision of an integrated live environment,
with an LLM agent in the loop instead of (or alongside) a human at the browser.

## Architecture

```
Agent (Vibe / Claude / any MCP client)
   │  MCP stdio, 22 tools
   ▼
smalltalk-interop-mcp-server (Python, via uvx)
   │  HTTP (localhost:8086)
   ▼
SIS — PharoSmalltalkInteropServer (in-image, Teapot + NeoJSON)
   │
   ▼
Live Pharo 13 image (headless)
```

And the other direction, entirely inside the image:

```
AgentSession (a Smalltalk object in the image) --Zinc--> Mistral API
   ^-- tool calls executed as native messages --v
Live Pharo object memory
```

The agent's unit of change is the *method*, not the file: it evaluates
Smalltalk code, reads and writes class/method sources, searches
implementors/references, imports/exports Tonel packages, and runs test
suites — all against the running image.

## What's here

| Path | Purpose |
|---|---|
| `Pharo.image` / `Pharo.changes` | The live image (SIS loaded, auto-starts on boot) |
| `pharo`, `pharo-ui`, `pharo-vm/` | Pharo 13 launcher scripts and VM |
| `keepalive.st` | Startup script that keeps the headless image alive |
| `start-pharo-server.cmd` | Double-click launcher for the headless server |
| `start-pharo-desktop.cmd` | Double-click launcher for the Pharo desktop (GUI) |
| `restart-server.cmd` | Health check + restart: starts the headless server if it is down, never a second VM |
| `snapshot.cmd` / `restore.cmd` | Whole-world undo: copy/restore the image files |
| `demo/` | The agent's actual eval payloads + one-command replay script |
| `gen_gosper.py` | RLE verifier + eval-payload generator for the Gosper gun |
| `gen_fractals.py` | Composes the diamond + fractal replay payloads from `src/` |
| `gen_agent.py` | Generator for the AgentSession build payloads (`demo/agent-*.json`) |
| `src/` | Durable source of truth: Tonel export of live packages |
| `AGENTS.md` | Operating rules for AI agents working in this directory |
| `MANUAL.md` | Plain user's guide: setup, desktop tour, troubleshooting |
| `DESKTOP.md` | Learning note: the agentic process of building a custom desktop (the method; the private art is not published) |
| `TODO.md` | Roadmap / next steps |

## Starting the server

Double-click `start-pharo-server.cmd`, or from Git Bash:

```
./pharo --headless Pharo.image st keepalive.st
```

SIS auto-starts on image boot and listens on **port 8086**
(`PHARO_SIS_PORT` to change). Stop with Ctrl+C.

Persistence caveat: the image on disk only changes when it is *saved*
(`Smalltalk snapshot: true andQuit: false`). Code compiled into a running
image is lost if the VM dies unsaved — which is why the demo exports
Tonel sources to `src/` and saves the image at the end.

## The Pharo desktop

`start-pharo-desktop.cmd` opens the same image with the full Pharo GUI —
browser, playground, inspector, the original live environment. SIS still
serves on 8086, so an agent session and a human share the same live
world: the agent can compile methods (they appear in the browser),
evaluate code, open windows on your screen, and verify its own UI work
via `/read-screen`.

One hard rule: **never run two VMs on the same image files** — stop the
headless server (close its console or `taskkill /IM PharoConsole.exe`)
before opening the desktop, and vice versa.

Verify it is up:

```
curl -X POST http://localhost:8086/eval/ \
  -H "Content-Type: application/json" \
  -d '{"code":"3+4"}'
```

## Connecting an agent

Register the MCP server with your client. Vibe / Claude Code / Cursor
`mcp.json` entry:

```json
{
  "mcpServers": {
    "smalltalk-interop": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/mumez/smalltalk-interop-mcp-server.git",
        "smalltalk-interop-mcp-server"
      ],
      "env": { "SIS_PORT": "8086" }
    }
  }
}
```

Exposed tools: `eval`, `get_class_source`, `get_method_source`,
`get_class_comment`, `search_classes_like`, `search_methods_like`,
`search_implementors`, `search_references`, `list_packages`, `list_classes`,
`export_package`, `import_package` (Tonel), `run_package_test`,
`run_class_test`, `install_project`, `read_screen`, `get_settings`,
`apply_settings`, and more.

## The MVP: an agent develops Conway's Game of Life, live

A coding agent built a package with a test suite — no files, no build
system, no IDE. Every change was compiled into the running image through
the SIS API, test-first. The exact payloads it sent are in `demo/`, and the
whole sequence replays with one command:

```
python demo/replay_demo.py
```

What happens (actual output, abridged):

```
== Create GameOfLife class (empty shell, no behaviour yet)
   -> {"result": "created", "success": true}
== Compile storage: initialize/withAll:/liveCells/isAliveAt:
   -> {"result": "storageCompiled", "success": true}
== Create GameOfLifeTest (TestCase subclass)
   -> {"result": "testClassCreated", "success": true}
== Compile 5 tests describing the rules of the game
   -> {"result": "testsCompiled", "success": true}
== Run tests BEFORE implementation (expect RED)
   -> "5 ran, 0 passed, 0 failures, 5 errors"
== Compile the evolution rules: neighbours + nextGeneration
   -> {"result": "evolutionCompiled", "success": true}
== Run tests AFTER implementation (expect GREEN)
   -> "5 ran, 5 passed, 0 failures, 0 errors"
== Export durable sources (Tonel) into src/
   -> GameOfLife exported, GameOfLife-Tests exported
```

The red run fails because `nextGeneration` does not exist yet — the tests
describe the rules of the game before any implementation exists. Then the
agent compiles the evolution rules into the live classes, re-runs, and the
suite goes green. Full transcript: `demo/replay-output.txt`. Durable
sources land in `src/` as plain Tonel files, reviewable in git.

The package also has a graphical view, added live by an agent while the
desktop was open: `(GameOfLife new withAll: { 1@1. 2@2. 0@3. 1@3. 2@3 })
openEvolutionView` opens an animated Roassal grid — watch a glider crawl
while SIS's screen-structure endpoint (`/read-screen`) lets the agent
verify what it drew. Human and agent share the same live world.

The showpiece is Bill Gosper's 1970 glider gun — the first known pattern
with unbounded growth, firing a glider every 30 generations, forever:

```smalltalk
GameOfLife gosperGun openGunView
```

It was added the disciplined way: the canonical RLE was fetched from a
GitHub mirror (LifeWiki is Cloudflare-walled), verified by local
simulation *before* touching the image (36 cells, one extra glider per
30-generation cycle), then driven test-first into the live system —
red, implement, green (7/7). The replay demo builds it too, so a fresh install gets the complete package. `gen_gosper.py` holds the verifier and
payload generator; `demo/gosper-*.json` are the exact evals used.

Its companion is the classic puffer train - a B-heptomino kept alive by
two LWSS escorts, lurching across the grid at c/2 while laying a dirty
debris trail (`GameOfLife pufferTrain openPufferView`). Same
discipline: the canonical RLE (Golly's pattern collection, from Alan
Hensel's lifebc) was verified by local simulation before touching the
image - the population must grow and the engine must advance exactly
c/2 - then driven test-first into the live system (red, implement,
green, 8/8). `gen_puffer.py` holds the verifier and payload generator;
`demo/puffer-*.json` are the exact evals used; the replay demo builds
it into fresh images too.

The image also carries a small fractal gallery in its own `Fractals`
package (tests in `Fractals-Tests`) — generator programs,
not data: `SierpinskiCarpet level: n` (ternary-digit exclusion rule,
8^n cells), `DragonCurve order: n` (the Heighway dragon by
deterministic doubling), and `TwinDragon order: n` (two dragons tail
to tail — the plane-tiling Davis-Knuth twindragon). Each was built
test-first and cross-verified against a Python reference point for
point, and each opens a full-screen view: `openCarpetView`,
`openDragonView`, `openTwinDragonView`.

## BoingBall: the classic Amiga demo, live on your desktop

The `BoingBall` package is the 1983 Amiga boing ball demo rebuilt as
a live Morphic animation: a checkered sphere, rendered pixel-by-pixel
from a lit unit sphere and rotated through 24 cached phases, bouncing
across the world with its shadow - and *boinging* on every bounce.
It was developed live by an agent while the user watched the desktop,
and it doubles as a live-systems architecture experiment:

- **Variants are two-line subclasses.** All state is per-class (class
  instance variables, not shared class variables), each variant
  overrides only hooks - `primaryColor`, `startPosition:`,
  `startVelocity`, `bouncePitch` - so three balls fly simultaneously
  with independent form caches and processes:

  ```smalltalk
  BoingBall start.       "red,   180 Hz, enters upper-left, moving right"
  BlueBoingBall start.   "blue,  260 Hz, enters lower-right, moving left"
  GreenBoingBall start.  "green, 320 Hz, drops from top-center"
  ```

- **The sound is synthesized in the image.** `BoingSound` builds the
  descending boooing - a sine sweeping down one octave with a decay
  envelope - into an in-memory WAV and plays it through
  `PlaySound(SND_ASYNC|SND_MEMORY)` via ThreadedFFI (`winmm.dll`).
  No sound files, no external tooling. Per-ball and global debounces
  keep overlapping bounces from rattling into machine-gun fire.

- **It survives live surgery.** Slots were added to the running
  class (Shift migrates live instances) and methods were recompiled
  while the balls were animating - the loops pick up new compiled
  methods o
← Back to results