@chloejs/core 0.2.3 → 0.3.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/README.md +2 -1
- package/channels/shared.ts +20 -2
- package/channels/slack.ts +9 -6
- package/channels/telegram.ts +129 -14
- package/core/clock.ts +24 -7
- package/core/current.ts +19 -0
- package/core/db.ts +20 -0
- package/core/notes.ts +4 -4
- package/core/paths.ts +36 -5
- package/core/root.ts +1 -1
- package/core/settings.ts +46 -5
- package/core/steps.ts +16 -1
- package/core/turn.ts +49 -18
- package/index.ts +3 -3
- package/load/load.ts +172 -38
- package/model/tools/files.ts +18 -4
- package/model/tools/gmail.ts +3 -3
- package/model/tools/index.ts +2 -2
- package/model/tools/memory.ts +19 -14
- package/model/tools/own_files.ts +46 -0
- package/model/tools/run_script.ts +3 -3
- package/model/tools/send_email.ts +36 -9
- package/ops/test.ts +448 -15
- package/package.json +1 -1
- package/serve/changes.ts +61 -0
- package/serve/files.ts +26 -4
- package/serve/http.ts +77 -5
- package/serve/login.ts +46 -3
- package/serve/memory.ts +43 -34
- package/serve/site.ts +6 -1
- package/server.ts +39 -20
- package/services/emailService.ts +144 -8
- package/services/filesService.ts +28 -17
- package/services/gmailService.ts +3 -3
- package/services/historyService.ts +372 -0
- package/services/index.ts +3 -3
- package/services/ownFilesService.ts +141 -0
- package/services/scriptsService.ts +12 -7
- package/model/tools/write_skill.ts +0 -31
package/core/steps.ts
CHANGED
|
@@ -19,7 +19,9 @@ import { randomUUID } from "node:crypto";
|
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
|
|
21
21
|
import { deliver, owner as whoOwns } from "#chloe/model/ask.ts";
|
|
22
|
+
import { duringRun } from "#chloe/core/current.ts";
|
|
22
23
|
import { db } from "#chloe/core/db.ts";
|
|
24
|
+
import { afterRun, beforeRun } from "#chloe/services/historyService.ts";
|
|
23
25
|
import { oneLineSummary } from "#chloe/core/markdown.ts";
|
|
24
26
|
import type { Agent, Job } from "#chloe/load/load.ts";
|
|
25
27
|
import { ask as askModel, type Message } from "#chloe/model/model.ts";
|
|
@@ -336,21 +338,29 @@ async function drive(ctx: Ctx): Promise<Result> {
|
|
|
336
338
|
signal: ctx.signal,
|
|
337
339
|
};
|
|
338
340
|
|
|
341
|
+
// Whatever the memory holds when the run stops, finished or waiting, is what it did.
|
|
342
|
+
const committed = (end: { summary?: string | null; error?: string }) =>
|
|
343
|
+
afterRun(ctx.agent, ctx.runId, { job: ctx.job.id, source: sourceOf(ctx.runId), ...end });
|
|
344
|
+
await beforeRun(ctx.agent, ctx.runId);
|
|
345
|
+
|
|
339
346
|
try {
|
|
340
|
-
const value = await ctx.job.run!(api);
|
|
347
|
+
const value = await duringRun(ctx.runId, () => ctx.job.run!(api));
|
|
341
348
|
ctx.parked = undefined;
|
|
342
349
|
const reply = typeof value === "string" ? value : JSON.stringify(value ?? { ok: true }, null, 2);
|
|
343
350
|
const summary = summarise(ctx.job, value);
|
|
344
351
|
finish(ctx, reply, summary);
|
|
352
|
+
await committed({ summary });
|
|
345
353
|
return { runId: ctx.runId, text: reply, summary, reply: chatReply(ctx.job, value) ?? summary ?? undefined, steps: ctx.lines.length, cost: ctx.cost, parked: false };
|
|
346
354
|
} catch (error) {
|
|
347
355
|
if (error instanceof Waiting) {
|
|
348
356
|
save(ctx);
|
|
357
|
+
await committed({ summary: `waiting on ${ctx.parked?.who ?? "an answer"}` });
|
|
349
358
|
return { runId: ctx.runId, text: error.message, steps: ctx.lines.length, cost: ctx.cost, parked: true };
|
|
350
359
|
}
|
|
351
360
|
ctx.parked = undefined;
|
|
352
361
|
const why = error instanceof Error ? error.message : String(error);
|
|
353
362
|
fail(ctx, why);
|
|
363
|
+
await committed({ error: why });
|
|
354
364
|
if (error instanceof Unanswered || error instanceof Changed) {
|
|
355
365
|
return { runId: ctx.runId, text: why, steps: ctx.lines.length, cost: ctx.cost, parked: false };
|
|
356
366
|
}
|
|
@@ -358,6 +368,11 @@ async function drive(ctx: Ctx): Promise<Result> {
|
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
|
|
371
|
+
/** The channel a run came in on, as its row says. */
|
|
372
|
+
function sourceOf(runId: string): string {
|
|
373
|
+
return (db.prepare("select source from runs where id = ?").get(runId) as { source?: string } | undefined)?.source ?? "unknown";
|
|
374
|
+
}
|
|
375
|
+
|
|
361
376
|
/**
|
|
362
377
|
* The replay. A step that is already in the record hands back what it returned
|
|
363
378
|
* and does not run. The name is checked as well as the place, because a job
|
package/core/turn.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import { duringRun } from "#chloe/core/current.ts";
|
|
8
9
|
import { db } from "#chloe/core/db.ts";
|
|
9
10
|
import { oneLineSummary } from "#chloe/core/markdown.ts";
|
|
10
11
|
import type { Agent, ChatHistory, Skill } from "#chloe/load/load.ts";
|
|
11
12
|
import { ask, type Attachment, type Message, type ToolCall } from "#chloe/model/model.ts";
|
|
12
13
|
import { recall, remember } from "#chloe/model/memory.ts";
|
|
13
14
|
import { describe, type Approve, type Call, type Tool, type Tools } from "#chloe/model/tool.ts";
|
|
15
|
+
import { afterRun, beforeRun } from "#chloe/services/historyService.ts";
|
|
14
16
|
|
|
15
17
|
export interface Ask {
|
|
16
18
|
agent: Agent;
|
|
@@ -35,6 +37,11 @@ export interface Ask {
|
|
|
35
37
|
* written. Never the final answer, which is what turn() returns.
|
|
36
38
|
*/
|
|
37
39
|
said?: (text: string) => void;
|
|
40
|
+
/**
|
|
41
|
+
* The person on the other end of a channel, by name. The model is told it is
|
|
42
|
+
* talking to them, so it writes to them as "you" rather than about them.
|
|
43
|
+
*/
|
|
44
|
+
talkingTo?: string;
|
|
38
45
|
/** Who this run is for, as an address. One column, and the team version reads it. */
|
|
39
46
|
owner?: string;
|
|
40
47
|
/** Answer tools from here instead of running them. For evals. */
|
|
@@ -66,7 +73,7 @@ const MAX_STEPS = 40;
|
|
|
66
73
|
* Runs a prompt: ask a model, run the tools it asked for, put the answers
|
|
67
74
|
* back, ask again, until it stops asking.
|
|
68
75
|
*/
|
|
69
|
-
export async function turn({ agent, prompt, attachments, model, thread, source, job, history, said, owner, instead, signal }: Ask): Promise<Result> {
|
|
76
|
+
export async function turn({ agent, prompt, attachments, model, thread, source, job, history, said, talkingTo, owner, instead, signal }: Ask): Promise<Result> {
|
|
70
77
|
const runId = randomUUID();
|
|
71
78
|
const using = model ?? agent.model;
|
|
72
79
|
const tools = { ...(agent.tools ?? {}), skill: skillTool(agent.skills) };
|
|
@@ -77,7 +84,7 @@ export async function turn({ agent, prompt, attachments, model, thread, source,
|
|
|
77
84
|
).run(runId, agent.name, started, source, job ?? null, using, prompt, owner ?? null);
|
|
78
85
|
|
|
79
86
|
const messages: Message[] = [
|
|
80
|
-
{ role: "system", content: systemPrompt(agent) },
|
|
87
|
+
{ role: "system", content: systemPrompt(agent, talkingTo && { name: talkingTo, source, asYouGo: Boolean(said) }) },
|
|
81
88
|
...(thread ? recall(thread, { ...shown(history), tools: true }) : []),
|
|
82
89
|
{ role: "user", content: prompt, attachments },
|
|
83
90
|
];
|
|
@@ -87,33 +94,43 @@ export async function turn({ agent, prompt, attachments, model, thread, source,
|
|
|
87
94
|
const calls: Result["calls"] = [];
|
|
88
95
|
let cost = 0;
|
|
89
96
|
let steps = 0;
|
|
97
|
+
// An eval answers every tool itself, so nothing it does is written anywhere.
|
|
98
|
+
const committed = (end: { summary?: string; error?: string }) =>
|
|
99
|
+
instead ? Promise.resolve() : afterRun(agent, runId, { job, source, ...end });
|
|
100
|
+
if (!instead) await beforeRun(agent, runId);
|
|
90
101
|
|
|
91
102
|
try {
|
|
92
|
-
const done = await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const done = await duringRun(runId, () =>
|
|
104
|
+
loop({
|
|
105
|
+
model: using,
|
|
106
|
+
messages,
|
|
107
|
+
tools,
|
|
108
|
+
maxSteps: agent.maxSteps ?? MAX_STEPS,
|
|
109
|
+
signal,
|
|
110
|
+
instead,
|
|
111
|
+
onStep: (line) => {
|
|
112
|
+
trace.push(line);
|
|
113
|
+
if (said && line.say?.trim() && line.wants?.length) said(line.say);
|
|
114
|
+
if (line.tool) calls.push({ tool: line.tool, args: line.args, result: line.result });
|
|
115
|
+
save(runId, trace.filter((one) => (one as { say?: string }).say !== undefined).length, costOf(trace), trace);
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
106
119
|
cost = done.cost;
|
|
107
120
|
steps = done.steps;
|
|
108
121
|
if (done.stopped) {
|
|
109
122
|
fail(runId, done.text, steps, cost, trace);
|
|
123
|
+
await committed({ error: done.text });
|
|
110
124
|
return { runId, text: done.text, steps, cost, calls };
|
|
111
125
|
}
|
|
112
126
|
finish(runId, done.text, steps, cost, trace);
|
|
127
|
+
await committed({ summary: oneLineSummary(done.text) });
|
|
113
128
|
if (thread) remember(thread, "assistant", done.text, calls);
|
|
114
129
|
return { runId, text: done.text, steps, cost, calls };
|
|
115
130
|
} catch (error) {
|
|
116
|
-
|
|
131
|
+
const why = String(error instanceof Error ? error.message : error);
|
|
132
|
+
fail(runId, why, steps, cost, trace);
|
|
133
|
+
await committed({ error: why });
|
|
117
134
|
throw error;
|
|
118
135
|
}
|
|
119
136
|
}
|
|
@@ -276,8 +293,22 @@ function skillTool(skills: Skill[]): Tool {
|
|
|
276
293
|
};
|
|
277
294
|
}
|
|
278
295
|
|
|
279
|
-
|
|
296
|
+
/**
|
|
297
|
+
* The agent's own instructions talk about people in the third person, because
|
|
298
|
+
* they describe them. This is what stops that carrying over into a
|
|
299
|
+
* conversation with one of them.
|
|
300
|
+
*/
|
|
301
|
+
function talkingWith({ name, source, asYouGo }: { name: string; source: string; asYouGo: boolean }): string {
|
|
302
|
+
return (
|
|
303
|
+
`## Who you are talking to\n\n` +
|
|
304
|
+
`You are talking with ${name} on ${source}, directly. Write to them as "you", never by name or in the third person.` +
|
|
305
|
+
(asYouGo ? " What you write before calling a tool is sent to them straight away, so write that to them too." : "")
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function systemPrompt(agent: Agent, person?: { name: string; source: string; asYouGo: boolean } | "" | undefined): string {
|
|
280
310
|
const parts = [agent.instructions];
|
|
311
|
+
if (person) parts.push(talkingWith(person));
|
|
281
312
|
if (agent.skills.length > 0) {
|
|
282
313
|
parts.push(
|
|
283
314
|
"## Your skills\n\n" +
|
package/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// The server itself is `server.ts`, and it is run rather than imported.
|
|
16
16
|
|
|
17
17
|
// An agent, and the jobs it runs.
|
|
18
|
-
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";
|
|
18
|
+
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 SelfImprovement, type Memory, type ChatHistory } from "./load/load.ts";
|
|
19
19
|
export { defineJob } from "./load/job.ts";
|
|
20
20
|
export { prompt, isPrompt, oneLineSummary, type Prompt } from "./core/markdown.ts";
|
|
21
21
|
|
|
@@ -35,8 +35,8 @@ export { canReach, deliver, owner, reachBy, split, type Send } from "./model/ask
|
|
|
35
35
|
|
|
36
36
|
// The floor: where things are, what the box was told, staying inside a
|
|
37
37
|
// folder, a small file an agent keeps, the history.
|
|
38
|
-
export { agentDir, ROOT, STATE } from "./core/paths.ts";
|
|
39
|
-
export { readSettings, setting, settings, type Settings } from "./core/settings.ts";
|
|
38
|
+
export { agentDir, MEMORIES, ROOT, STATE } from "./core/paths.ts";
|
|
39
|
+
export { readSettings, reloadSettings, setting, settings, unclaimed, type Settings } from "./core/settings.ts";
|
|
40
40
|
export { confine } from "./core/confine.ts";
|
|
41
41
|
export { note, type Note } from "./core/notes.ts";
|
|
42
42
|
export { copyDatabase, DATABASE, db, trim } from "./core/db.ts";
|
package/load/load.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
// An agent is declared, not found. Each one is a defineAgent(...) with a name,
|
|
2
|
-
// and chloe.config.ts at the top of the repo lists them.
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// and chloe.config.ts at the top of the repo lists them. Every job, tool and
|
|
3
|
+
// channel is named in the definition. skills/ is the one folder read by
|
|
4
|
+
// looking, because a skill is loaded only when the model asks for it.
|
|
5
|
+
import { mkdir, readFile, readdir } from "node:fs/promises";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
6
7
|
import { registerHooks } from "node:module";
|
|
7
8
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
-
import { dirname, relative } from "node:path";
|
|
9
|
+
import { dirname, join, relative } from "node:path";
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
11
|
import { getCallSites } from "node:util";
|
|
11
12
|
|
|
12
13
|
import type { z } from "zod";
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
-
import { readPrompt, settingsAndBody, type Prompt } from "#chloe/core/markdown.ts";
|
|
15
|
+
import { MEMORIES, ROOT, setAgentDirs } from "#chloe/core/paths.ts";
|
|
16
|
+
import { isPrompt, readPrompt, settingsAndBody, type Prompt } from "#chloe/core/markdown.ts";
|
|
16
17
|
import { parse } from "#chloe/timer/cron.ts";
|
|
17
18
|
import type { Definition as JobFile } from "./job.ts";
|
|
18
19
|
import type { Tool, Tools } from "#chloe/model/tool.ts";
|
|
19
20
|
import { memoryTools } from "#chloe/model/tools/memory.ts";
|
|
21
|
+
import { ownFiles } from "#chloe/model/tools/own_files.ts";
|
|
20
22
|
import { runScripts } from "#chloe/model/tools/run_script.ts";
|
|
21
|
-
import {
|
|
23
|
+
import { makeRepo } from "#chloe/services/historyService.ts";
|
|
24
|
+
import { shareLogin } from "#chloe/serve/login.ts";
|
|
22
25
|
import type { Work } from "#chloe/core/steps.ts";
|
|
23
26
|
|
|
24
27
|
export const CONFIG = `${ROOT}/chloe.config.ts`;
|
|
@@ -52,7 +55,7 @@ export type Binding = (agent: Home) => Tools;
|
|
|
52
55
|
|
|
53
56
|
/** What defineAgent is given. */
|
|
54
57
|
export interface Definition {
|
|
55
|
-
/** What the run history, its
|
|
58
|
+
/** What the run history, its memory and its pages are filed under. Do not change it once it has run. */
|
|
56
59
|
name: string;
|
|
57
60
|
/** What the page calls it, when that is not its name: "C.C.". Free to change. */
|
|
58
61
|
label?: string;
|
|
@@ -70,11 +73,11 @@ export interface Definition {
|
|
|
70
73
|
* runs, browsable and editable from the site.
|
|
71
74
|
*
|
|
72
75
|
* Every agent has one, and always has list_notes, read_notes, search_notes
|
|
73
|
-
* and write_notes on it. Left unsaid it is
|
|
74
|
-
*
|
|
75
|
-
* a folder with a person. Every file served out of it
|
|
76
|
-
* agent's own audit log first. See serve/memory.ts for
|
|
77
|
-
* optional.
|
|
76
|
+
* and write_notes on it. Left unsaid it is its own folder inside `memory/`
|
|
77
|
+
* beside the agents, which is a git repository, so this is only worth writing
|
|
78
|
+
* down when the agent shares a folder with a person. Every file served out of it
|
|
79
|
+
* is written to that agent's own audit log first. See serve/memory.ts for
|
|
80
|
+
* why that log is not optional.
|
|
78
81
|
*/
|
|
79
82
|
memory?: Memory;
|
|
80
83
|
/** The tools the runtime can give any agent, each switched on or off here. */
|
|
@@ -86,7 +89,7 @@ export interface Definition {
|
|
|
86
89
|
* its id. What `features` turns on is added to these and not listed here.
|
|
87
90
|
*/
|
|
88
91
|
tools?: (Tool | Tools | Binding)[];
|
|
89
|
-
/** Each job
|
|
92
|
+
/** Each job: one imported, or markdownJob("jobs/<id>.md") for one that is only a prompt. */
|
|
90
93
|
jobs?: (JobFile<any, any> | MarkdownJob)[];
|
|
91
94
|
/** Each way in: `[telegramChannel({ ... }), apiChannel()]`. Each one carries its own name. */
|
|
92
95
|
channels?: Channel[];
|
|
@@ -113,6 +116,14 @@ export function defineAgent(definition: Definition): Defined {
|
|
|
113
116
|
/** What chloe.config.ts exports: every agent this box runs. */
|
|
114
117
|
export interface Config {
|
|
115
118
|
agents: Defined[];
|
|
119
|
+
login?: {
|
|
120
|
+
/**
|
|
121
|
+
* A name like "example.com" makes the one login cover every site under
|
|
122
|
+
* it, so another app can ask GET /api/check whether its visitor is signed
|
|
123
|
+
* in. Leave it out and the login is for this site's own name only.
|
|
124
|
+
*/
|
|
125
|
+
domain?: string;
|
|
126
|
+
};
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
/** The default export of chloe.config.ts: every agent to run. */
|
|
@@ -122,21 +133,51 @@ export function defineConfig(config: Config): Config {
|
|
|
122
133
|
|
|
123
134
|
/**
|
|
124
135
|
* A job that is only a prompt, kept whole in one markdown file with its
|
|
125
|
-
* settings (`cron`, `description`, `timezone`, `model`) at the top. The path
|
|
126
|
-
*
|
|
136
|
+
* settings (`cron`, `description`, `timezone`, `model`) at the top. The path is
|
|
137
|
+
* inside the agent's folder, and the file's name is the job's id.
|
|
127
138
|
*/
|
|
128
139
|
export interface MarkdownJob {
|
|
129
140
|
markdownJob: string;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
/**
|
|
133
|
-
* A job that is words and nothing else
|
|
134
|
-
* `
|
|
144
|
+
* A job that is words and nothing else: `markdownJob("jobs/<id>.md")` in
|
|
145
|
+
* `agent.ts`. The file name is the job's id.
|
|
135
146
|
*/
|
|
136
147
|
export function markdownJob(file: string): MarkdownJob {
|
|
137
148
|
return { markdownJob: file };
|
|
138
149
|
}
|
|
139
150
|
|
|
151
|
+
/** The settings a markdown job may have at its top, and no others. */
|
|
152
|
+
export const MARKDOWN_JOB_SETTINGS = ["cron", "description", "timezone", "model"];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* What is wrong with a markdown job, in one sentence, or undefined when it
|
|
156
|
+
* would load. Stricter than the loader: a setting it does not know is refused
|
|
157
|
+
* here rather than ignored.
|
|
158
|
+
*/
|
|
159
|
+
export function markdownJobProblem(text: string): string | undefined {
|
|
160
|
+
const { settings, body } = settingsAndBody(text);
|
|
161
|
+
if (!body.trim()) return "it has no prompt under its settings";
|
|
162
|
+
const unknown = Object.keys(settings).filter((key) => !MARKDOWN_JOB_SETTINGS.includes(key));
|
|
163
|
+
if (unknown.length) return `it has ${unknown.join(", ")} at the top, and a job only reads ${MARKDOWN_JOB_SETTINGS.join(", ")}`;
|
|
164
|
+
if (settings.cron) {
|
|
165
|
+
try {
|
|
166
|
+
parse(settings.cron);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return `its cron line does not read: ${error instanceof Error ? error.message : String(error)}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (settings.timezone) {
|
|
172
|
+
try {
|
|
173
|
+
new Intl.DateTimeFormat("en-US", { timeZone: settings.timezone });
|
|
174
|
+
} catch {
|
|
175
|
+
return `${settings.timezone} is not a timezone, like America/New_York`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
140
181
|
/** One markdown file out of an agent's `skills/` folder. */
|
|
141
182
|
export interface Skill {
|
|
142
183
|
name: string;
|
|
@@ -184,8 +225,13 @@ export interface Job {
|
|
|
184
225
|
export interface Features {
|
|
185
226
|
/** list_notes, read_notes, search_notes and write_notes on its memory. On unless this says false. */
|
|
186
227
|
memory?: boolean;
|
|
187
|
-
/**
|
|
188
|
-
|
|
228
|
+
/**
|
|
229
|
+
* list_own_files, read_own_file and write_own_file, to change the plain text
|
|
230
|
+
* in its own folder: its instructions, its skills, its markdown jobs. Off
|
|
231
|
+
* unless this says. `true` is every ending in PLAIN_TEXT; an object narrows
|
|
232
|
+
* that or keeps a path back. Every write is a git commit under its name.
|
|
233
|
+
*/
|
|
234
|
+
selfImprovement?: boolean | SelfImprovement;
|
|
189
235
|
/**
|
|
190
236
|
* run_script, to run a file in its own scripts/ folder. Off unless this says
|
|
191
237
|
* true, and refused as it loads when that folder has no scripts.
|
|
@@ -193,17 +239,47 @@ export interface Features {
|
|
|
193
239
|
runScripts?: boolean;
|
|
194
240
|
}
|
|
195
241
|
|
|
242
|
+
/** `selfImprovement` with its file endings worked out, which is what the tools are given. */
|
|
243
|
+
export interface OwnFileRules extends SelfImprovement {
|
|
244
|
+
files: string[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The plain text an agent may change when `selfImprovement` is `true`, without the dots. */
|
|
248
|
+
export const PLAIN_TEXT = ["md", "txt", "html", "json", "yml", "yaml", "csv"];
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Which of its own files an agent may change: `{ except: ["PERMISSIONS.md"] }`,
|
|
252
|
+
* or `{ files: ["md"] }` for less than the plain text it would get from `true`.
|
|
253
|
+
*
|
|
254
|
+
* Code never, whatever `files` says: nothing in tools/, services/, channels/ or
|
|
255
|
+
* scripts/, and nothing ending in .ts or .js. Nor its evals/, which say what a
|
|
256
|
+
* good run of it looks like, nor its memory, which is write_notes.
|
|
257
|
+
*/
|
|
258
|
+
export interface SelfImprovement {
|
|
259
|
+
/** File endings it may write, without the dot. PLAIN_TEXT when it says none. */
|
|
260
|
+
files?: string[];
|
|
261
|
+
/** Paths inside its folder it may read and never write, like a file of permissions it obeys. */
|
|
262
|
+
except?: string[];
|
|
263
|
+
}
|
|
264
|
+
|
|
196
265
|
/** Where an agent remembers things, shown on the site beside its own pages. */
|
|
197
266
|
export interface Memory {
|
|
198
267
|
/**
|
|
199
|
-
* An absolute path. Unset, it is
|
|
200
|
-
*
|
|
268
|
+
* An absolute path. Unset, it is the agent's own folder inside `memory/`
|
|
269
|
+
* beside the agents, which the runtime makes one git repository.
|
|
201
270
|
*/
|
|
202
271
|
folder?: string;
|
|
203
272
|
/** What the site calls it. "Memory" when nothing is said. */
|
|
204
273
|
label?: string;
|
|
205
|
-
/**
|
|
206
|
-
|
|
274
|
+
/**
|
|
275
|
+
* When a change becomes a git commit. `"each run"`: whatever a run changed
|
|
276
|
+
* is committed when it ends, under the agent's name, and the folder is made
|
|
277
|
+
* a repository of its own if it is not one. `true`: every write from the
|
|
278
|
+
* site and from write_notes is its own commit, with a message, for a folder
|
|
279
|
+
* shared with a person. `false`: never. Unsaid, it is "each run" for the
|
|
280
|
+
* folder the runtime keeps and false for one named here.
|
|
281
|
+
*/
|
|
282
|
+
commit?: boolean | "each run";
|
|
207
283
|
}
|
|
208
284
|
|
|
209
285
|
/**
|
|
@@ -228,6 +304,12 @@ export interface Channel {
|
|
|
228
304
|
name: string;
|
|
229
305
|
/** How much of a conversation on this channel a turn is shown. */
|
|
230
306
|
chatHistory?: ChatHistory;
|
|
307
|
+
/**
|
|
308
|
+
* The options it was made with, written out. A reload restarts a running
|
|
309
|
+
* channel when this changes, since what `start` was given is fixed for as
|
|
310
|
+
* long as it runs.
|
|
311
|
+
*/
|
|
312
|
+
madeWith?: string;
|
|
231
313
|
/** Starts listening. `agent` is read again for every message, so an edit is live. */
|
|
232
314
|
start(agent: () => Agent | undefined): Running;
|
|
233
315
|
}
|
|
@@ -262,12 +344,27 @@ export interface Agent extends Omit<Definition, "instructions" | "tools" | "jobs
|
|
|
262
344
|
}
|
|
263
345
|
|
|
264
346
|
/**
|
|
265
|
-
* Where
|
|
266
|
-
*
|
|
267
|
-
*
|
|
347
|
+
* Where the memories are kept: `memory/` beside the agents, which the runtime
|
|
348
|
+
* makes one git repository with a folder per agent inside it, so one history
|
|
349
|
+
* covers every agent and the agents' own folders stay source and nothing else.
|
|
350
|
+
* A memory that names its own folder keeps its history in that folder instead.
|
|
351
|
+
*/
|
|
352
|
+
export function memoryRoot(memory?: Memory): string {
|
|
353
|
+
return memory?.folder || MEMORIES;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Where one agent remembers things: what it said, or its own folder inside
|
|
358
|
+
* `memory/`. Named after the agent rather than its folder, so a folder that is
|
|
359
|
+
* renamed still finds the same memory.
|
|
268
360
|
*/
|
|
269
361
|
export function memoryFolder(name: string, memory?: Memory): string {
|
|
270
|
-
return memory?.folder ||
|
|
362
|
+
return memory?.folder || join(MEMORIES, name);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** When a change to this memory becomes a commit. See Memory. */
|
|
366
|
+
function commitsWhen(memory?: Memory): Memory["commit"] {
|
|
367
|
+
return memory?.commit ?? (memory?.folder ? false : "each run");
|
|
271
368
|
}
|
|
272
369
|
|
|
273
370
|
/** An agent's folder as the repo sees it, for saying where something is wrong. */
|
|
@@ -287,15 +384,18 @@ export async function loadAll(): Promise<Map<string, Agent>> {
|
|
|
287
384
|
})) as { default?: Config };
|
|
288
385
|
const listed = module.default?.agents;
|
|
289
386
|
if (!Array.isArray(listed)) throw new Error("chloe.config.ts does not export defineConfig({ agents: [...] }) as its default.");
|
|
387
|
+
shareLogin(module.default?.login?.domain);
|
|
290
388
|
|
|
291
389
|
const folders = new Map<string, string>();
|
|
390
|
+
const memories = new Map<string, string>();
|
|
292
391
|
for (const one of listed) {
|
|
293
392
|
if (!one?.name) throw new Error("chloe.config.ts lists an agent with no name.");
|
|
294
393
|
if (folders.has(one.name)) throw new Error(`chloe.config.ts lists two agents called ${one.name}.`);
|
|
295
394
|
folders.set(one.name, one.folder);
|
|
395
|
+
memories.set(one.name, memoryFolder(one.name, one.memory));
|
|
296
396
|
}
|
|
297
397
|
// Before anything is bound, so a tool that asks where its agent lives is told.
|
|
298
|
-
setAgentDirs(folders);
|
|
398
|
+
setAgentDirs(folders, memories);
|
|
299
399
|
|
|
300
400
|
const all = new Map<string, Agent>();
|
|
301
401
|
for (const one of listed) all.set(one.name, await resolveAgent(one));
|
|
@@ -324,12 +424,26 @@ async function resolveAgent(definition: Defined): Promise<Agent> {
|
|
|
324
424
|
const { tools, jobs, channels, ...rest } = definition;
|
|
325
425
|
// Worked out once, here, so the site, the memory tool and a job's
|
|
326
426
|
// work.memory all mean the same folder without any of them saying it again.
|
|
327
|
-
const memory = {
|
|
427
|
+
const memory = {
|
|
428
|
+
...definition.memory,
|
|
429
|
+
folder: memoryFolder(name, definition.memory),
|
|
430
|
+
commit: commitsWhen(definition.memory),
|
|
431
|
+
};
|
|
432
|
+
// Made now, so committing what a run changed has a folder to name.
|
|
433
|
+
await mkdir(memory.folder, { recursive: true }).catch(() => undefined);
|
|
434
|
+
if (memory.commit === "each run") {
|
|
435
|
+
// Without git, or on a folder it cannot write, the agent still runs: it only
|
|
436
|
+
// has no history. The repository is the memory root, shared by every agent.
|
|
437
|
+
await makeRepo(memoryRoot(definition.memory), name).catch((error: unknown) =>
|
|
438
|
+
console.error(`${where}: its memory could not be made a git repository:`, error instanceof Error ? error.message : error),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const home = { name, folder, memory };
|
|
328
442
|
return {
|
|
329
443
|
...rest,
|
|
330
444
|
memory,
|
|
331
445
|
instructions: await readPrompt(definition.instructions, { dir: folder, where }),
|
|
332
|
-
tools: toolsOf([...featureTools(definition.features,
|
|
446
|
+
tools: toolsOf([...featureTools(definition.features, where), ...(tools ?? [])], home, where),
|
|
333
447
|
skills: await skillsIn(`${folder}/skills`),
|
|
334
448
|
jobs: await jobsOf(name, folder, jobs ?? []),
|
|
335
449
|
channels: channelsOf(channels ?? [], where),
|
|
@@ -357,10 +471,20 @@ export function hasChannel(agent: Pick<Agent, "channels">, name: string): boolea
|
|
|
357
471
|
}
|
|
358
472
|
|
|
359
473
|
/** What an agent's `features` turn on, as tools. The memory tools unless it says memory: false. */
|
|
360
|
-
|
|
474
|
+
/** What `selfImprovement` comes to: `true` is the plain text, an object is itself. */
|
|
475
|
+
export function ownFileRules(self: true | SelfImprovement): OwnFileRules {
|
|
476
|
+
const said = self === true ? {} : self;
|
|
477
|
+
return { ...said, files: said.files ?? PLAIN_TEXT };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function featureTools(features: Features = {}, where: string): (Tools | Binding)[] {
|
|
481
|
+
const self = features.selfImprovement;
|
|
482
|
+
if (typeof self === "object" && self.files && self.files.length === 0) {
|
|
483
|
+
throw new Error(`${where}: selfImprovement is true, or says which files it may change, like { files: ["md"] }.`);
|
|
484
|
+
}
|
|
361
485
|
return [
|
|
362
|
-
...(features.memory === false ? [] : [memoryTools(
|
|
363
|
-
...(
|
|
486
|
+
...(features.memory === false ? [] : [memoryTools()]),
|
|
487
|
+
...(self ? [ownFiles(ownFileRules(self))] : []),
|
|
364
488
|
...(features.runScripts ? [runScripts()] : []),
|
|
365
489
|
];
|
|
366
490
|
}
|
|
@@ -393,13 +517,18 @@ async function skillsIn(dir: string): Promise<Skill[]> {
|
|
|
393
517
|
return skills;
|
|
394
518
|
}
|
|
395
519
|
|
|
396
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* The jobs an agent names, in the order it names them, then every markdown
|
|
522
|
+
* file in its jobs/ that is not already one of those or the words of one.
|
|
523
|
+
*/
|
|
397
524
|
export async function jobsOf(agent: string, dir: string, list: (JobFile<any, any> | MarkdownJob)[]): Promise<Job[]> {
|
|
398
525
|
const jobs: Job[] = [];
|
|
399
|
-
|
|
400
|
-
const job = "markdownJob" in one ? await fromMarkdown(agent, dir, one.markdownJob) : await fromCode(agent, dir, one);
|
|
526
|
+
const add = (job: Job) => {
|
|
401
527
|
if (jobs.some((other) => other.id === job.id)) throw new Error(`${agent}: two jobs are called ${job.id}.`);
|
|
402
528
|
jobs.push(job);
|
|
529
|
+
};
|
|
530
|
+
for (const one of list) {
|
|
531
|
+
add("markdownJob" in one ? await fromMarkdown(agent, dir, one.markdownJob) : await fromCode(agent, dir, one));
|
|
403
532
|
}
|
|
404
533
|
return jobs;
|
|
405
534
|
}
|
|
@@ -457,7 +586,12 @@ async function fromCode(agent: string, dir: string, definition: JobFile<any, any
|
|
|
457
586
|
cron: checked(definition.cron, where),
|
|
458
587
|
timezone: definition.timezone ?? "UTC",
|
|
459
588
|
model: definition.model,
|
|
460
|
-
files: [
|
|
589
|
+
files: [
|
|
590
|
+
...new Set([
|
|
591
|
+
...(isPrompt(definition.markdown) ? [relative(dir, join(dir, definition.markdown.file))] : []),
|
|
592
|
+
...[`jobs/${id}.md`, `jobs/${id}.ts`].filter((file) => existsSync(`${dir}/${file}`)),
|
|
593
|
+
]),
|
|
594
|
+
],
|
|
461
595
|
};
|
|
462
596
|
|
|
463
597
|
// A job has no prompt, and the empty string is what says so everywhere else.
|
package/model/tools/files.ts
CHANGED
|
@@ -53,19 +53,33 @@ export function search_in({ root, what, id = "search_notes" }: Folder) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* `commit` makes every write a git commit, for a folder that is a repo, under
|
|
58
|
+
* `author` when there is one. `memory` says this folder is the agent's memory,
|
|
59
|
+
* so the run writing it lists the commit.
|
|
60
|
+
*/
|
|
61
|
+
export function write_in({
|
|
62
|
+
root,
|
|
63
|
+
what,
|
|
64
|
+
id = "write_notes",
|
|
65
|
+
commit = false,
|
|
66
|
+
author,
|
|
67
|
+
memory = false,
|
|
68
|
+
}: Folder & { commit?: boolean; author?: string; memory?: boolean }) {
|
|
58
69
|
return tool({
|
|
59
70
|
id,
|
|
60
71
|
description:
|
|
61
72
|
`Write one file in ${what}. This replaces the file, so include everything you want kept: ` +
|
|
62
|
-
`read it first unless it is new
|
|
73
|
+
`read it first unless it is new. To add to the end of a file, such as a log or a list that only ` +
|
|
74
|
+
`grows, set append instead of writing it out again.` +
|
|
63
75
|
(commit ? " Committed as it is written, so `message` is required." : ""),
|
|
64
76
|
inputSchema: z.object({
|
|
65
77
|
path: z.string(),
|
|
66
78
|
content: z.string().min(1),
|
|
79
|
+
append: z.boolean().optional().describe("Add content to the end of the file instead of replacing it."),
|
|
67
80
|
message: z.string().optional().describe("Commit message saying what changed. Required here."),
|
|
68
81
|
}),
|
|
69
|
-
execute: ({ path, content, message }) =>
|
|
82
|
+
execute: ({ path, content, append, message }) =>
|
|
83
|
+
writeFiles(root, path, content, { commit, message, append, author, in: memory ? "memory" : undefined }),
|
|
70
84
|
});
|
|
71
85
|
}
|
package/model/tools/gmail.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// write. All the model chooses is how far back and how many.
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { readEmailMessages, readOneEmailMessage } from "#chloe/services/gmailService.ts";
|
|
8
8
|
import { tool } from "#chloe/model/tool.ts";
|
|
9
9
|
|
|
10
10
|
interface Options {
|
|
@@ -46,7 +46,7 @@ export function read_mail({
|
|
|
46
46
|
}),
|
|
47
47
|
execute: ({ days: back, limit, messageId }) =>
|
|
48
48
|
messageId
|
|
49
|
-
?
|
|
50
|
-
:
|
|
49
|
+
? readOneEmailMessage({ search, what, days: back ?? days, limit: limit ?? 10, messageId })
|
|
50
|
+
: readEmailMessages({ search, days: back ?? days, limit: limit ?? 10 }),
|
|
51
51
|
});
|
|
52
52
|
}
|
package/model/tools/index.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// import { read_mail, read_web } from "@chloejs/core/tools";
|
|
9
9
|
//
|
|
10
|
-
// The notes tools,
|
|
11
|
-
// them on with `features` in its definition.
|
|
10
|
+
// The notes tools, the own-file tools and run_script are not
|
|
11
|
+
// here: an agent turns them on with `features` in its definition.
|
|
12
12
|
//
|
|
13
13
|
// The work itself is in services/, published as "@chloejs/core/services", and a job calls it
|
|
14
14
|
// from a step rather than coming through here. If a job imports this file,
|