Skip to content
Radiant LabsRadiant Labs

Core concepts

Resources

Resources are shared state that systems read and write through typed parameters.

Resources are shared state that systems read and write. Every piece of data a system needs — configuration, conversation history, the output of a previous system — is a resource that the framework injects as a typed parameter.

use polaris_ai::system::resource::{GlobalResource, LocalResource};
use polaris_ai::system::param::{Res, ResMut};
use polaris_ai::system::system;
#[system]
async fn greet(config: Res<AppConfig>, mut memory: ResMut<ContextManager>) {
memory.messages.push(format!("Hello from {}", config.agent_name));
}

You declare what you need, and the framework provides it. There is no manual lookup, no string keys, and no runtime casting.

Resources fall into two categories based on their lifetime and mutability.

Global resources live for the lifetime of the server. They are read-only and shared across all execution contexts. Configuration, model registries, and tool registries are typical global resources.

Local resources are created fresh for each execution context. They are mutable and isolated, so concurrent agent executions never interfere with each other. Conversation history and loop state are typical local resources.

Scope Marker trait Lifetime Access
Global GlobalResource Server Res<T> (read-only)
Local LocalResource Per-context Res<T> or ResMut<T>

A resource is any type that implements a marker trait. For a global resource, implement GlobalResource. For a local resource, implement LocalResource.

use polaris_ai::system::resource::{GlobalResource, LocalResource};
struct AppConfig {
agent_name: String,
model_id: String,
}
impl GlobalResource for AppConfig {}
struct ContextManager {
messages: Vec<String>,
}
impl LocalResource for ContextManager {}
impl ContextManager {
fn new() -> Self {
Self { messages: Vec::new() }
}
}

The marker traits do not require methods. The marker traits exist only to enforce the access rules at the type level: you cannot accidentally take ResMut<AppConfig> on a global resource, and you cannot share a local resource across contexts.

You register resources on the Server before execution starts. Global resources are inserted directly. Local resources are registered with a factory function that produces a fresh instance for each context.

use polaris_ai::system::server::Server;
let mut server = Server::new();
server.insert_global(AppConfig {
agent_name: "research-agent".into(),
model_id: "claude-sonnet-4-20250514".into(),
});
server.register_local(ContextManager::new);

Plugins typically handle registration in their build() method, so you rarely call these methods directly unless you are writing a plugin or setting up a server by hand.

Systems declare resource dependencies as function parameters. Res<T> provides read-only access and walks up the context hierarchy to find the resource — it can reach global resources from any context. ResMut<T> provides mutable access but is scoped to the current context only.

use polaris_ai::system::system;
use polaris_ai::system::param::{Res, ResMut};
#[system]
async fn act(
config: Res<AppConfig>,
mut memory: ResMut<ContextManager>,
) -> String {
let prompt = format!("{}: processing {} messages", config.agent_name, memory.messages.len());
memory.messages.push(prompt.clone());
prompt
}

Both Res<T> and ResMut<T> dereference to T, so you access fields and methods directly.

When a system returns a value, the next system in the graph can read it with Out<T>. The type parameter matches the return type of the preceding system.

use polaris_ai::system::param::Out;
struct AnalysisResult {
summary: String,
}
#[system]
async fn analyze(config: Res<AppConfig>) -> AnalysisResult {
AnalysisResult {
summary: format!("Analysis by {}", config.agent_name),
}
}
#[system]
async fn report(result: Out<AnalysisResult>, mut memory: ResMut<ContextManager>) {
memory.messages.push(result.summary.clone());
}

Outputs belong to the current context and do not resolve through parent scopes. Out<T> reads the most recently stored output of type T, not necessarily the immediately preceding node. Clear stale outputs with ctx.clear_outputs() when reusing a context for a new independent turn. Successful execution moves the last output into ExecutionResult; it does not clear every typed output slot.

When a fallible system fails, recovery systems on an error edge can read the error context with ErrOut<T>. This is covered in detail in Error handling and retries.

Here is a complete example that defines both resource types, registers them, and uses them in systems that form a sequential pipeline.

use polaris_ai::system::resource::{GlobalResource, LocalResource};
use polaris_ai::system::param::{Res, ResMut, Out};
use polaris_ai::system::server::Server;
use polaris_ai::system::system;
struct AppConfig {
agent_name: String,
}
impl GlobalResource for AppConfig {}
struct ReactState {
is_complete: bool,
}
impl LocalResource for ReactState {}
#[system]
async fn initialize(config: Res<AppConfig>) -> String {
format!("Starting {}", config.agent_name)
}
#[system]
async fn process(
greeting: Out<String>,
mut state: ResMut<ReactState>,
) {
state.is_complete = true;
}
let mut server = Server::new();
server.insert_global(AppConfig { agent_name: "my-agent".into() });
server.register_local(|| ReactState { is_complete: false });

The initialize system reads the global configuration through Res<AppConfig>. The process system reads the output of initialize through Out<String> and mutates the local state through ResMut<ReactState>. The global resource is shared and immutable; the local resource is private to this execution context and freely mutable.

For how resources are organized into parent-child scopes, see Context hierarchies. For the graph that connects these systems, see Graphs.