chloejsnpm i chloejs

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 run

Adding 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.

chloe/do/email.ts:15

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.

chloe/load/load.ts:179

agentDirfunction

export function agentDir(name: string): string

One agent's own folder: its skills, scripts, evals and prompts.

chloe/core/paths.ts:18

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.

chloe/core/steps.ts:85

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.

chloe/core/steps.ts:668

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.

chloe/model/model.ts:104

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.

chloe/core/steps.ts:104

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.

chloe/model/model.ts:34

Bindingtype

export type Binding = (agent: Home) => Tools;

A set of tools made for one agent as it loads, like memory().

chloe/load/load.ts:46

canReachfunction

export function canReach(address: string, agent = ""): boolean

Whether a channel that is running for that agent could deliver to that address.

chloe/model/ask.ts:50

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 }.

chloe/load/load.ts:156

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.

chloe/load/load.ts:169

confinefunction

export function confine(root: string, input: string): string

Resolve 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.

chloe/core/confine.ts:18

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.

chloe/core/db.ts:71

DATABASEconst

DATABASE

The 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.

chloe/core/db.ts:12

dbconst

db

The SQLite handle every run, step and conversation is written to.

chloe/core/db.ts:16

defineAgentfunction

export function defineAgent(definition: Definition): Defined

Declares an agent. List it in chloe.config.ts for it to run.

chloe/load/load.ts:80

defineConfigfunction

export function defineConfig(config: Config): Config

The default export of chloe.config.ts: every agent to run.

chloe/load/load.ts:97

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.

chloe/load/job.ts:50

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.

chloe/model/ask.ts:62

Homeinterface

export interface Home {
  name: string;
  folder: string;
}

The agent, as far as a tool bound to it needs to know.

chloe/load/load.ts:40

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.

chloe/do/web.ts:149

isPrivatefunction

export function isPrivate(address: string): boolean

Loopback, private ranges, link local, multicast, and the same in IPv6, including an IPv4 address carried inside an IPv6 one in any spelling.

chloe/do/web.ts:55

isPromptfunction

export function isPrompt(value: unknown): value is Prompt

Whether a value is a declared prompt rather than words written inline.

chloe/core/markdown.ts:58

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.

chloe/load/load.ts:129

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.

chloe/core/steps.ts:30

listfunction

export async function list(root: string, path?: string)

List a folder. path is relative to root, and omitting it means the top.

chloe/do/files.ts:21

loadfunction

export async function load(name: string): Promise<Agent>

One agent by name, or a throw that names the agents there are.

chloe/load/load.ts:226

loadAllfunction

export async function loadAll(): Promise<Map<string, Agent>>

Every agent chloe.config.ts lists, by name.

chloe/load/load.ts:195

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.

chloe/do/mail.ts:28

markdownJobfunction

export function markdownJob(file: string): MarkdownJob

A job that is words and nothing else, named in agent.ts as markdownJob("jobs/<id>.md"). The file name is the job's id.

chloe/load/load.ts:114

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.

chloe/model/model.ts:43

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.

chloe/do/mail.ts:38

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.

chloe/core/steps.ts:70

namesfunction

export async function names(): Promise<string[]>

Every agent's name, in the order chloe.config.ts lists them.

chloe/load/load.ts:221

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.

chloe/core/notes.ts:27

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.

chloe/core/notes.ts:17

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.

chloe/do/mail.ts:58

ownerfunction

export function owner(agent = ""): string

Who a run belongs to when nothing says otherwise.

chloe/model/ask.ts:82

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.

chloe/do/web.ts:15

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.

chloe/core/steps.ts:620

parkedRunsfunction

export function parkedRuns(): ParkedRun[]

Every run waiting on a person right now.

chloe/core/steps.ts:631

promptfunction

export function prompt(file: string): Prompt

Declares words in a markdown file inside the agent's folder: its instructions, or a job's prompt.

chloe/core/markdown.ts:53

Promptinterface

export interface Prompt {
  file: string;
}

Words in a file, read when they are needed rather than as the agent loads.

chloe/core/markdown.ts:45

reachByfunction

export function reachBy(channel: string, send: Send, agent = ""): void

Called 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.

chloe/model/ask.ts:24

readfunction

export async function read(root: string, path: string)

Read one file. path is relative to root and cannot leave it.

chloe/do/files.ts:34

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.

chloe/do/web.ts:178

readSettingsfunction

export function readSettings(tracked: unknown, local: unknown): Settings

The 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.

chloe/core/settings.ts:96

Resultinterface

export interface Result {
  exitCode: number;
  stdout: string;
  stderr: string;
}

What a command came back with.

chloe/do/run.ts:8

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.

chloe/core/steps.ts:205

ROOTconst

ROOT

The folder that holds chloe.config.ts, found by walking up from where the process was started.

chloe/core/root.ts:26

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.

chloe/do/run.ts:26

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.

chloe/load/load.ts:162

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.

chloe/core/steps.ts:138

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.

chloe/do/scripts.ts:33

scriptsfunction

export async function scripts(agent: string): Promise<string[]>

What this agent has in scripts/, sorted. Nothing hidden.

chloe/do/scripts.ts:17

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.

chloe/do/email.ts:25

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.

chloe/model/ask.ts:15

settingfunction

export function setting(value: string, fromEnv: string): string

A 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.

chloe/core/settings.ts:113

settingsconst

settings: Settings

The 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().

chloe/core/settings.ts:107

Settingstype

export type Settings = z.infer<typeof schema>;

Every setting there is, as the schema defines it.

chloe/core/settings.ts:65

Skillinterface

export interface Skill {
  name: string;
  description: string;
  body: string;
}

One markdown file out of an agent's skills/ folder.

chloe/load/load.ts:119

splitfunction

export function split(address: string): { channel: string; to: string }

An address, channel:who, as its two halves.

chloe/model/ask.ts:38

STATEconst

STATE

Everything 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.

chloe/core/paths.ts:29

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.

chloe/core/steps.ts:682

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.

chloe/model/tool.ts:17

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.

chloe/model/tool.ts:9

Toolstype

export type Tools = Record<string, Tool>;

Keyed by the name the model calls them by.

chloe/model/tool.ts:27

trimfunction

export function trim(keepDays = 60): void

Drop runs older than this. Called once at startup.

chloe/core/db.ts:91

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.

chloe/core/turn.ts:48

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.

chloe/core/turn.ts:34

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.

chloe/model/model.ts:22

waitingForfunction

export function waitingFor(agent: string, job: string): boolean

Is this job already waiting on somebody? Then it does not start again.

chloe/core/steps.ts:660

waitingOnfunction

export function waitingOn(who: string, agent = ""): ParkedRun | undefined

The oldest question this person has not answered, if there is one. Only that agent's, when it says which.

chloe/core/steps.ts:653

workfunction

export async function work(options: {
  agent: Agent;
  job: Job;
  source?: string;
  signal?: AbortSignal;
}): Promise<Result>

Start a job from the beginning.

chloe/core/steps.ts:173

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.

chloe/core/steps.ts:117

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.

chloe/do/files.ts:71