ChloeGet Started

Docs

Tools

What a tool is, the ones chloe ships and what each is bound to, and how to write your own.


A tool is something a model may call: a description, an input schema and one function. An agent hands its tools over in tools, keyed by the name the model sees, and a job's agent step hands over its own.

What a tool can reach is fixed in your file, not chosen by the model. Each tool chloe ships is a function you call with a binding (the folder, the mail search, the address to send to), and the model fills in only what its input schema asks for. A search a model can write is a filter, and it widens the moment a turn goes wrong. A binding it cannot write is a boundary.

The tools that ship

Import a set as a whole and bind each tool you want: import * as fs from "@chloejs/core/tools/fs", then tools: { fsReadFile: fs.readFile({ root: "/srv/notes", what: "the shared notes" }) }.

Set Tools Bound to The model chooses
fs listFiles, readFile, searchFiles, writeFile, editFile root, one folder, and what, its name in words. Writes take commit: true to commit each one. A path inside that folder.
web readPage() nothing Which public page to read.
gmail readEmail, replyEmail, sendEmail A Gmail search, or the From and To of what it sends. See Connections. How far back and how many; which listed message to answer.
calendar listEvents, addEvent Which calendars. The dates, and what an event says.
drive searchFiles, readFile A Drive search, like one folder. Words to look for; which listed file to read.
resend sendEmail The From and To. The subject and the body.
email startConversation Which email channel. Who, from the channel's allowFrom, and what to say.

The memory, self and script tools are not imported: features on the agent switches them on. See An agent.

Sending an email

gmail.sendEmail and resend.sendEmail take the same options, and the model writes only the subject and the body.

Option Default What it controls
from required The From line, like "Shop <orders@myshop.com>". Through Gmail, it must be the signed-in account or an alias Google verified for it. Through Resend, an address on a domain Resend sends for.
to required Who it goes to. The model cannot change it.
when required When to use it, in your words. Shown to the model.
replyTo from Where a reply goes.
tag none Put in front of every subject, [tag], so an inbox can filter it.
markdown off Sends the body as HTML with a plain text copy. Off, plain text as written.
keep none A folder in its memory, like "outbox", that gets a copy of each email sent.

To mail somebody and read their reply, use the email channel instead.

Your own tool

Made with the AI SDK's tool(), imported from "ai". The work is a plain function in the agent's services/, so a job can call it too, and the tool is only how a model reaches it:

// Where one of the customer's own orders is. The shop's site signs its
// customers in and asks for each chat's pass with their customer id as the
// visitor, so the runtime hands this tool who it is talking to, and the tool
// looks only at that customer's orders: a stranger who guesses an order number
// learns nothing. The order system is read through services/, the same as
// every job reads it.
import { tool } from "ai";
import { z } from "zod";

import { ordersBy } from "../services/ordersService.ts";

export const orderStatus = tool({
  title: "Looking up the order",
  description: "Where one of this customer's orders is: when it was placed, whether it has shipped, and the date they were promised. Takes the order number, like A-4417.",
  inputSchema: z.object({ id: z.string().describe("The order number, like A-4417.") }),
  execute: async ({ id }, { context }) => {
    // "web:<their customer id>", set by the runtime from the pass, never by the model.
    const user = (context as { user?: string } | undefined)?.user ?? "";
    if (!user.startsWith("web:")) return "Only a customer signed in on the shop's site can look up an order here.";
    const found = (await ordersBy(user.slice(4))).find((one) => one.id.toLowerCase() === id.trim().toLowerCase());
    if (!found) return `None of your orders is ${id}.`;
    return { id: found.id, placed: found.placed, paid: found.paid, shipped: found.shipped ?? "not yet", promised: found.promised };
  },
});

What chloe reads on a tool:

Field What it does
title Shown while the tool runs, on a channel that shows progress: "Looking up the order".
needsApproval The AI SDK's. In a job's agent step the run waits for a person's yes; in a turn the call is refused.
needs The connection it works through, like Google. The dashboard says what that connection is missing, and chloe starts the sign-in when the tool finds nobody signed in.
overview A function giving a few lines about what the tool reaches now (the folders, the tables), put at the top of every turn that has the tool.

needs and overview are not AI SDK fields, so add them after: Object.assign(tool({ ... }), { needs: crm }).

A tool's second argument holds context. agentOf(context) is the agent it runs for, and in a turn context.user is who sent the message, as <channel>:<id> (telegram:12345, web:<visitor>). The runtime sets both; the model never does.

What happens when one fails

A missing tool, bad arguments, or a tool that throws goes back to the model as text, and the turn carries on. So a run that worked may still hold failures: read its steps, not only its reply.

A job never imports a tool. It calls the plain function from a step, and a job that imports a tool fails npm run test.