@chloejs/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/channels/api.ts +41 -0
- package/channels/shared.ts +250 -0
- package/channels/slack.ts +390 -0
- package/channels/telegram.ts +396 -0
- package/core/clock.ts +126 -0
- package/core/confine.ts +45 -0
- package/core/db.ts +117 -0
- package/core/markdown.ts +95 -0
- package/core/notes.ts +44 -0
- package/core/paths.ts +29 -0
- package/core/root.ts +26 -0
- package/core/settings.ts +124 -0
- package/core/steps.ts +896 -0
- package/core/turn.ts +314 -0
- package/do/email.ts +45 -0
- package/do/files.ts +96 -0
- package/do/mail.ts +155 -0
- package/do/run.ts +56 -0
- package/do/scripts.ts +49 -0
- package/do/web.ts +192 -0
- package/index.ts +52 -0
- package/load/job.ts +84 -0
- package/load/load.ts +478 -0
- package/model/ask.ts +84 -0
- package/model/claude.ts +261 -0
- package/model/memory.ts +68 -0
- package/model/model.ts +185 -0
- package/model/tool.ts +53 -0
- package/model/tools/files.ts +71 -0
- package/model/tools/gmail.ts +43 -0
- package/model/tools/index.ts +28 -0
- package/model/tools/memory.ts +23 -0
- package/model/tools/run_script.ts +44 -0
- package/model/tools/send_email.ts +29 -0
- package/model/tools/web.ts +23 -0
- package/model/tools/write_skill.ts +31 -0
- package/ops/account.ts +109 -0
- package/ops/agent.ts +290 -0
- package/ops/check.ts +37 -0
- package/ops/evals.ts +206 -0
- package/ops/install.sh +101 -0
- package/ops/test.ts +1976 -0
- package/package.json +65 -0
- package/scorers/calls.ts +50 -0
- package/scorers/expectations.ts +118 -0
- package/scorers/index.ts +5 -0
- package/serve/alerts.ts +79 -0
- package/serve/errors.ts +10 -0
- package/serve/files.ts +70 -0
- package/serve/http.ts +767 -0
- package/serve/login.ts +299 -0
- package/serve/memory.ts +372 -0
- package/serve/page.ts +142 -0
- package/serve/pass.ts +45 -0
- package/serve/recentWork.ts +69 -0
- package/serve/site.ts +409 -0
- package/serve/tokens.ts +132 -0
- package/server.ts +170 -0
- package/timer/cron.ts +92 -0
- package/timer/every.ts +153 -0
- package/timer/index.ts +4 -0
package/do/scripts.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Running one of an agent's own scripts.
|
|
2
|
+
//
|
|
3
|
+
// The boundary: an agent can run anything in its own scripts/ folder and
|
|
4
|
+
// nothing anywhere else. The name is checked against what is on disk, so no
|
|
5
|
+
// input can be a path, and arguments are passed one at a time, never as a
|
|
6
|
+
// shell string.
|
|
7
|
+
//
|
|
8
|
+
// The tool a model reaches is model/tools/run_script.ts. A job calls
|
|
9
|
+
// these from a step.
|
|
10
|
+
import { readdir } from "node:fs/promises";
|
|
11
|
+
|
|
12
|
+
import { agentDir } from "#chloe/core/paths.ts";
|
|
13
|
+
import { settings } from "#chloe/core/settings.ts";
|
|
14
|
+
import { run, type Result } from "./run.ts";
|
|
15
|
+
|
|
16
|
+
/** What this agent has in scripts/, sorted. Nothing hidden. */
|
|
17
|
+
export async function scripts(agent: string): Promise<string[]> {
|
|
18
|
+
const dir = `${agentDir(agent)}/scripts`;
|
|
19
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
20
|
+
return entries
|
|
21
|
+
.filter((e) => e.isFile() && !e.name.startsWith("."))
|
|
22
|
+
.map((e) => e.name)
|
|
23
|
+
.sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run one. A name not on disk is refused rather than resolved as a path, which
|
|
28
|
+
* is what stops a `../` in a name reaching anything else on the box.
|
|
29
|
+
*
|
|
30
|
+
* `cwd` defaults to the scripts folder, so a script may use relative paths.
|
|
31
|
+
* An agent whose scripts work on a tree somewhere else passes that instead.
|
|
32
|
+
*/
|
|
33
|
+
export async function script(
|
|
34
|
+
agent: string,
|
|
35
|
+
name: string,
|
|
36
|
+
args: string[] = [],
|
|
37
|
+
{ timeoutMs = 300_000, cwd }: { timeoutMs?: number; cwd?: string } = {},
|
|
38
|
+
): Promise<Result & { script: string; args: string[] }> {
|
|
39
|
+
const available = await scripts(agent);
|
|
40
|
+
if (!available.includes(name)) {
|
|
41
|
+
throw new Error(`No script called ${JSON.stringify(name)}. You have: ${available.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
const dir = `${agentDir(agent)}/scripts`;
|
|
44
|
+
// A script is someone else's program, so what it needs from settings reaches
|
|
45
|
+
// it the way a program expects, as an environment variable.
|
|
46
|
+
const env: Record<string, string> = settings.google.GA_KEY_FILE ? { GA_KEY_FILE: settings.google.GA_KEY_FILE } : {};
|
|
47
|
+
const result = await run(`${dir}/${name}`, args, { timeoutMs, cwd: cwd ?? dir, env });
|
|
48
|
+
return { script: name, args, ...result };
|
|
49
|
+
}
|
package/do/web.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Reading one web page, as text.
|
|
2
|
+
//
|
|
3
|
+
// Only public addresses: this box serves its own sites and the runtime on
|
|
4
|
+
// loopback ports, and a page that redirects to one of them must not be read
|
|
5
|
+
// back to whoever asked. Every redirect is checked the same way as the first
|
|
6
|
+
// address.
|
|
7
|
+
//
|
|
8
|
+
// The tool a model reaches is model/tools/web.ts, which calls this.
|
|
9
|
+
import { lookup } from "node:dns";
|
|
10
|
+
import { request as http } from "node:http";
|
|
11
|
+
import { request as https } from "node:https";
|
|
12
|
+
import { isIP, type LookupFunction } from "node:net";
|
|
13
|
+
|
|
14
|
+
/** One fetched web page as plain text, in slices when it is longer than one. */
|
|
15
|
+
export interface Page {
|
|
16
|
+
url: string;
|
|
17
|
+
status: number;
|
|
18
|
+
title?: string;
|
|
19
|
+
/** The page as plain text, links written as `[text](url)`. */
|
|
20
|
+
text: string;
|
|
21
|
+
/** Where the next slice starts, when the page was longer than one. */
|
|
22
|
+
next?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Enough for a results table, short enough not to fill a turn. */
|
|
26
|
+
const SLICE = 20_000;
|
|
27
|
+
const HOPS = 5;
|
|
28
|
+
const LARGEST = 5_000_000;
|
|
29
|
+
|
|
30
|
+
function privateV4([a, b]: number[]): boolean {
|
|
31
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) ||
|
|
32
|
+
(a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** An IPv6 address as its eight 16-bit groups, however it was written. */
|
|
36
|
+
function groups(address: string): number[] | null {
|
|
37
|
+
let text = address.toLowerCase().replace(/%.*$/, "");
|
|
38
|
+
const v4 = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
|
|
39
|
+
if (v4) {
|
|
40
|
+
const [a, b, c, d] = v4.slice(1).map(Number);
|
|
41
|
+
text = text.slice(0, v4.index) + `${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
|
42
|
+
}
|
|
43
|
+
const [head, tail] = text.split("::");
|
|
44
|
+
const left = head ? head.split(":") : [];
|
|
45
|
+
const right = tail ? tail.split(":") : [];
|
|
46
|
+
const fill = text.includes("::") ? 8 - left.length - right.length : 0;
|
|
47
|
+
const all = [...left, ...Array(fill).fill("0"), ...right].map((one) => parseInt(one, 16));
|
|
48
|
+
return all.length === 8 && all.every((one) => one >= 0 && one <= 0xffff) ? all : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loopback, private ranges, link local, multicast, and the same in IPv6,
|
|
53
|
+
* including an IPv4 address carried inside an IPv6 one in any spelling.
|
|
54
|
+
*/
|
|
55
|
+
export function isPrivate(address: string): boolean {
|
|
56
|
+
if (isIP(address) === 4) return privateV4(address.split(".").map(Number));
|
|
57
|
+
const g = groups(address);
|
|
58
|
+
if (!g) return true;
|
|
59
|
+
const inner = [g[6] >> 8, g[6] & 0xff];
|
|
60
|
+
// ::/96 and ::ffff:0:0/96 carry an IPv4 address, and 64:ff9b::/96 translates to one.
|
|
61
|
+
if (g.slice(0, 5).every((one) => one === 0) && (g[5] === 0 || g[5] === 0xffff)) {
|
|
62
|
+
return (g[5] === 0 && g[6] === 0) || privateV4(inner);
|
|
63
|
+
}
|
|
64
|
+
if (g[0] === 0x64 && g[1] === 0xff9b && g.slice(2, 6).every((one) => one === 0)) return privateV4(inner);
|
|
65
|
+
return (g[0] & 0xfe00) === 0xfc00 || (g[0] & 0xffc0) === 0xfe80 || (g[0] & 0xff00) === 0xff00 || g[0] === 0x2001 && g[1] === 0xdb8;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The name is resolved once, here, and the socket connects to what was
|
|
70
|
+
* checked, so a name cannot answer public for the check and private for the
|
|
71
|
+
* connection.
|
|
72
|
+
*/
|
|
73
|
+
const publicOnly: LookupFunction = (host, options, done) => {
|
|
74
|
+
lookup(host, { ...options, all: true }, (error, found) => {
|
|
75
|
+
if (error) return done(error, "", 0);
|
|
76
|
+
const bad = found.find((one) => isPrivate(one.address));
|
|
77
|
+
if (bad) return done(new Error(`${host} is a private address, and those are not read.`), "", 0);
|
|
78
|
+
if (options.all) return (done as any)(null, found);
|
|
79
|
+
done(null, found[0].address, found[0].family);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
interface Reply {
|
|
84
|
+
status: number;
|
|
85
|
+
location?: string;
|
|
86
|
+
type: string;
|
|
87
|
+
body: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function get(url: URL): Promise<Reply> {
|
|
91
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`Only http and https pages, not ${url.protocol}`);
|
|
92
|
+
// A literal address is connected to without a lookup, so it is checked here.
|
|
93
|
+
const literal = url.hostname.replace(/^\[|\]$/g, "");
|
|
94
|
+
if (isIP(literal) && isPrivate(literal)) throw new Error(`${url.hostname} is a private address, and those are not read.`);
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const request = (url.protocol === "https:" ? https : http)(
|
|
97
|
+
url,
|
|
98
|
+
{
|
|
99
|
+
lookup: publicOnly,
|
|
100
|
+
headers: {
|
|
101
|
+
"User-Agent": "Mozilla/5.0 (compatible; chloe)",
|
|
102
|
+
Accept: "text/html,text/plain,application/json,*/*",
|
|
103
|
+
"Accept-Encoding": "identity",
|
|
104
|
+
},
|
|
105
|
+
timeout: 30_000,
|
|
106
|
+
},
|
|
107
|
+
(response) => {
|
|
108
|
+
const type = String(response.headers["content-type"] ?? "");
|
|
109
|
+
const chunks: Buffer[] = [];
|
|
110
|
+
let size = 0;
|
|
111
|
+
response.on("data", (chunk: Buffer) => {
|
|
112
|
+
size += chunk.length;
|
|
113
|
+
if (size > LARGEST) return request.destroy(new Error(`${url.href} is over ${LARGEST / 1_000_000} MB.`));
|
|
114
|
+
chunks.push(chunk);
|
|
115
|
+
});
|
|
116
|
+
response.on("end", () => {
|
|
117
|
+
const charset = /charset=([\w-]+)/i.exec(type)?.[1] ?? "utf-8";
|
|
118
|
+
let decoder: TextDecoder;
|
|
119
|
+
try {
|
|
120
|
+
decoder = new TextDecoder(charset);
|
|
121
|
+
} catch {
|
|
122
|
+
decoder = new TextDecoder();
|
|
123
|
+
}
|
|
124
|
+
resolve({ status: response.statusCode ?? 0, location: response.headers.location, type, body: decoder.decode(Buffer.concat(chunks)) });
|
|
125
|
+
});
|
|
126
|
+
response.on("error", reject);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
request.on("timeout", () => request.destroy(new Error(`${url.href} did not answer in 30 seconds.`)));
|
|
130
|
+
request.on("error", reject);
|
|
131
|
+
request.end();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", mdash: "-", ndash: "-" };
|
|
136
|
+
|
|
137
|
+
function decode(text: string): string {
|
|
138
|
+
// Some sites write   with no semicolon, and browsers forgive it.
|
|
139
|
+
return text.replace(/ (?!;)/gi, " ").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, name: string) => {
|
|
140
|
+
if (name[0] === "#") {
|
|
141
|
+
const code = name[1].toLowerCase() === "x" ? parseInt(name.slice(2), 16) : Number(name.slice(1));
|
|
142
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
143
|
+
}
|
|
144
|
+
return ENTITIES[name.toLowerCase()] ?? whole;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** HTML to readable text: blocks become lines, cells are split by " | ", links keep their address. */
|
|
149
|
+
export function htmlToText(html: string, base: string): { title?: string; text: string } {
|
|
150
|
+
const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];
|
|
151
|
+
const text = html
|
|
152
|
+
.replace(/\s+/g, " ")
|
|
153
|
+
.replace(/<(script|style|noscript|svg|head)\b[\s\S]*?<\/\1>/gi, "")
|
|
154
|
+
.replace(/<!--[\s\S]*?-->|<![^>]*>/g, "")
|
|
155
|
+
.replace(/<a\b[^>]*?href\s*=\s*(["'])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi, (_whole, _quote, href: string, inner: string) => {
|
|
156
|
+
const words = inner.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
157
|
+
let address = decode(href);
|
|
158
|
+
try {
|
|
159
|
+
address = new URL(address, base).href;
|
|
160
|
+
} catch {}
|
|
161
|
+
if (!words) return "";
|
|
162
|
+
return /^(javascript|mailto):|^#/.test(href) ? words : `[${words}](${address.replace(/ /g, "%20")})`;
|
|
163
|
+
})
|
|
164
|
+
.replace(/<\/(td|th)>/gi, " | ")
|
|
165
|
+
.replace(/<br\s*\/?>|<\/(p|div|tr|li|h[1-6]|table|section|article|header|footer)>/gi, "\n")
|
|
166
|
+
.replace(/<\/?[a-z][^>]*>/gi, " ");
|
|
167
|
+
return {
|
|
168
|
+
title: title ? decode(title).replace(/\s+/g, " ").trim() : undefined,
|
|
169
|
+
text: decode(text)
|
|
170
|
+
.split("\n")
|
|
171
|
+
.map((line) => line.replace(/[ \t]+/g, " ").replace(/(\s*\|\s*)+$/, "").trim())
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join("\n"),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Fetches `address` and returns it as text, a slice at a time starting at `from`. */
|
|
178
|
+
export async function readPage(address: string, from = 0): Promise<Page> {
|
|
179
|
+
let url = new URL(address);
|
|
180
|
+
for (let hop = 0; ; hop++) {
|
|
181
|
+
const reply = await get(url);
|
|
182
|
+
if (reply.status >= 300 && reply.status < 400 && reply.location) {
|
|
183
|
+
if (hop >= HOPS) throw new Error(`More than ${HOPS} redirects from ${address}.`);
|
|
184
|
+
url = new URL(reply.location, url);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!/text|json|xml/.test(reply.type)) throw new Error(`${url.href} is ${reply.type || "not text"}, which this cannot read.`);
|
|
188
|
+
const { title, text } = /html/.test(reply.type) ? htmlToText(reply.body, url.href) : { title: undefined, text: reply.body };
|
|
189
|
+
const end = from + SLICE;
|
|
190
|
+
return { url: url.href, status: reply.status, title, text: text.slice(from, end), next: end < text.length ? end : undefined };
|
|
191
|
+
}
|
|
192
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// What chloe is, to the repo that installs it.
|
|
2
|
+
//
|
|
3
|
+
// Everything an agent, a job, a tool or a channel is written with is named
|
|
4
|
+
// here, and nothing else in this folder is anybody's business. Import it as
|
|
5
|
+
// "@chloejs/core": the three lines below say the rest.
|
|
6
|
+
//
|
|
7
|
+
// import { defineJob, tool, note } from "@chloejs/core";
|
|
8
|
+
// import { telegramChannel } from "@chloejs/core/channels/telegram"; // reaching an agent
|
|
9
|
+
// import { calls, expectations } from "@chloejs/core/scorers"; // marking a run
|
|
10
|
+
//
|
|
11
|
+
// Adding a name here is publishing it, and taking one away is a break, so this
|
|
12
|
+
// file is the one place to look when you want to know what may move freely.
|
|
13
|
+
// The server itself is `server.ts`, and it is run rather than imported.
|
|
14
|
+
|
|
15
|
+
// An agent, and the jobs it runs.
|
|
16
|
+
export { defineAgent, defineConfig, markdownJob, load, loadAll, names, type Agent, type Channel, type ChannelRoute, type Running, type Job, type Skill, type Binding, type Home, type Features, type ChatHistory } from "./load/load.ts";
|
|
17
|
+
export { defineJob } from "./load/job.ts";
|
|
18
|
+
export { prompt, isPrompt, oneLineSummary, type Prompt } from "./core/markdown.ts";
|
|
19
|
+
|
|
20
|
+
// A job: code first, with a model where a step needs judgement and an agent
|
|
21
|
+
// where the order of the work cannot be known in advance.
|
|
22
|
+
export { work, resume, answer, sweep, parkedRuns, waitingFor, waitingOn, checkInput, WrongInput, type AgentStep, type AskStep, type Line, type ModelStep, type ParkedRun, type Result as RunResult, type Work } from "./core/steps.ts";
|
|
23
|
+
|
|
24
|
+
// A prompt: ask a model, run the tools it asked for, ask again.
|
|
25
|
+
export { turn, type Result as TurnResult } from "./core/turn.ts";
|
|
26
|
+
|
|
27
|
+
// What a model can be asked to do, and how it is asked.
|
|
28
|
+
export { tool, type Approve, type Call, type Tool, type Tools } from "./model/tool.ts";
|
|
29
|
+
export { ask, via, type Attachment, type Message } from "./model/model.ts";
|
|
30
|
+
|
|
31
|
+
// Reaching a person, and being reached back.
|
|
32
|
+
export { canReach, deliver, owner, reachBy, split, type Send } from "./model/ask.ts";
|
|
33
|
+
|
|
34
|
+
// The floor: where things are, what the box was told, staying inside a
|
|
35
|
+
// folder, a small file an agent keeps, the history.
|
|
36
|
+
export { agentDir, ROOT, STATE } from "./core/paths.ts";
|
|
37
|
+
export { readSettings, setting, settings, type Settings } from "./core/settings.ts";
|
|
38
|
+
export { confine } from "./core/confine.ts";
|
|
39
|
+
export { note, type Note } from "./core/notes.ts";
|
|
40
|
+
export { copyDatabase, DATABASE, db, trim } from "./core/db.ts";
|
|
41
|
+
|
|
42
|
+
// What a job can do without asking anybody: running a command, sending mail,
|
|
43
|
+
// reading mail, reading and writing files in one folder, running one of an
|
|
44
|
+
// agent's own scripts, reading a web page. The same work offered
|
|
45
|
+
// to a model instead is "@chloejs/core/tools", and each of those is a wrapper over one
|
|
46
|
+
// of these.
|
|
47
|
+
export { run, type Result } from "./do/run.ts";
|
|
48
|
+
export { send, type Address } from "./do/email.ts";
|
|
49
|
+
export { messages, oneMessage, type Message as Mail } from "./do/mail.ts";
|
|
50
|
+
export { list, read, search, write } from "./do/files.ts";
|
|
51
|
+
export { script, scripts } from "./do/scripts.ts";
|
|
52
|
+
export { readPage, htmlToText, isPrivate, type Page } from "./do/web.ts";
|
package/load/job.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A job written as code, for when markdown frontmatter is not enough.
|
|
2
|
+
//
|
|
3
|
+
// It takes one of two things, and this is the line the whole repo turns on:
|
|
4
|
+
//
|
|
5
|
+
// markdown: the job is a prompt. A model reads the situation and decides.
|
|
6
|
+
// run: the job is code. Nothing asks a model unless the code does.
|
|
7
|
+
import type { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import type { Prompt } from "#chloe/core/markdown.ts";
|
|
10
|
+
import type { Work } from "#chloe/core/steps.ts";
|
|
11
|
+
|
|
12
|
+
export interface Definition<
|
|
13
|
+
State extends z.ZodType = z.ZodType<Record<string, unknown>>,
|
|
14
|
+
Result = unknown,
|
|
15
|
+
Input extends z.ZodType = z.ZodType<Record<string, unknown>>,
|
|
16
|
+
> {
|
|
17
|
+
/**
|
|
18
|
+
* What the run history files it under, and what `npm run agent` and the
|
|
19
|
+
* evals call it. Name the file after it: jobs/<id>.ts.
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* When it runs by itself. Five fields: minute, hour, day of month, month,
|
|
24
|
+
* day of week. Without one it runs only when somebody starts it.
|
|
25
|
+
*/
|
|
26
|
+
cron?: string;
|
|
27
|
+
/** The prompt: a string for a one-liner, or `prompt("./name.md")`. */
|
|
28
|
+
markdown?: string | Prompt;
|
|
29
|
+
/**
|
|
30
|
+
* The job, when it is code. Branch with `if`, loop with `for`, and put every
|
|
31
|
+
* piece of work inside a `step`: a step is written down so it never runs
|
|
32
|
+
* twice, and a line outside one runs again every time the job resumes.
|
|
33
|
+
*/
|
|
34
|
+
run?: (work: Work<z.infer<State>, z.infer<Input>>) => Promise<Result>;
|
|
35
|
+
/**
|
|
36
|
+
* What this job is started with, when it is started by hand rather than by
|
|
37
|
+
* its cron line: a channel command, the API, or `npm run agent`. The shape is
|
|
38
|
+
* the contract, and a caller that does not fit it is refused before the run
|
|
39
|
+
* begins rather than halfway through it.
|
|
40
|
+
*
|
|
41
|
+
* A channel sends a fixed envelope, so a job meant to be reachable from one
|
|
42
|
+
* takes `text` and whichever of `from`, `chat`, `user`, `thread` and
|
|
43
|
+
* `replyTo` it cares about. See https://chloejs.org/docs/jobs.
|
|
44
|
+
*
|
|
45
|
+
* A job with a cron line and a required field cannot run on that line, so
|
|
46
|
+
* give those fields a default.
|
|
47
|
+
*/
|
|
48
|
+
input?: Input;
|
|
49
|
+
/**
|
|
50
|
+
* Plain messages, with no command, that this job answers instead of the
|
|
51
|
+
* agent's chat: `answers: (text) => text.includes("https://a.co/")`. A
|
|
52
|
+
* channel that sees one starts the job with the whole message as `text`.
|
|
53
|
+
* Code, not a model: it is asked of every message, so it has to be quick and
|
|
54
|
+
* certain. The first job that says yes gets the message.
|
|
55
|
+
*/
|
|
56
|
+
answers?: (text: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* What a finished run did, in one line, from what `run` returned: "15 sites,
|
|
59
|
+
* all up". It is what the overview shows. A job without one shows nothing
|
|
60
|
+
* there, unless it returned a string.
|
|
61
|
+
*/
|
|
62
|
+
summary?: (result: Result) => string;
|
|
63
|
+
/**
|
|
64
|
+
* What a chat is sent when this job was started from one, from what `run`
|
|
65
|
+
* returned. Whole, not cut to one line. The summary when unsaid.
|
|
66
|
+
*/
|
|
67
|
+
reply?: (result: Result) => string;
|
|
68
|
+
/** The shared store every step can read and write. It survives a pause. */
|
|
69
|
+
state?: State;
|
|
70
|
+
timezone?: string;
|
|
71
|
+
/** When this job should not run on the agent's own model. */
|
|
72
|
+
model?: string;
|
|
73
|
+
/** One line on what it does, shown beside the id. */
|
|
74
|
+
description?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Only here so a job file is type checked as it is written. */
|
|
78
|
+
export function defineJob<
|
|
79
|
+
State extends z.ZodType = z.ZodType<Record<string, unknown>>,
|
|
80
|
+
Result = unknown,
|
|
81
|
+
Input extends z.ZodType = z.ZodType<Record<string, unknown>>,
|
|
82
|
+
>(definition: Definition<State, Result, Input>): Definition<State, Result, Input> {
|
|
83
|
+
return definition;
|
|
84
|
+
}
|