@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.
@@ -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
- * The record a loop runs on, kept in a store: one key per message, holding the items a model reads
3
- * for it, and for a tool message the line a person sees. It is read once, then kept in memory, and
4
- * the oldest messages are deleted past a retention cap.
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
- export declare function createRecord(store: Store, log: Log, limits?: {
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, and delete the oldest past the cap. A tool
71
- * message the store refuses is still kept in memory, since its call has run. `read` is given
72
- * for what came of a model request that had read the record up to that number: when a user
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
- append(entry: Omit<Entry, "id" | "at">, id?: string, read?: number): Promise<Entry>;
76
- /** Whether nothing is kept. */
77
- empty(): Promise<boolean>;
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 number
89
- * up to which every user message was read, for `append` and `saidAfter`: one still being
90
- * written is not read yet.
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 one numbered `read`, which `input` gave. */
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
- * The record a loop runs on, kept in a store: one key per message, holding the items a model reads
3
- * for it, and for a tool message the line a person sees. It is read once, then kept in memory, and
4
- * the oldest messages are deleted past a retention cap.
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 the record up to the message of this number,
22
- * when a user message was said after that: the model reads it ahead of that message, which it
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
- /** Whether a user message follows the one numbered `read`: one `stored` already, when asked so. */
100
- const saidAfter = (records, read, stored = false) => records.some((record) => record.entry.role === "user" && seqOf(record.key) > read && !(stored && record.writing));
101
- export function createRecord(store, log, limits = { keep: KEEP, keepBytes: KEEP_BYTES }) {
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, and delete the oldest past the cap. A tool
148
- * message the store refuses is still kept in memory, since its call has run. `read` is given
149
- * for what came of a model request that had read the record up to that number: when a user
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
- async append(entry, id, read) {
153
- const state = await load();
154
- const key = keyOf(++state.last);
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
- state.records.push(record);
168
- state.bytes += record.bytes;
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: record.entry });
171
- record.writing = false;
221
+ await store.set({ key: record.key, value: entry });
172
222
  }
173
223
  catch (error) {
174
- if (entry.role !== "tool") {
175
- state.records.splice(state.records.indexOf(record), 1);
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 number
224
- * up to which every user message was read, for `append` and `saidAfter`: one still being
225
- * written is not read yet.
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 { records, last } = await load();
229
- const unwritten = records.find((record) => record.writing);
230
- const read = unwritten ? seqOf(unwritten.key) - 1 : last;
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 numbered up to `read`. */
394
+ /** Put the late messages placed up to `read`. */
240
395
  const putLate = (read) => {
241
- while (late[0] && seqOf(late[0].key) <= read)
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 one numbered `read`, which `input` gave. */
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()).records, read, true);
430
+ return saidAfter(own(await load()), read, true);
276
431
  },
277
432
  };
278
433
  }