Give AI agents tools
A capability is a named function in the policy that your app can run, and that an AI model can call as a tool. Because it lives in the policy, which tools a customer's agent gets can follow their plan, and the tool's code travels with the policy. This page covers the tool side.
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: {
ai_token: { label: 'AI token', price: { amount: 0.00002 } }
}
plans: {
free: {
default: true
entitlements: {
chat: { limit: { credit: 'ai_token', value: 50_000 } }
}
}
pro: {
entitlements: {
chat: { limit: { credit: 'ai_token', mode: 'soft', value: 2_000_000 } }
}
}
}
}`);
await limitr.setCapabilities(`
quote_tokens: {
description: 'Quote the price of a number of AI tokens, in US dollars'
parameters: [
{ name: 'tokens', description: 'How many tokens', schema_type: 'number' }
]
plans: ['pro'] // only Pro customers get this tool
result: 'usd'
#[run]
fn quote() {
const tokens = (self.input.tokens ?? 0) as float;
const usd = root.policy.credit_exchange('ai_token', 'usd', tokens);
self.output.usd = '$' + usd.round(2);
}
}`);
await limitr.ensureCustomer('ada', 'pro');
await limitr.ensureCustomer('bo', 'free');
// Tool definitions for the Claude API, filtered by what each customer's plan allows.
console.log('bo:', await limitr.claudeTools('bo'));
console.log('ada:', JSON.stringify(await limitr.claudeTools('ada'), null, 2));
// When the model calls the tool, run it and send back the result.
const toolUse = {
type: 'tool_use',
id: 'toolu_01',
name: 'quote_tokens',
input: { tokens: 1_234_567 },
};
console.log(await limitr.claudeToolUse(toolUse, 'ada'));
// Or run a capability directly.
console.log(await limitr.runCapability('quote_tokens', { tokens: 250_000 }, 'ada'));
It prints:
bo: []
ada: [
{
"name": "quote_tokens",
"description": "Quote the price of a number of AI tokens, in US dollars",
"input_schema": {
"type": "object",
"properties": {
"tokens": {
"type": "number",
"description": "How many tokens"
}
},
"required": [
"tokens"
]
}
}
]
{ type: 'tool_result', tool_use_id: 'toolu_01', content: '$24.69' }
$5
Defining a capability
| Field | Notes |
|---|---|
description | What the tool does. The model reads it. |
parameters | A list of { name, description, schema_type, required }. schema_type is a JSON Schema type (string, number, boolean, object, array); required defaults to true. |
result | The output field returned as the result (default 'result'). |
plans | Plans whose customers can use it. Null or empty: every plan. |
customers | Customer IDs that can use it. Null or empty: everyone. |
version | Optional. |
#[run] functions | The tool's code. The arguments are in self.input; write the result to self.output. Several #[run] steps run in the order they're written. |
Add capabilities with setCapabilities(stof), or write them under capabilities in the policy. Either way, each one
is named after its field. See Capabilities for running them from your app and loading
data from APIs.
Return text. The Claude API expects a tool result's content as a string, so build the result as one (above:
'$' + ...).
Running tools
| Call | Returns |
|---|---|
claudeTools(customerId?) | Tool definitions for the Claude API's tools parameter, filtered by the customer's access. |
claudeToolUse(toolUse, customerId?) | Runs a tool_use block from the model's response and returns the tool_result block to send back. Null when the tool doesn't exist or the customer can't use it. |
runCapability(name, args, customerId?) | Runs a capability directly and returns its result. |
A typical loop: pass claudeTools(userId) with the request; for each tool_use block in the response, call
claudeToolUse(block, userId) and send the results back in the next message.
Metering tool use
Capabilities don't meter anything on their own. Gate and count a tool like any other feature: an entitlement per
tool (or one for all tools), with allow(userId, 'tool_calls', 1) before running it.
What a capability can do
It runs in the policy's sandbox: it can read the policy (root.policy), do math, and call
host functions your app registers, and HTTP only for hosts your app allows. Keep anything it
creates in self.input and self.output, which are replaced on every run.