Write a typed tool
This guide writes a typed tool: a component that exports its own interface, with real types and doc comments, and nothing from cosmonic:agent. The host turns that interface into tool definitions a model can call. The example converts temperatures, so it needs no network and no files.
Typed tools explains how the host translates between the model's JSON and your types.
Before you start
- Rust 1.94 or later with the
wasm32-wasip2target (rustup target add wasm32-wasip2). wash2.7.0 or later.wasm-tools, to build the WIT package the tool's descriptions come from.- Cosmonic Desktop 0.5.32 or later, to offer the tool to agents on your machine.
- Optional: a coding agent, such as Claude Code or Codex, if you'd rather it write and build the tool for you.
Build the tool
Write it yourself, step by step, or hand it to a coding agent.
- By hand
- With a coding agent
1. Create the project
cargo new --lib units-tool
cd units-tool
mkdir witReplace Cargo.toml with:
[package]
name = "units-tool"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.62.0"
[profile.release]
opt-level = "s"
lto = true
strip = true2. Write the interface
The interface is the tool. Write it in WIT (WebAssembly Interface Types), the interface language components use, much as a .proto file describes a gRPC service. Save it as wit/units.wit:
/// Convert between units of measure. Needs no network and no files.
package example:units@0.1.0;
/// Temperature conversions.
@since(version = 0.1.0)
interface temperature {
/// A temperature scale.
enum scale {
/// Degrees Celsius.
celsius,
/// Degrees Fahrenheit.
fahrenheit,
/// Kelvin.
kelvin,
}
/// Why a conversion failed.
variant error {
/// The value is below absolute zero. The payload says by how much.
below-absolute-zero(string),
}
/// Arguments for `convert`.
record convert-args {
/// The temperature to convert.
value: f64,
/// The scale `value` is in.
%from: scale,
/// The scale to convert to.
to: scale,
}
/// What `convert` returns.
record convert-result {
/// The converted temperature, rounded to two decimal places.
value: f64,
}
/// Convert a temperature from one scale to another. Use it rather than
/// doing the arithmetic yourself.
convert: async func(args: convert-args) -> result<convert-result, error>;
}
/// A component that converts units, reaching nothing.
@since(version = 0.1.0)
world converter {
export temperature;
}Follow these rules, because the host builds the model's view of the tool from them:
- Each exported function is one tool. Its name, in snake case, is the tool's name (
convert). Its doc comment is the tool's description, written for the model: say what it does and when to use it. - Take one record. WIT has no doc comments on parameters, but it has them on record fields. When a function's only parameter is a record, the host offers its fields (
value,from,to) as the tool's arguments, each described by its comment. - Use real types. An
enumbecomes a fixed list of choices, anoption<T>field may be left out (write its default in the comment), and the host checks the model's JSON against the types before your code runs. - Return a
result. Itserrgoes back to the model as an error it can read and react to, not as a failed call. - The world is the reach. This world imports nothing, so the tool can reach nothing. A tool that calls a web API would
import wasi:http/client@0.3.0, and one that reads a folder would importwasi:filesystem; each host then decides which hosts or folder that import can actually touch.
Functions that take or return resources, streams or futures have no JSON form and are not offered as tools.
3. Implement it
Replace src/lib.rs with:
mod bindings {
wit_bindgen::generate!({
path: "wit",
world: "converter",
});
}
use bindings::exports::example::units::temperature::{
ConvertArgs, ConvertResult, Error, Guest, Scale,
};
/// Every scale through kelvin.
fn to_kelvin(value: f64, scale: Scale) -> f64 {
match scale {
Scale::Celsius => value + 273.15,
Scale::Fahrenheit => (value - 32.0) * 5.0 / 9.0 + 273.15,
Scale::Kelvin => value,
}
}
fn from_kelvin(kelvin: f64, scale: Scale) -> f64 {
match scale {
Scale::Celsius => kelvin - 273.15,
Scale::Fahrenheit => (kelvin - 273.15) * 9.0 / 5.0 + 32.0,
Scale::Kelvin => kelvin,
}
}
struct Component;
impl Guest for Component {
async fn convert(args: ConvertArgs) -> Result<ConvertResult, Error> {
let kelvin = to_kelvin(args.value, args.from);
if kelvin < 0.0 {
return Err(Error::BelowAbsoluteZero(format!(
"{:.2} below absolute zero",
-kelvin
)));
}
let value = (from_kelvin(kelvin, args.to) * 100.0).round() / 100.0;
Ok(ConvertResult { value })
}
}
bindings::export!(Component with_types_in bindings);Nothing here mentions agents, models or JSON. It is an ordinary library with a WIT interface.
4. Build it and check its reach
cargo build --target wasm32-wasip2 --release
wash inspect target/wasm32-wasip2/release/units_tool.wasmThe output ends with the one export, example:units/temperature@0.1.0. The imports above it are the console, clock and exit interfaces that Rust's standard library brings in. There is no wasi:http, no wasi:sockets and no wasi:filesystem.
5. Build the WIT package
A built component keeps its types but not its doc comments, so the descriptions travel in a separate, published WIT package. Build it from the same WIT:
wasm-tools component wit wit --wasm -o units-wit.wasmunits-wit.wasm is the package's binary encoding, with your doc comments in it. A host uses it only when its interface matches the component's types exactly.
6. Push it to Desktop's registry
Push the component and the WIT package to the registry built into Desktop, and note the digest each push prints:
wash oci push -o json --insecure oci.localhost:8200/tools/units:0.1.0 \
target/wasm32-wasip2/release/units_tool.wasm
wash oci push -o json --insecure oci.localhost:8200/tools/wit/units:0.1.0 \
units-wit.wasm--insecure is there because the built-in registry speaks plain HTTP on your own machine.
This prompt builds a different tool, one that converts lengths, from the finished temperature example in the agent-sdk repository. Paste it into your coding agent, in an empty folder:
Build me a typed tool for the Cosmonic Agent SDK that converts lengths between
metres, feet and miles. Start from the units-tool example: clone
https://github.com/cosmonic-labs/agent-sdk and work from components/units-tool,
which converts temperatures. A typed tool is an ordinary WIT interface: each
exported function is one tool, it takes one record so each field can carry a doc
comment, it uses real types, and it returns a result. Write doc comments for the
model to read. Build it with cargo build --target wasm32-wasip2 --release and
build its WIT package with wasm-tools component wit wit --wasm -o lengths-wit.wasm.
Push the component to Cosmonic Desktop's local registry as
oci.localhost:8200/tools/lengths:0.1.0 and the WIT package as
oci.localhost:8200/tools/wit/lengths:0.1.0, using wash oci push -o json --insecure
so you can read each digest. Don't add any network or filesystem imports. When
you're done, give me both image references pinned by digest.Read the WIT it wrote before you go on. Write the interface explains the rules it should follow, because the host builds the model's view of the tool from them.
A coding agent like Claude Code or Codex runs on your machine with your permissions. What it builds runs in the sandbox, with only the access the host grants it.
Offer it in Cosmonic Desktop
Add the tool to Desktop's tool catalog. These steps use the temperature tool's names. If your coding agent built the lengths tool, use the references it gave you and the name lengths.
- In Desktop, select Settings in the sidebar, then Tools under Agent workspace, and select Add a tool.
- Under Where it comes from, choose Image.
- Fill in:
- Image reference: the component, pinned by digest, such as
oci.localhost:8200/tools/units@sha256:…. A tag alone is refused, so the tool can't change under you. - WIT package: the WIT package, pinned by digest, such as
oci.localhost:8200/tools/wit/units@sha256:…. Without it, agents see the tool's functions by name only. - Hosts it may reach: leave it empty. This tool needs no network.
- Leave It reads a folder you choose for each session unchecked.
- Name:
units. Title:Unit converter.
- Image reference: the component, pinned by digest, such as
- Select Review. Under What the agent receives, check that
convertis listed with the descriptions you wrote. If Desktop says agents see these tools by name only, the WIT package is missing or doesn't match the component. - Select Add tool.
Unit converter is now in Add tools yourself… when you start a session, and an agent can find it with Let the agent find tools on. A tool that calls a web API lists its hosts in Hosts it may reach, and Desktop shows them in the review and on the tool's card.
Offer it in Cosmonic Control
On Control, a typed tool is a component in the agent's workload. Push it to a registry your cluster can pull from, add it to the workload, and name it in tool-components:
hostInterfaces:
- namespace: cosmonic
package: agent
interfaces: [tools]
version: 0.4.1
name: cosmonic-agent-tools
config:
components: agent
tool-components: units
components:
- name: units
image: ghcr.io/you/units-tool@sha256:…The tool reaches nothing, so it needs no allowedHosts. Give an agent a tool covers hosts, folders and stores.
Related
- How big should a tool be? helps you decide what one tool should do.
- cosmonic:agent interfaces covers
tool-provider, the other way to build a tool, which lists its own definitions and parses JSON itself.