MESH ONLINECODENAME:
v0.34

Discover Capabilities

Query the mesh by what you need, not by who has it. There is no registry to ask and no address to know: every node folds the announcements it hears into a local index, and discovery is a read of that index.

Two surfaces, two questions

Filter nodes answers which machines fit — GPUs with enough VRAM, a region, a model, a tag you invented. It returns node ids. This is placement.

List tools answers what can I call — the named, schema'd operations peers have announced. It returns descriptors. This is the agent surface, and it is what lowers into an LLM's tool array.

The same announcement can feed both.

Discovery is a local read

Both surfaces read the node's own folded index. Nothing goes on the wire when you call them, which has three consequences worth holding onto:

  • They are fast and synchronous in the bindings whose types allow it. A discovery call is not a network round trip.
  • They can be empty and correct. An announcement that has not arrived yet is indistinguishable from one that was never made.
  • They are a snapshot. The answer was true when the index last changed, not necessarily now.

Folding takes a moment — do not poll for it

An announcement propagates asynchronously, and multi-hop: a match can come from a node several hops away that you have never connected to. So the index right after a peer announces is often still empty.

The wrong fix is a polling loop. Every binding exposes a watch: take a list baseline, then subscribe, and changes are pushed the moment the fold mutates. It is event-driven off the fold's change signal, so an idle mesh costs zero periodic work.

Each binding's watch takes an optional interval. It is a staleness ceiling, not a poll rate — a safety-net re-diff at least that often. Leave it unset for pure event-driven behaviour. Reading it as "how often to check" produces a timer nobody needed.

Discovery is advisory

Finding a node that can do something gets you no claim on it. There is no exclusivity in a discovery result, no reservation, and no promise the node will still be there when you call.

If you need to atomically claim a contended exclusive resource — a GPU island, an accelerator slot, a licensed seat — that is the gang-claim scheduler, not find_nodes. Using discovery as a booking system is how two callers end up believing they own the same device.

Seeing is not calling

A capability you can discover is not a capability you can invoke. Visibility and invocability are separate, and the check happens on invoke — see Errors for what a refusal looks like when it arrives.

Richer predicates (numeric, semver, AND/OR/NOT), the scoping model, and the CLI equivalent (net-mesh cap query --tag …) are in Capabilities.

Discover it — Rust

List tools

rust
// `agent` is a Mesh node handshaked with a host that announced tools.
let tools = agent.list_tools(None);
for t in &tools {
    println!("{} v{}  tags={:?}", t.tool_id, t.version, t.tags);
}

list_tools takes an optional &TagMatcher — pass None for everything.

Watch for changes

rust
use futures::StreamExt;
 
for t in agent.list_tools(None) { /* baseline */ }
 
let mut watch = agent.watch_tools(None, None);   // matcher, staleness ceiling
while let Some(change) = watch.next().await {
    println!("{change:?}");   // added / removed / publisher-count change
}

The second argument is the optional Duration staleness ceiling. None is pure event-driven. Dropping the stream stops the substrate task.

Filter nodes by capability

rust
use net_sdk::capabilities::CapabilityFilter;
 
let filter = CapabilityFilter {
    require_gpu: true,
    min_vram_gb: Some(24),
    ..Default::default()
};
let nodes: Vec<u64> = mesh.find_nodes(&filter);

find_nodes is not async — it reads the local index and returns node ids directly. find_nodes_scoped narrows to a tenant, region or subnet pool.

Pick one node

rust
use net_sdk::capabilities::CapabilityRequirement;
 
let req = CapabilityRequirement::from_filter(filter).prefer_vram(1.0);
let target: Option<u64> = mesh.find_best_node(&req);

find_best_node applies the requirement's weights and returns one winner instead of the whole matching set. Each weight scores one axis of what a candidate announced about itself — system memory, GPU VRAM, model inference speed in tokens/sec, and the share of its models already loaded — and each is clamped to [0.0, 1.0]. Ties, including the case where every weight is zero, resolve to the lowest matching node id.

None means nothing matched; node id 0 is a real id, so match on the Option rather than comparing against zero. find_best_node_scoped applies a scope first, so a peer outside the scope cannot win on capacity.

Same local-index read as find_nodes: no network, no await, and only peers whose announcements have already arrived.

Lower a descriptor for an LLM

rust
use net_sdk::tool::formats::openai;
 
let lowered: Vec<_> = tools.iter().map(openai::to_openai_tool).collect();

to_openai_tool produces an entry that drops straight into an OpenAI-compatible tools array. anthropic, mcp and gemini modules sit beside it with the same shape, and each has a lower_* counterpart for parsing the model's reply back into a call spec.

Verify it worked

rust
let tools = agent.list_tools(None);
assert!(
    tools.iter().any(|t| t.tool_id == "web_search"),
    "web_search did not fold — is the pair handshaked and started?",
);

The full announce → list → lower → invoke loop is sdk/examples/tool_calling.rs, which runs in CI.

Next: Invoke a capability.

§ parity · Capability discoveryfrom the capability record
Rust supportedNode / TS supportedPython supportedGo supportedC supported