RuimteDocs

Give agents a tool

A tool that every agent Ruimte starts on the machine finds and calls, in a chat or a terminal.

This page gives agents a tickets tool that answers the open tickets of a tracker. It builds on the server part of Server and secrets, which keeps the tracker's key.

The files

manifest.json
server/index.ts

A tool needs the permission agents.tools:

{
    "id": "acme.tickets",
    "name": "Tickets",
    "version": "1.0.0",
    "api": 1,
    "publisher": "Acme",
    "description": "Your tracker's open tickets, for you and your agents.",
    "color": "#ea580c",
    "permissions": {
        "agents.tools": "Lets your agents read the open tickets"
    }
}

With agents.tools, Ruimte starts the server part with the machine, since an agent can only find a tool that runs.

Define the tool

In server/index.ts, defineAgentTool takes a description the agent reads to decide when to call it, and parameters as a JSON Schema of its input. The type between the angle brackets types that input in run:

import { defineAgentTool, secrets } from '@ruimte/extension/server';

defineAgentTool<{ state?: 'open' | 'closed' }>({
    id: 'tickets',
    description: 'Lists the tickets of this project in the tracker, one per line: key, state, title',
    parameters: {
        type: 'object',
        properties: {
            state: {
                type: 'string',
                enum: ['open', 'closed']
            }
        }
    },
    run: async ({ state = 'open' }, { caller }) => {
        const key = await secrets.get('trackerKey');

        if (key === undefined) {
            return 'No tracker key yet. Ask the person to add it under Settings, Extensions, Tickets.';
        }

        const response = await fetch(`https://tracker.example.com/api/tickets?state=${state}`, {
            headers: {
                Authorization: `Bearer ${key}`
            }
        });
        const tickets: { key: string; state: string; title: string }[] = await response.json();

        return tickets.map((ticket) => `${ticket.key}\t${ticket.state}\t${ticket.title}`).join('\n');
    }
});

A tool answers in text, since an agent reads text. Anything else is sent as JSON. caller is the node id of the terminal or chat whose agent called it. Write a failure as an answer the agent can act on, like the missing key above.

How an agent finds it

Every agent Ruimte starts gets Ruimte's own tools, tool_list and tool_run among them. tool_list answers each tool on the machine with its id, its description and the schema of its input. tool_run calls one by its id, which is your extension's id and the tool's joined by a slash, such as acme.tickets/tickets, and hands your run the input the agent gave. Without one, the tool gets {}. In a chat, it looks like this:

Check it

Run bun run dev, then ask a chat on the machine which tickets are open. It calls tool_list, finds yours, and runs it. When it does not, check that the manifest names agents.tools, and that bun run dev printed no error from the server part.

A person can reach the same work by voice. Chats and agents has defineVoiceAction, which you describe the same way.