a self-modifying agentic system

The agent rewrites itself while it runs.

Thetis is an agent whose loop, tools and chat interface are WebAssembly components it can rewrite while it is running. Every change is compiled, validated, versioned, and reversible.

Thetis was a shapeshifter, and Peleus won her by keeping his hold through fire, water, lion and serpent until she was herself again.

αthe why

Shaped by what you asked it last week

Every agent you can buy arrived finished. How it works and what it can reach were settled at build time, by strangers, for everyone at once. Thetis leaves those questions open and works them out in the conversation, with the person having it.

Say a price on some page matters to you, and it has no way to read that page: it writes itself the tool mid-reply, compiles it, and calls it before it answers. Say the timestamps under every message are clutter, and it edits the chat interface it is serving you, and the clutter is gone. Work that comes round again can be written down as a skill, a markdown file in skills/ that it reads back into any conversation that needs it. None of this is a feature request. It happens between one message and the next.

The same mechanism keeps it current with the rest of the world. A model released this week, or a service it has never heard of, is something it can integrate for itself, in place, without waiting on anybody's roadmap. The evidence is the shelf of tools it has already built: BigQuery, Notion, a browser it drives page by page, git plumbing, web search. None of it came in the box.

The gains hold. Every conversation runs on its own branch of the source, so an experiment stays where it was made; the changes that earn it get merged, and every conversation after that one starts from the merged version. A month of small requests leaves a month of small permanent improvements behind.

Six months in, the thing answering you is one that nobody shipped.

All of which stays survivable because a change has to compile, load, and pass a smoke test before it can go live, and trunk only advances when a person merges it.

βthe contract

The grip

The forms are the guests, and the contract that holds them while they change is the grip, written once in wit/thetis.wit.

One native binary, the orchestrator, is the trusted kernel. It owns the network, the filesystem, the database and the build toolchain, and hands guests narrow, mediated slices of them through that contract. Guests hold no ambient authority: everything they can observe or affect is an import declared in the file. Changing the file rebuilds every guest, so it stays orchestrator-owned and the dev kit refuses agent writes that touch it.

Guests are instantiated per call, in a fresh store. That is what makes hot swapping safe. A turn already in flight finishes on the component it started with, and the next call picks up the new one, so no turn ever straddles two revisions and a swap needs no quiet moment to land in.

Diagram: the orchestrator owns network, filesystem, database and build toolchain; the grip contract mediates everything the agent, gateway and tool guests can reach.
γthe forms

Three kinds of guest

Every aspect is a component built from source in the repository, and every one of them can be replaced while the process keeps running.

δthe change

What the agent can do to itself

These tools appear in the model's tool list whenever the dev kit is available. Every mutating one rebuilds the target immediately and returns the compiler's verdict in the tool result, so the model reads its own build errors and fixes them inside a single turn instead of waiting for a human to relay them.

ToolEffect
new_tool(name, description)Scaffolds a tool crate, builds it, loads it
write_code(target, path, contents)Replaces a file, rebuilds, hot-swaps
patch_code(target, path, old_text, new_text)Exact-match patch, rebuilds, hot-swaps
read_code / list_codeInspection
add_dependency / remove_dependencyEdits a component's dependencies, rebuilds under a timeout
branch_status / branch_logWhere this conversation's branch stands, and its history
reset_branch(rev)Restores the branch to rev as a new commit — nothing rewritten
update_from_trunkBrings the latest trunk into the branch; conflicts come back marked

target is self, tool:<name>, or gateway:<name>. A tool created this way is callable on the very next loop iteration. A change the agent makes to itself takes effect when the current turn ends, so the turn that writes a new loop finishes on the old one. Every conversation runs on its own git branch of the source tree, and every green build lands there as a commit. One verb is deliberately missing: merging to trunk. Landing a conversation's changes is a human decision, made in the UI.

εthe hold

Staying recoverable

The system is built so that no self-modification can make it unreachable.

  • Validation gate. A candidate must compile, load as a component for its world, and pass a smoke test before it goes live: the agent answers a health probe, a gateway serves its index page, a tool returns a valid manifest. A candidate that fails is recorded and set aside, and the running revision is untouched.
  • Epoch watchdog. Every guest call runs under a wall-clock budget and a limit on how long it may execute without yielding to a host call. An infinite loop in the agent traps in seconds and the process is unharmed.
  • Circuit breaker. Repeated traps from one aspect roll it back to its last known-good build automatically, and the incident is written into the conversation so the user sees what happened.
  • Branches and the build cache. Every green build is a commit on the conversation's branch, and the built component — with its smoke-test verdict — sits in a content-addressed cache keyed by the source tree that produced it. The binary is looked up by the source, so the code the agent reads is always the code that is running, and rolling back means putting the tree back at a commit whose key is green.
  • Trunk stays human. Experiments live on the conversation's branch. Trunk — what every new conversation starts from — only ever advances when a person merges a branch into it.
  • /admin. A control panel rendered by the orchestrator itself, with no WebAssembly in its path. It shows trunk's commits and every conversation's branch and worker, with the levers to stop, merge and reset beside them, and keeps working when every guest and every worker is broken.

Writes are constrained too: paths cannot escape an aspect's source tree, and devkit.protected_files can put any file off limits. The grip itself, wit/thetis.wit, is always refused — the agent can rewrite what it does, never what it is allowed to reach.

Diagram: a conversation's branch leaves trunk, green builds land as commits, a failed candidate never lands, reset_branch restores a green commit, and merging back to trunk is a human decision.
ζthe surface

The chat surface

The web UI is an ES-module app with no build step, embedded in the gateway component. Adding a file means one line in assets.rs; adding a client action means one function in handlers.rs and one entry in its dispatch table.

Each conversation carries a mode, Agent by default. Plan withholds every tool that would change something and refuses those tools at dispatch too, so a model that remembers a tool from earlier in the conversation still cannot call it. Models are a per-conversation override chosen from THETIS_MODELS. Skills are markdown files in skills/, attached per conversation and appended to the system prompt; editing one takes effect on the next turn, with nothing to restart and nothing to register. The tools panel shows what the model is offered here, asked of the agent itself rather than reconstructed, so it cannot drift from reality. Images can be pasted, dropped or picked from the file browser, and travel base64-encoded end to end.

Cache hits show in the transcript under each reply, because a saving you cannot see is one you cannot trust. On a three-turn conversation with the full tool surface, the opening turn cost $0.0106 and every turn after it reported 99% of its prompt served from cache, at $0.0010.

ηthe reach

Work that outruns one conversation

A conversation can fan out. spawn_agent starts a child session with its own event log, context window and turn loop, working in the same checkout, so the parent pays for the conclusion instead of every step that produced it. Children cannot spawn children: unbounded fan-out is one of the recurring ways multi-agent systems collapse, and one level keeps the parent identifiable as the thing responsible for the work.

Notes saved with remember survive restarts and self-modification. Compaction sheds the middle of a long conversation, keeping head, tail and every user message, and never edits the event log. The log is append-only; a compaction records which spans it stands for, and rehydration projects the log through those records.

The tool surface is scoped to what a conversation looks like it needs, because large flat catalogs cost accuracy as well as tokens. tool_search is always offered, even in read-only modes, so the escape hatch cannot itself be scoped away.

Terminals open on registered ssh hosts, from a registry the publish filter never sees. The same agent answers in Discord, where every session is stamped read-only and there is no command to change that.

the shelf so far — tools/ in the repo, every one scaffolded in a conversation

bq-describe bq-execute bq-jobs bq-list bq-peek bq-profile bq-query bq-query-cost bq-results config-probe git-commit git-file git-repo git-whoami notion-comment-add notion-comment-list notion-database-list notion-database-query notion-database-schema notion-page-content notion-page-create notion-page-get notion-page-update notion-search notion-users web-browser-click web-browser-close web-browser-console web-browser-evaluate web-browser-hover web-browser-navigate web-browser-network web-browser-screenshot web-browser-snapshot web-browser-state web-browser-status web-browser-tabs web-browser-type web-browser-wait web-content web-search web-summarize
!read this
sharp edges

Be clear-eyed about what enabling terminal access means: the agent can run anything you can. Sessions keep their working directory and state between commands, and filesystem.enabled, filesystem.allow_delete, terminal.enabled and control.allow_restart each turn a slice of that off.

The roots are the boundary. Every path is resolved, symlinks followed, and must land inside filesystem.roots, which defaults to the project root alone.

Two things in the README are listed under Not yet built. The Docker exec sandbox is defined and wired but stubbed, so with THETIS_SANDBOX=false the agent is not offered code-execution tools rather than handed tools that fail. MCP imports exist and return empty; no client is connected yet.

θthe start

Running it

Requires the Rust toolchain with the wasm32-wasip2 target.

rustup target add wasm32-wasip2

Set an OpenRouter key and start it. Guests are built automatically on boot.

OPENROUTER_API_KEY=sk-... cargo run --release -p thetis

Then open http://127.0.0.1:7777.

Without an API key

A scriptable stand-in speaks enough of the streaming protocol to exercise the whole system at no cost: token deltas, tool calls, usage accounting.

cargo run --release --bin mock-llm
OPENROUTER_API_KEY=test OPENROUTER_BASE_URL=http://127.0.0.1:7788 \
THETIS_MODEL=mock/echo cargo run --release -p thetis

From there, the docs cover the rest: getting started, architecture, self-modification, recoverability, what reaches beyond one conversation and configuration.

Peleus did not stop the changing. He kept his grip while it happened, and the shape that came out the other side was the one he wanted. Thetis changes all day, under a contract that never lets go.