Tools

A tool is a capability the model can invoke during a conversation. Rienda ships a couple of built-in tools, and you can add your own in JavaScript. A custom tool lives in its own directory under ~/.rienda/tools:

~/.rienda/tools/<tool-name>/index.js

The directory name is the tool name the model calls, so ~/.rienda/tools/word-count defines a tool called word-count.

Tools are global: they are read from ~/.rienda and never from a workspace, so a tool changes Rienda everywhere. A user tool that shares a name with a built-in shadows it, which is how you tune or replace built-in behavior.

Because a tool runs with your privileges on your machine, it is trusted. Rienda does not sandbox extension scripts: install only tools you trust.

Your first tool

// ~/.rienda/tools/word-count/index.js
module.exports = {
  description: "Counts the words of a file in the workspace.",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "Path relative to the workspace." },
    },
    required: ["path"],
    additionalProperties: false,
  },
  execute: function(ctx, args) {
    const text = ctx.file.read(args.path);
    return String(text.trim().split(/\s+/).length);
  },
};

For the model to use it, an agent must declare the tool in its definition:

---
description: A careful agent.
model: openai/gpt-4o
tools:
  - word-count
---

Anatomy

A tool file exports a single object with three fields.

FieldTypeRequiredPurpose
descriptionstringyesWhat the tool does and when to use it. Shown to the model.
parametersobjectyesA JSON Schema for the arguments. Passed to the provider verbatim.
executefunctionyesRuns one invocation. See below.

A file that is missing any of these is skipped, and the problem is reported as a diagnostic. It never crashes a run.

execute(ctx, args)

execute is called once per invocation. It receives the runtime context ctx (see The context) and args, the arguments the model sent, already decoded from JSON into a plain object that matches parameters.

It must return one of two shapes:

ReturnResult the model reads
a stringthat text, as a successful result
{ text: string, isError?: boolean }text, marked as an error when isError is true

Anything else (a number, a boolean, an array, null, an object without a text string) is treated as an error, so a mistake is reported instead of reaching the model as a surprising value. To return structured data, serialize it yourself:

return JSON.stringify({ count: words, file: args.path });

A throw fails the invocation, never the run: the model receives the error text and the conversation continues.

The context

Every tool and hook receives the same ctx. It groups its capabilities in namespaces and exposes a few top-level values.

Top level

MemberTypeDescription
ctx.workdirstringAbsolute workspace directory. The base for every relative path.
ctx.sessionobject{ id, agentId, modelId, workdir } of the current run.
ctx.agentobjectThe running agent: name (its id) plus its whole frontmatter (description, model, tools, hooks, config) and systemPrompt (the Markdown body). Per-agent settings live under ctx.agent.config.
ctx.configobjectThe global config.yaml, read-only. Put custom settings under its config block, keyed by tool or hook name.
ctx.log(text)Streams text to the front end. Not capped.
ctx.sleep(ms)Sleeps for ms milliseconds, or returns early when the run is interrupted.
ctx.confirm({ title, body })booleanAsks the user a yes/no question. Blocks until they answer; returns true on approval, false on refusal.
ctx.notify({ title, body })Shows a non-blocking notice to the user.

ctx.file

Reads and writes the file system. Relative paths resolve against ctx.workdir.

MemberDescription
ctx.file.read(path, opts?)Returns the file content as text. opts = { encoding? } (only "utf8" for now). Invalid UTF-8 is replaced, never raised.
ctx.file.write(path, data, opts?)Writes data, creating parent directories. opts = { append? }: by default the file is truncated.
ctx.file.exists(path)Returns whether the path exists, file or directory.
ctx.file.list(path, opts?)Lists a directory as [{ name, path, isDir }]. opts = { recursive?, respectIgnoreFiles? }, both true by default. respectIgnoreFiles honors the .gitignore and .ignore of the tree.

ctx.env

MemberDescription
ctx.env.get(name)Returns the value of an environment variable, or null when it is unset.

ctx.http

MemberDescription
ctx.http.fetch(url, opts?)Sends an HTTP request and returns { status, headers, body }. opts = { method?, headers?, body?, timeout_ms? }. The response is returned whatever the status, so you decide how to handle it. headers is a flat string map and body a string sent as-is.

ctx.system

MemberDescription
ctx.system.exec(command, opts?)Runs command through the shell. opts = { cwd?, timeout_ms?, env? }. Returns { code, stdout, stderr } (code is -1 when a signal killed the process). Output is streamed to the front end live, like the built-in shell tool.
ctx.system.which(name)Returns the absolute path of an executable, or null when it is not on PATH.

Examples

Read a JSON file and return one field:

module.exports = {
  description: "Reads a field from a JSON file in the workspace.",
  parameters: {
    type: "object",
    properties: { path: { type: "string" }, field: { type: "string" } },
    required: ["path", "field"],
    additionalProperties: false,
  },
  execute: function(ctx, args) {
    const data = JSON.parse(ctx.file.read(args.path));
    return JSON.stringify(data[args.field]);
  },
};

Ask the user before doing something risky:

module.exports = {
  description: "Publishes the current branch.",
  parameters: { type: "object", properties: {}, additionalProperties: false },
  execute: function(ctx) {
    if (!ctx.confirm({ title: "Publish", body: "Push the current branch?" })) {
      return { text: "cancelled by the user", isError: true };
    }
    const result = ctx.system.exec("git push");
    return result.code === 0
      ? "pushed"
      : { text: result.stderr, isError: true };
  },
};

Call an API with ctx.http.fetch:

module.exports = {
  description: "Returns the latest release tag of a GitHub repository.",
  parameters: {
    type: "object",
    properties: { repo: { type: "string" } },
    required: ["repo"],
    additionalProperties: false,
  },
  execute: function(ctx, args) {
    const res = ctx.http.fetch(
      "https://api.github.com/repos/" + args.repo + "/releases/latest",
    );
    if (res.status !== 200) {
      return { text: "HTTP " + res.status, isError: true };
    }
    return JSON.parse(res.body).tag_name;
  },
};

Behavior and limits

  • Live output. Output written with ctx.log and everything a ctx.system.exec command prints reaches the front end while the tool runs, so a long tool shows progress instead of looking frozen.
  • Typed returns. execute must return a string or { text, isError }; anything else becomes an error result. There is no implicit serialization.
  • Output cap. The text execute returns is capped (256 KiB, cut on a rune boundary). Streamed output is never capped.
  • No timeout. A tool runs until it returns or the run is interrupted. Controls such as timeout_ms in ctx.system.exec are yours to add.
  • Fresh state. Every invocation runs in a fresh runtime, so nothing persists in memory between calls. To remember something, write a file.
  • Name rules. A tool name is 1 to 64 characters of letters, digits, underscores or hyphens.