Exposing tools
Tools are how the agent acts. With #[kiki::agent_tools], the async methods of an impl block become tools the agent can call — no manual schema writing, no protocol boilerplate.
The macro
use kiki::prelude::*;
#[kiki::agent_tools]
impl MyApp {
/// Play a track. This doc comment becomes the tool description.
async fn play(&mut self, track: TrackRef) -> Result<()> {
Ok(())
}
/// Set the volume from 0.0 to 1.0.
async fn set_volume(&mut self, level: f32) -> Result<()> {
Ok(())
}
}For each async method, the macro generates the tool registration, a JSON input schema derived from the argument types, and the glue that decodes a call, runs your method, and returns the result. The doc comment becomes the tool's description — which is what the agent reads when deciding whether to use it, so write it for the agent as much as for humans.
What makes a good tool
- Name it as an action —
play,archive_thread,set_volume. Verbs the agent composes into a plan. - Use typed arguments — a
TrackRefor an enum, not a free-form string. The generated schema keeps the agent's calls well-formed. - Keep the surface small — expose the actions that matter, not every internal method.
- Return something useful — what you return becomes the agent's next observation. Say what happened.
Tools respect permissions
A tool can only do what your app's capabilities allow. Declare what play needs:
#[kiki::app(id = "io.kiki.player", type = DesktopApp)]
#[kiki::requires(Capability::AudioOutput)]
struct MyApp { /* ... */ }When the agent calls play, the OS checks the grant first. A tool that tries to exceed its permissions fails cleanly rather than doing something unauthorized.
Public types must be schemas
If a tool's argument or return type crosses your app's boundary, it must be a published schema. TrackRef above references a registry schema; the OS generates the binding at install. This is what keeps every app able to understand every other app's data.
Next: Durable execution for long-running tools, and Schemas for the types they exchange.