Skip to content
arcshipsPublic

About

A plugin runtime for programs that keep running — Rust core, TypeScript and Python plugins, across processes and machines.

Topics

Resources

Contributing

Security policy

Stars

59 stars

Watchers

0 watching

Forks

Repository files navigation

rutis

A plugin runtime for programs that keep running

Plugins say what they need and what they provide; rutis decides when they start, when they stop, and when they start again.
A Rust core · plugins in TypeScript and Python · across processes and machines

crates.io npm PyPI docs.rs CI License: MIT

Quick start · Guides · API docs · 中文


Editors, chat bots, agents, composable servers: once a program accepts plugins, it runs into the same questions. In what order do plugins start? What happens while a dependency is missing? When a service is replaced, who has to restart? Did unloading leave anything behind? Does changing one setting mean restarting the whole process?

rutis turns those questions into declarations. A plugin states which services it depends on and the runtime does the rest: it starts the plugin once its dependencies are there, stops it when they go away, and reloads it when a provider is replaced. Everything a plugin registers while starting is cleaned up exactly once, in reverse order, when it stops.

The model comes from Cordis in the TypeScript ecosystem. rutis is its idiomatic Rust implementation, and carries the same model to other languages and other machines.

Features

  • Dependencies drive the lifecycle — declare what you depend on; when to start, stop and reload is up to the runtime. Typed plugins keep the declared dependencies and the ones actually used in agreement at compile time.
  • Cleanup you can rely on — each plugin runs in its own fiber. Services, listeners and child plugins are registered under it and released exactly once, LIFO, on unload; a failed load rolls back what it had registered.
  • Change without downtime — hot-update configuration, swap providers, add and remove plugins; only what depends on the change restarts.
  • Plugins in other languages — TypeScript, JavaScript and Python plugins follow the same model. Services are called across languages, and a plugin need not know what its peers are written in or where they run.
  • Many nodes — hosts link over WebSocket and TLS to share services, run plugins on another machine, forward events, and reconnect after a drop.
  • Data-driven — rutis-loader describes the plugins to run as layered configuration and keeps reconciling it; rutis-host runs plugins without a line of Rust.

Quick start

In Rust

cargo add rutis
cargo add tokio --features full
use std::sync::Arc;
use rutis::{BoxFuture, CordisError, Ctx, Effect, Plugin, Typed, TypedPlugin};

/// A service is a type.
struct Greeting(String);

/// Provides Greeting. What apply registers is released when the plugin stops.
struct Greeter(&'static str);

impl Plugin for Greeter {
    fn name(&self) -> &str { "greeter" }

    fn apply<'a>(&'a self, ctx: &'a Ctx) -> BoxFuture<'a, Result<Effect, CordisError>> {
        Box::pin(async move {
            ctx.provide(Greeting(format!("hello from {}", self.0)))?;
            Ok(Effect::Done)
        })
    }
}

/// Depends on Greeting: starts when it appears, restarts when it is replaced.
struct Listener;

impl TypedPlugin for Listener {
    type Deps = (Arc<Greeting>,);

    fn name(&self) -> &str { "listener" }

    fn apply<'a>(&'a self, _: &'a Ctx, (greeting,): Self::Deps) -> BoxFuture<'a, Result<Effect, CordisError>> {
        Box::pin(async move {
            println!("{}", greeting.0);
            Ok(Effect::Done)
        })
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ctx = Ctx::root()?;
    let listener = ctx.plugin(Typed::new(Listener));  // waits for a Greeting

    let english = ctx.plugin(Greeter("English"));
    (&english).await?;
    (&listener).await?;                               // hello from English

    english.dispose().await?;                         // the listener stops…
    let esperanto = ctx.plugin(Greeter("Esperanto"));
    (&esperanto).await?;
    (&listener).await?;                               // …and starts again: hello from Esperanto

    ctx.shutdown().await?;
    Ok(())
}

Nothing touched Listener: when the provider changed, it stopped and started again on its own. Run it from the repository with cargo run -p rutis --example quickstart.

Without Rust

npx @arcships/rutis-host new weather --lang node
cd weather && npm install
npx rutis-host dev          # run the plugin, reload when files change

For Python, create the project with uvx rutis-host new weather --lang python, then uv sync and uv run rutis-host dev.

import { definePlugin } from '@arcships/rutis'

interface Llm {
  ask(question: string): Promise<string>
}

export default definePlugin<{ city?: string }>({
  inject: ['llm'],                             // services it needs: starts once all are there
  provides: { weather: { today: 'async' } },   // services it offers, and how each method is called
  apply(ctx, config) {
    const llm = ctx.use<Llm>('llm')
    const city = config.city ?? 'Oslo'
    ctx.provide('weather', {
      today: () => llm.ask(`weather in ${city}`),
    })
  },
})

llm can come from another plugin in the same process, from a Python plugin, or from another machine; this plugin stays the same. The full workflow is in TypeScript plugins and Python plugins.

How it works

A plugin is a unit of assembly: one apply provides services, registers listeners and records cleanup. Each plugin runs in a fiber, and the fiber's state is driven by its dependencies:

stateDiagram-v2
    direction LR
    [*] --> Pending
    Pending --> Loading : dependencies ready
    Loading --> Active : apply succeeds
    Loading --> Failed : apply fails, rolled back
    Active --> Unloading : dependency gone / config update / dispose
    Failed --> Unloading : dependency back / config update / restart
    Unloading --> Pending : cleanup done
    Unloading --> Disposed : terminated
    Disposed --> [*]
Loading

Services are registered by type. The event bus dispatches in four ways: emit, parallel, serial and waterfall. When a provider unloads, the plugins that depend on it are evicted and load again once a new provider appears.

In one line: declare dependencies → gated loading → provider changes → consumers reload on their own.

Packages

For Rust (crates.io) Node (npm) Python (PyPI)
The core rutis
Writing plugins rutis-sdk (dylib plugins) @arcships/rutis rutis
Running plugins in your app rutis-loader, rutis-bridge @arcships/rutis-runtime rutis
A host without Rust rutis-host @arcships/rutis-host rutis-host

The core is versioned on its own and is at 0.6. The other packages form a release train, released together at one version: currently 0.7, built on core 0.6.

Documentation

Built with rutis

Project
rutis-host A host without Rust: runs TypeScript, JavaScript and Python plugins from a rutis.json, reloads them during development, links machines.
rutis-agent · rutis-cli A minimal coding agent in which the model service, tools, streaming driver and TUI are all plugins. Try it offline with cargo run -p rutis-cli -- --scripted.
rutis-dsh Runs the full dsh web interface inside a rutis host, with model calls served by aimux in the same process.
aimux-llm Wraps aimux as an LLM service plugin.

Platforms and status

rutis is at 0.x and its API is still evolving. Breaking changes are listed in the release notes and come with a migration guide.

  • The core is pure Rust with tokio, tokio-util and thiserror as its only dependencies; it needs Rust 1.85 or later.
  • Language runtimes and rutis-host run on Linux and macOS; on Windows, use WSL. They need Node 24+ or Python 3.12+.
  • dylib plugins load on Linux, macOS and Windows x64 (MSVC).

Contributing

Issues and pull requests are welcome: bugs, places where the docs are unclear, features you would like. Please read the contributing guide first, and report security issues privately as described in the security policy.

Acknowledgements and license

The design of rutis comes from Cordis by Shigma. Without Cordis's thinking about plugins, contexts and dependencies, this project would not exist.

Released under the MIT license.

About

A plugin runtime for programs that keep running — Rust core, TypeScript and Python plugins, across processes and machines.

Topics

Resources

Contributing

Security policy

Stars

59 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages