Resources (Rust)
A resource is named application state the agent can read, and optionally follow. Actions change things; resources report the current value.
Registering a resource
Section titled “Registering a resource”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.
Pushing updates
Section titled “Pushing updates”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.
Wire behavior
Section titled “Wire behavior”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.
Registering after listen
Section titled “Registering after listen”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.