Files
sausage/DECISIONS.md
T
surma d7b78ed0fd core: review fixes, init builds the agent, console without a terminal
Activation path (review T6R): a broken plugin or a stray file in
/plugins blocks no activation; candidates replace their own entries;
discovery has a 10 s deadline and its process is stopped after the
answer; an unchanged hash keeps plugin.prev.wasm; activations run in
their own task; the watcher is not recursive; no duplicate services;
rpc.retired waits for live processes of its version; reads and hashes
on blocking threads.

Guest I/O and per-call paths: a stdin write error is EOF, stdout and
stderr are drained before the status; stderr is lossy UTF-8 and goes
to the journal from its own reader; compiled components are freed;
shutdown starts no new processes; lock queues drop stopping processes;
compile before the admission gate; CommandPre per component;
core.unlock in order; null errors are successes; the journal thread
survives SQLite errors.

sausage init builds the agent with cargo.build; sausage without a
command creates ./world. The console reads plain lines without a
terminal. PROTOCOL.md and DECISIONS.md describe the decided rules. The
flake tests the whole workspace and checks the packaged cargo.build.
2026-10-23 02:25:03 +01:00

4.1 KiB

Decisions

Why Sausage is the way it is. PROTOCOL.md and the code own the details. When this file disagrees with them, they win.

The shape

  • JSON-RPC, not component interfaces. Every language can read and write lines of JSON, and models know JSON-RPC from LSP and MCP. A call has a result, like an LLM tool call.
  • A process per call. WASI has no threads, so processes give parallelism. A new version applies at the next call, and old processes retire after a grace period.
  • Attached by default. A process does not outlive its caller by accident. A long-running process must ask for detach.
  • Services in one file. Only the core starts and restarts them, so upgrades and singletons need no logic in plugins.
  • Files to inspect, calls to act. This keeps the core API small, and the agent's file tools are enough to explore the world.
  • The core accepts only .wasm files. It knows no source language. cargo is one way to get a .wasm file.
  • The world folder is the source of truth. Copy a plugin folder into another world, and it works there.
  • The agent is a normal plugin. It can read and upgrade its own source, and change how it does inference.

Interfaces

  • Generated imports and interface checks, without version numbers. A model that translates a description by hand makes errors. A generator does not. The check catches the most common error in a world that changes itself: a change to one plugin breaks another one.
  • A small schema subset. With Draft 7 and a fixed subset, our own check can decide every case. It never has to guess.
  • Docs are part of the interface. Other agents work from them, so the SDK requires a doc comment on each method, type, and field.
  • The check covers calls, not state. Old and new versions share /state. A plugin keeps its state format compatible, or migrates it in rpc.retired.

Minimal on purpose

The core does only what a plugin cannot do for itself. Everything else can be a plugin, or a change that the agent makes to itself.

  • No permissions, no limits, no hiding. Any process can call, upgrade, or stop any plugin, read and write every file, and use the network. The checks protect against mistakes, not against plugins. Locks are advisory.
  • Locks are in the core. A plugin cannot see that another process died, so it cannot release that process's lock. The core can.
  • Our own VFS, in its own crate. It works on Linux and macOS, it makes /proc live, and plugin-served files need it later. Mounts now, overlays later.
  • TCP endpoints for now. They need no host code. VFS pipes can replace them later behind the same SDK API.
  • Simple scheduling for now. An epoch tick every 10 milliseconds keeps the core responsive and makes kills reliable.
  • The agent is dumb. It has a short prompt, a few tools, an append-only history that it sends in full, and no compaction. When the context is too large, it stops sending the oldest turns, so it never gets stuck.

Small choices

  • Plain POSIX files. Programs like tempfile, git, and SQLite expect normal files. Atomic writes use a temporary file and rename.
  • Tests run under WASI. cargo test on the host cannot find failures that occur only in WASI, for example a missing WASI function or a panic at start. The test runner uses the same Wasmtime and the same mounts as the core.
  • The SDK is not published. cargo points sausage-plugin = "0.1" at its own copy, which it also shows in /plugins/cargo/sdk/.
  • Discovery has a deadline of 10 seconds. It is an internal call of the core, not a limit on plugins. Without it, one plugin that does not answer would block every activation.
  • A broken plugin blocks nobody. A plugin without a document is already broken, so the check cannot protect it. The graph shows it as "unavailable", and core.revert and force always work.
  • rpc.retired is best effort. It does not run if the host stops first. A persistent queue of retirements would be more core for a rare case, so a new version checks its own migration marker instead.