Reference
@chloejs/ core
Everything an agent, a job, a tool or a channel is written with.
What chloe is, to the repo that installs it.
Everything an agent, a job, a tool or a channel is written with is named
here, and nothing else in this folder is anybody's business. Import it as
"@chloejs/core": the lines below say the rest. A tool is made with the AI
SDK's tool(), imported from "ai".
import { defineJob, note } from "@chloejs/core";
import { run, deliverEmail } from "@chloejs/core/services"; // the work, for a job
import * as gmail from "@chloejs/core/tools/gmail"; // the work, for a model
import { telegramChannel } from "@chloejs/core/channels"; // reaching an agent
import { calls, expectations } from "@chloejs/core/scorers"; // marking a runAdding a name here is publishing it, and taking one away is a break, so this
file is the one place to look when you want to know what may move freely.
The server is startChloe: npx chloe runs it through server.ts, and a script
with no chloe.config.ts calls it with its own agents.
AbilitiesAgentAgentConfigagentDiragentIdsagentOfAgentStepanswerAnswerAskOptionsAskStepasOwnerasPersonAttachmentCallcanReachChannelChannelRouteChatHistorycheckArgsChloeToolConfigconfineConnectioncopyDatabaseDataDATABASEdbDeclaredSettingsdeclareSettingsdefineAgentdefineConfigDefinedAgentdefineJobdeliverEnvelopeisPromptJobJobConfigKEYSlearnModelsLineloadloadAllloadSettingsmarkdownJobMarkdownJobMEMORIESMemoryModelStepnameInEnvNeedsSignInnoteNoteoneLineSummaryownerParkedRunparkedRunspersonAskingPlainSteppromptPromptreachByreadSettingsresumeROOTRunningRunOptionsRunResultSendsettingsSettingsShapeSignInsignInFolderSkillsplitstartChloeSTATEStepContextsweepToolContextToolstrimturnTurnResultunclaimedUsageLimitwaitingForwaitingOnwhereKeyGoesworkWorkWrongArgs
Abilitiesinterface
export interface Abilities {
/**
* Adds the tool `scriptRun`, which lets the agent run any file in its own
* `scripts/` folder. Off by default.
*
* Each script runs as a program, so it must be executable (`chmod +x`, with
* a `#!` first line). It runs inside the `scripts/` folder, and gets the
* agent's memory folder in the `MEMORY_FOLDER` environment variable. If this
* is on and `scripts/` is empty, the agent does not load.
*
* Write a skill that tells the agent when to use each script.
*/
scripts?: boolean;
/**
* Adds the tools `folderListFiles`, `folderReadFile` and
* `folderSearchFiles`, which read the folders named in `paths`, like a
* project's code, and never write to them. Off by default, and adds nothing
* until `paths` names a folder.
*
* The model names a folder by the last part of its path, so no two may end
* the same. In a git repository, what git ignores is left out (`.env.local`,
* `dist/`), and so is any `.env`, `.git`, `secrets` and `node_modules`
* anywhere. A guest on a web channel gets them only if the channel names
* them, like any other tool.
*/
folders?: boolean | FoldersAbility;
}
interface Abilities {
/**
* Adds the tools that read the Gmail account Google is signed in to,
* `emailReadMail`, and with the fields below, `emailReplyMail` and
* `emailSendMail`. Off by default. `true` reads the inbox of the last 7
* days and nothing else. Give fields to say which mail, and to let it reply
* or send. Needs the Google sign-in.
*/
googleMail?: boolean | GoogleMailAbility;
/**
* Adds the tool `calendarListEvents` for a Google calendar, and with
* `add`, `calendarAddEvent`. Off by default. `true` reads the signed-in
* account's own calendar for the next 7 days. Needs the Google sign-in.
*/
googleCalendar?: boolean | GoogleCalendarAbility;
/**
* Adds the tools `driveSearchFiles`, `driveReadFile` and `driveDownloadFile`,
* which find files in Google Drive, read them as text, and download one as
* it is into the agent's received files for a script. Off by default. `true` reaches all of
* Drive: give `search` to keep it to some files. Needs the Google sign-in.
*/
googleDrive?: boolean | GoogleDriveAbility;
}
interface Abilities {
/**
* Adds the tool `shellRun`, which runs a shell command in the machine's
* workspace as the shell user, through the proxy chloe runs for it. Off by
* default.
*
* Only in a machine of its own (`machine.own`): on a shared computer the
* shell is the whole computer. Refused for the rest of a reply once a tool
* that reads from outside has answered in it, so a crafted mail cannot ask
* for a command that carries the notes away. Its own output does not count
* as from outside, so two commands in one reply are fine.
*/
shell?: boolean;
}
interface Abilities {
/**
* Adds the memory tools: `memoryListFiles`, `memoryReadFile`,
* `memorySearchFiles` and `memoryEditFile`. They work on
* the agent's memory folder (see `memory` on the agent). On by default.
*/
memory?: boolean;
/**
* Adds the tool `memoryWriteUserNotes`, which keeps one note about each
* person the agent talks to, on any channel. Off by default.
*
* The notes are files in the agent's memory, like
* `users/telegram-12345.md`. chloe picks whose note it is from who sent the
* message, and the model cannot choose. The model sees that person's note
* at the start of each reply to them. One person on two channels has two
* notes.
*/
memoryPerUser?: boolean;
}
interface Abilities {
/**
* Adds the tool `resendSendEmail`, which sends one email through Resend to
* people you name. Off by default, and adds nothing until `from` and `to`
* are filled in. Needs `connections.resend.api_key`: without it nothing is
* sent and nothing fails.
*/
resend?: boolean | ResendAbility;
}
interface Abilities {
/**
* Adds the tool `selfWriteFile`, which lets the agent change its own files:
* its purpose, skills and jobs, and its code (`agent.ts`, jobs, tools,
* services, channels, scripts). On by default, which suits an agent that is
* still being shaped; switch it off once it does what you want.
*
* Each change is saved as a git commit, so you can see it and undo it on
* the dashboard. A change with code in it is loaded and type checked first,
* and put back with the reason when that fails. The agent can only use this
* tool in a reply to a message from its owner. It cannot use it after
* another tool in the same reply has read something from outside the agent
* (an email, a web page, a script's output, one of its past runs). It never
* writes its `evals/` folder or its memory.
*
* Warning: code the agent writes then runs on your machine with the same
* rights as chloe.
*
* Every agent can already read its own files in every turn
* (`ownListFiles`, `ownReadFile`) and its past runs (`ownListRuns`,
* `ownReadRun`), and try one of its jobs (`ownTryJob`), in replies to its
* owner. It does not need this setting for that.
*/
selfImprovement?: boolean;
}
interface Abilities {
/**
* Adds the tools `webReadPage` (read one public web page as plain text) and
* `webSearch` (search the web). Off by default. `webReadPage` reads only
* public addresses and does not run the page's JavaScript. `webSearch` goes
* to Brave Search when `connections.brave.api_key` is set, and to
* DuckDuckGo when it is not. Switch the search tool off on its own to read
* pages only.
*/
web?: boolean;
}Tools that chloe can add to an agent, each switched on or off with true
or false, for example abilities: { scripts: true }. A few take their
fields instead of true, like googleMail: { search: "in:inbox", days: 3 };
the file says only what differs from the default. The page's Abilities view
switches them and fills the fields in, by changing this object in
agent.ts. allAbilities() is the list, with what each one is when unsaid.
A plugin adds its own to this interface from its folder.
Agentinterface
export interface Agent extends Omit<DefinedAgent, "purpose" | "tools" | "jobs" | "channels" | "memory" | "model" | "connections" | "run" | "ask"> {
/**
* The model the agent uses: its own `model`, or `model.defaultModel` from
* settings. Always set. An AI SDK model shows here by its name, like
* `"anthropic/claude-opus-5-5"`.
*/
model: string;
/** The agent's memory, with `folder` and `commit` always filled in. See `Memory`. */
memory: Memory & { folder: string };
/** The agent's purpose as text, read from its file when it is in one. */
purpose: string;
/** The file the purpose was read from, as a path inside the agent's folder. Not set when it was written as a string. */
purposeFile?: string;
/**
* The full paths of the files that the purpose and the jobs' prompts
* add with `prompt(file, { include })`. chloe watches them like the agent's
* own folder.
*/
included?: string[];
/** Every tool the agent has: its own `tools`, plus the ones `abilities` and `connections` add. */
tools?: Tools;
/** Where each tool that is not the agent's own comes from, like `abilities.memory` or `connection github`. */
toolsFrom?: Record<string, string>;
/** The tools `without` took out, so the page can show them switched off. */
toolsOff?: Tools;
/** The agent's skills, read from its `skills/` folder: each `<name>.md` and each `<name>/SKILL.md`. */
skills: Skill[];
/** The agent's jobs, loaded, including the jobs named on its channels. */
jobs: Job[];
/** The agent's channels. */
channels: Channel[];
/** The agent's MCP connections. */
connections: ToolConnection[];
}One agent after chloe has loaded it: its purpose read, its tools
ready, and its skills and jobs loaded. load and loadAll return these,
and a channel's check and start get one.
AgentConfiginterface
export interface AgentConfig {
/**
* The agent's id, like `"inbox"`. Its run history and its default memory
* folder are filed under it, and its channel tokens go under `agents.<id>`
* in settings. Required.
*
* Do not change it after the agent has run: its old runs and memory stay
* under the old id. To change the name people see, use `name`.
*/
id: string;
/**
* The agent's name, like `"C.C."`. The dashboard shows it, the agent is
* told it is its name at the top of every turn, and the email channel sends
* mail under it. You can change it at any time. Default: the `id`.
*/
name?: string;
/**
* The agent's folder, which holds its purpose, skills, jobs, scripts
* and evals. Paths you give `prompt()` and `markdownJob()` start here.
* Default: the folder of the file that calls `defineAgent`.
*
* If chloe cannot tell which file that is, it throws an error. Then set
* `folder: import.meta.dirname`.
*/
folder?: string;
/**
* The model the agent uses. You can give it in two ways:
*
* - A name, like `"anthropic/claude-sonnet-5"`. chloe reaches it the first
* way that works on this machine, in the order of `model.preferredRoute`
* in settings (a subscription through the `claude`, `codex`, `antigravity`
* or `opencode` program, the provider's own API key, or an AI gateway).
* - An AI SDK model, like `anthropic("claude-opus-5-5")`. It goes straight
* to that provider, set up the way you set up its package.
*
* Default: `model.defaultModel` in settings. If neither is set, the agent
* does not load. A job can use another model with its own `model`.
*/
model?: string | SdkModel;
/**
* Auto: each message in a conversation goes first to the quick model
* (`model.quickModel`, or the quick one of the route `model` goes by), which
* says whether it needs `model`. It answers the easy ones itself. Once a
* conversation has gone to `model` it stays there for the hour. Jobs keep
* their own model. Default: false.
*/
auto?: boolean;
/** One line on what the agent does. The dashboard shows it wherever agents are listed. Required. */
identity: string;
/**
* The agent's memory: a folder it reads and writes between runs. You can
* browse and edit it on the dashboard. See `Memory`.
*
* Default: a folder named after the agent's `id`, inside `data/memory/` in
* your project. chloe makes that a git repository and saves what each run
* changed as a commit. Set this only to use another folder, for example one
* you share with a person.
*
* The memory tools read and write it (see `abilities.memory`). Each time the
* dashboard reads a file from it, chloe first writes that down in the
* agent's audit log.
*/
memory?: Memory;
/**
* Turns on tools that chloe has built in: the memory tools,
* `selfWriteFile`, `scriptRun` and `memoryWriteUserNotes`. See `Abilities`.
* Memory and SelfImprovement are On by default.
*/
abilities?: Abilities;
/**
* The agent's purpose: what it is and how it works. The model reads
* it before it answers a message or runs a markdown job. Required.
*
* Give `prompt("purpose.md")` to keep it in a file inside the
* agent's folder, or a string with the words themselves. A plain string is
* always used as the words, never as a file path.
*/
purpose: string | Prompt;
/**
* The tools the model can call, keyed by the name the model sees:
* `{ weather, gmailReadEmail: gmail.readEmail({ ... }) }`. Make your own
* with the AI SDK's `tool()`. Default: none.
*
* Every tool gets the agent in its `context`. Read it with
* `agentOf(context)`.
*
* Do not list the tools that `abilities` and `connections` add: chloe adds
* them for you. A tool here with the same name replaces the added one. You
* also add nothing for signing in: when a tool needs an outside account
* (like Google for `gmail.readEmail`), chloe handles the sign-in.
*/
tools?: Tools;
/**
* Tools the agent has switched off, by the name each has in `tools` or the
* name its ability gives it, like `["memoryEditFile"]`. A ability's tools
* can be named whether it is on or not. The dashboard's Tools page writes
* this list when you switch one tool off, so a tool keeps the options it
* was bound with. Default: none.
*/
without?: string[];
/**
* The agent's jobs. Each one is a job made with `defineJob` and imported
* from its file, or `markdownJob("jobs/<id>.md")` for a job that is only a
* prompt. A job that is not in this list never runs. Default: none.
*/
jobs?: (JobConfig<any, any, any> | MarkdownJob)[];
/**
* The ways people and other systems reach the agent, each under its own
* name and made by its function, like `{ telegram: telegramChannel({
* allowFrom: [123456789] }), api: apiChannel() }`. A channel you wrote
* yourself goes in the same way. The key is the channel's `name`. Default:
* none. The owner can always talk to the agent in the dashboard's chat.
*/
channels?: ChannelsConfig;
/**
* The MCP servers this agent uses. (An MCP server is a service's list of
* tools for models.) For example:
* `[mcpConnection({ name: "github", url, token: process.env.GITHUB_TOKEN })]`.
*
* chloe asks each server for its tools when the agent loads, and adds them
* to the agent's tools. Only this agent gets them. If a server does not
* answer, the agent loads without its tools and the dashboard says why.
* Default: none.
*/
connections?: ToolConnection[];
/**
* When the model must stop, as the AI SDK's `stopWhen`: `isStepCount(20)`,
* or a list of conditions. It applies to each reply to a message and each
* run of a markdown job. A step is one call to the model, and the tools it
* asked for. Default: `isStepCount(40)`.
*
* When this stops the model, the reply is "Stopped after N steps without
* finishing." Agent steps in code jobs have their own `stopWhen`.
*/
stopWhen?: StopCondition<any> | StopCondition<any>[];
/**
* Decides, before each tool call, whether the call may run, as the AI SDK's
* `toolApproval`. It applies to replies to messages and to markdown jobs.
* When it gives no decision for a call, the tool's own `needsApproval`
* decides. Default: every call runs, unless its tool's `needsApproval` says
* otherwise.
*
* A refused call does not run, and the model is told why. A call that needs
* a person to say yes (`"user-approval"`) is refused too, because a reply
* cannot pause to ask. Agent steps in code jobs have their own
* `toolApproval`, and they can pause to ask.
*/
toolApproval?: ToolApprovalConfiguration<any, any>;
}
interface AgentConfig {
/**
* Where the agent works. With `sandbox`, in a machine of its own, which
* this copy makes and keeps as its definition says, and shows on its
* page:
*
* ```ts
* workspace: { sandbox: { type: "podman", image: "./machine/Dockerfile" } }
* ```
*
* Its folder is lent to the machine, so an edit here is live there.
* Default: none, and the agent runs in this copy. See docs/machines.md.
*/
workspace?: Workspace;
}The options you give defineAgent. Only id, identity and
purpose are required.
agentDirfunction
export function agentDir(id: string): stringReturns an agent's own folder: the folder its agent.ts is in, with its
skills, scripts, evals and prompts. Throws if no agent with this id has
loaded.
agentIdsfunction
export async function agentIds(): Promise<string[]>Returns the id of every agent chloe.config.ts lists: those that run here
in the order it lists them, then those in a machine of their own. It loads
every agent to find them.
agentOffunction
export function agentOf(context: unknown): ToolContext["agent"]Returns the agent a tool is running for. Pass it the context your
execute got. Throws if the context has no agent.
AgentStepinterface
export interface AgentStep<O extends Shape = Shape, T extends Tools = Tools> {
/** What you want done (the goal), not how to do it. Required. */
prompt: string;
/**
* The tools the model may call, keyed by name, each made with the AI SDK's
* `tool()`. The model can call nothing else. Required, and it must not be
* empty (with no tools, use `work.model()`).
*
* Each tool needs an `execute` function. A tool that changes the agent
* itself, like `selfWriteFile`, is not allowed: only the owner can ask for
* that, in a chat.
*/
tools: T;
/**
* The shape the final answer must have, as for `work.model()`. If the
* answer does not fit, the model is told what was wrong and asked once more.
* Default: none, and the step returns the model's final answer as text.
*/
output?: O;
/**
* When the model must stop, as the AI SDK's `stopWhen`: `isStepCount(8)`,
* `hasToolCall("done")`, or a list of them. It is checked after each round
* of tool calls. Default: `isStepCount(10)`, which is 10 rounds of tool calls.
*
* If this stops the model before it gives its final answer, it is asked
* once more, with every tool call refused, to answer from what it has done.
* The step fails only when that answer does not come.
*/
stopWhen?: StopCondition<NoInfer<T>> | StopCondition<NoInfer<T>>[];
/**
* The most this step may spend, in dollars, like `0.5`. It must be more
* than 0. Off by default.
*
* It is checked each time the model answers. The answer that goes over the
* limit is already paid for, so the step can spend a little more than this:
* set it at the point where you want the step to give up. If the model has
* spent this much and still asks for tools, the step fails with an error.
*
* Without a budget, `stopWhen` is the only limit, and 10 rounds with a large
* model can cost a lot.
*/
budget?: number;
/**
* Decides, before each tool call runs, whether it may run. It sees the input
* the model chose. This is the AI SDK's `toolApproval`: one function for all
* calls, or an object with a value or a function per tool name. The values:
* - "approved": the call runs.
* - "denied", with a reason: the call does not run, and the model is told the reason.
* - "user-approval": a person must say yes first.
*
* When this gives no answer for a call, the tool's own `needsApproval`
* decides. Default: none, so every call runs unless the tool's
* `needsApproval` says a person must say yes.
*
* When a person must say yes, the run pauses and asks the run's owner yes or
* no. On yes, the call runs and the step goes on from there. On no, or if
* nobody answers in 2 hours, the call does not run and the model is told.
* A run with no owner never runs such a call.
*/
toolApproval?: ToolApprovalConfiguration<NoInfer<T>, any>;
/**
* The model for this step only, when it should be different from the rest
* of the job: a name like "anthropic/claude-haiku-4.5", or an AI SDK model.
* Default: the model the job runs on.
*/
model?: string | SdkModel;
/** Extra instructions for the model: what it should know before it starts. Default: none. */
instructions?: string;
/**
* The most minutes the step may take, every model call and tool call
* included. It then fails saying it ran out of time, and a tool still
* running is told to stop. Time spent waiting for a person to approve a
* call does not count. Default: 30. 0 is no limit of its own.
*/
timeLimit?: number;
}The options for work.agent(). You give a goal and some tools. The model
decides which tools to call, and in what order.
Use this only when you cannot know the order of the work in advance. If you
know the steps, use work.step(). If you know the question, use one
work.model().
answerfunction
export async function answer(runId: string, reply: string, agents: Map<string, Agent>): Promise<Result>Gives a person's reply to the run that is waiting for it, and continues the run. Returns the run's result: finished, or paused again (for example when the reply did not fit and the question was sent again).
agents is every loaded agent by id, as loadAll() returns them. Throws
when the run is not waiting for an answer.
Answertype
export type Answer<O extends Shape> = O extends z.ZodType ? z.infer<O> : O extends Output ? InferGenerateOutput<O> : never;The type of an answer in a given Shape, after it has been checked. For a zod schema, this is z.infer of it.
AskOptionsinterface
export interface AskOptions {
/** The question or message for the agent. Required. */
prompt: string;
/**
* The id of a conversation to continue. Pick any string, and use the same
* one each time. The model first sees the latest 10 messages of that
* conversation, and the question and answer are added to it. Default:
* none, so each `ask` starts fresh.
*/
thread?: string;
/** Where the question came from, as the run history shows it. Default: `"terminal"`. */
source?: string;
/** Cancels the model calls when it is aborted. */
signal?: AbortSignal;
/**
* Settings for a script that has no `chloe.config.ts`, in the same shape as
* `settings` in that file. If the project has a `chloe.config.ts`, leave
* this out: giving it there throws an error.
*/
settings?: DeclaredSettings;
}The options for agent.ask.
AskStepinterface
export interface AskStep<S extends z.ZodType> {
/** The question to send. Required. */
question: string;
/**
* The shape the person's answer must have, as a zod schema, like
* `z.boolean()`, `z.enum(["small", "large"])`, or
* `z.array(z.enum(["red", "green", "blue"]))` for any number of them.
* Required.
*
* Plain words count: "yes", "ok" or "no" fit `z.boolean()`, "12" fits
* `z.number()`, and a choice may be written in any case or as its number
* in the list, with commas between several. The question is sent with a
* short hint, like "(yes or no)". On channels that have buttons, a yes or no
* question, or a list of 8 choices or fewer, is sent with a button for each
* answer, and a list of which several may be picked with one to tick each.
*
* If an answer does not fit, the person is asked again, up to 3 times. After
* that, the run stops with an error.
*/
answer: S;
/**
* Who to ask, as an address "channel:who", like "telegram:12345".
* Default: the run's owner (`work.owner`). If there is no owner and no
* `who`, the step fails.
*/
who?: string;
/** How long to wait for an answer: a number and then `m`, `h` or `d`, like "30m", "4h" or "2d". Default: "2h". */
within?: string;
/**
* The answer to use if nobody answers in time. Default: none, and the run
* stops with an error that says nobody answered.
*/
otherwise?: z.infer<S>;
}The options for work.ask(): a question for a person. The run pauses until
they answer. Code checks their answer against answer: no model reads it.
asOwnerfunction
export function asOwner<T>(work: () => T): TRuns work as the owner, whoever it was started for. For what belongs to
the agent rather than to the person it is talking with, like the mailbox an
email channel reads and sends from.
asPersonfunction
export function asPerson<T>(person: string | undefined, work: () => T): TRuns work for person: the email of somebody the owner invited, or
channel:id for somebody who is not one of them. undefined is the owner.
Attachmentinterface
export interface Attachment {
/** The file's type, such as `"image/jpeg"` or `"application/pdf"`. Required. */
mediaType: string;
/** The file's contents, as base64 text. Required. */
data: string;
/** The file's name, if you know it. */
filename?: string;
}A file sent to the model with a message, such as a photo or a PDF.
Callinterface
export interface Call {
/** The name the model called the tool by. */
toolName: string;
/** The arguments the model passed. */
input: unknown;
/** What the tool returned, or what the model was told when it failed or was refused. */
output: unknown;
/**
* `true` when `toolApproval` or the tool's `needsApproval` did not allow the
* call. The tool did not run, and `output` is what the model was told.
*/
refused?: boolean;
}One tool call the model made: the tool, what it was called with, and what came back.
canReachfunction
export function canReach(address: string, agent = ""): booleanReturns true if a running channel can send to this address for this
agent. It checks the channel part only, not that the person exists.
Returns false for text that is not an address.
Channelinterface
export interface Channel {
/**
* The channel's name, like `"telegram"` or `"api"`. The run history and the
* dashboard show it. Two channels of one agent cannot have the same name.
* The channel named `"api"` is the one that lets a token reach the agent.
* Required.
*/
name: string;
/** How much of the conversation on this channel the model sees with each new message. See `ChatHistory`. Default: the last 10 messages. */
chatHistory?: ChatHistory;
/**
* The options the channel was made with, as JSON text. The dashboard shows
* them, with secrets hidden. When this text changes on a reload, chloe
* restarts the channel, because a running channel keeps the options it
* started with.
*/
madeWith?: string;
/**
* A code job that handles every message on this channel, in place of the
* agent's normal reply. The agent loads it as one of its jobs, but only a
* message on this channel can start it. Do not also put it in the agent's
* `jobs`, and do not give it a `cron`: either one stops the agent from
* loading.
*/
job?: JobConfig<any, any, any, any>;
/**
* The connection this channel works through, the same way a tool names one.
* For example, an email channel on Gmail needs Google. The agent's
* Connections page on the dashboard lists it with what is missing, and you
* sign in there.
*/
needs?: Connection;
/**
* Tools the channel brings with it, added to the agent's tools under
* `channel <name>`: the email channel's tool for starting a conversation.
*/
brings?: Tools;
/**
* Called when the agent loads, to check that this channel can work with
* the agent. If it cannot, throw an error that says why: the agent then
* does not load.
*/
check?(agent: Agent): void;
/**
* Starts the channel, so it begins to listen for messages. Call `agent()`
* for each message to get the agent as it is now, so edits take effect at
* once. It returns `undefined` if the agent is gone.
*/
start(agent: () => Agent | undefined): Running;
}A channel: a way for people or other systems to reach an agent, like
Telegram or the API. List channels in the agent's channels:
channels: [telegramChannel({ ... })]. You only write one yourself for a
platform that chloe does not ship.
ChannelRouteinterface
export interface ChannelRoute {
/** The path, like `/chloe/v1/<agent id>/<channel name>`. It must match the request's path exactly. */
path: string;
/**
* The HTTP methods it answers. Default: `["POST"]`. A platform that checks
* the address with a GET before it uses it needs both. Answer that GET only
* when the caller knows the channel's secret.
*/
methods?: ("GET" | "POST")[];
/** Answers one request. */
handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
}One web address that a running channel answers on chloe's web server. It is answered before the login.
ChatHistoryinterface
export interface ChatHistory {
/** How many of the latest messages to show. A message and its reply count as two. Default: 10. */
messages?: number;
/** Leaves out messages older than this many days. Default: no limit. */
days?: number;
}How much of a conversation the model sees with each new message on a channel. A conversation is one chat, or one topic in a forum. Jobs never see one. Older messages are not deleted: they are only not shown to the model.
checkArgsfunction
export function checkArgs(job: Job, sent: unknown): DataChecks what a job is started with against the job's args schema, and
returns the checked values. Throws WrongArgs when they do not fit.
The message keys (text, from, chat and the other Envelope fields)
are removed first: they go to work.input, never to args. Then:
- A job with no
argsschema takes nothing else. Any other key is an error. - A job with a schema that the clock starts is checked with
{}. So a job with a required field and a cron line fails on its first scheduled run.
It runs before the run is created, so whoever started the job gets the error at once.
ChloeTooltype
export type ChloeTool = Tool & {
/**
* What the tool does, in one word: `read`, `write`, `edit`, `list`,
* `search`, `run` or `fetch`. Each tool that ships says it, and a run's
* record carries it beside the call, so anything that shows a run can show
* what a call did without a list of tool names to keep. Add one to a tool
* of your own when one of the words fits: `Object.assign(tool({ ... }), { kind: "read" })`.
* Off by default: a tool without one is shown as plain text.
*/
kind?: ToolKind;
/**
* What the dashboard calls the tool: a verb and what it works on, like
* "Read file". The tool's name is what the model calls it by and is not
* for people to read. Off by default: the dashboard shows the name in words.
*/
title?: string;
/**
* Returns a few lines about what the tool can reach right now, such as the
* folders in a memory or the tables in a database.
*
* chloe puts this text at the top of the prompt each time the model gets the
* tool, so the model knows where things are before it starts. It is called
* again each time. If it throws, its text is left out.
*/
overview?: () => Promise<string> | string;
/**
* The connection this tool works through, such as Google.
*
* The Connections page on the dashboard, and the lines chloe prints as it
* starts, show what that connection still needs. When the tool throws
* `NeedsSignIn` in a chat, chloe stops the reply and sends the person that
* connection's sign-in link.
*/
needs?: Connection;
/**
* Set to `true` when the tool only reads or changes the agent's own folder,
* memory or skills, so nothing it returns came from outside. Off by default.
*
* A tool without it counts as reading from outside (mail, a web page, a
* script, an MCP server). Once such a tool has answered, every
* `changesAgent` tool is refused for the rest of that reply, because the
* outside text may be what asked for the change.
*/
own?: boolean;
/**
* Set to `true` when the tool only reads: it sends nothing and changes
* nothing outside the agent. Off by default.
*
* In a trial run a call to a tool without it, and not `own`, is written
* into the run's record and not made, so a tool that forgets to say it
* only reads is held back rather than let through.
*/
onlyReads?: boolean;
/**
* Set to `true` to give this tool to the model only when the agent's owner
* sent the message. Off by default.
*
* Replies to anybody else, and markdown jobs, do not get it. Use it for a
* tool that shows what other people said, such as the agent's past runs.
*/
forOwner?: boolean;
/**
* Set to `true` when the tool changes the agent itself: its purpose,
* skills or jobs. Off by default.
*
* The model gets it only when the agent's owner sent the message and is
* allowed to change the agent. A job never gets it: an agent step given one
* fails. It is also refused once a tool that is not `own` has answered in
* the same reply.
*/
changesAgent?: boolean;
/**
* Set to `true` when outside text could be what asks for this tool, so it is
* refused for the rest of a reply once a tool that is not `own` has answered
* in it, the way tools that change the agent are. Off by default. The shell
* in a machine of its own is one: a crafted mail could tell the agent to run
* a command that carries its notes away.
*/
afterOutside?: boolean;
/**
* The most minutes one call may take. It is then told to stop, through the
* `abortSignal` its `execute` is handed, and the model is told it ran out
* of time and can try another way. Default: 10, as long as one model call.
* 0 is no limit of its own.
*/
timeLimit?: number;
/**
* Set to `true` when what the tool returns is the reply itself, as
* `{ text, options?, multiple? }`: the reply ends once it answers, and the
* person is sent `text`, with `options` as buttons where the channel has
* them (`multiple` when several may be picked). The model is not asked
* again. Off by default. `chatAsk` is one.
*/
endsReply?: boolean;
};A tool for the model: a tool made with the AI SDK's tool(), plus a few
optional fields that chloe reads.
tool() drops fields it does not know, so add these after you make the tool:
Object.assign(tool({ ... }), { needs: google }).
Configinterface
export interface Config {
/** Every agent to run. An agent that is not in this list does not run. Required. */
agents: DefinedAgent[];
/**
* Your settings: which model to use, how mail is sent, what a remote
* dashboard may do, and more. See `Settings` for every one. A setting you
* leave out keeps its default.
*
* This file is in source control, so never write a secret here. Write
* `process.env.SOME_NAME` instead, and put the value in `.env`. chloe reads
* no setting from the environment by itself.
*/
settings?: DeclaredSettings;
}What chloe.config.ts exports: the agents to run, and the settings.
confinefunction
export function confine(root: string, input: string): stringReturns the full path of input, after checking that it is inside the
folder root. Throws if it is not. Use it before you read or write a file
whose path came from a model or a person.
input can be relative to root, or a full path inside it. If the path
exists, links in it are followed first, so a link inside the folder cannot
lead out of it. The path does not have to exist yet, so you can check a new
file before you write it.
It also refuses any path that goes through .git, .ssh, secrets or
node_modules, any .env file, and the state folder from a folder that
holds it.
Connectioninterface
export interface Connection {
/** The connection's name, as shown on the dashboard, such as `"google"`. `NeedsSignIn` uses the same name. Required. */
name: string;
/** What the connection is for, in one line, shown on the dashboard. Required. */
does: string;
/**
* The settings it reads, as paths such as `"connections.resend.api_key"`.
* Shown on the dashboard. Never put the values here. Required (can be empty).
*/
settings: string[];
/** How a person signs in from a chat. Leave it out if they cannot sign in from a chat. */
signIn?: SignIn;
/**
* Returns what is still missing before the connection works, one short
* line each. Returns an empty list when it is ready. Shown on the dashboard
* and printed when chloe starts.
*/
missing(): Promise<string[]>;
}An outside account or service that a tool works through, such as Google.
A tool names its connection in its needs field. chloe then asks the
connection what is missing, shows that on the dashboard, and runs its
sign-in when the tool throws NeedsSignIn.
copyDatabasefunction
export async function copyDatabase(to: string): Promise<{ path: string; bytes: number }>Copies the whole database into the file to, and returns the file's path
and its size in bytes. The copy is complete and correct even while runs are
writing. Makes the folder if it is missing.
Datatype
export type Data = Record<string, unknown>;An object with string keys and values of any type. It is the type of
work.state and work.args when the job has no zod schema for them.
DATABASEconst
DATABASEThe path of the SQLite file that holds the run history and the
conversations. Default: agents.db inside STATE.
To move it, set CHLOE_DB in the environment or in .env. A change needs
a restart. ":memory:" keeps the database in memory only, so nothing is
written to disk.
dbconst
dbThe open SQLite database in DATABASE (a DatabaseSync from
node:sqlite). chloe writes every run, with its steps, to the runs
table, and every conversation message to the messages table.
DeclaredSettingstype
export type DeclaredSettings = { [K in keyof Settings]?: Deep<Settings[K]> };The type of settings in chloe.config.ts. It has the same shape as
Settings, but every part is optional, at every level. Anything you leave
out keeps its default.
declareSettingsfunction
export function declareSettings(said: DeclaredSettings | undefined): voidPuts the settings from chloe.config.ts into settings, over the
defaults. loadAll() and loadSettings() call it for you, before any agent
loads. If a value is not valid, it throws and changes nothing.
defineAgentfunction
export function defineAgent(definition: AgentConfig): DefinedAgentDefines an agent. It is usually the default export of agent.ts in the
agent's folder. Add what it returns to agents in chloe.config.ts, or the
agent does not run.
defineConfigfunction
export function defineConfig(config: Config): ConfigReturns the config you give it, unchanged. Use it as the default export of
chloe.config.ts, so your editor checks the config as you write it.
You can write the agents and their jobs in this file too, so a small
project fits in one file. Code inside if (import.meta.main) runs only
when you run the file yourself with node chloe.config.ts, not when chloe
reads it:
if (import.meta.main) console.log(await agent.run({ job: hello }));DefinedAgentinterface
export interface DefinedAgent extends AgentConfig {
/** The agent's folder: the one you set, or the folder of the file that called `defineAgent`. */
folder: string;
/**
* Runs one of this agent's code jobs in this process, and waits for it to
* finish:
*
* ```ts
* import chloe from "./agent.ts";
* import checkWeather from "./jobs/check-weather.ts";
* const { text } = await chloe.run({ job: checkWeather, input: { location: "London" } });
* ```
*
* The run is saved in the run history like any other run. If the job pauses
* to wait for an answer from a person, `run` returns at once with
* `parked: true`.
*
* It loads every agent first, so use it in a script, not in a loop.
*
* Where the settings come from:
*
* - If there is a `chloe.config.ts` in the folder you run the script from,
* or in a folder above it, that file holds the settings, and it must list
* this agent.
* - If there is none, the script is a project of its own with only this
* agent, and `settings` are its settings. `node morning.ts` runs it.
*/
run<ArgsIn>(options: RunOptions<ArgsIn>): Promise<RunResult>;
/**
* Asks this agent one thing and waits for its answer, the same way as a
* message on a channel: with its purpose, its tools and its memory.
* The model keeps calling tools until it has an answer.
*
* ```ts
* const { text } = await chloe.ask({ prompt: "What is on my plate this week?" });
* ```
*
* The reply is saved in the run history like any other. The question does
* not count as a message from the owner, so tools kept for the owner (like
* `ownReadRun`) are not given. Reading its own folder (`ownReadFile`) is.
*
* It loads every agent first, so use it in a script, not in a loop. The
* settings come from the same place as for `run`.
*/
ask(options: AskOptions): Promise<TurnResult>;
}What defineAgent returns: your AgentConfig with folder filled in, plus
run and ask to use the agent from a script. Add it to agents in
chloe.config.ts.
defineJobfunction
export function defineJob<
State = Data,
Result = unknown,
Args = Data,
ArgsIn = Args,
>(definition: JobConfig<State, Result, Args, ArgsIn>): JobConfig<State, Result, Args, ArgsIn>Defines a job. It returns what you give it, unchanged, so that your editor checks the job as you write it.
Give an id and a description, and then either run or markdown,
not both:
export default defineJob({
id: "hello",
description: "Hello has been said.",
run: async (work) => work.step("say hello", () => "hello"),
});If run returns data and not words, use response to turn the data into
words for the chat and the overview:
export default defineJob({
id: "site-check",
description: "Every site that is down has been reported.",
run: async (work) => ({ checked: 12, down: ["a.co"] }),
response: (r) => (r.down.length === 0 ? `All ${r.checked} sites up.` : `${r.down.join(", ")} down.`),
});Every option is on JobConfig, with a note on what it does. Your editor
lists them and checks them as you write.
deliverfunction
export async function deliver(address: string, text: string, agent = "", choices?: string[], multiple?: boolean): Promise<void>Sends a message to an address, such as telegram:12345, through the
channel running for that agent. Pass choices to show the possible answers
as buttons, where the channel can, and multiple when several may be
picked.
Throws if no running channel can reach the address. In a trial run it sends nothing, and the run's record keeps what it would have sent.
Envelopeinterface
export interface Envelope {
/** The text of the message that started the run. */
text: string;
/** The channel it came in on, like "telegram". */
from: string;
/**
* The chat it was sent in, as the channel names it. To ask a question back
* in the same chat, use `${from}:${chat}` as `who` in `work.ask()`.
*/
chat: string;
/** The title of the chat, when it has one (a group, for example). */
chatTitle: string;
/** The name of the person who sent it. */
user: string;
/** The sender's id on that channel: an email address, a Telegram user id, or a web visitor's id. */
userId: string;
/** The conversation the message belongs to: one per chat, or one per topic in a forum. */
thread: string;
/** The text of the message this one replies to, when it is a reply. */
replyTo: string;
}Where a run came from and what was said. It has the same fields for every
channel. A job reads it as work.input.
Every field is a string, and is "" when it is not known. A run the clock started has every field empty.
isPromptfunction
export function isPrompt(value: unknown): value is PromptReturns true if the value was made with prompt(), and false for anything else, such as words written as a plain string.
Jobinterface
export interface Job {
/** The id of the agent the job belongs to. */
agent: string;
/** The job's id. The run history, the API and the evals use it. */
id: string;
/** One line on what the job does. */
description?: string;
/** When the job runs by itself, as a cron line. Not set when it runs only when somebody starts it. */
cron?: string;
/** The timezone of `cron`, like `"America/New_York"`. `"UTC"` when the job did not set one. */
timezone: string;
/** The model the job uses in place of the agent's. Not set when it uses the agent's model. */
model?: string;
/** The job's prompt, for a job that is a prompt. An empty string (`""`) for a code job. */
prompt: string;
/** The job's code, for a job that is code. Not set for a prompt job. */
run?: (work: Work<Data>) => Promise<unknown>;
/** Turns what `run` returned into words. See `response` in `JobConfig`. */
response?: (result: unknown) => string;
/**
* The job's files, as paths inside the agent's folder, the markdown first.
* The dashboard shows them. A markdown job lists its file. A code job lists
* the file its `markdown` prompt is in, and `jobs/<id>.ts` and
* `jobs/<id>.md` if they exist. A code file with any other name is not
* listed.
*/
files: string[];
/** The zod schema of the job's `state`, if it keeps one. */
state?: z.ZodType;
/** The zod schema of the values the job takes when somebody starts it by hand. See `args` in `JobConfig`. */
args?: z.ZodType;
/**
* The channels whose messages this job handles. Set only for a job named in
* a channel's `job` option. Only a message on those channels starts it: it
* has no schedule, no `/command`, and the dashboard and the API cannot
* start it.
*/
channels?: string[];
}One job of an agent, after chloe has loaded it: when it runs, and its code
or its prompt. You find these in agent.jobs. To write a job, use
defineJob or markdownJob.
JobConfiginterface
export interface JobConfig<
State = Data,
Result = unknown,
Args = Data,
/** The type `args` accepts before it is parsed. `agent.run` uses it so your editor can check `input`. */
ArgsIn = Args,
> {
/**
* The job's id, like `"morning-report"`. The run history files its runs
* under it, and `npx chloe agent <agent> <job>` and the evals use it.
* People can start the job from a chat by sending `/<id>`. Required.
*
* Do not change it after the job has run. A job in a file named
* `jobs/<id>.ts` is shown on the dashboard with that file. Any other file
* name works too, and one file can hold several jobs.
*/
id: string;
/**
* One line on what is true when a run is done, not what the job does:
* "The warehouse knows about every late order", not "Finds late orders".
* The dashboard shows it next to the id. Required.
*/
description: string;
/**
* When the job runs by itself, as a cron line with five fields: minute,
* hour, day of month, month, day of week. For example, `"0 7 * * *"` is
* every day at 07:00. You can also write `every.day.at("07:00")`, with
* `every` from `@chloejs/core/timer`.
*
* Default: none, so the job runs only when somebody starts it. A cron line
* that cannot be read stops the agent from loading.
*/
cron?: string;
/** The timezone for `cron`, like `"America/New_York"`. Daylight saving time is handled for you. Default: `"UTC"`. */
timezone?: string;
/**
* The model for this job, when it should not use the agent's model. A name
* like `"anthropic/claude-haiku-4.5"`, or an AI SDK model like
* `anthropic("claude-opus-5-5")`. Default: the agent's model. A model
* picked for this job on the dashboard, or with `/models` in a chat, comes
* first.
*/
model?: string | SdkModel;
/**
* The job's prompt, for a job that is a prompt: a string, or
* `prompt("jobs/morning.md")` for a file inside the agent's folder. The
* agent answers it with its purpose and its tools. Give either
* `markdown` or `run`, not both.
*/
markdown?: string | Prompt;
/**
* The job's code, for a job that is code. Use `if` and `for` as normal, and
* put each piece of real work (sending, writing, spending, calling a
* service) inside `work.step()`. Give either `run` or `markdown`, not both.
*
* This matters because a job that pauses to wait for an answer runs again
* from the top when it continues. A finished step gives back its saved
* result and does not run again. Code outside a step runs again each time,
* so if it sends or writes something, that happens twice.
*/
run?: (work: Work<State, Args>) => Promise<Result>;
/**
* A zod schema for the values the job takes when somebody starts it by
* hand, from the API or `npx chloe agent`. Read them as `work.args`. If the
* values do not fit the schema, the job is refused before the run starts.
*
* Default: none, and then the job refuses any values it is sent. Most jobs
* need none: the message that started the job is always in `work.input`
* (`text`, `from`, `chat`, `user`, `thread`, `replyTo` and the rest). See
* https://chloejs.org/docs/jobs.
*
* In a chat, `/<id> some words` fills the fields of `args` in order, and the
* last field takes the rest of the line. A job with a `cron` line cannot run
* on it if `args` has a required field, so give those fields a default.
*/
args?: z.ZodType<Args, ArgsIn>;
/**
* A zod schema for data that every step of a run can read and change. Read
* it as `work.state` and change it with `work.setState()`. It is kept while
* the job pauses to wait for an answer.
*
* It starts as the schema's parse of `{}`, so give a field a default to
* start it filled.
*/
state?: z.ZodType<State>;
/**
* Turns what `run` returned into the words people see. A chat gets the
* whole text, and the overview on the dashboard shows its first line.
*
* Default: if `run` returns a string, that string is used. Otherwise there
* are no words.
*/
response?: (result: Result) => string;
}The options you give defineJob. Every job needs an id, a
description, and exactly one of run (the job is code) or markdown
(the job is a prompt). Your editor cannot catch a job with both or with
neither, but the agent will not load with one.
KEYSconst
KEYS: string[]The settings that hold secrets, as paths with dots, like
"connections.resend.api_key". Put each of these in .env, and pass it in
from your config with process.env. "agents" means the channel tokens of
every agent.
learnModelsfunction
export async function learnModels(): Promise<void>Asks each route what it runs, and fetches the gateway's prices.
The lists are what people can pick a model from (with /models in a chat, or
on the dashboard) when model.models in settings is empty: each plugin's route is
asked, and the gateway at model.gatewayUrl with
/chat/completions at the end changed to /models. The prices are used to
work out what a call on a provider's own key cost.
chloe's server calls this when it starts and each time it reloads the config. Call it yourself only in a script that asks models without the server. It never throws: a route that does not answer offers nothing of its own until the next try.
Lineinterface
export interface Line {
/**
* The step's place in the run, counting from 0, in the order the job called
* its steps. When a paused run continues, steps are matched by this number
* and by `name`.
*/
seq: number;
/** The name the job gave the step, like "fetch orders" in `work.step("fetch orders", ...)`. */
name: string;
/** The kind of step: `work.step()`, `work.model()`, `work.ask()`, `work.agent()` or `work.wait()`. */
kind: "step" | "model" | "ask" | "agent" | "wait";
/** When the step finished, as an ISO date string. */
at: string;
/** How long the step took, in milliseconds. Always 0 for an ask. For a wait, how long it waited. */
ms: number;
/** What the step spent on models, in dollars. 0 for a step that asked no model. */
cost: number;
/** How many model answers in the step came with no price. Set only when there were some: the step then spent at least `cost`. */
unpriced?: number;
/** What the step returned. Not set when the step failed. */
result?: unknown;
/**
* For a model or agent step: the name of the model it used.
* For an ask: who was asked, and whether they answered in time, like "telegram:12345 answered".
*/
note?: string;
/** For a model or agent step: the prompt it was given. */
prompt?: string;
/** For an ask: the question that was sent. */
question?: string;
/** For an ask: what the person typed, before it was checked. Not set when nobody answered. */
reply?: string;
/** For an agent step: every tool call, in order, including the calls that were not allowed to run. */
calls?: Call[];
/** Why the step failed (the error message). The run usually stops here, unless the job catches the error. */
failed?: string;
}One finished step of a job's run, as saved in the run history.
When a paused run continues, each finished step returns its saved result and does not run again.
loadfunction
export async function load(id: string): Promise<Agent>Loads one agent from chloe.config.ts, by its id. It loads every agent to
do so. If no agent has that id, it throws an error that lists the ids there
are.
loadAllfunction
export async function loadAll(given?: Config): Promise<Map<string, Agent>>Loads every agent that chloe.config.ts lists, and returns them by id. It
also reads the settings from that file.
Give it a config to use that one and not read chloe.config.ts, for a
script that is a project of its own.
loadSettingsfunction
export async function loadSettings(): Promise<void>Reads the settings from chloe.config.ts into settings, and does not
load the agents. Use it in a script that needs a setting before chloe is
running. loadAll already does this.
markdownJobfunction
export function markdownJob(file: string): MarkdownJobAdds a job that is only a prompt, kept in one markdown file. Put it in
jobs in agent.ts: markdownJob("jobs/<id>.md"). The path is inside the
agent's folder, and the file name without .md is the job's id.
The file may start with a block of settings between two --- lines. chloe
reads only cron, description, timezone (default "UTC") and model
there, all optional, and ignores any other key. The prompt is the text
under the block. Without cron, the job runs only when somebody starts it.
MarkdownJobinterface
export interface MarkdownJob {
/** The path of the markdown file, inside the agent's folder. */
markdownJob: string;
}A job that is only a prompt, kept in one markdown file. Made with markdownJob().
MEMORIESconst
MEMORIESThe folder that holds the agents' memories, one folder per agent. By
default chloe makes it one git repository, and commits what each run
changed. An agent can keep its memory somewhere else with memory in its
agent.ts.
Default: memory inside STATE. To move it, set CHLOE_MEMORY in the
environment or in .env. A change needs a restart.
Memoryinterface
export interface Memory {
/**
* The memory folder, as a full path like `"/home/you/notes"`. Default: a
* folder named after the agent's `id`, inside `data/memory/` in your
* project. chloe makes `data/memory/` one git repository for all the
* agents.
*/
folder?: string;
/** The name the dashboard shows for the memory. Default: `"Memory"`. */
label?: string;
/**
* When a change to the memory is saved as a git commit:
*
* - `"each run"`: when a run ends, or pauses to wait for an answer,
* everything it changed is saved in one commit under the agent's id. If
* the folder is not a git repository, chloe makes it one.
* - `true`: every write, by the agent (`memoryEditFile`)
* or from the dashboard, is its own commit with a message. The folder
* should already be a git repository. Use this for a folder you share
* with a person.
* - `false`: never.
*
* Default: `"each run"` for the default folder, and `false` for a folder you
* set in `folder`.
*/
commit?: boolean | "each run";
}Where and how an agent keeps its memory. Give it as memory on the agent.
The dashboard shows the memory next to the agent's own files.
ModelStepinterface
export interface ModelStep<O extends Shape> {
/** The question for the model. Required. */
prompt: string;
/**
* The shape the answer must have: a zod schema, or an AI SDK output like
* `Output.choice({ options })`. See `Shape`. Required.
*
* If the answer does not fit, the model is told what was wrong and asked
* once more. If it still does not fit, the step fails.
*
* `Output.text()` is not allowed: the answer must be data your code can check.
*/
output: O;
/**
* The model for this step only, when it should be different from the rest
* of the job: a name like "anthropic/claude-haiku-4.5", or an AI SDK model.
* Default: the model the job runs on.
*/
model?: string | SdkModel;
/** Extra instructions for the model, sent before the question. Default: none. */
instructions?: string;
/**
* The most minutes the step may take, both tries included. It then fails
* saying it ran out of time. Default: 10. 0 is no limit of its own.
*/
timeLimit?: number;
}The options for work.model(): one question for a model. The answer must
come back in the shape you set, so your code can use it.
nameInEnvfunction
export function nameInEnv(path: string[]): stringReturns the name to give a secret in .env: CHLOE_, then each part of
the setting's path in capitals, joined with _. For example,
["connections", "resend", "api_key"] gives
CHLOE_CONNECTIONS_RESEND_API_KEY.
A capital letter inside a word starts a new part (defaultModel gives
DEFAULT_MODEL), and any character that is not a letter or a digit becomes
_.
This is only a suggested name. chloe never reads it by itself: your config
reads it, with process.env.
NeedsSignInclass
export class NeedsSignIn extends Error {Throw this from a connection's code when signing in would fix the error: no one has signed in, the sign-in expired or was taken back, or it does not allow this action.
When a tool throws it during a chat, the model's reply stops, and chloe starts the connection's sign-in and sends the link to the person. So write the message for a person, not for the model. In a job, with no one to answer, the run fails with the message.
notefunction
export function note<T>(agent: string, name: string, shape: z.ZodType<T>): Note<T>Returns a small JSON file, <name>.json, in the memory folder of the agent
with the id agent. Use it for something a job needs to remember until its
next run.
shape is the zod schema the file is read with. End it with
.catch(...), so a file that is missing or broken reads as that default
value and does not stop the run.
Noteinterface
export interface Note<T> {
/** The full path of the file. */
path: string;
/**
* Reads the file and checks it with the schema. If the schema ends in
* `.catch(...)`, a missing or broken file gives that default value.
* Without it, a missing or broken file throws.
*/
read(): Promise<T>;
/**
* Writes `value` as the whole file, and returns it. The old file is
* replaced in one step, so a reader never sees half a file. Makes the
* folder if it is missing.
*/
write(value: T): Promise<T>;
}A small JSON file an agent keeps in its memory, read with a zod schema.
Made by note().
oneLineSummaryfunction
export function oneLineSummary(text: string, max = 200): stringTurns the start of a markdown reply into one plain line. It removes list
marks, emphasis and heading marks, and joins the lines. If the line is
longer than max characters, it is cut and ends with …. Default max:
200.
It only shortens the text. It does not summarise what the text means.
ownerfunction
export function owner(agent = ""): stringReturns the address of the agent's owner, such as telegram:12345. A job's
questions and approvals go to this person when no one else is named.
It is owner in settings, if set. If not, it is the first id in
allowFrom of the agent's Telegram, Slack, Discord or WhatsApp channel.
Returns
"" if there is neither.
ParkedRuninterface
export interface ParkedRun {
/** The id of the run. */
id: string;
/** The id of the agent. */
agent: string;
/** The id of the job. */
job: string;
/** The address the question went to, like "telegram:12345". */
who: string;
/** The question that was sent. */
question: string;
/** When the question was sent, as an ISO date string. */
asked: string;
/** When the wait ends, as an ISO date string. After that, `sweep()` stops waiting and continues the run. */
expires: string;
/** The answers that fit, when they can be listed, so a page can show each as a button. */
options?: string[];
/** Set when several of `options` may be picked, sent back as one answer with commas between. */
multiple?: boolean;
}A run that is waiting for a person's answer: what it asked, who it asked, and when the wait ends.
parkedRunsfunction
export function parkedRuns(): ParkedRun[]Returns every run that is waiting for a person right now. A run in work.wait() is not one.
personAskingfunction
export function personAsking(): string | undefinedWho the work going on is for, as asPerson() was given it. undefined is the owner.
PlainStepinterface
export interface PlainStep {
/**
* The most minutes the step may take. It then fails saying it ran out of
* time, and the signal its function was handed fires, so a `fetch` or a
* child process given that signal stops. Default: 10. 0 is no limit of its
* own.
*
* Code that does not pass the signal on goes on running in the background
* after the step has failed, and what it returns is dropped.
*/
timeLimit?: number;
}The options for work.step().
promptfunction
export function prompt(file: string, options: { include?: Record<string, string> } = {}): PromptPoints at words kept in a markdown file: an agent's purpose, or a
job's markdown prompt. The path is inside the agent's folder unless it is
a full path. If the file starts with a settings block between two ---
lines, only the text under it is used.
include adds other files after the words, each wrapped in a tag named
after its key. Use it for material the words are about that is kept
somewhere else:
prompt("purpose.md", { include: { cv: "/home/you/cv.md" } })gives the words, then <cv>, the text of the file, and </cv>. Each key
must start with a letter and hold only letters, digits, _ and -, like
cv. chloe watches these
files like the agent's own, so an edit takes effect within a second. If one
is missing, the agent does not load, and the error says which file.
Promptinterface
export interface Prompt {
/** The path of the markdown file, inside the agent's folder unless it is a full path. */
file: string;
/** Other files to add after the words, each wrapped in a tag named after its key. See `prompt()`. */
include?: Record<string, string>;
}Words kept in a markdown file, made with prompt(). chloe reads the file
when the agent loads, and again each time the file changes.
reachByfunction
export function reachBy(channel: string, send: Send, agent = ""): voidTells chloe how to send messages on a channel, so a job's ask can reach
people there. A channel you write yourself calls this once, when it starts.
channel is the first part of an address, such as telegram. Pass the
agent's id as agent so that only this agent's messages go out this way:
two agents on Telegram are two bots, and a question from one must not come
from the other. Without agent, it is used for any agent that has no way
of its own on that channel.
readSettingsfunction
export function readSettings(declared: unknown): SettingsReturns the full settings: the defaults, with the values in declared
written over them, one value at a time. It does not change settings.
It checks declared first, and throws an error that says what is wrong:
a name that is not a setting, a value of the wrong type, or a word that is
not one of the allowed values. It also refuses cloud, page and
dashboard, which are gone, and says so.
resumefunction
export async function resume(runId: string, agents: Map<string, Agent>, signal?: AbortSignal): Promise<Result>Continues a run that paused to wait for a person. It runs the job again
from the top, and each finished step returns its saved result. You usually
call answer() or sweep(), which call this.
agents is every loaded agent by id, as loadAll() returns them.
signal, when given, becomes work.signal.
Throws when the run does not exist or is not waiting, or when its agent or job is gone or is no longer code.
ROOTconst
ROOTThe folder that holds chloe.config.ts, found by walking up from where the
process was started, or that folder itself when none above it has one. A
script with no config is a project of its own: its .env, run history and
memories are kept beside where it was started, as they would be beside a config.
Runninginterface
export interface Running {
/** Stops the channel. */
stop(): void;
/**
* The web addresses this channel answers on chloe's web server, for a
* platform that sends messages in (a webhook). Default: none.
*
* They are answered before the login, so check every request yourself, for
* example with a secret the platform signs it with.
*/
routes?: ChannelRoute[];
}A channel that has started. Channel.start returns it.
RunOptionstype
export type RunOptions<ArgsIn> = {
/** The job to run: a code job (one with `run`) of this agent, imported from its file. Required. */
job: JobConfig<any, any, any, ArgsIn>;
/** Where the run came from, as the run history shows it. Default: `"terminal"`. */
source?: string;
/** Cancels the model calls when it is aborted. */
signal?: AbortSignal;
/**
* Settings for a script that has no `chloe.config.ts`, in the same shape as
* `settings` in that file. If the project has a `chloe.config.ts`, leave
* this out: giving it there throws an error.
*/
settings?: DeclaredSettings;
} & ({} extends ArgsIn ? { input?: ArgsIn & Partial<Envelope> } : { input: ArgsIn & Partial<Envelope> });The options for agent.run.
input is what the job starts with, as the API would send it. The message
keys (text, from, chat, user, thread, replyTo and the rest) go
to work.input. Everything else must fit the job's args, and becomes
work.args. Your editor checks it, and chloe checks it again before the
run starts. You can leave input out only when the job's args has no
required field.
RunResultinterface
export interface Result {
/** The id of the run in the run history. */
runId: string;
/**
* What the job's `run` returned, as text: a string as it is, anything else
* as JSON (`{ "ok": true }` when it returned nothing).
*
* When the run is waiting for a person: "waiting on" and their address.
* When the run ended because nobody answered, or because the job was
* changed while the run was paused: the reason.
*/
text: string;
/**
* The start of `reply` as one plain line, up to 200 characters, for the run
* list on the dashboard. `null` when the job gave no words. Not set when the
* run did not finish.
*/
summary?: string | null;
/**
* What a chat is sent: the text the job's `response` made from the result,
* or else the string `run` returned. If `response` throws, a line that says
* it failed. Not set when there are no words, or when the run did not finish.
*/
reply?: string;
/** How many steps the run has taken so far, counting any that failed. */
steps: number;
/** What the run has spent so far, in dollars, counting only the answers that came with a price. */
cost: number;
/** How many model answers came with no price. Above 0, the run spent at least `cost`. */
unpriced: number;
/** `true` when the run is paused, waiting for a person's answer or in `work.wait()`. */
parked: boolean;
}What a job's run returns, when it finishes or when it pauses to wait for a person.
Sendtype
export type Send = (to: string, text: string, choices?: string[], multiple?: boolean) => Promise<void>;A function that sends a message to one person on a channel.
to is the part of the address after the :, such as 12345 in
telegram:12345. choices lists the possible answers when there are only a
few, so the channel can show them as buttons. With multiple, the person
may pick several, and sends them as one message with together().
settingsconst
settings: SettingsThe settings in use right now. They start as the defaults. When chloe reads
chloe.config.ts (in loadAll() or loadSettings()), it writes the
config's settings into this same object.
So read a value at the moment you need it. Do not keep a copy: a copy does not change when the config changes.
Settingsinterface
export interface Settings {
/** Which models the agents use, and how chloe reaches them. */
model: {
/**
* The model an agent uses when its `agent.ts` does not set one, like
* "anthropic/claude-sonnet-5". Default: "" (none).
*
* With no default, every agent must set its own model, or it fails to
* load. `npx chloe setup` writes this for you.
*/
defaultModel: string;
/**
* The order in which chloe tries the ways to reach a model. For each call,
* it uses the first one that can run the model's provider and is set up on
* this machine (its program is installed, or its key is set). The others
* are skipped.
*
* Default: each plugin's routes, then `["direct", "gateway"]`, so a
* subscription (the claude, codex, antigravity and opencode routes) is
* used before a key that charges per call. `Routes` says what each one is.
*
* A model given in `agent.ts` as an AI SDK model, like
* `anthropic("claude-opus-5-5")`, always goes "direct", whatever this says.
*/
preferredRoute: Route[];
/**
* The models you can pick from when you change the model of a chat, an
* agent or a job (with `/models` in a chat, or on the dashboard). The
* models the agents and jobs already use are always on the list too.
*
* Default: `[]`. Empty lists every model this machine can reach, which is
* often hundreds. Either way, only models this machine can run are listed.
*/
models: string[];
/**
* The address of the gateway for the "gateway" route. Any gateway that
* takes OpenAI-style chat completions requests works. The address ends in
* `/chat/completions`.
* Default: "https://ai-gateway.vercel.sh/v1/chat/completions".
*
* chloe also asks this gateway for its list of models and their prices
* (at `/models` in place of `/chat/completions`). It uses the prices to
* work out what a call with `model.keys` cost.
*/
gatewayUrl: string;
/**
* The key for the gateway at `model.gatewayUrl`. It is a secret: put it in
* `.env`. Default: "" (none). With no key, the "gateway" route is skipped.
*/
key: string;
/**
* Your own API key for each provider, `anthropic` and `openai`, from the
* provider's console. With a key, chloe can send that provider's models
* straight to its API (the "direct" route). Each call is charged.
* Default: "" for both (none).
*
* They are secrets: put them in `.env`. The "claude" route never gets the
* Anthropic key, so the Claude Code CLI stays on your subscription.
*/
keys: Record<Provider, string>;
/**
* The longest one model call may take, in minutes, on any route: one
* answer from Claude Code, Codex, opencode or a provider's API. A call
* still going then is stopped (a program is killed) and fails with a
* message that says so, so a stuck call fails its run instead of holding
* its job for ever. Default: 10. 0 is no limit.
*
* An agent step makes one call per answer, so this is not a limit on the
* whole step.
*/
timeLimit: number;
/**
* The model that grades the agent's answers when you run
* `npx chloe evals`. Default: "anthropic/claude-sonnet-5".
*/
judgeModel: string;
/**
* The quick model: what an agent on Auto hands a message that does not
* need its own model, and what gives each new conversation in the
* dashboard chat a short name. Default: "", which takes the quick model of
* the route the agent's own model goes by, so the same account pays (the
* Claude CLI's "haiku", for one). With neither, Auto is not offered and
* the agent's own model names its conversations.
*/
quickModel: string;
/**
* The command each route that runs a program starts. Change one if the
* program has another name, or is not on the PATH (then give its full
* path). If chloe cannot find a program, it skips that route.
*/
program: Programs;
};
/** How mail is sent. */
email: {
/**
* The service that sends mail when a job calls `deliverEmail()` without
* naming a service. Default: "resend" while the Resend plugin is
* there, and "none" without it.
*
* - "none": sends nothing. It only writes the subject and the receivers
* to the log. Use it for tests, or on a machine with no mail account.
* - A plugin's, like "resend" (with `connections.resend.api_key`) or
* "gmail" (from the Google account in `connections.google`).
* `EmailProviders` lists them.
*
* The `gmail.sendEmail` and `resend.sendEmail` tools always use their own
* service. But with "none", nothing is sent at all, not even by them.
*/
provider: EmailProvider;
};
/**
* The outside services that tools and channels work through, one section
* each. The Connections page on the dashboard shows what each one still
* needs.
*/
connections: Connections;
/**
* Each agent's channel tokens, under the agent's `id` from its `agent.ts`.
* Default: `{}` (none).
*
* They are secrets: put them in `.env`, like
* `agents: { shop: { telegram: process.env.CHLOE_AGENTS_SHOP_TELEGRAM } }`.
*
* If you rename an agent, rename its entry here too. chloe writes a warning
* to its log when an entry matches no agent.
*/
agents: Record<string, AgentSettings>;
/**
* Where chloe's web server listens. It serves the dashboard, the API, and
* the addresses some channels need. A change needs a restart.
*/
serve: {
/**
* The network address the server listens on. Default: "127.0.0.1", so
* only this machine can reach it.
*
* "0.0.0.0" listens on every address the machine has. A container needs
* this, so the machine around it can reach the server.
*
* Warning: use "0.0.0.0" only with a proxy in front and the port closed
* to everything else. The login lockout trusts the visitor's address that
* the proxy writes. It does not protect you if a stranger can reach the
* port without going through the proxy.
*/
host: string;
/**
* The port the server listens on. Default: 3067.
*
* If your host gives you the port in an environment variable, write
* `port: Number(process.env.PORT)`.
*/
port: number;
/**
* The address the dashboard is opened at from other machines, like
* "https://chloe.example.com", when your own web server or private
* network puts it there. Agents use it to send their owner a link to a
* file in their memory. Read on each turn, so it needs no restart.
* Default: "" (unknown, and an agent gives the path on the dashboard).
*/
url: string;
/**
* A key that signs a request in as the owner, sent as
* `Authorization: Bearer <key>`. It is for the copy of chloe that made
* this one's machine, and shows this copy's agent on its page: that copy
* writes it here. It reaches everything the owner's login does, so keep
* it long and in `.env`. Default: "" (none: only the login signs anybody
* in).
*/
ownerKey: string;
/**
* Minutes with nothing to do before this copy stops itself: no request,
* no run going or waiting, and no job due in the next few. For a machine
* of its own, which the copy that made it starts again when it is asked
* for, so it costs nothing while it sleeps. A copy
* whose agents bind a channel other than api never stops, because that
* channel listens. Default: 0 (never).
*/
stopAfter: number;
};
/** The machine chloe runs in, when it runs in one of its own. The machine plugin fills in every field. */
machine: MachineSettings;
/**
* The owner of every agent's runs, written as `channel:id`, like
* "telegram:123456789". When a job stops to ask a question or to get a tool
* call approved, it asks this person. A message from this person counts as
* a message from the owner.
*
* Default: "" (none). Then each agent's owner is the first person in the
* `allowFrom` list of its channel.
*/
owner: string;
/**
* The folder that holds the `node` program the background service runs,
* like "/usr/local/bin". Only `npx chloe install` reads it.
* Default: "" (the folder of the `node` found on the PATH when you run
* `npx chloe install`).
*/
node: string;
}Every setting chloe has. In chloe.config.ts, write only the ones you want
to change, in settings: { ... }. The rest keep their defaults.
Put secrets (keys, tokens, passwords) in .env, and pass each one in with
process.env, like
connections: { resend: { api_key: process.env.CHLOE_CONNECTIONS_RESEND_API_KEY } }.
chloe never reads a setting from the environment by itself.
Shapetype
export type Shape = z.ZodType | Output;The shape a model's answer must have. It is one of these:
- a zod schema, like
z.object({ name: z.string() }). This is the same asOutput.object({ schema }). Output.choice({ options })from the AI SDK, to pick one item from a list.Output.array({ element })from the AI SDK, for a list of items.
SignIninterface
export interface SignIn {
/**
* Starts a sign-in for whoever is asking (`personAsking()`, and keep it in
* `signInFolder()`) and returns what to send the person: `say` is the words,
* and `link` is the address they open. chloe sends the link exactly as you
* return it, after `say`, behind the words "Open the sign-in page", so
* `say` should not print the address again.
*
* Leave out `link` when there is nothing to open yet (for example, the
* account has no app set up). Then put the steps to follow in `say`.
*
* `code` is for a sign-in where the person types a code on the page the
* link opens (Microsoft's and GitHub's device sign-in). The dashboard opens
* the link and shows the code on its own, with its own words, so put the
* code in `say` as well for a chat.
*/
start(): Promise<{ say: string; link?: string; code?: string }>;
/**
* Returns `true` if a chat message is the answer to a sign-in that is
* waiting, such as a pasted code. chloe checks every message with it, so keep
* it fast: read files, nothing slower. Be strict: a message it returns `true`
* for never reaches the model.
*/
answers(text: string): boolean;
/**
* Finishes the sign-in with that answer and returns what to tell the person.
* When it fails, throw an error whose message tells the person what to do.
*/
finish(text: string): Promise<string>;
}How a person signs in to a connection from a chat. chloe runs these three functions itself. The model never starts a sign-in, never sees the link and never sees the answer, because a model asked to copy a long link changes it.
signInFolderfunction
export function signInFolder(name: string, person = personAsking()): stringThe folder a connection keeps its sign-in in, for whoever is asking: name
inside the state folder for the owner, and a folder of their own under its
people/ for anybody else. Keep everything a person signed in to there,
and nothing of theirs anywhere else. Not made here.
Skillinterface
export interface Skill {
/** The skill's name: `name` in the file's top block, else the file name without `.md`, or the folder's name. */
name: string;
/** One sentence on when to use the skill: `description` in the file's top block. Empty if not set. */
description: string;
/** The text of the skill, under the top block. */
body: string;
/** The file's path inside the agent's folder, like `skills/deploys.md` or `skills/fill-pdfs/SKILL.md`. */
file: string;
/** For a skill that is a folder, its path inside the agent's folder, like `skills/fill-pdfs`. */
folder?: string;
}One skill: a markdown file in the agent's skills/ folder, or a folder
there with a SKILL.md in it and whatever else the skill needs (scripts,
longer notes, templates). The model sees each skill's name and description,
and reads the body only when it needs it.
splitfunction
export function split(address: string): { channel: string; to: string }Splits an address such as telegram:12345 into its two parts:
{ channel: "telegram", to: "12345" }. Throws if the text is not an address.
startChloefunction
export async function startChloe(given?: Config, options: { remote?: boolean } = {}): Promise<void>Starts chloe in this process: every agent's cron lines and channels, and
chloe's web server. The promise resolves once everything has started, and
chloe keeps running until the process stops. To run one job and exit, use agent.run.
npx chloe calls it with no options, and it reads chloe.config.ts. A
script with no chloe.config.ts can pass the agents and settings that file
would hold, so one file can be a whole project:
await startChloe({ agents: [inbox], settings: { agents: { inbox: { telegram: process.env.TELEGRAM_TOKEN } } } });If you pass them and there is a chloe.config.ts, it throws an error. After
you edit that script or its .env, restart it: node --watch does that for
you.
remote: true is npx chloe --remote, for a server reached over SSH: it
listens on every address and serves the page over HTTPS, with a certificate
it makes and keeps in the state folder, so the browser warns once. Plain
HTTP still answers on loopback, for the commands that talk to it.
STATEconst
STATEThe state folder, where chloe keeps its own records about the agents: the
run history, the account, the tokens, the Google sign-in, and the log of
which memory files were read. What the agents write is in MEMORIES,
which is inside this folder by default.
Default: data inside ROOT. To move it, set CHLOE_STATE in the
environment or in .env. A change needs a restart.
Keep it out of git: it holds secrets. npx chloe setup adds data to
.gitignore. Warning: git clean -x deletes it.
StepContextinterface
export interface StepContext {
/**
* Fires when the step runs out of time or the run is stopped. Pass it to
* `fetch`, `spawn` and anything else that takes an `AbortSignal`.
*/
signal: AbortSignal;
}What work.step() hands its function.
sweepfunction
export async function sweep(agents: Map<string, Agent>): Promise<void>Stops waiting for every question whose time to answer has passed, and
every work.wait() whose time is up, and continues each of those runs:
- a
waitgoes on past it; - an
askwithotherwisegoes on with that value; - an
askwithout it ends the run with an error; - a tool call waiting for a yes does not run, and the model is told.
The clock calls this every minute. A waiting run keeps its job from starting again, so this is what lets the job run again.
ToolContextinterface
export interface ToolContext {
/** The agent the tool runs for: its `id`, its `folder`, and its `memory` folder and how it is committed. */
agent: { id: string; folder: string; memory: { folder: string; commit?: boolean | "each run" } };
/**
* Who sent the message, as `channel:id`, such as `telegram:12345`. chloe sets
* it from the sender, never from the model. Not set in a job.
*/
user?: string;
}What chloe passes to every tool it runs, as context in the second argument
of execute. Read the agent with agentOf(context).
Toolstype
export type Tools = Record<string, ChloeTool>;A set of tools. Each key is the name the model calls the tool by, such as gmailReadEmail.
trimfunction
export function trim(keepDays = 60): voidDeletes the runs that started more than keepDays days ago. Default: 60.
chloe calls it once each time it starts.
turnfunction
export async function turn(options: Ask): Promise<Result>Sends one message to an agent and returns its reply. The model may call tools: each time it does, the tools run and their answers go back to the model, until it answers without calling a tool.
The model gets the agent's purpose, its tools and its skills. Each step is saved to the run history as it happens.
The run stops early when the agent's stopWhen is met (default: 40 rounds
of tool calls), and text then says so. A tool that fails does not stop
the run: the model is told what went wrong. Throws when asking the model
fails.
One reply at a time in a conversation. A message there while a reply is
going joins it, when it is from the same user and carries no attachments:
the model reads it before its next step, the reply answers both, and this
call returns at once with joined. Any other message waits for that reply
to end, then has its own.
TurnResultinterface
export interface Result {
/** The id of the run in the run history. */
runId: string;
/**
* The model's final answer. When the run stopped early, the reason
* instead, like "Stopped after 40 steps without finishing." When a tool
* needs someone to sign in: what failed, and how to sign in.
*/
text: string;
/** How many times the model answered during the run. */
steps: number;
/** What the run spent, in dollars, counting only the answers that came with a price. */
cost: number;
/** How many model answers came with no price. Above 0, the run spent at least `cost`, and how much more is unknown. */
unpriced: number;
/** Every tool call made, in order, with the input it was given and the output it returned. */
calls: Call[];
/**
* Set when the message was handed to the reply already going in its
* conversation, which answers it: `runId` is that reply's, `text` is empty,
* and nothing ran.
*/
joined?: boolean;
/** Set when the reply ends with a question to pick an answer to (`chatAsk`): the answers, to show as buttons. */
options?: string[];
/** Set with `options` when several may be picked. */
multiple?: boolean;
}What turn() returns: the final answer, and every tool call made on the way.
unclaimedfunction
export function unclaimed(names: string[]): string[]Returns the names in the agents setting that match none of names (the
ids of the agents that loaded). Such an entry usually belongs to an agent
that was renamed.
UsageLimitclass
export class UsageLimit extends Error {}Thrown when the subscription a model is reached through, such as a Claude plan, has hit its usage limit. Every call fails until the limit resets.
The message is written for the person waiting for a reply, and a chat sends it to them as the reply.
waitingForfunction
export function waitingFor(agent: string, job: string): booleanReturns true when this agent's job has a run waiting for a person. The
clock does not start such a job on its schedule until the wait ends.
agent and job are ids.
waitingOnfunction
export function waitingOn(who: string, agent = ""): ParkedRun | undefinedReturns the oldest question sent to this address that has no answer yet, or
undefined when there is none. who is an address like "telegram:12345".
Give agent (an agent id) to look only at that agent's questions.
whereKeyGoesfunction
export function whereKeyGoes(path: string[]): stringReturns a sentence that tells a person where to put a secret: its name in
.env, and the line to write in chloe.config.ts. Use it in an error
message about a missing key.
workfunction
export async function work(options: {
/** The loaded agent the job belongs to. Required. */
agent: Agent;
/** The job to run. It must be written as code (have `run`). Required. */
job: Job;
/** Where the run came from, like "telegram" or "api". It is saved with the run. Default: "unknown". */
source?: string;
/**
* What to start the job with, as an object. The message keys (`text`,
* `from`, `chat` and the other `Envelope` fields) go to `work.input`.
* Everything else is checked against the job's `args` schema and goes to
* `work.args`. Default: nothing.
*/
input?: unknown;
/** Stops the run's model calls when it is aborted. The job sees it as `work.signal`. Default: none. */
signal?: AbortSignal;
/**
* Runs it as a trial. It reads for real, but what chloe would send to a
* person (a message through a channel with `deliver`, an email with
* `deliverEmail`, Gmail or an email channel) is written into the run's
* record instead, and nothing goes. A `work.ask` ends the run there, its
* question held back the same way, and a `work.wait` does not wait. Code
* that sends with its own `fetch` is not held back. A run started inside a
* trial is a trial too. Default: false.
*/
trial?: boolean;
}): Promise<Result>Starts a new run of a job written as code (a job with run), and waits
until the run finishes or pauses to wait for a person. The clock, the
channels and agent.run() call this for you.
Throws WrongArgs before the run starts when input does not fit the
job's args schema. Throws when the job is a prompt, or when a step throws
an error that the job does not catch.
Workinterface
export interface Work<State = Data, Args = Data> {
/**
* Runs `fn` once and saves what it returns. Use it for any work you can
* write as code: a request, a query, a file, an email.
*
* When a paused run continues, this returns the saved result and does not
* call `fn` again. If `fn` throws, the step throws too and the error message
* is saved. When the run continues, the step throws a plain `Error` with
* that same message, again without calling `fn`.
*
* The result is saved as JSON, so return plain data: a `Date`, for example,
* comes back as a string when the run continues. It must be under 64,000
* characters as JSON: return what the next step needs, not everything you
* read.
*
* `name` is shown in the run history. If you edit the job while a run is
* paused, and the steps no longer match by order and name, that run stops
* with an error instead of continuing.
*
* `fn` is handed a `signal` that fires when the step runs out of time (10
* minutes, unless `options.timeLimit` says) or the run is stopped: pass it
* to `fetch` and other calls that take one. See `PlainStep`.
*/
step<T>(name: string, fn: (step: StepContext) => Promise<T> | T, options?: PlainStep): Promise<T>;
/**
* Asks a model one question and returns the answer in the shape you set in
* `output`. If the answer does not fit, the model is told what was wrong and
* asked once more, and then the step fails. What it cost is saved with the
* run. See `ModelStep` for the options.
*/
model<O extends Shape>(name: string, options: ModelStep<O>): Promise<Answer<O>>;
/**
* Gives a model a goal and some tools, and lets it decide which tools to
* call and in what order. Returns its final answer in the shape of `output`.
*
* This gives the model the most freedom, so use it last: only when you
* cannot know the order of the work in advance. Set limits with `stopWhen`
* and `budget`. Every tool call is saved with the run. See `AgentStep` for
* the options.
*/
agent<O extends Shape, T extends Tools>(name: string, options: AgentStep<O, T> & { output: O }): Promise<Answer<O>>;
/**
* Gives a model a goal and some tools, and lets it decide which tools to
* call and in what order. With no `output`, returns its final answer as
* text.
*
* This gives the model the most freedom, so use it last: only when you
* cannot know the order of the work in advance. Set limits with `stopWhen`
* and `budget`. Every tool call is saved with the run. See `AgentStep` for
* the options.
*/
agent<T extends Tools>(name: string, options: Omit<AgentStep<Shape, T>, "output">): Promise<string>;
/**
* Sends a person a question, and pauses the run until they answer. Returns
* their answer, checked against the `answer` schema.
*
* The run is saved while it waits, so chloe can restart in the meantime.
* When the answer comes, `run` starts again from the top, and every
* finished step returns its saved result. While the run waits, the job does
* not start again on its schedule.
*
* Call it between steps, never inside the function you give `work.step()`:
* that throws an error. See `AskStep` for the options.
*/
ask<S extends z.ZodType>(name: string, options: AskStep<S>): Promise<z.infer<S>>;
/**
* Pauses the run for a length of time, a number and then `m`, `h` or `d`,
* like "90m" or "2d". It carries on within a minute after the time is up.
*
* The run is saved while it waits, the same as for `work.ask()`, so chloe
* can restart in the meantime, and while it waits the job does not start
* again on its schedule. Call it between steps, never inside the function
* you give `work.step()`.
*/
wait(name: string, length: string): Promise<void>;
/**
* Data this run keeps between its steps. It starts as what the job's
* `state` schema makes of `{}` (so fields with a default start filled), or
* `{}` when the job has no schema. It is saved, so it is still there after
* the run pauses. Each run starts fresh.
*/
readonly state: State;
/** Changes `state`: the fields you give replace the old ones, and the other fields stay. Saved at once. */
setState(next: Partial<State>): Promise<void>;
/**
* What this run was started with, checked against the job's `args` schema.
* For a run the clock started, it is what the schema makes of `{}`, or `{}`
* when the job has no schema. It never changes: use `state` for data that
* changes.
*/
readonly args: Args;
/**
* Where this run came from and what was said: the channel, the chat, the
* sender and the text of the message. A channel fills it. For a run the API
* started, it holds what the caller sent. For a run the clock started, every
* field is "". You do not need an `args` schema to read it.
*/
readonly input: Envelope;
/**
* The address of the run's owner, like "telegram:12345". This is who
* `work.ask()` asks by default. It is `owner` in the settings, or else the
* first id in `allowFrom` of the agent's Telegram, Slack or WhatsApp
* channel. "" when there is none.
*/
readonly owner: string;
/** The id of the agent this job belongs to. */
readonly agentId: string;
/**
* The full path of the agent's memory folder. When a job saves files there,
* read the path from here instead of writing it again in the job.
*/
readonly memory: string;
/** The id of this run in the run history. */
readonly runId: string;
/**
* Fires when the run is stopped by whoever started it (as
* `agent.run({ signal })` can), and when the run ends, so nothing started
* with it outlives the run. Every step also hands its function a signal of
* its own, which fires at the step's time limit too: inside a step, use that
* one.
*/
readonly signal: AbortSignal;
}What a job's run function gets: the steps it can take, and facts about
the run.
Put every piece of work inside a step. When a run pauses (for work.ask(),
work.wait(), or a tool call that needs a yes), it later continues by running run
again from the top. Each finished step then returns its saved result and
does not run again. Code outside a step runs again every time, so if it
sends, writes or spends something, that happens twice.
WrongArgsclass
export class WrongArgs extends Error {}The error thrown when what a job is started with does not fit its args schema. The message says what is wrong.