Make legacy core.exit share core.restart's supervisor gate and pending request. Seed the stable agent prompt and boot comments with the approval and shutdown contract, and check real init output under a deadline. Cover both aliases' gate, response grace, coalescing and exit status. Card: SAU-135
111 lines
6.1 KiB
Markdown
111 lines
6.1 KiB
Markdown
# 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
|
||
|
||
- **Restart is a clean stop supervised by `#loop`.** `core.restart` requires
|
||
the host startup marker `SAUSAGE_LOOP=1`, which the loop exports for its
|
||
invocation. Without a supervisor, restarting would only stop the world.
|
||
A single pending request answers first, allows 250 ms for delivery, then
|
||
signals the existing shutdown path; it does not use `process::exit` or
|
||
bypass detached-process cleanup, lock release, or journal draining. The
|
||
executable returns dedicated restart code **75** after cleanup. `#loop`
|
||
refreshes and starts again on 75 (and other nonzero exits); 0 stops it.
|
||
`core.exit` is a compatibility alias with the same supervisor gate,
|
||
response grace, and coalescing; new callers use `core.restart`.
|
||
**HARD RULE: the mediator and all agents NEVER call `core.restart` without
|
||
the person's explicit approval each time.** This
|
||
approval rule also covers legacy `core.exit`; it must not bypass the gate.
|
||
|
||
- **YAML for configuration, JSON for data.** A person edits configuration, so
|
||
it needs comments. `init` writes commented templates instead of asking
|
||
questions, so there is one way to configure a world.
|
||
- **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/`.
|
||
- **Compiled code is a cache, not state.** It lives outside the world, in
|
||
one cache folder that all worlds share: `wasmtime/` (owned by Wasmtime,
|
||
whose cleanup deletes unknown entries), `cargo-target/`, and
|
||
`sdk/<hash>/`. A path that depends only on the content of the SDK lets
|
||
cargo reuse the compiled SDK in every world. Tests run in the release
|
||
profile, so that they share compiled crates with the release build.
|
||
- **mold links host code where Nix provides it.** On Linux, the dev shell,
|
||
the Nix builds, and the packaged `sausage` (for the proc macros and build
|
||
scripts of plugins) link with mold, much faster than GNU ld. Plugins for
|
||
`wasm32-wasip2` link with rust-lld. Without Nix, the default linker stays.
|
||
- **No LTO.** Measured: fat LTO makes the binary 28 % smaller, but compiles
|
||
plugins and answers calls at most 5–8 % faster, for 4 more minutes per
|
||
release build. Thin LTO brings nothing.
|
||
- **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.
|