@agentorg/cli 0.0.7 → 0.0.8
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 -2
- package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +26 -12
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +72 -38
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +27 -17
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +405 -113
- package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +34 -0
- package/dist/node_modules/@agentorg/looper/dist/src/main.js +116 -0
- package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +97 -15
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +211 -56
- package/dist/node_modules/@agentorg/manager/dist/src/control.d.ts +1 -1
- package/dist/node_modules/@agentorg/manager/dist/src/control.js +1 -1
- package/dist/node_modules/@agentorg/shell/dist/app.css +47 -111
- package/dist/node_modules/@agentorg/shell/dist/app.js +139 -139
- package/dist/node_modules/assistant/dist/src/chats.d.ts +8 -6
- package/dist/node_modules/assistant/dist/src/chats.js +23 -25
- package/dist/node_modules/assistant/dist/src/engine.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Log, Model, ToolSpec } from "./types.js";
|
|
2
|
+
/** What the main loop is told. */
|
|
3
|
+
export declare const MAIN_INSTRUCTIONS = "You are the front desk. You do none of the work and you answer nobody: you say whose a message is, by calling `route` once.\n\nYou are given the pieces of work under way, each by its id and title, and what has come in where no work is listening yet.\n\n- Give it to a piece of work under way only when it points at that work explicitly: it names it, quotes it, or refers to what it is doing, and exactly one piece of work fits. Call `route` with that work's id. Its worker will read it with everything it already knows, and answer where it came in. From then on everything said there goes to that work.\n- Anything else is new work, also when it is on a subject like that of work under way. Call `route` with no `work`, and a `title` of a few words that says what the new work is.\n\nA wrong match stays, so when in doubt it is new work. Your only answer is the call.";
|
|
4
|
+
/** The one function the main loop calls. */
|
|
5
|
+
export declare const ROUTE: ToolSpec;
|
|
6
|
+
/** A piece of work as the main loop is told of it. */
|
|
7
|
+
export interface Underway {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
/** What the main loop decided: work under way that takes the messages, or new work's title. */
|
|
12
|
+
export type Routed = {
|
|
13
|
+
work: string;
|
|
14
|
+
} | {
|
|
15
|
+
title: string;
|
|
16
|
+
};
|
|
17
|
+
/** A work's title: the first line of `text` that says anything, in a few words. */
|
|
18
|
+
export declare const titled: (text: string) => string;
|
|
19
|
+
/** What has come in, as it is put to the main loop with the work under way. */
|
|
20
|
+
export declare const put: (texts: string[], underway: Underway[]) => string;
|
|
21
|
+
export declare function createMain(options: {
|
|
22
|
+
model: Model;
|
|
23
|
+
/** Which model to ask, none meaning the model's own default. */
|
|
24
|
+
chosen(): Promise<string | undefined>;
|
|
25
|
+
log: Log;
|
|
26
|
+
}): {
|
|
27
|
+
/**
|
|
28
|
+
* Say whose `texts` are, given the work under way: one decision at a time. Nothing is said
|
|
29
|
+
* for a decision that is cancelled. A request the model refuses rejects, as it refused.
|
|
30
|
+
*/
|
|
31
|
+
route(texts: string[], underway: Underway[]): Promise<Routed | undefined>;
|
|
32
|
+
/** Drop the decision being made: its request is cancelled, and it says nothing. */
|
|
33
|
+
cancel(): Promise<void>;
|
|
34
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The main loop: one per looper. It is given the messages that have come in where no piece of
|
|
3
|
+
* work is listening, and says whose they are: new work, or work under way. It does none of the
|
|
4
|
+
* work and answers nobody. It has one function to call, and each decision is one model request
|
|
5
|
+
* that holds nothing of the decisions before it.
|
|
6
|
+
*/
|
|
7
|
+
import { head } from "./record.js";
|
|
8
|
+
/** What the main loop is told. */
|
|
9
|
+
export const MAIN_INSTRUCTIONS = `You are the front desk. You do none of the work and you answer nobody: you say whose a message is, by calling \`route\` once.
|
|
10
|
+
|
|
11
|
+
You are given the pieces of work under way, each by its id and title, and what has come in where no work is listening yet.
|
|
12
|
+
|
|
13
|
+
- Give it to a piece of work under way only when it points at that work explicitly: it names it, quotes it, or refers to what it is doing, and exactly one piece of work fits. Call \`route\` with that work's id. Its worker will read it with everything it already knows, and answer where it came in. From then on everything said there goes to that work.
|
|
14
|
+
- Anything else is new work, also when it is on a subject like that of work under way. Call \`route\` with no \`work\`, and a \`title\` of a few words that says what the new work is.
|
|
15
|
+
|
|
16
|
+
A wrong match stays, so when in doubt it is new work. Your only answer is the call.`;
|
|
17
|
+
/** The one function the main loop calls. */
|
|
18
|
+
export const ROUTE = {
|
|
19
|
+
name: "route",
|
|
20
|
+
description: "Says whose what came in is: a piece of work under way, or new work.",
|
|
21
|
+
parameters: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
work: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "The id of the work under way that it points at. Leave it out for new work.",
|
|
27
|
+
},
|
|
28
|
+
title: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "For new work: a few words that say what it is.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** A work's title: the first line of `text` that says anything, in a few words. */
|
|
36
|
+
export const titled = (text) => head((text.split("\n").find((line) => /\S/.test(line)) ?? "").replace(/\s+/g, " ").trim(), 80);
|
|
37
|
+
/** The main loop is put the newest messages waiting, and the start of each: enough to place them. */
|
|
38
|
+
const PUT = { messages: 8, each: 4000 };
|
|
39
|
+
/** What has come in, as it is put to the main loop with the work under way. */
|
|
40
|
+
export const put = (texts, underway) => [
|
|
41
|
+
underway.length === 0 ? "No work is under way." : "Work under way:",
|
|
42
|
+
...underway.map(({ id, title }) => `- ${id}: ${title}`),
|
|
43
|
+
"",
|
|
44
|
+
"What came in:",
|
|
45
|
+
texts
|
|
46
|
+
.slice(-PUT.messages)
|
|
47
|
+
.map((text) => (text.length > PUT.each ? `${head(text, PUT.each)}…` : text))
|
|
48
|
+
.join("\n\n"),
|
|
49
|
+
].join("\n");
|
|
50
|
+
export function createMain(options) {
|
|
51
|
+
/** The decision being made now, and how to drop it. */
|
|
52
|
+
let deciding;
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* Say whose `texts` are, given the work under way: one decision at a time. Nothing is said
|
|
56
|
+
* for a decision that is cancelled. A request the model refuses rejects, as it refused.
|
|
57
|
+
*/
|
|
58
|
+
async route(texts, underway) {
|
|
59
|
+
let drop = () => { };
|
|
60
|
+
const dropped = new Promise((resolve) => {
|
|
61
|
+
drop = () => resolve(undefined);
|
|
62
|
+
});
|
|
63
|
+
const mine = { drop };
|
|
64
|
+
deciding = mine;
|
|
65
|
+
try {
|
|
66
|
+
const chosen = await Promise.race([options.chosen().then((model) => ({ model })), dropped]);
|
|
67
|
+
if (!chosen)
|
|
68
|
+
return undefined;
|
|
69
|
+
mine.requestId = crypto.randomUUID();
|
|
70
|
+
const asked = options.model.respond({
|
|
71
|
+
requestId: mine.requestId,
|
|
72
|
+
...(chosen.model ? { model: chosen.model } : {}),
|
|
73
|
+
instructions: MAIN_INSTRUCTIONS,
|
|
74
|
+
input: [{ type: "message", role: "user", content: put(texts, underway) }],
|
|
75
|
+
tools: [ROUTE],
|
|
76
|
+
});
|
|
77
|
+
// A model that never answers a request it was told to cancel holds nothing up.
|
|
78
|
+
asked.catch(() => { });
|
|
79
|
+
const answer = await Promise.race([asked, dropped]);
|
|
80
|
+
if (!answer)
|
|
81
|
+
return undefined;
|
|
82
|
+
const call = answer.output.find((item) => item.type === "function_call" && item.name === ROUTE.name);
|
|
83
|
+
let said = {};
|
|
84
|
+
if (call?.type === "function_call")
|
|
85
|
+
try {
|
|
86
|
+
said = (JSON.parse(call.arguments) ?? {});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Arguments that don't read: new work, as with no call.
|
|
90
|
+
}
|
|
91
|
+
if (underway.some(({ id }) => id === said.work))
|
|
92
|
+
return { work: said.work };
|
|
93
|
+
// Anything else starts new work, titled by what came in if the model gave no title.
|
|
94
|
+
if (typeof said.work === "string" && said.work !== "")
|
|
95
|
+
options.log.warn("The main loop named work that isn't under way; it is new work.", {
|
|
96
|
+
work: head(said.work, 128),
|
|
97
|
+
});
|
|
98
|
+
const title = titled(typeof said.title === "string" ? said.title : "");
|
|
99
|
+
return { title: title || titled(texts.join("\n")) };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (deciding === mine)
|
|
103
|
+
deciding = undefined;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
/** Drop the decision being made: its request is cancelled, and it says nothing. */
|
|
107
|
+
async cancel() {
|
|
108
|
+
const cut = deciding;
|
|
109
|
+
if (!cut)
|
|
110
|
+
return;
|
|
111
|
+
cut.drop();
|
|
112
|
+
if (cut.requestId)
|
|
113
|
+
await options.model.cancel({ requestId: cut.requestId }).catch(() => { });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for it, and for a tool message the line a person sees.
|
|
4
|
-
*
|
|
2
|
+
* What a loop runs on, kept in a store. A thread keeps its messages, one key each: the items a
|
|
3
|
+
* model reads for it, and for a tool message the line a person sees. A thread is read once, then
|
|
4
|
+
* kept in memory, and its oldest messages are deleted past a retention cap. A piece of work reads
|
|
5
|
+
* its context from the threads it follows: each message is kept once, in the thread where it is
|
|
6
|
+
* shown, and says which work it belongs to when that isn't its thread's own.
|
|
5
7
|
*/
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
import { Item, type Log, type Shown, type Store } from "./types.js";
|
|
@@ -38,6 +40,8 @@ export declare const Entry: z.ZodObject<{
|
|
|
38
40
|
}>>;
|
|
39
41
|
joined: z.ZodOptional<z.ZodLiteral<true>>;
|
|
40
42
|
ahead: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
work: z.ZodOptional<z.ZodString>;
|
|
44
|
+
place: z.ZodOptional<z.ZodNumber>;
|
|
41
45
|
}, z.core.$strip>;
|
|
42
46
|
export type Entry = z.output<typeof Entry>;
|
|
43
47
|
/** Where a record's messages are kept in its store. */
|
|
@@ -62,38 +66,116 @@ export declare const argumentsOf: (args: string) => string;
|
|
|
62
66
|
export declare function messageOf(entry: Entry): Shown;
|
|
63
67
|
/** Stored values read at once when what is kept is first loaded. */
|
|
64
68
|
export declare const READS = 16;
|
|
65
|
-
|
|
69
|
+
/** A user message as it is kept when said. */
|
|
70
|
+
export declare const userEntry: (text: string) => {
|
|
71
|
+
role: "user";
|
|
72
|
+
items: {
|
|
73
|
+
type: "message";
|
|
74
|
+
role: "user";
|
|
75
|
+
content: string;
|
|
76
|
+
}[];
|
|
77
|
+
};
|
|
78
|
+
/** A message as a thread holds it. */
|
|
79
|
+
interface Kept {
|
|
80
|
+
/** Its number in its thread. */
|
|
81
|
+
seq: number;
|
|
82
|
+
key: string;
|
|
83
|
+
entry: Entry;
|
|
84
|
+
bytes: number;
|
|
85
|
+
/** Set while a user message is being written, or taken: the model is sent it once stored. */
|
|
86
|
+
writing?: boolean;
|
|
87
|
+
}
|
|
88
|
+
interface Held {
|
|
89
|
+
records: Kept[];
|
|
90
|
+
last: number;
|
|
91
|
+
bytes: number;
|
|
92
|
+
}
|
|
93
|
+
/** A thread's messages in a store, by their numbers. */
|
|
94
|
+
export declare function createThread(store: Store, log: Log, limits?: {
|
|
66
95
|
keep: number;
|
|
67
96
|
keepBytes: number;
|
|
68
97
|
}): {
|
|
98
|
+
/** What it holds, read once. */
|
|
99
|
+
load: () => Promise<Held>;
|
|
69
100
|
/**
|
|
70
|
-
* Keep a message, under `id` or its own number
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* message was said since, it is kept as `ahead` of it.
|
|
101
|
+
* Keep a message in a thread already read, at once, under `id` or its own number; `stored`
|
|
102
|
+
* settles when it is written and the oldest past the cap are deleted. A tool message the store
|
|
103
|
+
* refuses is still kept in memory, since its call has run; any other is taken out again.
|
|
74
104
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
push(held: Held, entry: Omit<Entry, "id" | "at">, id?: string): {
|
|
106
|
+
record: Kept;
|
|
107
|
+
stored: Promise<void>;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Keep a message as `entry` from now on: at once in memory, then in the store. A write the
|
|
111
|
+
* store refuses leaves it as it was.
|
|
112
|
+
*/
|
|
113
|
+
rewrite(held: Held, record: Kept, entry: Entry): Promise<void>;
|
|
78
114
|
/** Messages before `before`, oldest first, as many as fit `limit` and `PAGE_BUDGET`. */
|
|
79
115
|
page(before: string | undefined, limit: number): Promise<{
|
|
80
116
|
messages: Shown[];
|
|
81
117
|
more: boolean;
|
|
82
118
|
}>;
|
|
119
|
+
};
|
|
120
|
+
export type Thread = ReturnType<typeof createThread>;
|
|
121
|
+
/**
|
|
122
|
+
* A piece of work's record: its messages across the threads it follows, in the order it took them.
|
|
123
|
+
* Its id is the name of its own thread, whose messages are its unless they name another work; in
|
|
124
|
+
* any other thread, the messages that name it are. A message's place is its number in its own
|
|
125
|
+
* thread until the work follows a second thread, and a count kept with each message after. A
|
|
126
|
+
* message waiting in a thread it follows, said before the work had the thread, names no work yet:
|
|
127
|
+
* the work takes it, in the order it was said, before it keeps anything else there.
|
|
128
|
+
*/
|
|
129
|
+
export declare function createRecord(options: {
|
|
130
|
+
id: string;
|
|
131
|
+
/** The threads it follows now, its own first. */
|
|
132
|
+
follows(): string[];
|
|
133
|
+
thread(name: string): Thread;
|
|
134
|
+
}): {
|
|
135
|
+
id: string;
|
|
136
|
+
/**
|
|
137
|
+
* Keep a message of its in `thread`, one of those it follows, after any waiting there. `read`
|
|
138
|
+
* is given for what came of a model request that had read it up to that place: when a user
|
|
139
|
+
* message was said since, it is kept as `ahead` of it.
|
|
140
|
+
*/
|
|
141
|
+
append(entry: Omit<Entry, "id" | "at" | "work" | "place" | "ahead">, at: {
|
|
142
|
+
thread: string;
|
|
143
|
+
id?: string;
|
|
144
|
+
read?: number;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
entry: Entry;
|
|
147
|
+
thread: string;
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* Take the messages waiting in `thread`, which it has come to follow, as its own: `joined`,
|
|
151
|
+
* when a turn of its is running. It says how many there were to answer: in its own thread,
|
|
152
|
+
* where nothing needs taking, those said since it last did anything.
|
|
153
|
+
*/
|
|
154
|
+
adopt(thread: string, joined: boolean): Promise<number>;
|
|
155
|
+
/** Whether it holds anything but what was said to it: it has had a turn that did something. */
|
|
156
|
+
begun(): Promise<boolean>;
|
|
157
|
+
/** Whether it has no message. */
|
|
158
|
+
empty(): Promise<boolean>;
|
|
159
|
+
/** The thread it answers in now. */
|
|
160
|
+
where(): Promise<string>;
|
|
161
|
+
/** Its first user message, for a title. */
|
|
162
|
+
opening(): Promise<string>;
|
|
83
163
|
/**
|
|
84
164
|
* The conversation to send: the newest exchanges (each from the user message that starts a turn
|
|
85
165
|
* on) that fit the budget, and always the latest two, so "go on" after a long turn still has
|
|
86
166
|
* that turn. When those two are over the budget, their oldest function arguments and outputs
|
|
87
167
|
* are left out, each call still paired with an output. A message said during a turn comes after
|
|
88
|
-
* whatever is `ahead` of it, so the model reads it as not yet answered. `read` is the
|
|
89
|
-
*
|
|
90
|
-
*
|
|
168
|
+
* whatever is `ahead` of it, so the model reads it as not yet answered. `read` is the place up
|
|
169
|
+
* to which every user message was read, for `append` and `saidAfter`: one still being written
|
|
170
|
+
* is not read yet. `thread` is where what comes of it is kept.
|
|
91
171
|
*/
|
|
92
172
|
input(): Promise<{
|
|
93
173
|
items: Item[];
|
|
94
174
|
read: number;
|
|
175
|
+
thread: string;
|
|
95
176
|
}>;
|
|
96
|
-
/** Whether a user message has been stored after the
|
|
177
|
+
/** Whether a user message of its has been stored after the place `read`, which `input` gave. */
|
|
97
178
|
saidAfter(read: number): Promise<boolean>;
|
|
98
179
|
};
|
|
99
180
|
export type LoopRecord = ReturnType<typeof createRecord>;
|
|
181
|
+
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for it, and for a tool message the line a person sees.
|
|
4
|
-
*
|
|
2
|
+
* What a loop runs on, kept in a store. A thread keeps its messages, one key each: the items a
|
|
3
|
+
* model reads for it, and for a tool message the line a person sees. A thread is read once, then
|
|
4
|
+
* kept in memory, and its oldest messages are deleted past a retention cap. A piece of work reads
|
|
5
|
+
* its context from the threads it follows: each message is kept once, in the thread where it is
|
|
6
|
+
* shown, and says which work it belongs to when that isn't its thread's own.
|
|
5
7
|
*/
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
import { Item } from "./types.js";
|
|
@@ -18,11 +20,17 @@ export const Entry = z.object({
|
|
|
18
20
|
/** For a user message said while a turn was running: it is part of that turn. */
|
|
19
21
|
joined: z.literal(true).optional(),
|
|
20
22
|
/**
|
|
21
|
-
* For what came of a model request that had read
|
|
22
|
-
*
|
|
23
|
-
* doesn't answer.
|
|
23
|
+
* For what came of a model request that had read its work up to this place, when a user message
|
|
24
|
+
* was said after that: the model reads it ahead of that message, which it doesn't answer.
|
|
24
25
|
*/
|
|
25
26
|
ahead: z.number().optional(),
|
|
27
|
+
/** The piece of work it belongs to, when that isn't its thread's own. */
|
|
28
|
+
work: z.string().optional(),
|
|
29
|
+
/**
|
|
30
|
+
* Its place in its work's order, once that work follows more than one thread. Without it, its
|
|
31
|
+
* place is its number in its thread.
|
|
32
|
+
*/
|
|
33
|
+
place: z.number().optional(),
|
|
26
34
|
});
|
|
27
35
|
/** Where a record's messages are kept in its store. */
|
|
28
36
|
export const MESSAGES = "msg/";
|
|
@@ -96,9 +104,13 @@ const kept = (entry) => ({
|
|
|
96
104
|
});
|
|
97
105
|
/** Stored values read at once when what is kept is first loaded. */
|
|
98
106
|
export const READS = 16;
|
|
99
|
-
/**
|
|
100
|
-
const
|
|
101
|
-
|
|
107
|
+
/** A user message as it is kept when said. */
|
|
108
|
+
export const userEntry = (text) => ({
|
|
109
|
+
role: "user",
|
|
110
|
+
items: [{ type: "message", role: "user", content: text }],
|
|
111
|
+
});
|
|
112
|
+
/** A thread's messages in a store, by their numbers. */
|
|
113
|
+
export function createThread(store, log, limits = { keep: KEEP, keepBytes: KEEP_BYTES }) {
|
|
102
114
|
let loaded;
|
|
103
115
|
const load = () => {
|
|
104
116
|
if (loaded)
|
|
@@ -126,7 +138,7 @@ export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_
|
|
|
126
138
|
log.warn("A stored message didn't read back; it is left out.", { key });
|
|
127
139
|
return;
|
|
128
140
|
}
|
|
129
|
-
const record = { key, entry: parsed.data, bytes: size(parsed.data) };
|
|
141
|
+
const record = { seq: seqOf(key), key, entry: parsed.data, bytes: size(parsed.data) };
|
|
130
142
|
records.push(record);
|
|
131
143
|
bytes += record.bytes;
|
|
132
144
|
});
|
|
@@ -143,57 +155,75 @@ export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_
|
|
|
143
155
|
return attempt;
|
|
144
156
|
};
|
|
145
157
|
return {
|
|
158
|
+
/** What it holds, read once. */
|
|
159
|
+
load,
|
|
146
160
|
/**
|
|
147
|
-
* Keep a message, under `id` or its own number
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* message was said since, it is kept as `ahead` of it.
|
|
161
|
+
* Keep a message in a thread already read, at once, under `id` or its own number; `stored`
|
|
162
|
+
* settles when it is written and the oldest past the cap are deleted. A tool message the store
|
|
163
|
+
* refuses is still kept in memory, since its call has run; any other is taken out again.
|
|
151
164
|
*/
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const key = keyOf(
|
|
165
|
+
push(held, entry, id) {
|
|
166
|
+
const seq = ++held.last;
|
|
167
|
+
const key = keyOf(seq);
|
|
155
168
|
const record = {
|
|
169
|
+
seq,
|
|
156
170
|
key,
|
|
157
171
|
entry: kept({
|
|
158
172
|
id: id ?? key.slice(MESSAGES.length),
|
|
159
173
|
at: new Date().toISOString(),
|
|
160
174
|
...entry,
|
|
161
|
-
...(read !== undefined && saidAfter(state.records, read) ? { ahead: read } : {}),
|
|
162
175
|
}),
|
|
163
176
|
bytes: 0,
|
|
164
177
|
writing: entry.role === "user",
|
|
165
178
|
};
|
|
166
179
|
record.bytes = size(record.entry);
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
held.records.push(record);
|
|
181
|
+
held.bytes += record.bytes;
|
|
182
|
+
const stored = (async () => {
|
|
183
|
+
try {
|
|
184
|
+
await store.set({ key, value: record.entry });
|
|
185
|
+
record.writing = false;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (entry.role !== "tool") {
|
|
189
|
+
held.records.splice(held.records.indexOf(record), 1);
|
|
190
|
+
held.bytes -= record.bytes;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
log.error("A tool message couldn't be stored; it is kept until a restart.", {
|
|
194
|
+
error: String(error),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
while (held.records.length > 1 &&
|
|
198
|
+
(held.records.length > limits.keep || held.bytes > limits.keepBytes)) {
|
|
199
|
+
const oldest = held.records.shift();
|
|
200
|
+
held.bytes -= oldest.bytes;
|
|
201
|
+
await store.delete({ key: oldest.key }).catch((error) => log.warn("An old message couldn't be deleted.", {
|
|
202
|
+
key: oldest.key,
|
|
203
|
+
error: String(error),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
207
|
+
return { record, stored };
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Keep a message as `entry` from now on: at once in memory, then in the store. A write the
|
|
211
|
+
* store refuses leaves it as it was.
|
|
212
|
+
*/
|
|
213
|
+
async rewrite(held, record, entry) {
|
|
214
|
+
const was = { entry: record.entry, bytes: record.bytes };
|
|
215
|
+
const swap = (to) => {
|
|
216
|
+
held.bytes += to.bytes - record.bytes;
|
|
217
|
+
Object.assign(record, to);
|
|
218
|
+
};
|
|
219
|
+
swap({ entry, bytes: size(entry) });
|
|
169
220
|
try {
|
|
170
|
-
await store.set({ key, value:
|
|
171
|
-
record.writing = false;
|
|
221
|
+
await store.set({ key: record.key, value: entry });
|
|
172
222
|
}
|
|
173
223
|
catch (error) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
state.bytes -= record.bytes;
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
log.error("A tool message couldn't be stored; it is kept until a restart.", {
|
|
180
|
-
error: String(error),
|
|
181
|
-
});
|
|
224
|
+
swap(was);
|
|
225
|
+
throw error;
|
|
182
226
|
}
|
|
183
|
-
while (state.records.length > 1 &&
|
|
184
|
-
(state.records.length > limits.keep || state.bytes > limits.keepBytes)) {
|
|
185
|
-
const oldest = state.records.shift();
|
|
186
|
-
state.bytes -= oldest.bytes;
|
|
187
|
-
await store.delete({ key: oldest.key }).catch((error) => log.warn("An old message couldn't be deleted.", {
|
|
188
|
-
key: oldest.key,
|
|
189
|
-
error: String(error),
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
return record.entry;
|
|
193
|
-
},
|
|
194
|
-
/** Whether nothing is kept. */
|
|
195
|
-
async empty() {
|
|
196
|
-
return (await load()).records.length === 0;
|
|
197
227
|
},
|
|
198
228
|
/** Messages before `before`, oldest first, as many as fit `limit` and `PAGE_BUDGET`. */
|
|
199
229
|
async page(before, limit) {
|
|
@@ -215,19 +245,144 @@ export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_
|
|
|
215
245
|
}
|
|
216
246
|
return { messages, more: start > 0 };
|
|
217
247
|
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A piece of work's record: its messages across the threads it follows, in the order it took them.
|
|
252
|
+
* Its id is the name of its own thread, whose messages are its unless they name another work; in
|
|
253
|
+
* any other thread, the messages that name it are. A message's place is its number in its own
|
|
254
|
+
* thread until the work follows a second thread, and a count kept with each message after. A
|
|
255
|
+
* message waiting in a thread it follows, said before the work had the thread, names no work yet:
|
|
256
|
+
* the work takes it, in the order it was said, before it keeps anything else there.
|
|
257
|
+
*/
|
|
258
|
+
export function createRecord(options) {
|
|
259
|
+
const { id } = options;
|
|
260
|
+
const placeOf = (record) => record.entry.place ?? record.seq;
|
|
261
|
+
const mine = (thread, record) => record.entry.work === undefined ? thread === id : record.entry.work === id;
|
|
262
|
+
/** Every thread it follows, read: what each holds, as it changes. */
|
|
263
|
+
const load = async () => {
|
|
264
|
+
const held = new Map();
|
|
265
|
+
for (const name of options.follows())
|
|
266
|
+
held.set(name, await options.thread(name).load());
|
|
267
|
+
return held;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Its own messages in the threads read, in order, as they stand now. Nothing waits between
|
|
271
|
+
* this and what is decided from it.
|
|
272
|
+
*/
|
|
273
|
+
const own = (held) => {
|
|
274
|
+
const records = [...held].flatMap(([thread, { records }]) => records.flatMap((record) => (mine(thread, record) ? [{ thread, record }] : [])));
|
|
275
|
+
// One thread's messages are in order already.
|
|
276
|
+
if (held.size > 1)
|
|
277
|
+
records.sort((a, b) => placeOf(a.record) - placeOf(b.record));
|
|
278
|
+
return records;
|
|
279
|
+
};
|
|
280
|
+
/** The highest place among its messages. */
|
|
281
|
+
const topOf = (records) => {
|
|
282
|
+
const last = records.at(-1);
|
|
283
|
+
return last ? placeOf(last.record) : 0;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Take the messages waiting in `thread` as its own, after all it holds: at once in memory, and
|
|
287
|
+
* `written` settles when each is stored, or rejects with one that isn't and stays waiting. Like
|
|
288
|
+
* a message being written, the model is sent none of them until all are stored.
|
|
289
|
+
*/
|
|
290
|
+
const take = (held, thread, joined) => {
|
|
291
|
+
const from = held.get(thread);
|
|
292
|
+
const waiting = thread === id ? [] : from.records.filter(({ entry }) => !entry.work);
|
|
293
|
+
let top = topOf(own(held));
|
|
294
|
+
const writes = waiting.map((record) => options.thread(thread).rewrite(from, record, {
|
|
295
|
+
...record.entry,
|
|
296
|
+
work: id,
|
|
297
|
+
place: ++top,
|
|
298
|
+
...(joined && record.entry.role === "user" ? { joined: true } : {}),
|
|
299
|
+
}));
|
|
300
|
+
for (const record of waiting)
|
|
301
|
+
record.writing = true;
|
|
302
|
+
const written = Promise.allSettled(writes).then((all) => {
|
|
303
|
+
for (const record of waiting)
|
|
304
|
+
record.writing = false;
|
|
305
|
+
const refused = all.find((one) => one.status === "rejected");
|
|
306
|
+
if (refused)
|
|
307
|
+
throw refused.reason;
|
|
308
|
+
});
|
|
309
|
+
return { taken: waiting.length, written };
|
|
310
|
+
};
|
|
311
|
+
/** Whether a user message of its follows the place `read`: one `stored` already, when asked. */
|
|
312
|
+
const saidAfter = (records, read, stored = false) => records.some(({ record }) => record.entry.role === "user" && placeOf(record) > read && !(stored && record.writing));
|
|
313
|
+
/** The thread of its newest user message: where it answers. Its own, with none. */
|
|
314
|
+
const whereOf = (records) => records.findLast(({ record }) => record.entry.role === "user")?.thread ?? id;
|
|
315
|
+
return {
|
|
316
|
+
id,
|
|
317
|
+
/**
|
|
318
|
+
* Keep a message of its in `thread`, one of those it follows, after any waiting there. `read`
|
|
319
|
+
* is given for what came of a model request that had read it up to that place: when a user
|
|
320
|
+
* message was said since, it is kept as `ahead` of it.
|
|
321
|
+
*/
|
|
322
|
+
async append(entry, at) {
|
|
323
|
+
const held = await load();
|
|
324
|
+
const { thread } = at;
|
|
325
|
+
// What waits there is taken and stored first, so this is never kept ahead of it: it joins
|
|
326
|
+
// a turn if this does, or comes of one. If the store won't take it, nothing is kept.
|
|
327
|
+
await take(held, thread, entry.joined === true || at.read !== undefined).written;
|
|
328
|
+
// From here to the push nothing waits, so its place and what is ahead of it are decided
|
|
329
|
+
// with the record as it stands.
|
|
330
|
+
const records = own(held);
|
|
331
|
+
const { record, stored } = options.thread(thread).push(held.get(thread), {
|
|
332
|
+
...entry,
|
|
333
|
+
...(thread === id ? {} : { work: id }),
|
|
334
|
+
...(held.size > 1 ? { place: topOf(records) + 1 } : {}),
|
|
335
|
+
...(at.read !== undefined && saidAfter(records, at.read) ? { ahead: at.read } : {}),
|
|
336
|
+
}, at.id);
|
|
337
|
+
await stored;
|
|
338
|
+
return { entry: record.entry, thread };
|
|
339
|
+
},
|
|
340
|
+
/**
|
|
341
|
+
* Take the messages waiting in `thread`, which it has come to follow, as its own: `joined`,
|
|
342
|
+
* when a turn of its is running. It says how many there were to answer: in its own thread,
|
|
343
|
+
* where nothing needs taking, those said since it last did anything.
|
|
344
|
+
*/
|
|
345
|
+
async adopt(thread, joined) {
|
|
346
|
+
const held = await load();
|
|
347
|
+
const { taken, written } = take(held, thread, joined);
|
|
348
|
+
await written;
|
|
349
|
+
if (thread !== id)
|
|
350
|
+
return taken;
|
|
351
|
+
const records = own(held);
|
|
352
|
+
const done = records.findLastIndex(({ record }) => record.entry.role !== "user");
|
|
353
|
+
return records.length - 1 - done;
|
|
354
|
+
},
|
|
355
|
+
/** Whether it holds anything but what was said to it: it has had a turn that did something. */
|
|
356
|
+
async begun() {
|
|
357
|
+
return own(await load()).some(({ record }) => record.entry.role !== "user");
|
|
358
|
+
},
|
|
359
|
+
/** Whether it has no message. */
|
|
360
|
+
async empty() {
|
|
361
|
+
return own(await load()).length === 0;
|
|
362
|
+
},
|
|
363
|
+
/** The thread it answers in now. */
|
|
364
|
+
async where() {
|
|
365
|
+
return whereOf(own(await load()));
|
|
366
|
+
},
|
|
367
|
+
/** Its first user message, for a title. */
|
|
368
|
+
async opening() {
|
|
369
|
+
const first = own(await load()).find(({ record }) => record.entry.role === "user");
|
|
370
|
+
return first ? textOf(first.record.entry) : "";
|
|
371
|
+
},
|
|
218
372
|
/**
|
|
219
373
|
* The conversation to send: the newest exchanges (each from the user message that starts a turn
|
|
220
374
|
* on) that fit the budget, and always the latest two, so "go on" after a long turn still has
|
|
221
375
|
* that turn. When those two are over the budget, their oldest function arguments and outputs
|
|
222
376
|
* are left out, each call still paired with an output. A message said during a turn comes after
|
|
223
|
-
* whatever is `ahead` of it, so the model reads it as not yet answered. `read` is the
|
|
224
|
-
*
|
|
225
|
-
*
|
|
377
|
+
* whatever is `ahead` of it, so the model reads it as not yet answered. `read` is the place up
|
|
378
|
+
* to which every user message was read, for `append` and `saidAfter`: one still being written
|
|
379
|
+
* is not read yet. `thread` is where what comes of it is kept.
|
|
226
380
|
*/
|
|
227
381
|
async input() {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const
|
|
382
|
+
const records = own(await load());
|
|
383
|
+
const last = topOf(records);
|
|
384
|
+
const unwritten = records.find(({ record }) => record.writing);
|
|
385
|
+
const read = unwritten ? placeOf(unwritten.record) - 1 : last;
|
|
231
386
|
const exchanges = [];
|
|
232
387
|
/** Said during a turn, and not yet put: what is ahead of them goes first. */
|
|
233
388
|
const late = [];
|
|
@@ -236,12 +391,12 @@ export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_
|
|
|
236
391
|
exchanges.push([]);
|
|
237
392
|
exchanges.at(-1)?.push(...entry.items);
|
|
238
393
|
};
|
|
239
|
-
/** Put the late messages
|
|
394
|
+
/** Put the late messages placed up to `read`. */
|
|
240
395
|
const putLate = (read) => {
|
|
241
|
-
while (late[0] &&
|
|
396
|
+
while (late[0] && placeOf(late[0]) <= read)
|
|
242
397
|
put(late.shift().entry);
|
|
243
398
|
};
|
|
244
|
-
for (const record of records) {
|
|
399
|
+
for (const { record } of records) {
|
|
245
400
|
// Nothing to send: a user message still being written, or a line with no items.
|
|
246
401
|
if (record.writing || record.entry.items.length === 0)
|
|
247
402
|
continue;
|
|
@@ -268,11 +423,11 @@ export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_
|
|
|
268
423
|
items[i] = { ...item, arguments: ARGUMENTS_LEFT_OUT };
|
|
269
424
|
}
|
|
270
425
|
}
|
|
271
|
-
return { items, read };
|
|
426
|
+
return { items, read, thread: whereOf(records.filter(({ record }) => !record.writing)) };
|
|
272
427
|
},
|
|
273
|
-
/** Whether a user message has been stored after the
|
|
428
|
+
/** Whether a user message of its has been stored after the place `read`, which `input` gave. */
|
|
274
429
|
async saidAfter(read) {
|
|
275
|
-
return saidAfter((await load())
|
|
430
|
+
return saidAfter(own(await load()), read, true);
|
|
276
431
|
},
|
|
277
432
|
};
|
|
278
433
|
}
|