cargo.build and the runner (review T22R): builds use the network again; core.unlock and core.notify in order under the runner; quiet test output; manifest preparation keeps [workspace], target tables, renamed and optional entries; cargo.rpc.discover; -32004 with data.output; retention of 10 builds; light, empty test worlds with an in-memory journal; only CARGO_/RUST_/SAUSAGE_ variables; stdin is null; the runner through --config. A compile cache that cannot be created is not fatal. Activation replaces plugin.wasm without a gap. Console and main: init ships the workspace Cargo.lock and creates the world atomically; each console command runs in its own task, so ps, stop, and kill work during a slow call; SAUSAGE_HEADLESS; the terminal mode is restored; signals during boot; chat text is not taken as a command. Tests: 20 more tests for PROTOCOL.md sections 2 to 10; the cargo tests choose the libtest binary from cargo's JSON output and time out.
Sausage
You can see how the sausage is made.
Sausage is a world of small WASI programs ("plugins") that you grow by
talking to it. A plugin is a .wasm file with JSON-RPC methods. Each call
to a plugin starts a new process. The agent is a plugin too: it writes new
plugins in Rust, builds them, tests them, activates them, and can change its
own code.
Start
You need Nix with flakes, and an API token for an LLM (OpenAI Responses or Anthropic Messages API). No clone needed:
nix run git+https://gitea.surma.technology/surma/sausage
This creates a world in ./world the first time (it builds the agent, which
takes a few minutes), then starts it with a console. Later runs start the
existing world. The two steps on their own:
nix run git+https://gitea.surma.technology/surma/sausage -- init # creates ./world and builds the agent
nix run git+https://gitea.surma.technology/surma/sausage -- run # starts the world and a console
init asks for the API shape (openai-responses or anthropic-messages),
the base URL, the model, and the token. It reads SAUSAGE_API,
SAUSAGE_BASE_URL, SAUSAGE_MODEL, and SAUSAGE_API_TOKEN first. The
values go to world/config/agent.json. Both commands take another folder as
an argument, for example ... -- run my-world.
To keep a world on the newest version, run it in a loop:
nix run git+https://gitea.surma.technology/surma/sausage#loop # ./world, created if needed
nix run git+https://gitea.surma.technology/surma/sausage#loop -- my-world # an existing world
When the host crashes or a plugin calls core.exit, the loop fetches the
newest commit and starts it again. quit ends the loop. Set SAUSAGE_FLAKE
to run another flake URL instead, for example your fork.
From a clone (git clone https://gitea.surma.technology/surma/sausage), the
same commands are nix run, nix run -- init, nix run -- run, and
nix run .#loop.
Headless
--headless (or SAUSAGE_HEADLESS=1) turns the console input off. The host
runs until SIGTERM or SIGINT (exit code 0) or until a plugin calls
core.exit (exit code 75, so the loop starts the newest version):
nix run git+https://gitea.surma.technology/surma/sausage#loop -- --headless my-world
The console
| Input | Effect |
|---|---|
| text | a message to the agent (agent.chat) |
<plugin>.<method> {json} |
a call. The console prints the result. |
ls [path], cat <path> |
the filesystem as a process sees it, with /proc and /run |
methods <plugin> |
the methods of a plugin, with params and docs |
ps |
the running processes |
log [plugin or pid] [-n N] |
the journal |
activate <plugin> <wasm>, revert <plugin> |
activate a new version, or go back |
stop <pid>, kill <pid> |
stop a process |
help, quit |
The world folder
plugins/<name>/plugin.wasm the current version of a plugin
plugins/<name>/source/ its source
state/<plugin>/ the state of a plugin, by convention
state/core/journal.sqlite the journal: stderr of all processes, and core events
config/boot.json the services that start with the world
config/agent.json the configuration of the agent
Each process also sees /proc (the processes and the state of the core),
/self (its own folder in /proc), and /run (shared files in memory).
To replace a plugin by hand, copy a .wasm file to its plugin.wasm. That
skips the interface checks. revert <plugin> goes back to the version
before the last activation.
More
- PROTOCOL.md: calls, processes, the filesystem, locks,
interfaces, and
cargo. Read this to write a plugin in any language. - DECISIONS.md: why Sausage is the way it is.
sdk/:sausage-plugin, the SDK for Rust plugins.examples/: small plugins.- AGENTS.md: how to work on Sausage itself.