Write new journal entries under logs/core and merge read-only legacy history without moving SQLite/WAL files. Atomically spool failed batches, retaining unspoolable entries for event-driven retries. Journal full trap diagnostics before UTF-8-safe status display. Port session overflow archives and followable line/byte excerpts to the still-shipped embedded agent. Document retention/recovery and cover the contracts with regressions. Include one rustfmt-only normalization in the existing SDK WASI test. Card: SAU-143
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 LLM: the OpenAI Responses API, an OpenAI-compatible Chat Completions API (OpenRouter, Ollama, and most other providers), or the 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: the
first build on a machine takes a few minutes, later worlds reuse the compiled
crates), 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 nothing. It writes world/config/agent.yaml, with a comment for
each field and an example for each API. Edit it before the first chat: choose
the API, the base URL, the model, the key, and the thinking level. Until then, a chat answers with
an error that names the missing field. Both commands take another folder as
an argument, for example ... -- run my-world.
Compiled code is cached outside the world, in $XDG_CACHE_HOME/sausage/
(~/.cache/sausage/ without the variable). All worlds share it.
--cache-dir <folder> changes it for init and run.
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.restart, the loop fetches the
newest commit and starts it again. The loop sets SAUSAGE_LOOP=1 to enable
that RPC; without it, restart returns an error and leaves the host running.
Restart answers first, then cleanly stops all processes and exits with code
75. quit (code 0) ends the loop. Set SAUSAGE_FLAKE to run another flake
URL instead, for example your fork.
The mediator and all agents NEVER call core.restart without the person's
explicit approval each time. This also applies to legacy core.exit, an
alias with the same supervisor gate, response-before-exit behavior, and
request coalescing.
See the protocol for the shutdown contract.
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.restart (requires SAUSAGE_LOOP=1; exit code 75 restarts the loop):
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.yaml the services that start with the world
config/agent.yaml 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.