Skip to content

Resources (C++)

A resource is a named value the agent can read, and optionally subscribe to. reader is the terminal step of the builder, so description and subscribe come before it.

The todo example publishes the resource URI todos://all:

builder.resource("todos://all")
.description("The complete todo list. Pushed on every mutation.")
.subscribe([state](tesseron::ResourceEmitter emitter) {
state->subscribers.push_back(std::move(emitter));
return tesseron::Subscription::without_teardown();
})
.reader([state]() -> boost::asio::awaitable<Result<Json>> { co_return todo_list_payload(state->todos); });

A reader returns the current value for every resources/read:

.reader([state]() -> boost::asio::awaitable<Result<Json>> {
co_return todo_list_payload(state->todos);
});

It runs on the host's I/O thread. A reader that fails answers an ActionError the same way a handler does. A reader that throws is caught and answered as -32603.

Registering a subscriber declares the resource subscribable: true in the manifest. Leave it off and a resources/subscribe for that name is answered -32003, the same answer an undeclared resource gets.

The subscriber starts pushing and hands back the thing that stops it:

.subscribe([state](tesseron::ResourceEmitter emitter) {
state->subscribers.push_back(std::move(emitter));
return tesseron::Subscription::without_teardown();
})

Subscription::with_teardown(...) is for a subscriber that registered an application listener. Subscription::without_teardown() is for a subscriber that registered nothing needing cleanup.

The acknowledgement goes out before the subscriber runs, so a value the subscriber emits immediately cannot overtake the response the agent is still waiting on. The acknowledgement is result: null, both for subscribe and unsubscribe.

One emitter belongs to one resources/subscribe, so the subscription id is already baked in. Copying is cheap and every copy pushes to the same subscriber, which lets a subscriber hand one to another thread.

emit is safe from any thread. It hops onto the host's own thread before touching the subscription, which is also where subscription liveness can be read without racing an unsubscribe. It is fire-and-forget: a value emitted after the agent unsubscribed, or after the transport closed, is dropped rather than queued.

The teardown runs on resources/unsubscribe and when the transport closes, whichever comes first. Write it so it is safe when nothing is listening any more, and let it own everything the subscriber registered.

Host::listen() returns a Host whose resource registry can change from any thread. Build a Resource value with the standalone ResourceDefinition chain, then register it after listening; a same-name registration replaces the previous resource, while HostBuilder still rejects duplicate names when listen() runs.

auto resource = tesseron::ResourceDefinition("todos://all")
.subscribe(start_todo_updates)
.reader(read_todos);
host.register_resource(resource);

host.remove_resource(name) returns bool, with false when no resource has that name, and a removal only notifies when it changes the registry. A live session receives resources/list_changed with the full current list after each registry change; without a live session, the next hello or resume announces the current registry, and removing a resource drops its active subscriptions and runs their teardowns.

{
"jsonrpc": "2.0",
"id": 1,
"result": { "value": [] }
}

resources/updated is a notification with no id:

{
"jsonrpc": "2.0",
"method": "resources/updated",
"params": { "subscriptionId": "sub-1", "value": [] }
}