A minimal Smalltalk agent harness for Pharo, inspired by Pi Coding Agent.
# SmallGPTalk
A minimal Smalltalk agent harness for Pharo 13 on macOS.
## Design principles
[Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent#philosophy)
is the main design reference: a small core, short instructions, and simple tool
protocols. Discover collaborations through executable Smalltalk examples and
TDD. Use composition. Add an abstraction when a behavior needs it.
The session owns execution and conversation history. A view can observe that
session without owning it. A `SmallGPTalkTurn` executes model requests and tools.
Its `SmallGPTalkExchange` records replies, context, and preview text.
Runs and time limits share a process supervisor. Each supplies its stop rule.
Provider details stay outside the core loop.
`evaluate` is the only initial image tool. It gives access to Pharo reflection,
compilation, and SUnit through Smalltalk expressions.
See [AGENTS.md](AGENTS.md) for the ten engineering principles and repository rules.
## Current status
SmallGPTalk has named agents, asynchronous sessions, retained exchanges,
evaluation objects, cancellation, fork, manual and automatic compaction,
context inspection, detachable chat views, and an OpenAI account connection.
Credentials use the macOS login Keychain.
See [verification](docs/verification.md) for check commands, the latest results,
and validation limits.
## Load and test
Use a clean Pharo 13 image with its matching changes and sources files.
Set `SMALLGPTALK_VM` if the VM is outside the default path in `scripts/test.sh`.
```sh
SMALLGPTALK_BASE_IMAGE=/path/to/Pharo.image python3 scripts/verify.py
```
See [verification](docs/verification.md) for native and live checks. The default
command also checks compiled source, Core alone, and queued refreshes. Use
`bash scripts/test.sh` with the same environment for SUnit only.
The test script copies the image to a new directory in `.build`, loads Tonel through
Metacello, and runs SUnit. Test failures produce a nonzero exit status. Offline
tests do not use an OpenAI account or the Keychain.
When available, `caffeinate` prevents idle system sleep while each Pharo process
runs. The assertion ends with the process and does not change system settings.
For a manual load, replace the path below with this checkout's absolute path:
```smalltalk
Metacello new
baseline: 'SmallGPTalk';
repository: 'tonel:///absolute/path/to/SmallGPTalk/src';
load.
```
The packages are `SmallGPTalk-Core`, `SmallGPTalk-Pharo`, `SmallGPTalk-OpenAI`,
`SmallGPTalk-UI`, and `SmallGPTalk-Tests`. The core has no UI or provider dependency.
## Connect an account
```smalltalk
account := SmallGPTalkOpenAIAccount openAI.
account isAuthenticated.
```
If no login is stored, start a login and open its authorization URL in a browser:
```smalltalk
login := account beginLogin.
login authorizationUrl.
```
Inspect `login state` and `login error` after the browser flow. `login cancel`
cancels the operation. `login wait` returns after cleanup. Do not wait in the UI
process if the window must remain responsive. The login limit is 15 minutes.
An occupied callback port returns a failed login without a usable URL.
The credential record uses service `SmallGPTalk.OpenAI`, account `default`, in
the user login Keychain. Credentials renew before expiry. `account logout`
deletes the record and invalidates its pending login. Native storage failures
are reported explicitly. Tokens are not passed through shell arguments.
Accounts for the same credential record share `SmallGPTalkAccountAccess` in
the image. It coordinates renewal, login commit, and logout. Logout from one
account cancels a login started by another account for that record. It waits
for a renewal that has already started, then deletes the renewed record.
Separate Keychain adapters use service and account as their coordination key.
Custom stores can supply `coordinationKey`; otherwise supply the same store
object to accounts that share credentials. This coordination does not span
separate Pharo processes.
An HTTP 401 model failure permits one credential renewal and one new attempt
with the same request body. A second rejection stops the request. Other HTTP
failures and broken streams are not retried. Completed tool results remain in
the request; the agent loop is not restarted. Renewal must be stored before
the new credentials can be used. A late rejection uses a newer stored token
without renewing it again, and cannot restore a deleted login.
`withHeadersDo:` is the account's request protocol. Its block must send only
one request; it can run twice after HTTP 401. Do not put image operations in
that block. The model adapter only sends its already encoded body there.
An active image can contain temporary token copies. Do not distribute a saved
image that has used credentials. The live check scripts exit without saving.
## Compose an agent and session
The provider reads the account catalog and validates each selection. Its
configured default is `gpt-5.6-luna` with effort `low`. If that pair is not
available, selection fails without choosing a substitute.
```smalltalk
provider := SmallGPTalkOpenAIProvider account: account.
provider models inspect.
model := provider defaultModel.
agent := SmallGPTalkAgent named: 'Ada'.
agent instructions: 'You are Ada, a Smalltalk agent in Pharo.
Use evaluate for image operations. Do not repeat completed changes.'.
session := agent newSessionWith: model.
session tools: { SmallGPTalkEvaluate new }.
run := session send: 'Evaluate 6 factorial and explain the result.'.
```
`send:` returns immediately. From another process, `run wait` returns the run
after completion, failure, or cancellation. Inspect `run state`, `run result`,
and `run error`. A successful OpenAI result is a `SmallGPTalkReply` with text.
More than one process can wait for the same run. Interrupting one waiting
process does not remove the completion signal needed by other waiters.
For an explicit selection, use `provider model: 'gpt-5.6-luna' effort: 'high'`.
Each catalog description answers `identifier` and `efforts`. Call
`provider refreshModels` to read the catalog again. A failed refresh preserves
the last valid descriptions. The low-level model and transport constructors
remain available for explicit composition and tests.
The model protocol is `respondTo:`. The model receives a request object, not
the session. Tests supply this protocol without a framework or live provider.
The history uses one reply protocol. `SmallGPTalkReply from:` adapts model
values at the boundary. A structured reply returns itself from
`asSmallGPTalkReply`. A simple value uses `SmallGPTalkValueReply`, which retains
the original object as its `result`. A string also supplies recorded text;
other values have no display text and cannot be sent as provider text.
Custom values can implement `asSmallGPTalkReply` without inheriting from a
SmallGPTalk class.
Replies answer `text`, `calls`, `context`, `usage`, `characterCount`, and
`result`. The loop validates and records them through these messages. The UI,
context measurement, and fork do not select behavior by reply class.
`run result` and `exchange response` return the response value: a structured
reply returns that reply, while a simple value returns the original object,
including nil. Fork copies reply records and their contexts. Simple values
remain shared by reference. Recorded string text does not change when the
original string changes.
## Inspect the objects
```smalltalk
session conversation inspect.
run exchange inspect.
run exchange context inspect.
run exchange modelContext inspect.
run exchange calls inspect.
```
For a particular call, `exchange contextForCall: call` returns the request that
produced it. Each complete reply retains that request. Fork maps these contexts
to the copied history and call records, while preserving live values by reference.
The chat's Inspect evaluations button opens a view with source and recorded
output, plus separate inspectors for the value, call, and originating context.
Use Refresh to update that view while its exchange is still active.
An exchange retains its request, replies, calls, state, and error. `context`
is its initial request. `modelContext` is the last selected model request,
including any summary. A call retains its arguments and evaluation operation.
The operation retains source, actual result, bounded output, and failure text.
Inspect `call operation` for the evaluation and `call result` for its live value.
Output is recorded once. Later changes to a result do not change that output.
Truncation sets `truncated` and adds an ellipsis. It does not shorten the real
result object. Compiler and ordinary execution errors are caught without
opening a debugger.
## Execution and limits
One run can be active per session. One run with image tools can hold the image
at a time. Calls execute in order after the complete reply has been validated.
Incomplete provider responses are discarded. Tool errors return to the model.
Completed conversation entries remain after failure.
`run cancel` or `session cancel` stops further work. Committed calls that have
not started receive cancelled results. Model requests, including summary
requests, start under the same lock used to accept cancellation. A request
whose start is refused does not invoke the model.
Completed image changes remain.
Ordinary Smalltalk workers stop before cancellation completes. Foreign calls
can delay termination. Image access has no sandbox or automatic rollback.
| Setting | Default | Configuration |
| --- | --- | --- |
| Tool-loop turns | 20 | `session maxTurns:` |
| Model request | 120 seconds | `session requestTimeoutSeconds:` |
| Evaluation | 10 seconds | `tool timeoutSeconds:` |
| Printed output | 20,000 characters | `tool outputLimit:` |
| Automatic compaction | 80% of model capacity, using estimated input when available | `session context autoCompactPercentage:` |
| Character fallback | 60,000 c