llm_tools is the function-calling registry for the chat
engine. When the model decides it needs live data to answer — the local
sub-board list, a BBS directory lookup, BBS-era history — it emits a tool
call; the registry runs the matching tool and feeds the result back to the
model. Tools are how the Guru answers from fact instead of from training
guesses.
The registry lives in exec/llm_tools.js; each tool is a separate file
under exec/llm_tools/.
On load, llm_tools.js reads every exec/llm_tools/*.js file in
alphabetical order and load()s it. Each tool file ends by calling
llm_tool_register(...) to publish itself. The engine then offers the
registered tools to the model on each turn and dispatches any tool calls the
model makes.
Adding a tool is drop-in: add a file, restart — no change to the engine, no recompile. A file that fails to load is warned-and-skipped, so one broken tool never disables the rest.
| Tool | Purpose |
|---|---|
this_bbs | Queries the local host BBS: today's live counters and lifetime totals (stats), message groups/subs, file libraries/dirs, door sections/doors, and recent messages on a sub. Honors each caller's access level — it only lists what that caller could see. |
bbs_directory | Queries the Synchronet BBS directory (sbbslist). lookup returns one BBS by name/sysop/hostname plus a live finger probe and reliability summary; list returns a filtered/sorted list (by software, network, sysop, recency) and a total count. |
external_archives | A curated index of BBS-era history and culture (textfiles.com, bbsdocumentary.com, the BBS Timeline). The model is told to call it before answering history questions, because training-data answers about door games, lawsuits, and old software are often wrong. |
relay_message | Stores a message for deferred delivery to a named recipient the next time they speak. Validates the recipient against the channel roster, chat history, and user base; primarily useful to the IRC adapter, which delivers queued messages. |
Tools run with the BBS's privileges, so the registry enforces a strict read-only contract. Read this before writing a tool:
password, api_key, token, secret, etc.), to catch accidental leaks.
Create exec/llm_tools/<name>.js. Define an executor and register it:
function my_tool(args, env) { // args: the model's structured arguments (validated against def below) // env: per-call context (e.g. the speaker) supplied by the engine // Return an object (auto-JSON-encoded) or a string. READ-ONLY work only. return { ok: true, answer: "..." }; } llm_tool_register({ name: "my_tool", execute: my_tool, def: { // the model-facing tool specification type: "function", "function": { name: "my_tool", description: "What this tool does and exactly when to call it.", parameters: { type: "object", properties: { topic: { type: "string", description: "..." } }, required: ["topic"] } } } });
Notes:
description is the model's only guide to when to call the tool — be specific, list trigger phrases, and say when not to call it. The bundled tools' descriptions are good models._ load first, so put helpers shared by several tools in a _-prefixed file (the bundled _common.js is the example).{ error: “...” } lets the model recover gracefully and tell the caller what went wrong.relay_message queues