Skip to content

Resources (Rust)

A resource is named application state the agent can read, and optionally follow. Actions change things; resources report the current value.

Resource::new(name, read) takes a synchronous callback that returns a future resolving to Result<Value, ActionError>. The callback runs on every resources/read, so it reads current state rather than a snapshot captured during registration.

The todo example registers a readable and subscribable todos://all resource like this:

fn todo_resource(todos: TodoList) -> Resource {
let resource_todos = todos.clone();
Resource::new("todos://all", move || {
let todos = resource_todos.clone();
async move {
let todos = todos.snapshot()?;
serde_json::to_value(todos).map_err(ActionError::internal)
}
})
.description("The complete todo list. Pushed on every mutation.")
.subscribe(move |emitter| {
let mut updates = todos.subscribe();
let task = tokio::spawn(async move {
while let Ok(todos) = updates.recv().await {
if let Ok(value) = serde_json::to_value(todos) {
emitter.emit(value);
}
}
});
Subscription::new(move || task.abort())
})
}

.description(...) publishes the text the agent sees. Calling .subscribe(..) marks the resource as subscribable and gives the callback one ResourceEmitter for that subscription.

ResourceEmitter::emit(Value) sends a resources/updated notification to the agent subscribed through that emitter. It is fire-and-forget. Emitting after the transport closes or after unsubscribe is dropped. Cloning an emitter keeps the same subscription id, which lets a spawned task keep pushing until its Subscription cleanup runs.

Subscription::new(stop) stores a FnOnce cleanup. The SDK runs it when the agent unsubscribes and when the transport closes. Use Subscription::without_cleanup() when the callback started nothing that needs teardown.

The subscribe callback is synchronous. Start the event source and return the subscription promptly. A spawned task, as in the example, can wait for updates without blocking the session loop.

resources/subscribe and resources/unsubscribe acknowledge with result: null. The acknowledgement is sent before the subscriber starts, so an immediate push cannot overtake it. Unsubscribing an unknown id is harmless.

Reading an undeclared resource returns TesseronErrorCode::ActionNotFound with Resource not readable: <name>. Subscribing to an undeclared or non-subscribable resource returns the same code with Resource not subscribable: <name>.

A reader can return ActionError for a domain failure. An unexpected reader error is reported as -32603 Internal error.

Clone the host handle into a spawned task when a resource needs to be added or removed after listen():

use serde_json::json;
use tesseron::{Resource, TesseronHost};
let host = builder.listen().await?;
let host_clone: TesseronHost = host.clone();
let resource = Resource::new("todos://all", || async { Ok(json!([])) });
tokio::spawn(async move {
host_clone.register_resource(resource);
host_clone.remove_resource("todos://all");
});

register_resource(&self, resource) upserts by name, replacing the descriptor, reader, and subscription handler while keeping the existing manifest slot. Replacing a resource stops its live subscriptions. remove_resource(&self, name) returns true when a resource was removed and false for an unknown name; removing a resource also stops its subscriptions.

After the session is welcomed, each call that changes the registry sends resources/list_changed with { "resources": [full manifest] }. Before welcome, or without a connected gateway, changes are silent and the next tesseron/hello or resume carries the new manifest. Notifications are sent for each change without coalescing.