Agent loop that lives inside a live Smalltalk/Pharo image — drives the entire environment via a single 'send' tool + OpenRouter
# smalltalk-agent An agent loop that lives **inside** a live Smalltalk/Pharo image. The agent communicates with [OpenRouter](https://openrouter.ai) and uses a single `send` tool to evaluate Smalltalk expressions — driving the entire environment as a live, persistent workspace. ## Why? Most agent-coding setups work against dead text (files on disk). Smalltalk dissolves the boundary between "code" and "running system": everything is a live object, changes are instant, the image persists, and tools (debugger, inspector, browser) are objects you can manipulate programmatically. The agent's work product isn't a pile of files — it's a live running system. ## Bootstrap on a fresh Pharo 14 image ### 1. Get an OpenRouter API key Go to https://openrouter.ai/keys and create a key. ### 2. Clone this repo and load the code Open a **Playground** in your fresh Pharo 14 image and evaluate: ```smalltalk Metacello new baseline: 'SmalltalkAgent'; repository: 'github://hermclaw/smalltalk-agent:main/src'; load. ``` This downloads the code via Iceberg (Pharo's git integration) and loads `SmalltalkAgent` (the core) plus its dependencies (Zinc HTTP, NeoJSON). ### 3. Run the agent ```smalltalk loop := SAAgentLoop new. loop apiKey: 'sk-or-v1-...your-key...'. loop model: 'anthropic/claude-sonnet-4'. "or any OpenRouter model" "Give it a task — it will build it live in the image:" loop run: 'Create a simple Counter class with increment/decrement methods, then open a Spec 2 window with two buttons and a label display that uses it.' ``` Watch the **Transcript** for progress. The agent will: 1. Inspect the environment (send expressions, read results) 2. Create classes and methods (live, no files) 3. Open UI windows (Spec 2) 4. Test what it built 5. Fix errors by reading the serialized exception output ## What's in here | File | What | |---|---| | `BaselineOfSmalltalkAgent` | Metacello baseline — loads dependencies + core | | `SAOpenRouterClient` | Zinc HTTP + NeoJSON client for OpenRouter `/chat/completions` | | `SAWorkspace` | Live evaluation context with persistent variable bindings | | `SASerializer` | Depth-limited object → text serialization for LLM consumption | | `SAAgentLoop` | The loop: conversation history → LLM → tool dispatch → repeat | | `SAAgentLoopTests` | Smoke tests for workspace, serializer, exception handling | ## Architecture ``` ┌─────────────────────────────────────────┐ │ SAAgentLoop (Pharo Process) │ │ │ │ messages ──► SAOpenRouterClient │ │ POST /chat/completions │ │ with `send` tool def │ │ │ │ │ ▼ │ │ tool_calls ──► SAWorkspace.evaluate │ │ (catch all exceptions) │ │ │ │ │ ▼ │ │ SASerializer ──► text result ──► LLM │ │ │ │ THE LIVE PHARO IMAGE │ │ (every object, every class, everything)│ └─────────────────────────────────────────┘ ``` ## The `send` tool The agent has exactly **one tool**: `send`, which evaluates a Smalltalk expression. That's it. The entire Smalltalk environment is the tool surface — - Create a class → `MyApp subclass: #Counter instanceVariableNames: 'value'` - Add a method → `Counter compile: 'increment ^ value := value + 1'` - Inspect an object → `Counter new inspect` - Open a window → `SpApplication new start` - Read the Transcript → `Transcript contents` The agent discovers capabilities by inspecting objects, not by reading tool schemas. ## Design notes - **Exceptions never crash the loop.** When `send` fails, the exception is serialized (class, message, stack frames) and returned as the tool result. The agent reads the error and iterates — like a human with a debugger. - **Workspace persistence.** Variables bound in one `send` call are available in the next (`x := 42` then `x + 1` works). - **Serialization limits.** `SASerializer` has configurable depth (default 3), item count (default 20), and string length (default 2000) to prevent huge objects from flooding the LLM context window. - **One tool.** Start minimal. If the agent struggles, add `inspect:`, `browse:`, `references:` — but a capable model can do all of that through `send` alone. ## License MIT ## Author [Hermclaw](https://github.com/hermclaw)