7.9 KiB
7.9 KiB
Decisions
This file lists the design decisions of the spike. (you) marks a decision that Surma made. (spike) marks a decision that Scout made alone during the night and that needs review.
Architecture
- (you) Actors are WASI commands (
wasm32-wasip2). They use stdin, stdout, files, and HTTP. No WIT interfaces, no component composition. - (you) Actors exchange newline-delimited JSON through the host, which is
the broker. No built-in request/response. A reply is a message with
reply_to. Delivery is at most once. - (you) The broker accepts only
.wasmfiles. It knows no source language. - (you)
cargois a built-in Rust compiler actor. (spike) It is native (host code), because cargo cannot run inside WASM. - (you) The agent is a normal WASM actor with the fixed ID
agent. It can read and upgrade its own source. - (spike) Three native actors:
broker,console,cargo. - (you) One Cargo workspace with two crates:
cli(binarysausage) andagent. (spike) Example actors are inexamples/, outside the workspace. - (you)
sausage initbuilds the agent and asks for the API shape, the endpoint, the model, and the token. (spike) The token can also come fromSAUSAGE_API_TOKEN. The source contains no endpoint and no token.
Messages
- (spike) Stdin line:
{"id", "from", "reply_to"?, "body"}. Stdout line:{"to"?, "id"?, "reply_to"?, "body"}. - (spike) The broker sets
from. An actor can choose its ownid. The broker then uses"<sender>/<id>", so IDs cannot collide or be faked. Without anid, the broker assignsm-<n>. - (you) Without
to, a message goes to the subscribers of the sender. (spike)subscribe/unsubscribemessages to the broker. The registry stores subscriptions, so they survive restarts and upgrades.initsubscribesconsoletoagent. - (spike) Errors from routing come back as
{"type": "undeliverable", "reason", "to"}frombroker. - (spike) Lifecycle:
{"type": "shutdown"}frombroker, then EOF on stdin. After 5 seconds the broker kills the instance. - (spike) Lines on stdout that are not JSON objects with
bodygo to the actor'sstderr.logwith a note.
Filesystem
- (you)
/dataprivate,/publicown public files,/actor/<id>/publicand/actor/<id>/sourceread-only for everyone. - (spike) The broker also writes
/actor/<id>/status.jsonand/actor/<id>/stderr.log.ls /actoris the registry. - (you) Commit on close: a write becomes visible only when the writer
closes the file. (spike) This also applies to
/data. Implementation: a wrapper around thewasmtime-wasifilesystem. An open for write goes to a hidden.sausage-tmp-*file next to the target. The descriptor drop renames it into place. Details:- A normal exit commits every open file (process exit closes files).
- A trap or kill discards uncommitted writes.
- An append or an edit without truncate copies the old content first.
- Other descriptors see the old content until the close.
- Directory operations take effect immediately.
- The layer hides
.sausage-tmp-*names from listings,stat,open,rename, andunlink. - Symlinks are disabled (they would break the path tracking).
- No
fsync. A power loss can lose committed data.
- (spike) Raw sockets are not available (no network permission), but the
wasi:socketsinterfaces are linked, so programs that import them still start.
Broker requests
- (spike)
spawn,upgrade,rollback,stop,start,remove,subscribe,unsubscribe,list. - (you)
spawnandupgradetakewasm, plus optionalsourceandbuilt_bymetadata. (spike) The broker copies the.wasmfile and the source folder intobroker/versions/<id>/<n>/, withmeta.json(paths, builder, requester, time). - (spike) Path resolution:
/actor/...is global, and/public/...and/data/...belong to the sender...is rejected. Native senders (the console) can give absolute host paths. - (you) Anyone can upgrade anyone. No permissions.
- (spike) Actor IDs:
a-z,0-9,-,_, at most 64 characters. - (spike)
removearchives every folder of the actor toarchive/. Nothing is deleted.
Upgrades, tests, rollback
- (spike) The tests are message transcripts in
source/tests/*.jsonl:{"send": body}and{"expect": subset-of-body, "to"?}. Each file runs in a fresh candidate on a copy of the actor's/dataand/public. The candidate has no HTTP access. Anexpectwaits up to 10 s. Without tests, the candidate must stay up for 2 s (smoke test). - (spike) Swap: hold the mailbox, stop the old instance gracefully,
snapshot
/data+/publicinto the old version's folder, install the new source in/actor/<id>/source, start the new instance, release the mailbox. - (spike) Restart after a crash: backoff 0.5 s × 2ⁿ, at most 30 s. The counter resets after 60 s of uptime.
- (spike) Parked message: when an instance traps, the broker keeps the last delivered message. It delivers that message again after the next upgrade, not after a plain restart (that would loop).
- (spike) Crash loop: 3 crashes within 30 s after an upgrade → automatic rollback to the previous version, with its data snapshot. The broker informs the actor that requested the upgrade.
- (spike)
rollbackwithoutversiongoes to the highest older version. It restores that version's snapshot, and it first snapshots the current state, so a roll-forward is possible. - (spike) Versions are per actor, not global.
Limits
- (spike) Mailbox: 1,000 messages. When it is full, the sender gets
undeliverable. - (spike) The broker waits 30 s when it writes to an actor's stdin. Then it drops the message and logs it.
- (spike) Time slices: the epoch advances every 10 ms, and each guest yields at each epoch.
- (spike) One
sausage runper world (file lock onworld/.lock). - (spike) HTTP for all running actors, with no allow-list yet.
Agent
- (you) API shapes: OpenAI Responses and Anthropic Messages, chosen by
"api"in/data/config.json. - (spike) Config fields:
api,base_url,model,token, optionalreasoning_effortandmax_tokens(16,000). OpenAI:POST {base_url}/v1/responseswithstore: falseand encrypted reasoning passed back. Anthropic:POST {base_url}/v1/messageswithx-api-key. - (spike) One shared conversation in
/data/history.json, in a neutral format, so the API shape can change. The history is limited to 600 entries, with the cut at a user message. There is no compaction yet. - (spike) A turn starts only for
{"type": "text"}messages withoutreply_to. Replies never start turns, so two actors cannot keep the agent in an endless loop. Other messages are collected and shown at the next turn. - (spike) Tools:
send_message(with optional wait for the reply),wait_for_messages,read_file,list_dir,write_file,copy_dir,remove_path. At most 40 steps per turn. Tool output is cut at 30 KB. - (spike) The final text of a turn goes to the sender, as a reply to the message that started the turn.
- (spike) A shutdown during a turn saves the turn to
/data/turn.json. The next start (for example after a self-upgrade) resumes the turn and answers the original message.
Console
- (spike)
@<actor> <json|text>, plain text toagent,/deploy,/list,/sleep,/await,/help,/quit./sleepand/awaitexist for scripted tests.
Known risks (accepted for now)
- Build scripts and proc macros run as native code in the host environment.
- Any actor can upgrade the agent with code that reads the token.
- Any actor can message, stop, upgrade, or remove any actor.
- A full copy of
/dataand/publicat every upgrade is slow for large data. - Mailboxes are in memory. A host crash loses queued messages.