Skip to content
Radiant LabsRadiant Labs

Core concepts

Plugins

Plugins add capabilities to the server — LLM providers, tools, tracing, and anything else your agent needs.

Plugins add capabilities to the server. Every piece of functionality in Polaris — LLM providers, tool registries, tracing, persistence — is delivered through a plugin. The server itself ships with no built-in behavior. You assemble what you need.

use polaris_ai::system::server::Server;
use polaris_ai::system::plugin::PluginGroup;
use polaris_ai::plugins::DefaultPlugins;
use polaris_ai::models::ModelsPlugin;
use polaris_ai::models::AnthropicPlugin;
Server::new()
.add_plugins(DefaultPlugins::new().build())
.add_plugins(ModelsPlugin)
.add_plugins(AnthropicPlugin::from_env("ANTHROPIC_API_KEY"))
.run().await?;

This server has tracing, time utilities, a model registry, and the Anthropic LLM provider. Each add_plugins call registers one plugin (or a group of plugins) and its resources.

A plugin supplies a synchronous build method. ready and cleanup are asynchronous; optional update and tick_schedules handle scheduled work.

use polaris_ai::system::plugin::{Plugin, PluginId, Version};
use polaris_ai::system::server::Server;
pub struct MetricsPlugin;
impl Plugin for MetricsPlugin {
const ID: &'static str = "myapp::metrics";
const VERSION: Version = Version::new(0, 1, 0);
fn build(&self, server: &mut Server) {
// Insert resources, register local resource factories
}
async fn ready(&self, server: &mut Server) {
// Cross-plugin setup after all plugins are built
}
async fn cleanup(&self, server: &mut Server) {
// Graceful shutdown (flush buffers, close connections)
}
fn dependencies(&self) -> Vec<PluginId> {
vec![]
}
}

Every plugin has a unique ID string and a semantic VERSION. The server uses the ID for duplicate detection and dependency resolution.

The build method is where you insert resources and register APIs. It runs once, in dependency order, when the server starts. Keep it lightweight — heavy initialization belongs in ready or in systems.

fn build(&self, server: &mut Server) {
server.insert_global(MetricsConfig { interval_secs: 30 });
server.register_local(MetricsBuffer::new);
}

If your plugin calls server.add_plugins(...) inside build, the sub-plugin is built immediately. This is how plugin groups compose.

The ready method runs after every plugin has been built, also in dependency order. Use it when your plugin needs resources that another plugin provides during its build phase.

async fn ready(&self, server: &mut Server) {
let config = server.get_resource::<MetricsConfig>()
.expect("MetricsConfig must be inserted during build");
// Validate, connect, or perform cross-plugin setup
}

The cleanup method runs when the server shuts down. Unlike build and ready, cleanup runs in reverse dependency order — dependents clean up before their dependencies. This means your plugin can still use the resources it depends on during cleanup.

A plugin declares its dependencies by returning a list of PluginId values from the dependencies method. The server validates that all dependencies are present and topologically sorts plugins so that dependencies are always built first.

use polaris_ai::models::ModelsPlugin;
fn dependencies(&self) -> Vec<PluginId> {
vec![PluginId::of::<ModelsPlugin>()]
}

server.finish().await returns a ServerBuildError when dependencies are missing or cyclic. Handle the error before creating contexts or accepting work.

Current Polaris plugins can declare the capabilities they provide, require or extend through PluginAccess. A capability is a resource or API type with a Contract version. This lets a consumer depend on ToolRegistry, for example, rather than a particular provider plugin.

  • provides::<T>(version) declares a capability this plugin inserts.
  • requires::<T>(version_req) declares read access to a required capability.
  • extends::<T>(version_req) declares a contribution to a capability, such as registering tools.
  • optionally_requires::<T>(version_req) allows an integration when a compatible provider exists.

The #[plugin] macro derives these declarations from typed Requires<T>, Extends<T> and Optional<T> build parameters. dependencies() remains available for pure ordering and ready-phase relationships. See capability contracts and the plugin macro for the full API.

You add plugins with server.add_plugins(...). This accepts a single plugin or a PluginGroupBuilder containing multiple plugins.

let mut server = Server::new();
server
.add_plugins(DefaultPlugins::new().build())
.add_plugins(ModelsPlugin)
.add_plugins(AnthropicPlugin::from_env("ANTHROPIC_API_KEY"));
server.run().await?;

Await finish() to build the server and then run work yourself. Await run() for a long-running service; it waits for shutdown. Await cleanup() after manually managed work to flush exporters and release plugin resources.

Polaris ships two plugin groups for common setups.

DefaultPlugins includes ServerInfoPlugin, TimePlugin, and TracingPlugin. It covers what most applications need for logging, timing, and server metadata.

use polaris_ai::plugins::DefaultPlugins;
server.add_plugins(DefaultPlugins::new().build());

You can customize it before building:

use polaris_ai::plugins::{DefaultPlugins, FmtConfig, TracingFormat};
use tracing::Level;
server.add_plugins(
DefaultPlugins::new()
.with_log_level(Level::DEBUG)
.with_fmt(FmtConfig::default().format(TracingFormat::Json))
.build()
);

MinimalPlugins includes only ServerInfoPlugin and TimePlugin. It is useful for tests where you do not want tracing output.

use polaris_ai::plugins::MinimalPlugins;
server.add_plugins(MinimalPlugins.build());

Both groups return a PluginGroupBuilder, which lets you disable individual plugins with .disable::<T>() or insert plugins at specific positions with .add_before::<P, Target>(plugin) and .add_after::<P, Target>(plugin).

Here is a complete plugin that provides application configuration as a global resource.

use polaris_ai::system::plugin::{Plugin, Version};
use polaris_ai::system::resource::GlobalResource;
use polaris_ai::system::server::Server;
pub struct AppConfig {
pub model_name: String,
pub max_turns: usize,
}
impl GlobalResource for AppConfig {}
pub struct AppConfigPlugin {
pub model_name: String,
pub max_turns: usize,
}
impl Plugin for AppConfigPlugin {
const ID: &'static str = "myapp::config";
const VERSION: Version = Version::new(0, 1, 0);
fn build(&self, server: &mut Server) {
server.insert_global(AppConfig {
model_name: self.model_name.clone(),
max_turns: self.max_turns,
});
}
}

Systems can then access AppConfig through Res<AppConfig>. The plugin owns the configuration; the systems consume it. This separation keeps your server assembly in one place and your agent logic in another.

Server::new()
.add_plugins(DefaultPlugins::new().build())
.add_plugins(AppConfigPlugin {
model_name: "claude-sonnet-4-20250514".into(),
max_turns: 10,
})
.run().await?;

For plugins that need to interact with other plugins during build through shared registries, see APIs. For a full walkthrough of building a plugin with ready, cleanup, and local resources, see the Writing custom plugins guide.