Reference
chloejs
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": the three lines below say the rest.
import { defineJob, tool, note } from "chloejs";
import { telegramChannel } from "chloejs/channels/telegram"; // reaching an agent
import { calls, expectations } from "chloejs/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 itself is server.ts, and it is run rather than imported.
AddressAgentagentDirAgentStepansweraskAskStepAttachmentBindingcanReachChannelChannelRouteconfinecopyDatabaseDATABASEdbdefineAgentdefineConfigdefineJobdeliverHomehtmlToTextisPrivateisPromptJobLinelistloadloadAllMailmarkdownJobMessagemessagesModelStepnamesnoteNoteoneMessageownerPageParkedRunparkedRunspromptPromptreachByreadreadPagereadSettingsResultresumeROOTrunRunningRunResultscriptscriptssearchsendSendsettingsettingsSettingsSkillsplitSTATEsweeptoolToolToolstrimturnTurnResultviawaitingForwaitingOnworkWorkwrite
Addressinterface
export interface Address {
/** The From line, e.g. "Backups <info@example.com>". */
from: string;
/** Who it goes to. */
to: string[];
/** Prefix put in front of every subject, so an inbox can be filtered. */
tag?: string;
}Who an agent's mail comes from, who it goes to, and the tag in front of every subject.
Agentinterface
export interface Agent extends Omit<Definition, "instructions" | "tools" | "jobs" | "channels"> {
folder: string;
instructions: string;
tools?: Tools;
skills: Skill[];
jobs: Job[];
channels: Record<string, Channel>;
}One agent as the runtime holds it: the definition with its instructions read, its tools bound, its skills loaded and its jobs resolved.
agentDirfunction
export function agentDir(name: string): stringOne agent's own folder: its skills, scripts, evals and prompts.
AgentStepinterface
export interface AgentStep<S extends z.ZodType = z.ZodType> {
/** What you want done, not how to do it. */
goal: string;
/** Everything it may do. Nothing outside this list is reachable from inside. */
tools: Tool[] | Tools;
/** The shape the final answer has to be in. Without one, you get its words. */
output?: S;
/** Most turns of the loop before it has to stop. Ten by default. */
maxSteps?: number;
/** When this step wants a model the rest of the job does not. */
model?: string;
/** What it should know before it starts. */
system?: string;
}Bounded autonomy. You give the goal and the tools, the model works out the
order. Reach for this only when the order cannot be known in advance: when
you know the steps, they are step calls, and when you know the question, it
is one model call.
answerfunction
export async function answer(runId: string, reply: string, agents: Map<string, Agent>): Promise<Result>Hand a person's answer to the run that was waiting for it.
askfunction
export function ask(request: Ask): Promise<Answer>Asks a model once and returns what it said, whichever way model.via sends
it. The only seam between a key and a subscription.
AskStepinterface
export interface AskStep<S extends z.ZodType> {
question: string;
/** The shape the person's answer has to be in. */
answer: S;
/** An address, "channel:who". Defaults to the run's owner. */
who?: string;
/** How long to wait: "30m", "4h", "2d". Two hours by default. */
within?: string;
/** What to carry on with when nobody answers. Without one, the job stops. */
otherwise?: z.infer<S>;
}A question for a person. The run parks, the question goes out to an address, and what they type is matched against the shape rather than read by a model.
Attachmentinterface
export interface Attachment {
/** Like "image/jpeg" or "application/pdf". */
mediaType: string;
/** The file, base64. */
data: string;
name?: string;
}A file handed to the model with a message: a photo or a PDF.
Bindingtype
export type Binding = (agent: Home) => Tools;A set of tools made for one agent as it loads, like memory().
canReachfunction
export function canReach(address: string, agent = ""): booleanWhether a channel that is running for that agent could deliver to that address.
Channelinterface
export interface Channel {
/** Starts listening. `agent` is read again for every message, so an edit is live. */
start(agent: () => Agent | undefined): Running;
}A way in to an agent, named in its definition: channels: { telegram }.
ChannelRouteinterface
export interface ChannelRoute {
/** Always a POST. */
path: string;
handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
}A path a running channel answers on the one port, outside the login.
confinefunction
export function confine(root: string, input: string): stringResolve input inside root, or throw. Accepts a path relative to the
folder or the absolute form of the same file. Symlinks are resolved first, so
a link inside the folder cannot point out of it. A path that does not exist
yet is fine: a write needs one.
copyDatabasefunction
export async function copyDatabase(to: string): Promise<{ path: string; bytes: number }>A whole copy of the database in the file to, taken from the open
connection, so it is consistent even while runs are writing to it.
DATABASEconst
DATABASEThe run history and the conversations, in one file inside the state folder. The tests set AGENTS_DB to ":memory:" so they never write into the real one.
defineAgentfunction
export function defineAgent(definition: Definition): DefinedDeclares an agent. List it in chloe.config.ts for it to run.
defineConfigfunction
export function defineConfig(config: Config): ConfigThe default export of chloe.config.ts: every agent to run.
defineJobfunction
export function defineJob<State extends z.ZodType = z.ZodType<Record<string, unknown>>, Result = unknown>(
definition: Definition<State, Result>,
): Definition<State, Result>Only here so a job file is type checked as it is written.
deliverfunction
export async function deliver(address: string, text: string, agent = "", choices?: string[]): Promise<void>Sends text to an address through whichever channel is running for that agent, with buttons when choices are given.
Homeinterface
export interface Home {
name: string;
folder: string;
}The agent, as far as a tool bound to it needs to know.
htmlToTextfunction
export function htmlToText(html: string, base: string): { title?: string; text: string }HTML to readable text: blocks become lines, cells are split by " | ", links keep their address.
isPrivatefunction
export function isPrivate(address: string): booleanLoopback, private ranges, link local, multicast, and the same in IPv6, including an IPv4 address carried inside an IPv6 one in any spelling.
isPromptfunction
export function isPrompt(value: unknown): value is PromptWhether a value is a declared prompt rather than words written inline.
Jobinterface
export interface Job {
agent: string;
/** What the run history, the API and `npm run evals` call it. */
id: string;
/** One line on what it does. */
description?: string;
/** When it runs by itself. Without one it runs only when somebody starts it. */
cron?: string;
timezone: string;
/** When this job should not run on the agent's own model. */
model?: string;
/** A job is one of these two and never both. */
prompt: string;
/** The job, when it is code rather than a prompt. */
run?: (work: Work<Record<string, unknown>>) => Promise<unknown>;
/** One line from what `run` returned. See defineJob. */
summary?: (result: unknown) => string;
/**
* The files it is written in, inside the agent's folder, words first. A
* job imported from code is found by its id: jobs/<id>.ts and jobs/<id>.md.
*/
files: string[];
/** The shape of that job's state, when it keeps any. */
state?: z.ZodType;
}One job of an agent's, as the loader resolved it: where its words are, when it runs, and whether it is code.
Lineinterface
export interface Line {
/** Its place in the order the job called things. This is the replay key. */
seq: number;
name: string;
kind: "step" | "model" | "ask" | "agent";
at: string;
ms: number;
cost: number;
result?: unknown;
/** For an ask: who was asked, and what they were asked. */
note?: string;
/** For a model step: what it was asked. */
prompt?: string;
/** For an ask: the question, and what the person typed before it was understood. */
question?: string;
reply?: string;
/** For an agent step: every tool it ran, in the order it ran them. */
calls?: { tool: string; args: unknown; result: unknown }[];
}One finished step, and the record that lets it not run twice.
listfunction
export async function list(root: string, path?: string)List a folder. path is relative to root, and omitting it means the top.
loadfunction
export async function load(name: string): Promise<Agent>One agent by name, or a throw that names the agents there are.
loadAllfunction
export async function loadAll(): Promise<Map<string, Agent>>Every agent chloe.config.ts lists, by name.
Mailinterface
export interface Message {
id: string;
threadId?: string;
subject?: string;
from?: string;
date?: string;
snippet?: string;
}One message from a mailbox, as a search hands it back.
markdownJobfunction
export function markdownJob(file: string): MarkdownJobA job that is words and nothing else, named in agent.ts as
markdownJob("jobs/<id>.md"). The file name is the job's id.
Messageinterface
export interface Message {
role: "system" | "user" | "assistant" | "tool";
content: string;
/** On a user message only. */
attachments?: Attachment[];
/** On an assistant message: the tools it asked for. */
tool_calls?: ToolCall[];
/** On a tool message: which call this answers. */
tool_call_id?: string;
}In the shape the gateway wants it, apart from attachments, which each way turns into its own.
messagesfunction
export async function messages({
search,
days = 7,
limit = 10,
}: {
search: string;
days?: number;
limit?: number;
}): Promise<{ query: string; count: number; messages: Message[] }>What the bound search matches. Nothing here widens it.
ModelStepinterface
export interface ModelStep<S extends z.ZodType> {
prompt: string;
/** The shape the answer has to be in. Free text cannot steer the next step. */
output: S;
/** When this one step wants a model the rest of the job does not. */
model?: string;
system?: string;
}One question for a model, inside a workflow that stays code: you know what to ask, and the answer has to come back in the shape you asked for.
namesfunction
export async function names(): Promise<string[]>Every agent's name, in the order chloe.config.ts lists them.
notefunction
export function note<T>(agent: string, name: string, shape: z.ZodType<T>): Note<T>A note by name, in that agent's own state folder. A shape with a catch
makes a note that is not there read as its default.
Noteinterface
export interface Note<T> {
path: string;
read(): Promise<T>;
write(value: T): Promise<T>;
}One JSON file an agent keeps, read and written against a schema.
oneMessagefunction
export async function oneMessage({
search,
what,
days = 7,
limit = 10,
messageId,
}: {
search: string;
what: string;
days?: number;
limit?: number;
messageId: string;
}): Promise<{ query: string; message: unknown }>One message in full. The search runs again first and an id it does not return is refused, which is what makes the binding a boundary rather than a filter. Same check as the tool, because a job is not more trusted than a model here: it is only more predictable.
ownerfunction
export function owner(agent = ""): stringWho a run belongs to when nothing says otherwise.
Pageinterface
export interface Page {
url: string;
status: number;
title?: string;
/** The page as plain text, links written as `[text](url)`. */
text: string;
/** Where the next slice starts, when the page was longer than one. */
next?: number;
}One fetched web page as plain text, in slices when it is longer than one.
ParkedRuninterface
export interface ParkedRun {
id: string;
agent: string;
job: string;
who: string;
question: string;
asked: string;
expires: string;
}A run waiting on a person: what it asked, who it asked, and when the wait runs out. Read by the page and by the sweep.
parkedRunsfunction
export function parkedRuns(): ParkedRun[]Every run waiting on a person right now.
promptfunction
export function prompt(file: string): PromptDeclares words in a markdown file inside the agent's folder: its instructions, or a job's prompt.
Promptinterface
export interface Prompt {
file: string;
}Words in a file, read when they are needed rather than as the agent loads.
reachByfunction
export function reachBy(channel: string, send: Send, agent = ""): voidCalled once per channel that can carry a question out. With an agent, it is that agent's way out only: two agents on Telegram are two bots, and a question from one must not arrive from the other.
readfunction
export async function read(root: string, path: string)Read one file. path is relative to root and cannot leave it.
readPagefunction
export async function readPage(address: string, from = 0): Promise<Page>Fetches address and returns it as text, a slice at a time starting at from.
readSettingsfunction
export function readSettings(tracked: unknown, local: unknown): SettingsThe two files merged and checked. Separate from reading them so it can be tested without a disk, and so the order that wins is one readable line.
Resultinterface
export interface Result {
exitCode: number;
stdout: string;
stderr: string;
}What a command came back with.
resumefunction
export async function resume(runId: string, agents: Map<string, Agent>, signal?: AbortSignal): Promise<Result>Carry on a job that was waiting for a person.
ROOTconst
ROOTThe folder that holds chloe.config.ts, found by walking up from where the
process was started.
runfunction
export function run(
file: string,
args: string[],
options: { timeoutMs?: number; cwd?: string; env?: Record<string, string> } = {},
): Promise<Result>Runs one command with its arguments, never through a shell, and cuts output that is very long.
Runninginterface
export interface Running {
stop(): void;
/** For a channel that is sent its messages: the paths it answers on the one port, outside the login. */
routes?: ChannelRoute[];
}A channel that has been started, and how to stop it again.
RunResultinterface
export interface Result {
runId: string;
text: string;
steps: number;
cost: number;
parked: boolean;
}What a run of a job came back with, whether it finished or parked.
scriptfunction
export async function script(
agent: string,
name: string,
args: string[] = [],
{ timeoutMs = 300_000, cwd }: { timeoutMs?: number; cwd?: string } = {},
): Promise<Result & { script: string; args: string[] }>Run one. A name not on disk is refused rather than resolved as a path, which
is what stops a ../ in a name reaching anything else on the box.
cwd defaults to the scripts folder, so a script may use relative paths.
An agent whose scripts work on a tree somewhere else passes that instead.
scriptsfunction
export async function scripts(agent: string): Promise<string[]>What this agent has in scripts/, sorted. Nothing hidden.
searchfunction
export async function search(root: string, query: string, folder?: string)Search a folder for text, case-insensitive. folder narrows it.
sendfunction
export async function send(
{ from, to, tag }: Address,
subject: string,
body: string,
): Promise<{ sent: true; id?: string; subject: string }>Sends one email and returns its id. The tag is put in front of the subject.
Sendtype
export type Send = (to: string, text: string, choices?: string[]) => Promise<void>;choices is every answer that fits, when there are few enough to list: a channel may show them as buttons.
settingfunction
export function setting(value: string, fromEnv: string): stringA setting, with an environment variable winning if there is one. Reading it here rather than at import time is what lets a test set one.
settingsconst
settings: SettingsThe settings this process started with: the schema's defaults, then
settings.json, then settings.local.json. One value can still be beaten
by an environment variable, through setting().
Settingstype
export type Settings = z.infer<typeof schema>;Every setting there is, as the schema defines it.
Skillinterface
export interface Skill {
name: string;
description: string;
body: string;
}One markdown file out of an agent's skills/ folder.
splitfunction
export function split(address: string): { channel: string; to: string }An address, channel:who, as its two halves.
STATEconst
STATEEverything the agents keep: their own folders (journals, metrics, notes) and
the run history. Unset, it is data/ inside the repo, which git ignores, so
a second clone keeps its own state. git clean -x would delete it.
sweepfunction
export async function sweep(agents: Map<string, Agent>): Promise<void>Give up on questions nobody answered. Called on the clock's tick.
A parked run holds its job, so a question left alone is a job that never runs again. This is what stops that.
toolfunction
export function tool<Schema extends z.ZodType>(definition: {
id: string;
description: string;
inputSchema: Schema;
execute: (input: z.infer<Schema>) => Promise<unknown> | unknown;
}): Tool<z.infer<Schema>>The type of execute's argument comes from the schema.
Toolinterface
export interface Tool<Input = any> {
id: string;
description: string;
inputSchema: z.ZodType<Input>;
execute: (input: Input) => Promise<unknown> | unknown;
}What a model can be handed: an id, a description a model reads, a schema for its arguments, and one function.
Toolstype
export type Tools = Record<string, Tool>;Keyed by the name the model calls them by.
trimfunction
export function trim(keepDays = 60): voidDrop runs older than this. Called once at startup.
turnfunction
export async function turn({ agent, prompt, attachments, model, thread, source, owner, instead, signal }: Ask): Promise<Result>Runs a prompt: ask a model, run the tools it asked for, put the answers back, ask again, until it stops asking.
TurnResultinterface
export interface Result {
runId: string;
text: string;
steps: number;
cost: number;
calls: { tool: string; args: unknown; result: unknown }[];
}What one turn came back with, including every tool call it made on the way.
viafunction
export function via(): "gateway" | "claude"Which way a model call goes. A box with a key uses it; a box with only a subscription falls through to the CLI, so a fresh clone runs either way without being told. MODEL_VIA settles it when both are there.
waitingForfunction
export function waitingFor(agent: string, job: string): booleanIs this job already waiting on somebody? Then it does not start again.
waitingOnfunction
export function waitingOn(who: string, agent = ""): ParkedRun | undefinedThe oldest question this person has not answered, if there is one. Only that agent's, when it says which.
workfunction
export async function work(options: {
agent: Agent;
job: Job;
source?: string;
signal?: AbortSignal;
}): Promise<Result>Start a job from the beginning.
Workinterface
export interface Work<State = Record<string, unknown>> {
/** Do something, once, and write down what it returned. */
step<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
/** Ask a model one question and get an answer in the shape you asked for. */
model<S extends z.ZodType>(name: string, options: ModelStep<S>): Promise<z.infer<S>>;
/** Hand a goal and some tools to a model and let it pick the order. The most autonomy, so the last resort. */
agent<S extends z.ZodType>(name: string, options: AgentStep<S> & { output: S }): Promise<z.infer<S>>;
agent(name: string, options: Omit<AgentStep, "output">): Promise<string>;
/** Stop and wait for a person. The process may restart while it waits. */
ask<S extends z.ZodType>(name: string, options: AskStep<S>): Promise<z.infer<S>>;
/** The shared store. Survives a pause. */
readonly state: State;
setState(next: Partial<State>): Promise<void>;
readonly owner: string;
/** Whose job this is. Notes, scripts and folders are filed under it. */
readonly agentName: string;
readonly runId: string;
readonly signal?: AbortSignal;
}What a job's run is handed.
writefunction
export async function write(
root: string,
path: string,
content: string,
{ commit = false, message }: { commit?: boolean; message?: string } = {},
)Write one file, replacing it. commit makes the write a git commit, for a
folder that is a repo, and then message is required.