Skip to main content

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​

FieldNotes
descriptionWhat the tool does. The model reads it.
parametersA list of { name, description, schema_type, required }. schema_type is a JSON Schema type (string, number, boolean, object, array); required defaults to true.
resultThe output field returned as the result (default 'result').
plansPlans whose customers can use it. Null or empty: every plan.
customersCustomer IDs that can use it. Null or empty: everyone.
versionOptional.
#[run] functionsThe 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​

CallReturns
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.